日志

  这篇我们要来实现网格游戏中最普遍也是最重要的算法基础——寻路算法,如果你只想使用寻路算法,可以直接使用
Unity提供的Navmeshagent和AI命名空间下的工具,不过因为这个项目尽可能不适用插件,并且我希望寻路算法更贴近
我的项目,尽可能更高效和可定制化,所以来自己实现一个A*寻路算法

在这里我不会写A星寻路算法的原理,网上有很多A星寻路的实现,我只会写我在把他运用到我的实际项目中遇到了哪些问题,做了哪些改进和定制化,在最后会贴上寻路代码

从c++到c#

在这个项目之前我已经学习过A寻路算法,它是Dijktstra算法的网格改进,但我只使用c++实现过它,当我把他转移到c#时就首先遇到了一些问题。
c++的stl提供了很多算法数据结构去优化效率,A
寻路的思想是总选择当前最好的选择,也就是基于寻路消耗估计的贪婪算法,所以我们需要从openList(所有可选择的点)中选择最小的,并且我们需要频繁的向其中插入和选择最小。在c++中我们可以使用priority_queue也就是大小堆实现,但目前的c#标准中并没有提供堆容器,可以引入一些外部库来解决,但我自己写了一个c#的priority_queue,因为大小堆算法也并不困难,并且我们只需要一些基础的功能。

namespace Tools
{public class PriorityQueue<T> where T : IComparable<T>{private List<T> queue;public T Top{get => queue[0];}public int Count{get => queue.Count;}public PriorityQueue(){queue = new List<T>();}public void Enqueue(T item){queue.Add(item);AdjustUp(queue.Count - 1);}public T Dequeue(){if(queue.Count == 0) return default(T);T temp = queue[0];queue[0] = queue[Count - 1];queue.RemoveAt(Count - 1);AdjustDown(0);return temp;}public bool Contains(T item){return queue.Contains(item);}public bool Exists(Predicate<T> match){return queue.Exists(match);}private void AdjustUp(int child){int parent = (child - 1) / 2;while(parent >= 0){if(queue[parent].CompareTo(queue[child]) != 0){T temp = queue[parent];queue[parent] = queue[child];queue[child] = temp;child = parent;parent = (child - 1) / 2;}else break;}}private void AdjustDown(int parent){int child = parent * 2 + 1;while(child < queue.Count){if(child + 1 < queue.Count && queue[child].CompareTo(queue[child + 1]) != 0) ++child;if(queue[parent].CompareTo(queue[child]) != 0){T temp = queue[parent];queue[parent] = queue[child];queue[child] = temp;parent = child;child = parent * 2 + 1;}else break;}}}
}

网格对应的坐标

接下来是一个GridPos的遗留问题,当我真正处理寻路时我意识到这个问题,之前我获取GridPos只是将其取整,但负数的取整是向0的(按照绝对值取整),导致正负数取整方向不同,我们需要制定一个方格(1*1)和坐标(x, y)对应的规则,我决定以左下角为方格的坐标,使用Mathf.floor
更新后的GridPos

/// <summary>
/// 网格位置(整数)
/// </summary>
public class GridPos
{public short x;public float y;public short z;public Vector3 Pos{get => new Vector3(x, y, z);}/// <summary>/// 将精确坐标转换为网格坐标/// </summary>/// <param name="pos"></param>/// <returns></returns>public static GridPos GetGridPos(Vector3 pos){//向左下角取整GridPos gridPos = new GridPos();gridPos.x = (short)Mathf.Floor(pos.x);gridPos.y = pos.y;gridPos.z = (short)Mathf.Floor(pos.z);return gridPos;}public bool Equal(GridPos other){if(other == null) return false;return x == other.x && z == other.z;}public string DebugStr{get { return "GridPos:" + x + " " + z; }}
}

PathPoint

我们需要一个算法中的数据结构来表示每一个点以及他的消耗估值,并且提供一些便捷的比较、debug、转换等方法,他应该是仅供算法使用的,即应当是一个类中类

