复盘:Unity RTS (4) 网格系统+路线规划

文章探讨了在实时战略(RTS)游戏中使用UnityNavigation的A*算法解决寻路问题的挑战,包括多角色碰撞、动态障碍物处理以及适用于帧同步的需求。同时介绍了流场寻路算法作为替代方案,以及如何构建网格系统和热力图进行路径规划。
摘要由CSDN通过智能技术生成

网络的部分搞定后,主要就是RTS的几大难点.

这里先复盘一下遇到的最大的坑:寻路系统

        方案的选择:

        1.基于路点寻路+A*算法的Unity Navigation

        优点:优化非常好,上手很简单,使用了地图烘焙

        缺点:

        1.多角色之间的碰撞和挤压很难看且难以避免

        如果是人型角色这倒没什么,谁没在CS里被挤过,但是一堆坦克挤来挤去就很酸爽了.这点亲身体会,另外可以参考这个帖子.

unity3d多人寻路问题方案_unity 多人寻路-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/u011926026/article/details/63682788#:~:text=%E7%9B%B8%E4%BF%A1%E5%A4%A7%E5%AE%B6%E7%94%A8unity3d%E8%87%AA%E5%B8%A6navmeshagent%E5%AF%BB%E8%B7%AF%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C%E4%B8%80%E5%AE%9A%E4%BC%9A%E7%A2%B0%E5%88%B0%E5%A4%9A%E4%BA%BA%E5%AF%BB%E8%B7%AF%E7%9B%B8%E4%BA%92%E6%8C%A4%E5%8E%8B%E7%9A%84%E9%97%AE%E9%A2%98%E3%80%82,%E8%BF%99%E4%B8%AA%E6%88%91%E5%B7%B2%E7%BB%8F%E8%A7%A3%E5%86%B3%E4%BA%86%EF%BC%8C%E8%80%8C%E4%B8%94%E5%B7%B2%E7%BB%8F%E5%BA%94%E7%94%A8%E5%88%B0%E6%88%91%E4%BB%AC%E7%9A%84%E9%A1%B9%E7%9B%AE%E4%B8%AD%EF%BC%8C%E6%84%9F%E8%A7%89%E8%BF%98%E4%B8%8D%E9%94%99%E3%80%82%20%E9%A6%96%E5%85%88%E6%8F%90%E4%B8%8B%E9%97%AE%E9%A2%98%E7%9A%84%E5%8E%9F%E5%9B%A0%EF%BC%9Anavmeshagent%E5%9C%A8%E5%AF%BB%E8%B7%AF%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C%E5%B9%B6%E4%B8%8D%E4%BC%9A%E6%8A%8A%E5%88%AB%E4%BA%BA%E5%BD%93%E6%88%90%E9%9A%9C%E7%A2%8D%E7%89%A9%EF%BC%8C%E8%80%8C%E6%98%AF%E5%9C%A8%E5%8D%B3%E5%B0%86%E5%92%8C%E5%85%B6%E4%BB%96agent%E7%A2%B0%E6%92%9E%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C%E9%80%9A%E8%BF%87%E5%87%8F%E9%80%9F%EF%BC%8C%E7%A2%B0%E6%92%9E%E8%BD%AC%E5%BC%AF%EF%BC%8C%E5%AE%9E%E7%8E%B0%E9%81%BF%E5%BC%80%E5%85%B6%E4%BB%96agent.

        总之一点,这个东西对于单人或者对挤压没那么敏感的游戏可以,否则得深入底层改一些东西.

        2.动态添加障碍物比如RTS这种能够随时随地建建筑,会产生一定bug

        3.不适用于帧同步(浮点\碰撞)

        2.现成的A*框架

        性能开销过大,适用于单人或者少量角色.

       *3.流场寻路算法(采用)

        思路参考:

        优点:大量单位公用同一份寻路结果节省了大量时间

        PS.寻路远非线路规划这么简单,因为没有单位能够完全按照规划路线行进,每个单位之间会相互影响,这个影响逻辑怎么写是最麻烦的,这不在本文复盘内容之内.

        

一.建立网格系统

        首先无论是A*还是流场寻路,都必须建立一个网格系统,网格系统听着复杂,其实就是写一些方法,将当前的Unity 转化成抽象的网格上的坐标.

比如我有一个位置Vector3(5,10.6,8),我想知道他在我抽象的网格世界的网格坐标是什么

就有了了Vector3Int currentGrid = Grid.FromWorldToCell(Vector3(5,10.6,8));

我这里的代码是用LStepLock(一个开源定点库)的LVector3,原理一样.

using Lockstep.Math;
using UnityEngine;

public class LGrid : MonoBehaviour
{
    [SerializeField] private LFloat cellSize;
    
