Unity2017新功能Tilemap地图编辑器的数据拓展和动态生成

在我来4399之前的上一家公司,我做了一个2D的对战游戏,地图编辑器的做法是用格子图片的预设一个个拼接成一张地图,每个格子上可以设置该格子的数据,比如图片名字,tile坐标,是否可通过,是否可销毁,是否是陷阱等等,具体做法就是把所有图片的预设都编一个id,然后在当前tile上填上对应的id,最后保存成一个json文件,在游戏里面动态生成。

以你们的想法,你们会觉得这种做法合不合适?反正在我看来,当时的做法确实不怎么好。首先我们需要手动的去制作tile图片的预设,而且没有一个好的可视化界面,只能拖到编辑器对应id上的FileInput,其实这里unity2017.2.0发布Tilemap后,TilePalette很好的解决了我的这个问题。再者,我们用图片的预设拼接成的地图,你们想想会有什么问题?性能!特别是一张超级大的地图,想想都可怕。做地图编辑器的初衷是为了方便策划自己去制作地图,结果反而策划在学习使用上觉得有点不便利了,这让我有点懊恼,想想也怪我。就在我思考怎么去优化地图编辑器的时候,unity2017.2.0发布了,新功能Tilemap的出现让我重新有了完善地图编辑器的想法。只不过我不会用在项目上了,因为我们项目不是用的unity2017的版本,而且我要辞职了。

好了,接下来我们来讲讲我重新做的这个地图编辑器吧。

先看看编辑器的界面,都有哪些功能。


1.基础数据结构。

// **********************************************************************
// Copyright (C) XM
// Author: 吴肖牧
// Date: 2018-02-15
// Desc: 
// **********************************************************************

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

namespace XMtileMap
{
    /// <summary>
    /// 所有地图数据的列表
    /// </summary>
    [CreateAssetMenu]
    public class TileMapSerialize : ScriptableObject
    {
        [SerializeField]
        public List<TileMapDataList> Data = new List<TileMapDataList>();
    }
    
    /// <summary>
    /// 对应地图的数据列表
    /// </summary>
    [Serializable]
    public class TileMapDataList
    {
        public List<TileMapData> tileMapDataList = new List<TileMapData>();
    }

    /// <summary>
    /// 对应地图的Tilemap数据
    /// </summary>
    [Serializable]
    public class TileMapData
    {
        public string TilemapName = "Tilemap";
        public int SortOrderIndex = 0;
        public int SortingLayerIndex = 0;
        public int OrderInLayer = 0;
        public List<TileInfo> tileInfoList;
    }

    /// <summary>
    /// 基础数据
    /// </summary>
    [Serializable]
    public class TileInfo
    {
        public Tile tile;
        public Vector3 pos;
        public Vector3Int ipos;
        // DOTO other data

    }
}

根据项目需求可以自行在TileInfo里面拓展数据结构。

这里需要注意的是,场景上不一定只有一个Tilemap组件,可能我们需要多个层,创建了多个Tilemap,所以数据结构里面我们是用List来保存多个Tilemap的,这里指TileMapData,为了不和自带的Tilemap类混淆了。

2.数据和文件的操作。

// **********************************************************************
// Copyright (C) XM
// Author: 吴肖牧
// Date: 2018-02-15
// Desc: 
// **********************************************************************

using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Tilemaps;

namespace XMtileMap
{
    public class XMMapData
    {
        public static string MapDataPath = "Assets/RefResources/ScriptableObjects/MapData.asset";
        public static string SourceDataPath = "Assets/XMtileMap/Data/SourceData.asset";
        public static string TargetDataPath = "Assets/XMtileMap/Data/TargetData.asset";

        private static TileMapSerialize mapData;
        /// <summary>
        /// 地图数据,游戏使用的数据
        /// </summary>
        public static TileMapSerialize MapData
        {
            get
            {
                if (mapData == null)
                {
                    // TODO runtime LoadData

#if UNITY_EDITOR
                    mapData = UnityEditor.AssetDatabase.LoadAssetAtPath<TileMapSerialize>(MapDataPath);
#endif
                }
                return mapData;
            }
        }