/// <summary>
/// 路径点
/// </summary>
private class PathPoint : IComparable<PathPoint>
{public short x;public short z;public PathPoint parent;public uint F;public uint G;public uint H;public PathPoint(short _x, short _z){x = _x;z = _z;F = 0;G = 0;H = 0;parent = null;}public void Init(PathPoint _parent, int _endX, int _endZ){parent = _parent;uint cost;if(x != parent.x && z != parent.z) cost = PathFinder.hypotenuseCost;else cost = PathFinder.legCost;G = parent.G + cost;H = (uint)(Mathf.Abs(_endZ - z) + Mathf.Abs(_endX - x)) * PathFinder.legCost;F = G + H;}public void TryUpdateParent(PathPoint newParent){uint cost;if(x != newParent.x && z != newParent.z) cost = PathFinder.hypotenuseCost;else cost = PathFinder.legCost;if(newParent.G + cost < G){parent = newParent;G = parent.G + cost;F = G + H;}}public bool Equal(PathPoint pathPoint){return (pathPoint.x == x && pathPoint.z == z);}public int CompareTo(PathPoint other){if(other.F < F) return 1;else return 0;}public Vector3 GetVector(){return new Vector3(x, 0, z);}public string DebugStr{get => x + "," + z;}
}

寻路时的碰撞体积

当真正投入到游戏时,我发现人物和怪物或者其他需要寻路的家伙都有不一样的碰撞体,并不是这个坐标处没有建筑物就可以通过,它可能需要周围几格都没有建筑物,有些飞行的怪物甚至可以忽视建筑物,所以我设置了一个IsWalkable函数而不是直接访问bool数组,它可以被定制(但目前我还没有定义对应的参数,只是使用了人物的大小),而且我们需要一个世界坐标(有正负)向数组index的转换以及越界检测,也可以在这里统一进行,相当于封装了对grids的访问,非常有利于debug和简化代码。

private static bool Walkable(bool[,] grids, int x, int z)
{short rows = (short)grids.GetLength(0);short columns = (short)grids.GetLength(1);x += columns / 2;z += rows / 2;return (x >= 0 && x < rows && z >= 0 && z < columns &&!grids[x, z] && (x - 1 < 0 || !grids[x - 1, z]) && (z - 1 < 0 || !grids[x, z - 1]) && (x - 1 < 0 || z - 1 < 0 || !grids[x - 1, z - 1]));
}

优化处理

仅保留拐点

我们已经可以通过寻路得到完整的PathPoint的列表,但我们应该直接返回这个列表吗?如果一次移动走了20格,我们就会返回20个格子,然后LocomotionController应该如何处理这20个格子?我们肯定希望开启协程来完成移动,更多的格子意味着更多的函数调用消耗。我们应该回想为什么要使用PathFinder,是为了绕开障碍物,如果没有PathFinder,我们就只能直线向目标移动,所以现在我们只需要确定保留那些需要拐弯(改变方向)的点,就可以很好的完成这件事,一次寻路可能只需要3-4此拐弯,而且debug时也更加方便的可以在地图上进行连线。
实现很简单,只需要额外记录一个lastDirection即可。

PathPoint p = closeList[closeList.Count - 1];
List<Vector3> path = new List<Vector3>();
//仅保留拐点
Vector2 lastDirection = new Vector2(0, 0);
while(p.parent != null)
{PathPoint q = p.parent;Vector2 direction = new Vector2(q.x - p.x, q.z - p.z);if(direction != lastDirection) path.Add(p.GetVector());lastDirection = direction;p = q;
}
path.Add(p.GetVector());
path.Reverse();
//以精确点替代
path[0] = start;
path[path.Count - 1] = end;

因为pathpoint只是算法中使用的数据结构,我们的对外接口应该是vector3,所以在这里转换为了vector3,并且为了后续的路径平滑更加精确和减少多余点,这里将起点和终点换为了精确坐标

路径平滑

即便是取消了方向相同的点,我们并没有本质解决那些没有必要的拐弯,有一些点本可以直接到达,但因为正方形网格的原因需要额外的拐弯(只能走直线边),所以我们应该尝试将能够直接连接的点相连淘汰掉中间的拐点