   /// <summary>
   /// 获得当前真实三维坐标对应的网格坐标
   /// </summary>
   /// <returns></returns>
    public LVector3 LWorldToCell(LVector3 world)
    {
        return new LVector3(
            LFloat.Divide(world.x, cellSize),
            LFloat.Divide(world.y, cellSize),
            LFloat.Divide(world.z, cellSize)
        );
    } 
   /// <summary>
   /// 获得当前网格(左下角)的真实三位坐标中
   /// </summary>
   /// <returns></returns>
    //默认网格初始位置0,0,0
    public LVector3 LCellToWorld(LVector3Int cell)
    {
        return new LVector3(
            (cell.x -1) * cellSize,
            (cell.y) * cellSize,
            (cell.z -1) * cellSize);
    }
   /// <summary>
   /// 获得当前网格(中心点)的真实三位坐标
   /// </summary>
   /// <returns></returns>
    public LVector3 LGetCellCenterWorld(LVector3 cell)
    {
        return new LVector3(
           (cell.x-1)*cellSize+cellSize/2,
           (cell.y)*cellSize,
           (cell.z-1)*cellSize+cellSize/2
        );
    } 
   /// <summary>
   /// 获得当前网格(中心点)的真实三位坐标*1000000L的坐标的值
   /// </summary>
   /// <param name="cell"></param>
   /// <returns></returns>
     public LVector3 LGetCellCenterWorld_s(LVector3 cell)
     {
         return new LVector3(
             ((cell._x-1)*cellSize+cellSize/2),
             (cell._y)*cellSize,
             (cell._z-1)*cellSize+cellSize/2
         );
     }
}

二.初始化导航地图

       这个步骤相当复杂,但是简单来说就是建立一个bool[,]的二维地图,尺寸比如500x500,值为false表示障碍物,true表示可通行.

        对于这次的复盘的任务而言就是传入一张这样的地图,起始点,终点,怎样找到一条从起点到终点的二维地图中的坐标数组

流场算法

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using Lockstep.Math;
using UnityEngine;

/// <summary>
/// 流场算法
/// </summary>
public class FlowFieldPath
{
    private bool[,] grid;
    public int[,] heatMap;
    private int targetX, targetY;
    private int[,] flowField;
    public bool ifGenerateFineshed = false;

    /// <summary>
    /// 初始化流场寻路:传入导航地图
    /// </summary>
    /// <param name="grid"></param>
    public FlowFieldPath(bool[,] grid)
    {
        this.grid = grid;
        heatMap = new int[grid.GetLength(0), grid.GetLength(1)];
        for (int i = 0; i < heatMap.GetLength(0); i++)
        {
            for (int j = 0; j < heatMap.GetLength(1); j++)
            {
                heatMap[i, j] = int.MaxValue;
            }
        }
    }

    /// <summary>
    /// 传入终点,根据重点生成导航热力图
    /// 1.创建新的线程
    /// </summary>
    /// <param name="target">终点</param>
    public void GenerateHeatMap(Vector2Int target)
    {
        new Thread(() => { GenerateHeatMap_T(target); }).Start();
    }

    /// <summary>
    /// 传入终点,根据重点生成导航热力图
    /// 2.正式开始生成热力图,生成的热力图
    /// </summary>
    /// <param name="target"></param>
    /// <returns></returns>
    public int[,] GenerateHeatMap_T(Vector2Int target)
    {
        heatMap = new int[grid.GetLength(0), grid.GetLength(1)];
        for (int i = 0; i < heatMap.GetLength(0); i++)
        {
            for (int j = 0; j < heatMap.GetLength(1); j++)
            {
                heatMap[i, j] = int.MaxValue;
            }
        }

        this.targetX = target.x;
        this.targetY = target.y;
        Queue<Vector2Int> queue = new Queue<Vector2Int>();
        queue.Enqueue(new Vector2Int(targetX, targetY));
        heatMap[targetX, targetY] = 0;

        while (queue.Count > 0)
        {
            Vector2Int current = queue.Dequeue();
            List<Vector2Int> neighbors = GetNeighbors(current);
            foreach (Vector2Int neighbor in neighbors)
            {
                if (grid[neighbor.x, neighbor.y])
                {
                    int newHeat = heatMap[current.x, current.y] +
                                  ((neighbor.x == current.x || neighbor.y == current.y) ? 1 : 2);
                    if (newHeat < heatMap[neighbor.x, neighbor.y])
                    {
                        heatMap[neighbor.x, neighbor.y] = newHeat;
                        queue.Enqueue(neighbor);
                    }
                }
            }
        }

        ifGenerateFineshed = true;
        return heatMap;
    }

    /// <summary>
    /// 获取热力图某点的热力值
    /// </summary>
    /// <param name="loc"></param>
    /// <returns></returns>
    public int GetHeatMapPoint(LVector2Int loc)
    {
        return heatMap[loc.x, loc.y];
    }

