【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粒子还是做好场景划分,可视才创建内容。

### Unity Tilemap 使用教程 #### 创建 Tilemap 和 Grid 组件 为了使用 Tilemap 功能,在场景中创建一个新的 GameObject 并添加 `Grid` 组件。这一步骤会自动为对象配置好网格结构,以便后续放置瓷砖[^1]。 ```csharp // 示例代码:通过脚本动态创建带有Grid组件的游戏物体 GameObject gridObject = new GameObject("MyGrid"); gridObject.AddComponent<Grid>(); ``` #### 配置 Tilemap 属性 在完成上述操作之后,向同一 GameObject 添加 `Tilemap` 组件来定义具体的地图属性。此时可以在 Inspector 中调整诸如 `Sort Order`, `Mode`, `Detect Chunk Culling Bounds` 等参数以优化渲染效果和性能表现[^2]。 - **Sort Order**: 控制瓦片地图开始渲染的位置方向。 - **Mode (渲染模式)**: 推荐采用 `Chunk` 方式来进行分组批处理渲染从而提高效率;而 `Individual` 则会使每一个瓦片独立绘制,通常不建议这样做除非有特殊需求。 对于更高级别的控制选项如 `Mask Interaction` 或者关联到 Sprite Mask 的设置,则允许开发者指定哪些部分应该被显示出来以及如何与其他UI元素互动. #### 设置材质与图层信息 给定合适的 `Material` 来决定最终视觉呈现样式,并利用 `Sorting Layer` 及其内部排序值 (`Order In Layer`) 实现多层图像间的相对位置关系管理. #### 添加碰撞体 为了让色能够感知并响应地形变化,还需要为目标 Tilemap 对象附加一个名为 `TileMapCollider2D` 的组件。它负责生成基于当前贴图布局的物理边界框,使得其他具有刚体的色可以通过触发事件等方式实现交互逻辑.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值