//连接可直达的点,去除不必要的拐点,进行路径平滑
List<Vector3> result = new List<Vector3>();
int pre = 0;
int next = path.Count - 1;
for(; next > pre; --next)
{UnityEngine.Debug.Log(path[pre]);UnityEngine.Debug.Log(path[next]);//相邻点无需检测肯定直接可达if(next == pre + 1 || DirectlyReachable(grids, path[pre], path[next])){UnityEngine.Debug.Log(true);result.Add(path[pre]);pre = next;next = path.Count;}else UnityEngine.Debug.Log(false);
}
//添加剩余不可简化的点
while(pre < path.Count)
{result.Add(path[pre++]);
}
return result;
private static bool DirectlyReachable(bool[,] grids, Vector3 start, Vector3 end)
{float gradient;if(end.x == start.x) gradient = float.MaxValue;//避免除0else gradient = (end.z - start.z) / (end.x - start.x);int startX = Mathf.FloorToInt(start.x);int startZ = Mathf.FloorToInt(start.z);int endX = Mathf.FloorToInt(end.x);int endZ = Mathf.FloorToInt(end.z);//斜率小于1时以x带入方程取点,大于1时代入z保证不会漏掉格子的检测if(Mathf.Abs(gradient) <= 1){int beginLoop = Mathf.Min(startX, endX), endLoop = Mathf.Max(startX, endX);for(int i = beginLoop + 1; i < endLoop; ++i){int x = i, z = Mathf.FloorToInt(gradient * (i - start.x) + start.z);if(!Walkable(grids, x, z)){UnityEngine.Debug.DrawLine(start, new Vector3(x, 0, z), Color.blue, 180);return false;}UnityEngine.Debug.DrawLine(start, new Vector3(x, 0, z), Color.yellow, 180);}}else{int beginLoop = Mathf.Min(startZ, endZ), endLoop = Mathf.Max(startZ, endZ);for(int i = beginLoop + 1; i < endLoop; ++i){int x = (gradient != float.MaxValue) ? Mathf.FloorToInt((i - start.z) / gradient + start.x) : startX;int z = i;if(!Walkable(grids, x, z)){UnityEngine.Debug.DrawLine(start, new Vector3(x, 0, z), Color.blue, 180);return false;}UnityEngine.Debug.DrawLine(start, new Vector3(x, 0, z), Color.yellow, 180);}}return true;
}

关于路径平滑这部分可以查看的资料

取整的处理

public static List<Vector3> FindPath(bool[,] grids, Vector3 start, Vector3 end)
//为了使角色不在寻路时走“回头路”,我们需要在取网格时根据方向而不是直接floor
//简单说应该朝着靠近的方向取整
int startX = Mathf.FloorToInt(start.x);
int endX = Mathf.FloorToInt(end.x);
int startZ = Mathf.FloorToInt(start.z);
int endZ = Mathf.FloorToInt(end.z);
if(end.x > start.x) startX++;
else if(end.x < start.x) endX++;
if(end.z > start.z) startZ++;
else if(end.z < start.z) endZ++;

总结

这部分是一个工具,是整个游戏中最重要的基础部分之一,它将会在之后直接支持我们的人物鼠标点击移动,人物拾取物品和攻击敌人,以及怪物的AI逻辑中涉及到移动的部分,它的效果将会在之后看到

