当前位置: 首页 > article >正文

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


http://www.kler.cn/a/522270.html

相关文章:

  • HarmonyOS DevEco Studio模拟器点击运行没有反应的解决方法
  • 《深度剖析Q-learning中的Q值:解锁智能决策的密码》
  • Linux线程安全
  • 算法12(力扣739)-每日温度
  • 一个简单的自适应html5导航模板
  • DeepSeek-R1:开源Top推理模型的实现细节、使用与复现
  • 字节启动AGI长期研究计划,代号Seed Edge
  • Nacos未授权新建用户漏洞(/nacos/v1/auth/users)
  • 深入理解JWT及其应用
  • 简易CPU设计入门:控制总线的剩余信号(一)
  • 粒子群算法 笔记 数学建模
  • 真正理解std::move
  • < OS 有关 > 阿里云 几个小时前 使用密钥替换 SSH 密码认证后, 发现主机正在被“攻击” 分析与应对
  • 关于Dubbo的面试题概念原理配置及代码
  • 【信息系统项目管理师-选择真题】2012上半年综合知识答案和详解
  • 十三先天记
  • 【面试】【前端】前端浏览器考题总结
  • 深度学习|表示学习|卷积神经网络|输出维度公式|15
  • HTML 符号详解
  • 安全策略初始实验
  • 30289_SC65XX功能机MMI开发笔记(ums9117)
  • 深入理解动态规划(dp)--(提前要对dfs有了解)
  • 【OMCI实践】ONT上线过程的omci消息(二)
  • leetcode 209. 长度最小的子数组
  • AI 模型评估与质量控制:生成内容的评估与问题防护
  • Web开发 -前端部分-CSS3新特性