        private static TileMapSerialize sourceData;
        /// <summary>
        /// 地图源数据,保存用
        /// </summary>
        public static TileMapSerialize SourceData
        {
            get
            {
                if (sourceData == null)
                {
                    // TODO runtime LoadData

#if UNITY_EDITOR
                    sourceData = UnityEditor.AssetDatabase.LoadAssetAtPath<TileMapSerialize>(SourceDataPath);
#endif
                }
                return sourceData;
            }
        }

        private static TileMapSerialize targetData;
        /// <summary>
        /// 副本数据,编辑用
        /// </summary>
        public static TileMapSerialize TargetData
        {
            get
            {
                if (targetData == null)
                {
                    // TODO runtime LoadData

#if UNITY_EDITOR
                    targetData = UnityEditor.AssetDatabase.LoadAssetAtPath<TileMapSerialize>(TargetDataPath);
#endif
                }
                return targetData;
            }
        }

        /// <summary>
        /// 地图ID
        /// </summary>
        public static int MapID = 0;

        /// <summary>
        /// A星寻路的地图数据
        /// </summary>
        public static Dictionary<Vector2,Point> map;

        public static Vector2 tileOffset2 = new Vector2(0.5f, 0.5f);
        public static Vector3 tileOffset3 = new Vector3(0.5f, 0.5f, -0.5f);

#if UNITY_EDITOR
        /// <summary>
        /// 载入地图数据
        /// </summary>
        /// <param name="path"></param>
        private static void LoadData(string path)
        {
            TileMapSerialize targetData = UnityEditor.AssetDatabase.LoadAssetAtPath<TileMapSerialize>(path);
            if (targetData == null)
            {
                string newPath = UnityEditor.EditorUtility.SaveFilePanelInProject("Save TileMapSerialize", "New TileMapSerialize", "asset", "Save TileMapSerialize", "Assets");

                if (newPath == "")
                    return;

                UnityEditor.AssetDatabase.CreateAsset(ScriptableObject.CreateInstance<TileMapSerialize>(), newPath);
                targetData = UnityEditor.AssetDatabase.LoadAssetAtPath<TileMapSerialize>(path);
            }
        }

        public static void SaveData(List<TileMapDataList> target, List<TileMapDataList> source)
        {
            target.Clear();
            foreach (var item in source)
            {
                TileMapDataList list = new TileMapDataList();
                list.tileMapDataList = new List<TileMapData>();
                foreach (var item1 in item.tileMapDataList)
                {
                    TileMapData mapdata = new TileMapData();
                    mapdata.OrderInLayer = item1.OrderInLayer;
                    mapdata.SortingLayerIndex = item1.SortingLayerIndex;
                    mapdata.SortOrderIndex = item1.SortOrderIndex;
                    mapdata.TilemapName = item1.TilemapName;
                    mapdata.tileInfoList = new List<TileInfo>();
                    if (item1.tileInfoList == null || item1.tileInfoList.Count == 0)
                    {
                        Debug.LogError(item1.TilemapName + " tileInfoList is null or count is zero");
                    }
                    else
                    {
                        foreach (var item2 in item1.tileInfoList)
                        {
                            TileInfo tile = new TileInfo();
                            tile.ipos = item2.ipos;
                            tile.pos = item2.pos;
                            tile.tile = item2.tile;
                            mapdata.tileInfoList.Add(tile);
                        }
                    }
                    list.tileMapDataList.Add(mapdata);
                }
                target.Add(list);
            }
            if (target == SourceData.Data)
            {
                UnityEditor.EditorUtility.SetDirty(TargetData);
                UnityEditor.EditorUtility.SetDirty(SourceData);
            }
            else if (target == TargetData.Data)
            {
                UnityEditor.EditorUtility.SetDirty(TargetData);
            }
            File.Copy(Application.dataPath + "/XMtileMap/Data/SourceData.asset", Application.dataPath + "/RefResources/ScriptableObjects/MapData.asset",true);
            UnityEditor.AssetDatabase.SaveAssets();
            UnityEditor.AssetDatabase.Refresh();
        }
        

        /// <summary>
        /// 保存json
        /// </summary>
        /// <param name="path"></param>
        /// <param name="data"></param>
        public static void SaveToJSON(string path, TileMapSerialize data)
        {
            Debug.LogFormat("Saving config to {0}", path);
            System.IO.File.WriteAllText(path, JsonUtility.ToJson(data, true));
        }

