[Unity] 在Unity中实现小地图(Minimap)
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Unity] 在Unity中实现小地图(Minimap)相关的知识,希望对你有一定的参考价值。
参考技术A 众所周知,小地图(或雷达)是用于显示周围环境信息的。首先,小地图是以主角为中心的。其次,小地图上应该用图标来代替真实的人物模型,因为小地图通常很小,玩家可能无法看清真实的模型。大多数小地图都是圆形的,所以本文也将创建圆形小地图。通常小地图周围还会有一些按钮和标签,本文也会介绍。
新建场景,导入Unity Chan模型作为玩家,导入两个机器人作为敌人。
添加一个新的相机。依次点击菜单项GameObject -> Camera新建相机并命名为Minimap Camera。然后将该相机设为Unity Chan的子对象,并将其坐标设为Unity Chan上方10个单位,把相机对准Unity Chan模型。
为了达到更好的效果,将position设为(0, 10, 0),rotation设为(90, 0, 0)。现在相机显示效果如下:
但这还不是小地图。现在运行场景,你可以看到只有上图中的内容显示出来。我们必须把小地图做成一个UI元素。
这里需要用到Render Texture来实现。依次点击菜单项Assets -> Create -> Render Texture新建Render Texture并命名为Minimap Render Texture。
选中Minimap Camera后在检视面板将Target Texture字段设为Minimap Render Texture。
现在运行场景会发现Minimap Camera中的内容不见了,这是因为它被显示到了新建的Render Texture中。
下面新建Canvas来添加UI元素。依次点击菜单项GameObject -> UI -> Canvas来新建Canvas。
这里需要使用Raw Image在Canvas中显示Render Texture的内容。依次点击菜单项GameObject -> UI -> Raw Image新建Raw Image,然后命名为Minimap Image,在检视面板中将Texture字段设为Minimap Render Texture。
现在Minimap Camera相机中的内容可以作为UI来显示了!
下面将小地图变为圆形。这里需要用到一张简单的遮罩纹理:
新建Image并为其添加Mask组件,将Image的Source Image字段设为上面的遮罩图片,并将Minimap Image设为Mask的子对象。
注意:为了达到更好的视觉效果,记得禁用遮罩纹理的Mipmap。
现在小地图显示效果如下:
小地图的白色背景看起来不太美观,给它加一个边框:
为了让整个小地图移动起来更方便,新建一个空的GameObject命名为Minimap,并将所有对象设为Minimap子对象。
最后将小地图移至屏幕右上角。
效果不错吧?但这还不是真正意义上的小地图,只是相机从顶部取景的图像而已。接下来通过Layer来做一些设置!
这里需要新建一个Layer。依次点击菜单项Edit -> Project Settings -> Tags and Layers新建Layer命名为Minimap。
然后新建三个球体。一个设为蓝色代表Unity Chan。将该球体设为Unity Chan的子对象,并将其Layer设为Minimap。
对两个机器人进行同样的操作,将球体改为红色。
现在最关键的一步来了!选中Main Camera并确保其Culling Mask中不包括Minimap这一层。
然后选中Minimap Camera让其Culling Mask只包括Minimap这一层。
现在这个小地图看起来就比较完善了!
还可以做一些调整。首先将Minimap Camera的Clear Flags设为Solid Color并将其颜色改为浅灰色,以便让小地图背景与小球的对比度更强。
还可以添加一些UI元素来操作小地图。这里使用标签作为示例,最后结果如下:
当角色或机器人移动时,小地图上的小球也会即时更新位置。
到此整个制作小地图的教程就结束了,如有任何问题,欢迎来下方评论区留言!
如何在Unity中实现AStar寻路算法及地图编辑器
文章目录
AStar算法
简介
Unity中提供了NavMesh
导航寻路的AI功能,如果项目不涉及服务端它应该能满足大部分需求,但如果涉及服务端且使用状态同步技术,可能需要服务端同时实现寻路功能,这时就需要考虑其它实现思路,而AStar寻路算法
则是常使用的一种。
AStar算法是一种静态路网中求解最短路径最有效的直接搜索方法,基于广度优先搜索(BFS)
和Dijkstra
算法,通过不断维护节点的代价来寻求代价最小的路径,代价的估价公式:F(N)=G(N) + H(N)
。
- G:理解为起始节点到当前节点的代价;
- H:理解为当前节点到终节点的代价。
其它概念:
- 开放集合:记录所有被考虑用来寻找最短路径的节点集合;
- 封闭集合:记录不会被考虑用来寻找最短路径的节点集合。
算法思路:
- 将起始节点放入开放集合;
- While循环重复以下步骤,直到结束条件满足:
- 在开放集合中寻找代价最小的节点,并把寻找到的节点作为Current当前节点;
- 将获取到的当前节点从开放集合移除放入封闭集合;
- 若当前节点已经是终节点,寻路结束,跳出While循环,否则继续执行以下操作;
- 获取当前节点的邻节点,并对每个邻节点执行以下步骤:
- 若邻节点为不可行走区域(障碍)或者邻节点已经在封闭集合中,不执行任何操作,Continue继续遍历下一个邻节点;
- 若邻节点不在开放集合中,将其放入开放集合,并将Current当前节点赋值给该邻节点的父节点,计算、记录该邻节点的G、H代价;
- 若邻节点在开放集合中,判断经Current当前节点到达该邻节点的G值是否小于原来的G值,若小于则将该邻节点的父节点设为当前节点,并重新计算该邻节点的G、H代价。
- 从终节点开始依次获取父节点放入一个列表,最终将列表做倒序操作就是最终寻路的路径。
实现
Node节点
地图网格由x * y个Node节点组成,定义节点类,变量包含节点的x、y索引值、父节点信息、G、H、F代价值以及是否为可行走区域的标识信息,代码如下:
namespace SK.Framework.AStar
public class Node
public int x;
public int y;
/// <summary>
/// 父节点
/// </summary>
public Node parent;
/// <summary>
/// 是否为可行走区域
/// </summary>
public bool IsWalkable get; private set;
/// <summary>
/// 起始节点到当前节点的代价
/// </summary>
public int gCost;
/// <summary>
/// 当前节点到终节点的代价
/// </summary>
public int hCost;
/// <summary>
/// 代价
/// </summary>
public int Cost get return gCost + hCost;
public Node(int x, int y, bool isWalkable)
this.x = x;
this.y = y;
IsWalkable = isWalkable;
节点间的估价
每向正上、下、左右方向走一步代价为1,根据勾股定理,每向斜方向走一步代价为
2
\\sqrt2
2,近似1.414,而为了便于计算、节省性能,我们将正方向移动一步的代价记为10,斜方向移动一步的代价记为14,都取int
整数。
//计算两节点之间的代价
private int CalculateCost(Node n1, Node n2)
//绝对值
int deltaX = n1.x - n2.x;
if (deltaX < 0) deltaX = -deltaX;
int deltaY = n1.y - n2.y;
if (deltaY < 0) deltaY = -deltaY;
int delta = deltaX - deltaY;
if (delta < 0) delta = -delta;
//每向正上、下、左、右方向走一步代价增加10
//每斜向走一步代价增加14(勾股定理,精确来说是近似14.14~)
return 14 * (deltaX > deltaY ? deltaY : deltaX) + 10 * delta;
算法核心
/// <summary>
/// 根据起始节点和终节点获取路径
/// </summary>
/// <param name="startNode">起始节点</param>
/// <param name="endNode">终节点</param>
/// <returns>路径节点集合</returns>
public List<Node> GetPath(Node startNode, Node endNode)
//开放集合
List<Node> openCollection = new List<Node>();
//封闭集合
HashSet<Node> closeCollection = new HashSet<Node>();
//起始节点放入开放集合
openCollection.Add(startNode);
//开放集合中数量为0时 寻路结束
while (openCollection.Count > 0)
//当前节点
Node currentNode = openCollection[0];
//遍历查找是否有代价更小的节点
//若代价相同,选择移动到终点代价更小的节点
for (int i = 1; i < openCollection.Count; i++)
currentNode = (currentNode.Cost > openCollection[i].Cost
|| (currentNode.Cost == openCollection[i].Cost
&& currentNode.hCost > openCollection[i].hCost))
? openCollection[i] : currentNode;
//将获取到的当前节点从开放集合移除放入封闭集合
openCollection.Remove(currentNode);
closeCollection.Add(currentNode);
//当前节点已经是终节点 寻路结束
if (currentNode == endNode)
break;
//获取邻节点
List<Node> neighbourNodes = GetNeighbouringNodes(currentNode, SearchMode.Link8);
//在当前节点向邻节点继续搜索
for (int i = 0; i < neighbourNodes.Count; i++)
Node neighbourNode = neighbourNodes[i];
//判断邻节点是否为不可行走区域(障碍)或者邻节点已经在封闭集合中
if (!neighbourNode.IsWalkable || closeCollection.Contains(neighbourNode))
continue;
//经当前节点到达该邻节点的G值是否小于原来的G值
//或者该邻节点还没有放入开放集合,将其放入开放集合
int cost = currentNode.gCost + CalculateCost(currentNode, neighbourNode);
if (cost < neighbourNode.gCost || !openCollection.Contains(neighbourNode))
neighbourNode.gCost = cost;
neighbourNode.hCost = CalculateCost(neighbourNode, endNode);
neighbourNode.parent = currentNode;
if (!openCollection.Contains(neighbourNode))
openCollection.Add(neighbourNode);
//倒序获取父节点
List<Node> path = new List<Node>();
Node currNode = endNode;
while (currNode != startNode)
path.Add(currNode);
currNode = currNode.parent;
//再次倒序后得到完整路径
path.Reverse();
return path;
邻节点的搜索方式
搜索邻节点时有两种搜索方式,四连通和八连通:
- 四连通:又称四邻域,是指对应节点的上、下、左、右四个方向为邻节点:
- 八连通:又称八邻域,是指对应节点的上、下、左、右、左上、右上、左下、右下八个方向为邻节点:
/// <summary>
/// 获取指定节点的邻节点
/// </summary>
/// <param name="node">指定节点</param>
/// <param name="searchMode">搜索方式 四连通/八连通</param>
/// <returns>邻节点列表</returns>
public List<Node> GetNeighbouringNodes(Node node, SearchMode searchMode)
List<Node> neighbours = new List<Node>();
switch (searchMode)
case SearchMode.Link4:
for (int i = -1; i <= 1; i++)
if (i == 0) continue;
int x = node.x + i;
if (x >= 0 && x < this.x)
neighbours.Add(nodesDic[x * this.x + node.y]);
int y = node.y + i;
if (y >= 0 && y < this.y)
neighbours.Add(nodesDic[node.x * this.x + y]);
break;
case SearchMode.Link8:
for (int i = -1; i <= 1; i++)
for (int j = -1; j <= 1; j++)
if (i == 0 && j == 0) continue;
int x = node.x + i;
int y = node.y + j;
if (x >= 0 && x < this.x && y >= 0 && y < this.y)
neighbours.Add(nodesDic[x * this.x + y]);
break;
return neighbours;
地图编辑器
简介
按住Ctrl + 鼠标左键
绘制地图障碍区域(如图所示,红色框区域即为障碍区域):
按住Alt + 鼠标左键
绘制地图可行走区域(清除障碍区域):
实现
绘制地图网格
Grid X、Y
组成地图网格(x * y);Grid Size
指定每个网格(节点)的大小:
//绘制地图网格
Handles.color = Color.cyan;
for (int i = 0; i <= x; i++)
Vector3 start = i * size * Vector3.right;
Vector3 end = start + y * size * Vector3.forward;
Handles.DrawLine(start, end);
for (int i = 0; i <= y; i++)
Vector3 start = i * size * Vector3.forward;
Vector3 end = start + x * size * Vector3.right;
Handles.DrawLine(start, end);
障碍/可行走区域
使用二维数组bool[,] map
存储各节点网格是否为可行走区域
Ctrl + 鼠标左键
标识障碍区域;Alt + 鼠标左键
标识可行走区域:
HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive));
//Ctrl + 鼠标左键 绘制障碍区域
//Alt + 鼠标左键 绘制可行走区域
var e = Event.current;
if (e != null && (e.control || e.alt) && (e.type == EventType.MouseDown || e.type == EventType.MouseDrag) && e.button == 0)
Ray ray = HandleUtility.GUIPointToWorldRay(e.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit))
int targetX = Mathf.CeilToInt(hit.point.x / size);
int targetY = Mathf.CeilToInt(hit.point.z / size);
if (targetX <= x && targetX > 0 && targetY <= y && targetY > 0)
map[targetX - 1, targetY - 1] = !e.control;
e.Use();
//红色框绘制障碍区域
Handles.color = Color.red;
for (int m = 0; m < x; m++)
for (int n = 0; n < y; n++)
if (!map[m, n])
Handles.DrawWireCube(new Vector3(m * size, 0f, n * size) + .5f * size * (Vector3.forward + Vector3.right), .9f * size * (Vector3.forward + Vector3.right));
地图数据存储
由于地图数据存储于bool[,] map
二维数组中,不支持序列化,因此将其转化为存储于Texture2D
类型资产中,实现方式如下:
//生成地图
if (GUILayout.Button("Generate Map Data"))
//选择保存路径
string filePath = EditorUtility.SaveFilePanel("Save Map Data", Application.dataPath, "New Map Data", "asset");
if (!string.IsNullOrEmpty(filePath))
//转化为Asset路径
filePath = filePath.Substring(filePath.IndexOf("Assets"));
//创建地图Tex
Texture2D bitmap = new Texture2D(x, y, TextureFormat.Alpha8, false);
byte[] bytes = bitmap.GetRawTextureData();
//默认全部为可行走区域
for (int i = 0; i < bytes.Length; i++)
bytes[i] = 0;
for (int m = 0; m < x; m++)
for (int n = 0; n < y; n++)
//黑色存储障碍区域 白色存储可行走区域
bytes[m * x + n] = (byte)(map[m, n] ? 255 : 0);
bitmap.LoadRawTextureData(bytes);
//创建、保存资产
AssetDatabase.CreateAsset(bitmap, filePath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
//选中
EditorGUIUtility.PingObject(bitmap);
源码以上传至SKFramework
框架Package Manager
中:
以上是关于[Unity] 在Unity中实现小地图(Minimap)的主要内容,如果未能解决你的问题,请参考以下文章
游戏开发环境Unity使用Mac电脑开发,开发环境的搭建(Mac mini M1 | VSCode | Git | 好用工具)
Unity中怎么做小地图?unity如何制作游戏中的小地图啊?
游戏开发环境Unity使用Mac电脑开发,开发环境的搭建(Mac mini M1 | VSCode | Git | 好用工具)