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

【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);
    }
}

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

相关文章:

  • 代码随想录算法训练营第三十八天-动态规划-完全背包-279.完全平方数
  • Flutter_学习记录_基本组件的使用记录
  • ZZNUOJ(C/C++)基础练习1011——1020(详解版)
  • Spring事务和事务传播机制
  • 使用 Redis List 和 Pub/Sub 实现简单的消息队列
  • WS2812 梳理和颜色表示方法的对比:RGB和HSV
  • 忘记宝塔的访问地址怎么找
  • 【教学类-89-02】20250128新年篇02——姓名藏头对联(星火讯飞+Python,五言对联,有横批)
  • 项目测试之MockMvc
  • 【数据结构与算法】九大排序算法实现详解
  • 中科大:LLM检索偏好优化应对RAG知识冲突
  • 面向对象设计原则 - SOLID原则 (基于C++)
  • [Dialog屏幕开发] 设置方式对话框
  • 使用eNSP配置GRE VPN实验
  • 基于51单片机和ESP8266(01S)、8X8点阵屏的二进制WiFi时钟
  • 什么是循环神经网络?
  • python.tkinter设计标记语言(渲染7-动态呈现标签) - 副本
  • 1.2第1章DC/DC变换器的动态建模-1.2Buck-Boost 变换器的交流模型--电力电子系统建模及控制 (徐德鸿)--读书笔记
  • game101 环节搭建 windows 平台 vs2022
  • doris:STRUCT
  • 【阅读笔记】New Edge Diected Interpolation,NEDI算法,待续
  • 跨域问题解释及前后端解决方案(SpringBoot)
  • 接口技术-第2次作业
  • Gradle配置指南:深入解析settings.gradle.kts(Kotlin DSL版)
  • AAAI2024论文合集解读|Multi-dimensional Fair Federated Learning-water-merged
  • IBMSamllPower服务器监控指标解读