        /// <summary>
        /// 添加地图数据
        /// </summary>
        /// <param name="pos">世界坐标</param>
        /// <param name="data">单位数据</param>
        public static void AddData(GameObject brushTarget, Vector3 pos, TileInfo data)
        {
            Tilemap tile = brushTarget.GetComponent<Tilemap>();
            foreach (var item in TargetData.Data[MapID].tileMapDataList)
            {
                if (item.TilemapName == tile.name)
                {
                    if (item.tileInfoList == null)
                    {
                        item.tileInfoList = new List<TileInfo>();
                    }
                    bool isadd = true;
                    for (int i = 0; i < item.tileInfoList.Count; i++)
                    {
                        if (item.tileInfoList[i].pos == pos)
                        {
                            isadd = false;
                            item.tileInfoList[i] = data;
                            break;
                        }
                    }
                    if (isadd)
                    {
                        item.tileInfoList.Add(data);
                    }
                }
            }
        }

        /// <summary>
        /// 清除地图数据
        /// </summary>
        /// <param name="pos">世界坐标</param>
        public static void ClearData(GameObject brushTarget, Vector3 pos)
        {
            Tilemap tile = brushTarget.GetComponent<Tilemap>();
            foreach (var item in TargetData.Data[MapID].tileMapDataList)
            {
                if (item.TilemapName == tile.name)
                {
                    for (int i = 0; i < item.tileInfoList.Count; i++)
                    {
                        if (item.tileInfoList[i].pos == pos)
                        {
                            item.tileInfoList.RemoveAt(i);
                        }
                    }
                }
            }
        }

        public static void ClearDataForPos(Vector3 pos)
        {
            foreach (var item in TargetData.Data[MapID].tileMapDataList)
            {
                for (int i = 0; i < item.tileInfoList.Count; i++)
                {
                    if (item.tileInfoList[i].pos == pos)
                    {
                        item.tileInfoList.RemoveAt(i);
                    }
                }
            }
        }

        /// <summary>
        /// 清空数据
        /// </summary>
        public static void ClearAllData()
        {
            TargetData.Data[MapID].tileMapDataList.Clear();
        }
#endif

    }
}

这里的重点是AddData方法,用Brush画刷调用此方法来添加数据,我们保存成ScriptableObject文件,如图



3.Tile的制作。

// **********************************************************************
// Copyright (C) XM
// Author: 吴肖牧
// Date: 2018-02-15
// Desc: 
// **********************************************************************

using UnityEngine;
using System;
using UnityEngine.Tilemaps;

namespace XMtileMap
{
    [Serializable]
    [CreateAssetMenu]
    public class XMTile : Tile
    {
        [SerializeField]
        public bool walkable = false;
        [SerializeField]
        public bool destroable = false;
        [SerializeField]
        public Sprite[] m_RandomSprites;
        [SerializeField]
        public Sprite[] m_AnimatedSprites;
        [SerializeField]
        public float m_MinSpeed = 1f;
        [SerializeField]
        public float m_MaxSpeed = 1f;
        [SerializeField]
        public float m_AnimationStartTime;

        public override void GetTileData(Vector3Int location, ITilemap tileMap, ref TileData tileData)
        {
            if (m_RandomSprites != null && m_RandomSprites.Length > 0 && m_AnimatedSprites != null && m_AnimatedSprites.Length > 0)
            {
                Debug.LogError("RandomSprites and AnimatedSprites can't exist at the same time");
                return;
            }
            base.GetTileData(location, tileMap, ref tileData);
            if (m_RandomSprites != null && m_RandomSprites.Length > 0)
            {
                long hash = location.x;
                hash = (hash + 0xabcd1234) + (hash << 15);
                hash = (hash + 0x0987efab) ^ (hash >> 11);
                hash ^= location.y;
                hash = (hash + 0x46ac12fd) + (hash << 7);
                hash = (hash + 0xbe9730af) ^ (hash << 11);
                UnityEngine.Random.InitState((int)hash);
                tileData.sprite = m_RandomSprites[(int)(m_RandomSprites.Length * UnityEngine.Random.value)];
            }
            if (m_AnimatedSprites != null && m_AnimatedSprites.Length > 0)
            {
                tileData.sprite = m_AnimatedSprites[m_AnimatedSprites.Length - 1];
            }
        }