    /// <summary>
    /// 根据热力图生成流场图
    /// </summary>
    /// <param name="target"></param>
    /// <returns></returns>
    public int[,] GenerateFlowField(Vector2Int target)
    {
        Debug.Log("确定流场目标:" + target.x + "," + target.y);
        GenerateHeatMap(target);
        flowField = new int[grid.GetLength(0), grid.GetLength(1)];
        for (int i = 0; i < flowField.GetLength(0); i++)
        {
            for (int j = 0; j < flowField.GetLength(1); j++)
            {
                List<Vector2Int> neighbors = GetNeighbors(new Vector2Int(i, j));
                int minHeat = int.MaxValue;
                foreach (Vector2Int neighbor in neighbors)
                {
                    if (grid[neighbor.x, neighbor.y] && heatMap[neighbor.x, neighbor.y] < minHeat)
                    {
                        minHeat = heatMap[neighbor.x, neighbor.y];
                        flowField[i, j] = DirectionToIndex(new Vector2Int(i, j), neighbor);
                    }
                }
            }
        }

        return flowField;
    }

    public int CheckDirection(LVector2Int loc)
    {
        return flowField[loc.x, loc.y];
    }

    private List<Vector2Int> GetNeighbors(Vector2Int point)
    {
        List<Vector2Int> neighbors = new List<Vector2Int>();
        for (int dx = -1; dx <= 1; dx++)
        {
            for (int dy = -1; dy <= 1; dy++)
            {
                if (dx == 0 && dy == 0) continue; // 跳过自身
                int nx = point.x + dx;
                int ny = point.y + dy;
                if (nx >= 0 && ny >= 0 && nx < grid.GetLength(0) && ny < grid.GetLength(1))
                {
                    neighbors.Add(new Vector2Int(nx, ny));
                }
            }
        }

        return neighbors;
    }


    private int DirectionToIndex(Vector2Int current, Vector2Int target)
    {
        Vector2Int direction = target - current;
        if (direction.x == 0 && direction.y == -1) return 0; // 上
        if (direction.x == 1 && direction.y == -1) return 1; // 右上
        if (direction.x == 1 && direction.y == 0) return 2; // 右
        if (direction.x == 1 && direction.y == 1) return 3; // 右下
        if (direction.x == 0 && direction.y == 1) return 4; // 下
        if (direction.x == -1 && direction.y == 1) return 5; // 左下
        if (direction.x == -1 && direction.y == 0) return 6; // 左
        if (direction.x == -1 && direction.y == -1) return 7; // 左上

        return -1;
    }

    public LVector2Int GetFlowFieldDirection(LVector2Int point)
    {
        int directionIndex = flowField[point.x, point.y];
        LVector2Int direction = IndexToDirection(directionIndex);
        return direction;
    }

    private LVector2Int IndexToDirection(int index)
    {
        switch (index)
        {
            case 0: return new LVector2Int(0, 1); // 上
            case 1: return new LVector2Int(1, 1); // 右上
            case 2: return new LVector2Int(1, 0); // 右
            case 3: return new LVector2Int(1, -1); // 右下
            case 4: return new LVector2Int(0, -1); // 下
            case 5: return new LVector2Int(-1, -1); // 左下
            case 6: return new LVector2Int(-1, 0); // 左
            case 7: return new LVector2Int(-1, 1); // 左上
            default: return new LVector2Int(0, 0); // 默认情况,没有方向
        }
    }
}

Unity网格合并插件是一种用于合并游戏场景中多个网格模型的工具。在游戏开发中,为了提高性能和优化资源的使用,常常需要将多个小块的网格模型合并成一个大块的模型。 使用Unity网格合并插件可以简化这一过程,减少开发者的工作量。它提供了一个用户友好的界面,让开发者可以轻松地选择需要合并的网格模型,并设置合并后模型的属性。 Unity网格合并插件可以帮助开发者解决以下问题: 1. 减少渲染调用:将多个小块的网格合并成一个大块的模型,可以减少渲染调用次数,提高渲染性能。 2. 优化资源使用:合并网格模型可以减少游戏运行时的内存消耗,提高游戏性能。 3. 简化碰撞检测:合并网格模型后,只需对一个大块的模型进行碰撞检测,可以优化游戏的物理运算。 使用Unity网格合并插件的步骤通常包括: 1. 导入网格模型:将需要合并的网格模型导入到Unity中。 2. 创建合并对象:在Unity中创建一个新的空游戏对象作为合并后模型的容器。 3. 设置合并属性:选择需要合并的网格模型,并设置合并后模型的属性,如材质、碰撞体等。 4. 运行合并操作:点击合并按钮,将选择的网格模型合并成一个大块的模型,并将其添加到合并对象中。 通过使用Unity网格合并插件,开发者可以有效地优化游戏的性能和资源的使用,提高游戏的流畅度和体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值