【Unity3D】实现2D小地图效果
目录
一、玩家脚本Player
二、Canvas组件设置
三、小地图相关
四、GameLogicMap脚本修改
基于:【Unity3D】Tilemap俯视角像素游戏案例-CSDN博客
2D玩家添加Dotween移动DOPath效果,移动完成后进行刷新小地图(小地图会顺便刷新大地图)
一、玩家脚本Player
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;
public class Player : MonoBehaviour
{
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Vector2 pos = Input.mousePosition;
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit2D hit = Physics2D.Raycast(ray.origin, ray.direction);
if (hit.collider != null)
{
Vector2 hitPos = hit.point;
Vector3Int v3Int = new Vector3Int(Mathf.FloorToInt(hitPos.x), Mathf.FloorToInt(hitPos.y), 0);
List<Vector3Int> pathPointList = GameLogicMap.Instance.PlayAstar(v3Int);
Vector3[] pathArray = new Vector3[pathPointList.Count];
int offset = pathPointList.Count - 1;
for (int i = pathPointList.Count - 1; i >= 0; i--)
{
Vector3Int pointPos = pathPointList[i];
Vector3 worldPos = GameLogicMap.Instance.GetWorldPos(pointPos);
pathArray[offset - i] = worldPos;
}
transform.DOPath(pathArray, 1f).OnComplete(() =>
{
Debug.Log("移动完成 更新小地图");
GameLogicMap.Instance.DrawSmallMap();
}).SetAutoKill(true);
}
}
}
public Vector3Int GetPos()
{
Vector3 pos = transform.position;
return new Vector3Int(Mathf.FloorToInt(pos.x - 0.5f), Mathf.FloorToInt(pos.y - 0.5f), 0);
}
}
二、Canvas组件设置
三、小地图相关
原理:利用2D游戏为了实现寻路而创建的二维数组去生成一张Texture2D纹理图,大地图是直接用int[,]map二维数组去创建,map[x,y]等于1是空地,不等于1是障碍物或不可穿越地形;
大地图上的玩家绿点是直接拿到玩家在map的坐标点直接绘制。
小地图是以玩家为中心的[-smallSize/2, smallSize/2]范围内进行绘制;小地图上的玩家绿点是直接绘制到中心点(smallSize.x/2, smallSize.y/2);
注意:绘制到Texture2D的像素点坐标是[0, size]范围的,则小地图的绘制是SetPixel(i, j, color),传递i, j是[0,size]范围的,而不要传递x, y,这个x,y的取值是以玩家点为中心的[-size/2, size/2]范围坐标值,如下取法:
x = 玩家点.x - size.x/2 + i;
y = 玩家点.y - size.y/2 + j;
用2层for遍历来看的话就是从(玩家点.x - size.x/2, 玩家点.y - size.y/2)坐标点,从下往上,从左往右依次遍历每个map[x,y]点生成对应颜色的像素点,构成一张Texture2D图片。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class SmallMapFor2D : MonoBehaviour
{
public Image smallImage;
public Vector2Int smallSize;
private Vector2Int lastSmallSize;
Texture2D smallImageTexture2D;
public Image bigImage;
Vector2Int bigSize;
Texture2D bigImageTexture2D;
private Vector3Int playerPoint;
//环境二维数组地图[静态] 若是动态需改为自定义类数组,此时是int值类型数组
private int[,] map;
//初始化大地图整体
public void InitBigMap(int[,] map, Vector2Int size)
{
//已初始化则直接退出
if (bigImageTexture2D != null)
return;
this.map = map;
this.bigSize = size;
bigImageTexture2D = new Texture2D(bigSize.x, bigSize.y);
bigImageTexture2D.filterMode = FilterMode.Point;
DrawBigMap();
}
public void DrawBigMap()
{
int mapWidth = map.GetLength(0);
int mapHeight = map.GetLength(1);
for (int i = 0; i < bigSize.x; i++)
{
for (int j = 0; j < bigSize.y; j++)
{
//判断是否在map范围内
if (i < mapWidth && j < mapHeight)
{
if (map[i, j] == 1) //空地
{
bigImageTexture2D.SetPixel(i, j, new Color(0.6f, 0.6f, 0.6f, 0.5f));
}
else //障碍物
{
bigImageTexture2D.SetPixel(i, j, new Color(0.3f, 0.3f, 0.3f, 0.5f));
}
}
else
{
//非map范围内
bigImageTexture2D.SetPixel(i, j, new Color(0.1f, 0.1f, 0.1f, 0.8f));
}
}
}
if (playerPoint != null)
{
//更新大地图玩家点
bigImageTexture2D.SetPixel(playerPoint.x, playerPoint.y, Color.green);
}
bigImageTexture2D.Apply();
//bigImage.material.SetTexture("_MainTex", bigImageTexture2D);
bigImage.material.mainTexture = bigImageTexture2D;
bigImage.SetMaterialDirty();
}
//动态绘制小地图 时机:玩家移动完成后
public void DrawSmallMap(Vector3Int playerPoint)
{
//中途可换小地图大小
if (this.lastSmallSize != null && this.lastSmallSize.x != smallSize.x && this.lastSmallSize.y != smallSize.y)
{
if (smallImageTexture2D != null)
{
Destroy(smallImageTexture2D);
smallImageTexture2D = null;
}
smallImageTexture2D = new Texture2D(smallSize.x, smallSize.y);
smallImageTexture2D.filterMode = FilterMode.Point;
}
this.lastSmallSize = smallSize;
this.playerPoint = playerPoint;
int mapWidth = map.GetLength(0);
int mapHeight = map.GetLength(1);
for (int i = 0; i < smallSize.x; i++)
{
for (int j = 0; j < smallSize.y; j++)
{
//中心点是人物点 故绘制点为如下
int x = playerPoint.x - smallSize.x / 2 + i;
int y = playerPoint.y - smallSize.y / 2 + j;
//判断是否在map范围内
if (x >= 0 && x < mapWidth && y >= 0 && y < mapHeight)
{
if (map[x, y] == 1) //空地
{
smallImageTexture2D.SetPixel(i, j, new Color(0.6f, 0.6f, 0.6f, 0.5f));
}
else //障碍物
{
smallImageTexture2D.SetPixel(i, j, new Color(0.3f, 0.3f, 0.3f, 0.5f));
}
}
else
{
//非map范围内
smallImageTexture2D.SetPixel(i, j, new Color(0.8f, 0f, 0f, 0.8f));
}
}
}
smallImageTexture2D.SetPixel(smallSize.x / 2, smallSize.y / 2, Color.green);
smallImageTexture2D.Apply();
//smallImage.material.SetTexture("_MainTex", smallImageTexture2D);
smallImage.material.mainTexture = smallImageTexture2D;
smallImage.SetMaterialDirty();
//更新大地图 因为玩家位置变化
//(可优化 不需要重新整张地图再绘制 只更新上一个玩家点和新玩家点,只是要保存的大地图原始数据、旧玩家点更新即可)
DrawBigMap();
}
}
小地图2张Image图片的材质球要使用不同的材质球实例
四、GameLogicMap脚本修改
主要变化新增和修改:
初始化绘制大地图和小地图
smallMapFor2D.InitBigMap(map, new Vector2Int(map.GetLength(0), map.GetLength(1)));
DrawSmallMap();
A*寻路方法返回路径数组,这是一个从终点到起点的数组,所以使用时是倒序遍历的。
public List<Vector3Int> PlayAstar(Vector3Int endPos)
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Tilemaps;
using UnityEngine.UI;
public class GameLogicMap : MonoBehaviour
{
public static GameLogicMap _instance;
public static GameLogicMap Instance
{
get { return _instance; }
}
public Grid terrainGrid;
private Tilemap terrainTilemap;
public Grid buildGrid;
private Tilemap buildTilemap;
public Player player;
public int[,] map;
private Vector3Int mapOffset;
public SmallMapFor2D smallMapFor2D;
private const int ConstZ = 0;
public class Point
{
public Vector3Int pos;
public Point parent;
public float F { get { return G + H; } } //F = G + H
public float G; //G = parent.G + Distance(parent,self)
public float H; //H = Distance(self, end)
public string GetString()
{
return "pos:" + pos + ",F:" + F + ",G:" + G + ",H:" + H + "\n";
}
}
private List<Point> openList = new List<Point>();
private List<Point> closeList = new List<Point>();
public LineRenderer lineRenderer;
private void Awake()
{
_instance = this;
}
void Start()
{
terrainTilemap = terrainGrid.transform.Find("Tilemap").GetComponent<Tilemap>();
buildTilemap = buildGrid.transform.Find("Tilemap").GetComponent<Tilemap>();
BoundsInt terrainBound = terrainTilemap.cellBounds;
BoundsInt buildBound = buildTilemap.cellBounds;
map = new int[terrainBound.size.x, terrainBound.size.y];
mapOffset = new Vector3Int(-terrainBound.xMin, -terrainBound.yMin, 0);
Debug.Log("mapOffset:" + mapOffset);
foreach (var pos in terrainBound.allPositionsWithin)
{
var sprite = terrainTilemap.GetSprite(pos);
if (sprite != null)
{
SetMapValue(pos.x, pos.y, 1); //空地1
}
}
foreach (var pos in buildBound.allPositionsWithin)
{
var sprite = buildTilemap.GetSprite(pos);
if (sprite != null)
{
SetMapValue(pos.x, pos.y, 2); //障碍2
}
}
smallMapFor2D.InitBigMap(map, new Vector2Int(map.GetLength(0), map.GetLength(1)));
DrawSmallMap();
}
//绘制小地图
public void DrawSmallMap()
{
//传递玩家在Map的坐标
smallMapFor2D.DrawSmallMap(ToMapPos(player.GetPos()));
}
private void SetMapValue(int x, int y, int value)
{
map[x + mapOffset.x, y + mapOffset.y] = value;
}
private Vector3Int ToMapPos(Vector3Int pos)
{
return pos + mapOffset;
}
public List<Vector3Int> PlayAstar(Vector3Int endPos)
{
endPos = ToMapPos(endPos);
Debug.Log(endPos);
openList.Clear();
closeList.Clear();
Vector3Int playerPos = player.GetPos();
playerPos = ToMapPos(playerPos);
openList.Add(new Point()
{
G = 0f,
H = GetC(playerPos, endPos),
parent = null,
pos = playerPos,
});
List<Vector3Int> resultList = CalculateAstar(endPos);
if (resultList != null)
{
lineRenderer.positionCount = resultList.Count;
for (int i = 0; i < resultList.Count; i++)
{
Vector3Int pos = resultList[i];
lineRenderer.SetPosition(i, GetWorldPos(pos));
}
}
else
{
Debug.LogError("寻路失败;");
}
return resultList;
}
public Vector3 GetWorldPos(Vector3Int pos)
{
pos.x = pos.x - mapOffset.x;
pos.y = pos.y - mapOffset.y;
return terrainTilemap.GetCellCenterWorld(pos);
}
private List<Vector3Int> CalculateAstar(Vector3Int endPos)
{
int cnt = 0;
while (true)
{
//存在父节点说明已经结束
if (openList.Exists(x => x.pos.Equals(endPos)))
{
Debug.Log("找到父节点~" + endPos + ",迭代次数:" + cnt);
List<Vector3Int> resultList = new List<Vector3Int>();
Point endPoint = openList.Find(x => x.pos.Equals(endPos));
resultList.Add(endPoint.pos);
Point parent = endPoint.parent;
while (parent != null)
{
resultList.Add(parent.pos);
parent = parent.parent;
}
return resultList;
}
cnt++;
if (cnt > 100 * map.GetLength(0) * map.GetLength(1))
{
Debug.LogError(cnt);
return null;
}
//从列表取最小F值的Point开始遍历
Point currentPoint = openList.OrderBy(x => x.F).FirstOrDefault();
string str = "";
foreach (var v in openList)
{
str += v.GetString();
}
Debug.Log("最小F:" + currentPoint.GetString() + "\n" + str);
Vector3Int pos = currentPoint.pos;
for (int i = -1; i <= 1; i++)
{
for (int j = -1; j <= 1; j++)
{
if (i == 0 && j == 0)
{
continue;
}
//过滤越界、墙体(map[x,y]不等于1)、已处理节点(存在闭合列表的节点)
Vector3Int tempPos = new Vector3Int(i + pos.x, j + pos.y, ConstZ);
if (tempPos.x < 0 || tempPos.x >= map.GetLength(0) || tempPos.y < 0 || tempPos.y >= map.GetLength(1)
|| map[tempPos.x, tempPos.y] != 1
|| closeList.Exists(x => x.pos.Equals(tempPos)))
{
continue;
}
//判断tempPos该节点是否已经计算, 在openList的就是已经计算的
Point tempPoint = openList.Find(x => x.pos.Equals(tempPos));
float newG = currentPoint.G + Vector3.Distance(currentPoint.pos, tempPos);
if (tempPoint != null)
{
//H固定不变,因此判断旧的G值和当前计算出的G值,如果当前G值更小,需要改变节点数据的父节点和G值为当前的,否则保持原样
float oldG = tempPoint.G;
if (newG < oldG)
{
tempPoint.G = newG;
tempPoint.parent = currentPoint;
Debug.Log("更新节点:" + tempPoint.pos + ", newG:" + newG + ", oldG:" + oldG + ",parent:" + tempPoint.parent.pos);
}
}
else
{
tempPoint = new Point()
{
G = newG,
H = GetC(tempPos, endPos),
pos = tempPos,
parent = currentPoint
};
Debug.Log("新加入节点:" + tempPoint.pos + ", newG:" + newG + ", parent:" + currentPoint.pos);
openList.Add(tempPoint);
}
}
}
//已处理过的当前节点从开启列表移除,并放入关闭列表
openList.Remove(currentPoint);
closeList.Add(currentPoint);
}
}
private float GetC(Vector3Int a, Vector3Int b)
{
return Math.Abs(a.x - b.x) + Math.Abs(a.y - b.y);
}
}