【Unity3D】Tilemap俯视角像素游戏案例
目录
一、导入Tilemap
二、导入像素风素材
三、使用Tilemap制作地图
3.1 制作Tile Palette素材库
3.2 制作地图
四、实现A*寻路
五、待完善
一、导入Tilemap
Unity 2019.4.0f1 已内置Tilemap
需导入2D Sprite、2D Tilemap Editor、以及一个我没法正常搜出的2D Tilemap Extras
GitHub - Unity-Technologies/2d-extras: Fun 2D Stuff that we'd like to share!
2D Tilemap Extras搜索对应Unity版本的下载压缩包并解压到工程Assets下
二、导入像素风素材
Assets · Kenney
三、使用Tilemap制作地图
3.1 制作Tile Palette素材库
首先打开Tile Palette窗口(相当于素材库)
创建一个TilePalette文件
此时你这里是空的,点击Edit,然后去到Project窗口选择所有图片拖追到Tile Palette窗口编辑区域。会生成这些Tile文件
每个tile文件都会有如上信息,图片、颜色、碰撞体类型(Sprite依赖精灵透明度生成碰撞盒、Grid直接生成矩形网格)
至此你就可以开始在Scene场景上用这个资源库去绘制2D地图了,但是为了效率制作有规则的地形,我们可以制作一些Rule Tile规则瓦片来进行加速绘制地形。
自上而下分别是:tile_0025、tile_0012、tile_0014、tile_0036、tile_0038、tile_0026、tile_0024、tile_0037、tile_0013、tile_0039、tile_0040、tile_0041、tile_0042。
这个九宫格红色×和绿色剪头分别代表:空地形、非空地形,如上图则是代表这个左上角的图片,它的出现规则是当左边和上边是空地形,且右边和下边是非空地形时会出现。这里有个小bug,即这组素材没有内边,例如弄一个“回”地形的中空地形会出现问题。
之后,将我们制作好的Rule Tile拖拽到Tile Palette素材库
为了直观化可以弄成3*3样式,如下,点击Edit,再进行如下操作、选中+绘制
类似的灰色的地形也是如此。
3.2 制作地图
摄像机调整,俯视角(正交),控制可视范围,如宽度[-20,20],那么就要设置Size为11.25
即20 * 高宽比(1080/1920)
创建Terrain地形tilemap
需要给Tilemap新增如下3个组件,并设置,其中Composite Collider 2D是合并碰撞盒,并采用几何网格形式合并(默认Outlines 边框碰撞体),必须要使用几何网格形式,因为我们之后要对这个2D碰撞体进行2D射线检测,若是边框碰撞体则无法正常射线检测到,你可以理解边框碰撞体是镂空的碰撞体,它只有边缘的2D线条是碰撞实体。
之后在Scene场景绘制地形即可,如下操作,先打开素材库Tile Palette,再选中画笔后,选择素材库的其中一个素材,例如泥土地形,然后直接去到Scene窗口左键白色描边格子绘制。注意要选中的是我们Rule Tile相关的泥土地形才能生效我们的九宫格规则去创建地形。
此时你会发现若想在地形上创类似树、房子、井盖等其他非地形素材时,会破坏已有地形的。
为此我们需要再创一个Build建筑Tilemap去绘制我们其他的非地形素材,注意这2个tilemap的位置、偏移、锚点啥的要保持一致,这个Tilemap不需要刚体、碰撞体。
需要将Build层级修改比Terrain大(Terrain Order In Layer是0)即可,如下
四、实现A*寻路
参考:【Unity3D】A*寻路(2D究极简单版)_unity2d a星巡路-CSDN博客
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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);
GameLogicMap.Instance.PlayAstar(v3Int);
}
}
}
public Vector3Int GetPos()
{
Vector3 pos = transform.position;
return new Vector3Int(Mathf.FloorToInt(pos.x - 0.5f), Mathf.FloorToInt(pos.y - 0.5f), 0);
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Tilemaps;
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;
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
}
}
//terrainTilemap.getworld
//PlayAstar(new Vector3Int(-8, -6, 0));
}
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 void 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("寻路失败;");
}
}
private 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;
}
//过滤越界、墙体(非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);
}
}
五、待完善
1、未有角色移动部分代码
2、A*寻路点击到的位置如果是障碍物(Build类型地形)那么就会死循环卡死,应该加层判断必须点击到的是非障碍物、可行走地形。
3、其他的游戏细节,例如如何与房子门交互,进门是换场景还是瞬移角色到另一个坐标(推荐是瞬移坐标),摄像机控制,可使用Cinemachine 2D的
例如:3个框代表3个场景,要做好场景划分,性能考虑按道理没有性能开销 都使用一个图集即可,若场景有2D粒子还是做好场景划分,可视才创建内容。