3D沙盒游戏开发日志4——网格寻路系统相关推荐

  1. 3D沙盒游戏开发日志12——Prefab和GameComponent

    日志 本篇将详细讲解第10篇日志中的Prefab与GameComponent部分(一定要先看第10篇) 如果说上一篇决策层的Brain和EventController是适用于其它生物和人物的话,那么本 ...

  2. 3D 沙盒游戏之地面网格设计

    背景 最近小组在探索研发一个 3D 的沙盒小游戏 demo.对于沙盒游戏来说,地面是必不可少的元素.为了降低难度,在这个 demo 中,地面将不涉及 y 轴坐标的变化,也就是使用一个与 xOz 平面平 ...

  3. 【Unity】MineCraft我的世界沙盒游戏开发流程

    一.插件介绍(必看) 核心插件:Uniblocks Voxel Terrain v1.4.1 将插件导入工程后,Uniblocks文件夹是关键,在文件夹中有材质.贴图.预制体.核心脚本等等. 1.&q ...

  4. 基于Unity3D的体素沙盒游戏设计与实现(上)

    基于Unity3D的体素沙盒游戏设计与实现 摘    要 随着计算机硬件和软件技术的逐步发展,世界游戏开发行业也在日益壮大,涌现出不少优秀的作品,逐渐成为各国文化创意领域一张闪亮的名片.本文以全球知名 ...

  5. 阐述沙盒游戏的历史和理论

    来自 GameRes:http://www.gameres.com/msg_233466.html "沙盒"现在是游戏圈中的热词.与"自由"或"爱&q ...

  6. linux沙盒游戏,沙盒游戏_PE沙盒游戏合集,欢迎⊙ω⊙_安卓应用游戏下载- AppChina应用汇...

    中文名:我的世界 原版名称:Minecraft 其他名称:麦块.MC.当个创世神 游戏类型:沙盒.生存.冒险 游戏平台:Windows.Linux.OS X,Android(Pocket Editio ...

  7. 游戏场景建模需要会美术画画吗?来看沙盒游戏是怎么做出来的

    做游戏场景设计可以不用画画吗? 游戏场景是怎么制作的?尤其是沙盒游戏,开放的世界对场景设计的要求更为严苛.场景制作当然需要美术做模型,但这不是问题的根本.从以下几个方面来说: 游戏标准尺寸 地形 路网 ...

  8. 泰拉瑞亚试图加载不正确的_盘点那些著名的沙盒游戏?泰拉瑞亚堪称2D沙盒之王...

    沙盒游戏是指那些自由性极高,可以自由创造的游戏,这一点就是他的个性,而正因为这点,沙盒类型的游戏受到了无数玩家的追捧,从而导致沙盒游戏越来越多,那么在如此多的沙盒游戏中又有哪些沙盒游戏是非常有名的呢? ...

  9. 这个沙盒游戏建立在数字时代,你能通关吗?

    从没有一个时代像数字时代一样拥有如此多的"连接".在沙盒游戏中,玩家用"连接"创造和改变世界:而身处数字时代就如同在一个巨大的沙盒游戏中,组织用"连接 ...

  10. 电脑鼠标失控自己乱点_在这款沙盒游戏里,你只需要乱点鼠标就能成为建筑艺术家...

    终于看到这么友好的建造类游戏了 我见过不少号称"自由度高.元素丰富且能够让玩家自由发挥"的沙盒游戏,其中最典型的例子,当然要属<我的世界>. 过去我社也常会报道一些例如 ...

最新文章

  1. Tensorflow C++ 编译和调用图模型
  2. python 背景建模高斯混合模型
  3. DHTMLX-Tabbar
  4. Spring创建对象的三种方式以及创建时间
  5. postgresql中装gis插件_Postgresql 空间扩展需要Postgis插件
  6. iOS之深入定制基于PLeakSniffer和MLeaksFinder的内存泄漏检测工具
  7. 从拉格朗日乘数法到KKT条件
  8. mybatisplus 一次性执行多条SQL语句插入(Mysql篇)
  9. python自学要多久-怎么自学python,大概要多久?
  10. 【leetcode】遍历二叉树从跟到叶子的核心代码
  11. 关于Xshell的使用和网络攻防原理
  12. 网络舆情监测与分析研判工作如何高效做好的解决方案
  13. python访问局域网下共享文件夹
  14. 计算机科学个人陈述中文,计算机专业个人陈述二十(计算机科学)
  15. 【宝塔面板】紧急安全更新通知
  16. mysql存emoji_如何在MySQL中存储emoji?
  17. 操作系统原理_田丽华(4)线程
  18. Spring Batch之读数据—读JSON文件(二十八)
  19. Gtk+/Gtkmm介绍与安装
  20. assertcontains php,PHP PHPUnit assertNotContainsOnly()用法及代码示例

热门文章

  1. 【Pytorch】pack_padded_sequence与pad_packed_sequence实战详解
  2. Visio方向键无法移动对象的解决办法[笔记本版]
  3. 来自《星际迷航》的灵感启发
  4. 泛零售数据中台建设之灵魂问答 | 奇点云CEO行在直播回顾
  5. C++ requires a type specifier for all declarations
  6. CTF unserialize3
  7. MAVEN工具篇——maven打包跳过测试
  8. 给程序进行简单的加壳
  9. 常用git命令指南总结
  10. 脑机接口专栏 | 如何分析静息状态的fMRI数据?(二)