Unity 3D\2D手机游戏开发:从学习到产品(第3版)
上QQ阅读APP看书,第一时间看更新

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 自定义窗口界面