在我来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和数据添加到我们的文件里面,很简单吧。最后我们通过保存的数据文件,就可以在游戏里面动态的创建地图了,同时也可以根据地图的数据来做我们的网络同步数据,角色移动,碰撞检测等等。