        public override bool GetTileAnimationData(Vector3Int location, ITilemap tileMap, ref TileAnimationData tileAnimationData)
        {
            if (m_AnimatedSprites != null && m_AnimatedSprites.Length > 0)
            {
                tileAnimationData.animatedSprites = m_AnimatedSprites;
                tileAnimationData.animationSpeed = UnityEngine.Random.Range(m_MinSpeed, m_MaxSpeed);
                tileAnimationData.animationStartTime = m_AnimationStartTime;
                return true;
            }
            return false;
        }
    }
}


我们自定义的一些变量,比如walkable(是否可通过),destroable(是否可销毁)等等,这些我们可以根据项目需求自行拓展,Tilemap本身的知识点这里不再详细解说,不明白的建议去看看官方的文档和例子。


4.Brush的制作。

// **********************************************************************
// Copyright (C) XM
// Author: 吴肖牧
// Date: 2018-02-15
// Desc: 
// **********************************************************************

using UnityEditor;
using UnityEngine;

namespace XMtileMap
{
    [CreateAssetMenu]
    [CustomGridBrush(false, true, false, "XM Brush")]
    public class XMBrush : GridBrush
    {
        public override void Paint(GridLayout gridLayout, GameObject brushTarget, Vector3Int position)
        {
            base.Paint(gridLayout, brushTarget, position);
            AddTileMapData(gridLayout, brushTarget, position);
        }

        public override void Erase(GridLayout gridLayout, GameObject brushTarget, Vector3Int position)
        {
            base.Erase(gridLayout, brushTarget, position);
            ClearTileMapData(gridLayout, brushTarget, position);
        }

        /// <summary>
        /// 添加地图数据
        /// </summary>
        /// <param name="grid"></param>
        /// <param name="position"></param>
        private void AddTileMapData(GridLayout gridLayout, GameObject brushTarget, Vector3Int position)
        {
            TileInfo data = new TileInfo
            {
                //tile的中心点为四个顶点的其中一个点,默认左下角,我们偏移一下保证和其他游戏对象的中心点一致,这里是还原创建Grid时的偏移,保证对象刚好在tile的中心点
                pos = gridLayout.CellToWorld(position) + XMMapData.tileOffset3,
                ipos = position
            };
            for (int i = 0; i < cells.Length; i++)
            {
                XMTile xmtile = (XMTile)cells[i].tile;
                data.tile = xmtile;
            }
            XMMapData.AddData(brushTarget, data.pos, data);
        }

        /// <summary>
        /// 清除地图数据
        /// </summary>
        /// <param name="position"></param>
        private void ClearTileMapData(GridLayout gridLayout,GameObject brushTarget, Vector3Int position)
        {
            Vector3 pos = gridLayout.CellToWorld(position) + XMMapData.tileOffset3;
            XMMapData.ClearData(brushTarget, pos);
        }
    }

    [CustomEditor(typeof(XMBrush))]
    public class XMBrushEditor : GridBrushEditor
    {
        private XMBrushEditor prefabBrush { get { return target as XMBrushEditor; } }

        public override void PaintPreview(GridLayout grid, GameObject brushTarget, Vector3Int position)
        {
            base.PaintPreview(grid, brushTarget, position);
        }

        public override void OnPaintInspectorGUI()
        {
            EditorGUILayout.LabelField(XMConst.CopyRight);
        }
        public override void OnPaintSceneGUI(GridLayout grid, GameObject brushTarget, BoundsInt position, GridBrushBase.Tool tool, bool executing)
        {
            base.OnPaintSceneGUI(grid, brushTarget, position, tool, executing);
            Handles.Label(grid.CellToWorld(new Vector3Int(position.x, position.y, position.z)), new Vector3Int(position.x, position.y, position.z).ToString());
        }
    }
}

这里的重点是AddTileMapData方法,通过Brush画刷的来保存我们制作地图的数据,还需要注意的一点是,我这里只写了Paint的画刷方式,需要更加全面的画刷方式请自行拓展。

5.创建地图。

// **********************************************************************
// Copyright (C) XM
// Author: 吴肖牧
// Date: 2018-02-15
// Desc: 
// **********************************************************************

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

