4.2 地图编辑器
在开始正式制作游戏之前,我们有必要先完成一个塔防游戏的地图编辑器。Unity编辑器的自定义功能非常强大,几乎可以把Unity编辑器扩展成任何界面。在示例中,我们将完成一个“格子”编辑系统,帮助我们输入塔防游戏的地图信息。
4.2.1 “格子”数据
新建工程,在Hierarchy窗口中单击鼠标右键,选择【Create Empty】创建一个空物体,然后创建脚本TileObject.cs指定给空物体,这里将空物体命名为Grid Object,这个类主要用于保存场景中的“格子”数据,代码如下所示。
using UnityEngine; public class TileObject : MonoBehaviour { public static TileObject Instance = null; //tile 碰撞层 public LayerMask tileLayer; //tile 大小 public float tileSize = 1; //x 轴方向tile数量 public int xTileCount = 2; //z 轴方向tile数量 public int zTileCount = 2; // 格子的数值,0表示锁定,无法摆放任何物体。1表示敌人通道,2表示可摆放防守单位 public int[] data; // 当前数据 id [HideInInspector] public int dataID = 0; [HideInInspector] // 是否显示数据信息 public bool debug = false; void Awake() { Instance = this; } // 初始化地图数据 public void Reset() { data = new int[xTileCount * zTileCount]; } // 获得相应tile的数值 public int getDataFromPosition(float pox, float poz) { int index = (int)((pox - transform.position.x)/ tileSize) * zTileCount + (int)((poz - transform.position.z)/tileSize); if (index < 0 || index >= data.Length) return 0; return data[index]; } // 设置相应tile的数值 public void setDataFromPosition( float pox, float poz, int number ) { int index = (int)((pox - transform.position.x) / tileSize) * zTileCount + (int)((poz - transform.position.z) / tileSize); if (index < 0 || index >= data.Length) return; data[index] = number; } // 在编辑模式显示帮助信息 void OnDrawGizmos() { if (! debug) return; if (data==null) { Debug.Log("Please reset data first"); return; } Vector3 pos = transform.position; for(int i=0; i<xTileCount; i++) // 画Z方向轴辅助线 { Gizmos.color = new Color(0, 0, 1, 1); Gizmos.DrawLine(pos + new Vector3(tileSize * i, pos.y, 0), transform.TransformPoint(tileSize * i, pos.y, tileSize * zTileCount)); for(int k=0; k<zTileCount; k++) // 高亮显示当前数值的格子 { if ( (i * zTileCount + k) < data.Length && data[i * zTileCount + k] == dataID) { Gizmos.color = new Color(1, 0, 0, 0.3f); Gizmos.DrawCube(new Vector3(pos.x + i * tileSize + tileSize * 0.5f, pos.y, pos.z + k * tileSize + tileSize * 0.5f), new Vector3(tileSize, 0.2f, tileSize)); } } } for(int k=0; k<zTileCount; k++)// 画X方向轴辅助线 { Gizmos.color = new Color(0, 0, 1, 1); Gizmos.DrawLine(pos + new Vector3(0, pos.y, tileSize * k), this.transform.TransformPoint(tileSize * xTileCount, pos.y, tileSize * k)); } } }
因为Unity目前不支持二维数组的序列化,所以本示例使用了一维数组data保存地图x、y的信息。GetDataFromPosition函数通过输入的坐标位置获取data数组的下标,其中一步计算是由输入的坐标减去当前物体transform的坐标值,这里要注意浮点数精度问题,比如有时1.54~0.54会得到0.9999999的结果(实际应当是1),0.9999999在转为整数后就会变为零,为了避免这个问题,最好将Grid Object的transform坐标值设为整数。
4.2.2 在Inspector窗口添加自定义UI控件
在Unity的编辑器中,当选中一个游戏体后,我们即可在【Inspector】窗口中设置它的详细属性。默认【Inspector】窗口中的选项都是预定的,Unity提供了API可以扩展【Inspector】窗口中的UI控件。
步骤 01 以本示例的地图编辑器为例,为了扩展TileObject这个类的【Inspector】窗口,我们创建了脚本TileEditor.cs,继承自Editor。因为它是一个编辑器脚本,所以必须放到Editor文件夹中,代码如下:
using UnityEngine; using UnityEditor; [CustomEditor(typeof(TileObject))] public class TileEditor : Editor { // 是否处于编辑模式 protected bool editMode = false; // 受编辑器影响的tile脚本 protected TileObject tileObject; void OnEnable() { // 获得tile脚本 tileObject = (TileObject)target; } // 更改场景中的操作 public void OnSceneGUI() { if(editMode) // 如果在编辑模式 { // 取消编辑器的选择功能 HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive)); // 在编辑器中显示数据(画出辅助线) tileObject.debug = true; // 获取Input事件 Event e = Event.current; // 如果是鼠标左键 if ( e.button == 0 && (e.type == EventType.MouseDown || e.type == EventType.MouseDrag) && ! e.alt) { // 获取由鼠标位置产生的射线 Ray ray = HandleUtility.GUIPointToWorldRay(e.mousePosition); // 计算碰撞 RaycastHit hitinfo; if (Physics.Raycast(ray, out hitinfo, 2000, tileObject.tileLayer)) { tileObject.setDataFromPosition(hitinfo.point.x, hitinfo.point.z, tileObject.dataID); } } } HandleUtility.Repaint(); } // 自定义Inspector窗口的UI public override void OnInspectorGUI() { GUILayout.Label("Tile Editor"); // 显示编辑器名称 editMode=EditorGUILayout.Toggle("Edit", editMode); // 是否启用编辑模式 tileObject.debug=EditorGUILayout.Toggle("Debug", tileObject.debug); // 是否显示帮助信息 string[] editDataStr = { "Dead", "Road", "Guard" }; tileObject.dataID = GUILayout.Toolbar(tileObject.dataID, editDataStr); EditorGUILayout.Separator(); // 分隔符 if(GUILayout.Button("Reset")) // 重置按钮 { tileObject.Reset(); // 初始化 } DrawDefaultInspector(); } }
步骤 02 添加一个碰撞Layer,这里设为tile,然后将Tile Layer设为tile,调整Tile Count的值即可改变地图大小;单击Reset按钮初始化数据,默认所有格子的值为0,如图4-1所示。
图4-1 地图格子
步骤 03 在Hierarchy窗口中单击鼠标右键,选择【3D Object】→【Plane】,创建一个平面并置于Grid Object层级下,将它的Layer设为tile,并取消选中【Mesh Renderer】复选框,我们主要使用它作为地面的碰撞层,如图4-2所示。
图4-2 创建碰撞物体
步骤 04 最后,选中【Edit】复选框,单击Dead(值0)、Road(值1)或Guard(值2)按钮就可以随意绘制地图数据了,如图4-3所示。
图4-3 选中【Edit】复选框
4.2.3 创建一个自定义窗口
除了自定义Inspector窗口,我们还可以创建一个独立的窗口编辑游戏中的设置,在Editor文件夹中创建自定义窗口脚本,示例代码如下:
using UnityEngine; using UnityEditor; public class TileWnd: UnityEditor.EditorWindow // 必须继承EditorWindow { // tile脚本 protected static TileObject tileObject; // 添加菜单栏选项 [MenuItem("Tools/Tile Window")] static void Create() { EditorWindow.GetWindow(typeof(TileWnd)); // 在场景中选中TileObject脚本实例 if (Selection.activeTransform! =null) tileObject = Selection.activeTransform.GetComponent<TileObject>(); } // 当更新选中新物体 void OnSelectionChange() { if (Selection.activeTransform ! = null) tileObject = Selection.activeTransform.GetComponent<TileObject>(); } // 显示窗口UI,大部分UI函数都在GUILayout和EditorGUILayout内 void OnGUI() { if (tileObject == null) return; // 显示编辑器名称 GUILayout.Label("Tile Editor"); // 在工程目录读取一张贴图 var tex = AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/GUI/butPlayer1.png"); // 将贴图显示在窗口内 GUILayout.Label(tex); // 是否显示Tile Object帮助信息 tileObject.debug = EditorGUILayout.Toggle("Debug", tileObject.debug); // 切换Tile Object的数据 string[] editDataStr = { "Dead", "Road", "Guard" }; tileObject.dataID = GUILayout.Toolbar(tileObject.dataID, editDataStr); EditorGUILayout.Separator(); // 分隔符 if(GUILayout.Button("Reset")) // 重置按钮 { tileObject.Reset(); // 初始化 } } }
在场景中选择Grid Object物体(TileObject实例),然后在菜单栏中选择【Tools】→【Tile Window】,即可打开自定义的窗口,如图4-4所示,这里只是演示了如何显示一些基本的UI。
图4-4 自定义窗口界面