namespace XMtileMap
{
    public class Map : MonoBehaviour
    {

        /// <summary>
        /// 设置Tile
        /// </summary>
        /// <param name="map"></param>
        /// <param name="pos"></param>
        /// <param name="tilebase"></param>
        public static void SetTile(Tilemap map, Vector3Int pos, TileBase tilebase)
        {
            map.SetTile(pos, tilebase);
        }

        /// <summary>
        /// 设置TileMap
        /// </summary>
        /// <param name="map"></param>
        /// <param name="tileMapDataList"></param>
        public static void SetTileMap(Tilemap map, TileMapData tileMapData)
        {
            if (tileMapData.tileInfoList != null)
            {
                foreach (var tile in tileMapData.tileInfoList)
                {
                    map.SetTile(tile.ipos, tile.tile);
                }
            }
        }

        /// <summary>
        /// 创建TileMap
        /// </summary>
        /// <param name="tilemapData">地图ID</param>
        public static void CreateTileMap(int mapid)
        {
            XMMapData.MapID = mapid;
            if (XMMapData.SourceData.Data.Count < mapid + 1)
            {
                Debug.LogError("MapID " + mapid.ToString() + " is null");
                return;
            }
            List<TileMapData> tilemapData = XMMapData.SourceData.Data[XMMapData.MapID].tileMapDataList;
            if (tilemapData == null)
            {
                Debug.LogError("MapID " + mapid.ToString() + " tileMapDataList is null");
                return;
            }

            GameObject grid = GameObject.Find("Grid");
            if (!grid)
            {
                grid = new GameObject("Grid");
                grid.AddComponent<Grid>();
            }
            //tile的中心点为四个顶点的其中一个点,默认左下角,我们偏移一下保证和其他游戏对象的中心点一致
            grid.transform.position = new Vector3(-0.5f, -0.5f, 0);

            for (int i = 0; i < grid.transform.childCount; i++)
            {
                Destroy(grid.transform.GetChild(i).gameObject);
            }
            for (int i = 0; i < tilemapData.Count; i++)
            {
                GameObject tilemap = new GameObject(tilemapData[i].TilemapName);
                tilemap.transform.SetParent(grid.transform);
                tilemap.transform.localPosition = Vector3.zero;
                Tilemap map = tilemap.AddComponent<Tilemap>();
                TilemapRenderer render = tilemap.AddComponent<TilemapRenderer>();
                render.sortOrder = (TilemapRenderer.SortOrder)tilemapData[i].SortOrderIndex;
                render.sortingOrder = tilemapData[i].OrderInLayer;
                render.sortingLayerName = SortingLayer.layers[tilemapData[i].SortingLayerIndex].name;
                SetTileMap(map, tilemapData[i]);
            }
            //初始化地图,绑定寻路数据
            InitMap();
        }
        
        /// <summary>
        /// 初始化地图,绑定寻路数据
        /// </summary>
        public static void InitMap()
        {
            //Debug.Log(XMMapData.mapSize);
            XMMapData.map = new Dictionary<Vector2, Point>();
            List<TileMapData> tilemapData = XMMapData.SourceData.Data[XMMapData.MapID].tileMapDataList;
            foreach (var item in tilemapData)
            {
                for (int i = 0; i < item.tileInfoList.Count; i++)
                {
                    int x = item.tileInfoList[i].ipos.x;
                    int y = item.tileInfoList[i].ipos.y;
                    //Debug.Log(x + " " + y);
                    XMMapData.map.Add(new Vector2(x, y), new Point(x, y));
                    bool walkable = ((XMTile)item.tileInfoList[i].tile).walkable;
                    if (walkable)
                    {
                        XMMapData.map[new Vector2(x, y)].Walkable = walkable;
                    }
                }
            }
        }
    }
}

下图是整个文件的结构。


编辑器的脚本我就不给出来了,太繁杂了。反正思路大概就是根据需求制作Tile,然后通过Brush画刷把对应的Tile和数据添加到我们的文件里面,很简单吧。最后我们通过保存的数据文件,就可以在游戏里面动态的创建地图了,同时也可以根据地图的数据来做我们的网络同步数据,角色移动,碰撞检测等等。

猜你喜欢

转载自blog.csdn.net/yye4520/article/details/80391897