淘先锋技术网

首页 1 2 3 4 5 6 7


返回目录

第三章 绘制地图


      初步完善地图编辑器(Map Graph)


到目前为止我们可以开心的绘制我们的地图了,但有不少小问题。一直开心忘我的绘制地图,却不知道地图已经绘制了多大,还要去看到底大小有没有超标。像其它地图编辑器都会有相关参数显示。我们也开始创建有些有助于绘制的显示参数,让它更像一个标准的地图编辑器,创建我们的MapGraph.cs


1        地图大小(Map Rect)


第一部要做的是确认地图的大小,我们要先确定我们地图绘制大小,这就可以知道我们绘制的东西是不是已经超越边界。比如一个10x10的地图:


你可以使用确定对角线Position的方式。


        public Vector3Int m_LeftDownPosition = Vector3Int.zero;
        public Vector3Int m_RightUpPosition = new Vector3Int(9, 9, 0);
        public RectInt mapRect
        {
            get
            {
                return new RectInt(
                    m_LeftDownPosition.x,
                    m_LeftDownPosition.y,
                    m_RightUpPosition.x - m_LeftDownPosition.x + 1,
                    m_RightUpPosition.y - m_LeftDownPosition.y + 1);
            }
        }


也可以直接使用矩形,我们这里直接使用整形的矩形RectInt


        public RectInt m_MapRect = new RectInt(0, 0, 10, 10);
        public Vector3Int leftDownPosition
        {
            get { return new Vector3Int(m_MapRect.xMin, m_MapRect.yMin, 0); }
        }
        public Vector3Int rightUpPosition
        {
            get { return new Vector3Int(m_MapRect.xMax - 1, m_MapRect.yMax - 1, 0); }
        }


接下来我们添加一些常用的属性(Property),地图的宽(width)与高(height)


        public int width
        {
            get { return m_MapRect.width; }
        }
        public int height
        {
            get { return m_MapRect.height; }
        }


2        在Scene面板中绘制地图边框(Border)


Unity中绘制辅助线的方式有多种,比如“Gizmos”和“Handles”。其中Gizmos只能在OnDrawGizmos()OnDrawGizmosSelected()两个UnityCallback中使用,而Handles范围就广一些,而且功能更强大。我们需要在Scene中绘制CellPosition,所以使用两种混用的方法。你还需要知道,Handles是在UnityEditor的命名空间中,在MonoBehaviour中需要在#if UNITY_EDITOR#endif之间使用

  • OnDrawGizmos():无论是否被选中,都在Scene面板中渲染;
  • OnDrawGizmosSelected():只有选中状态下,才在Scene面板中渲染。

我们先在MapGraph.cs中创建这一领域。


#if UNITY_EDITOR
using UnityEditor;
#endif
…
#if UNITY_EDITOR
        private void OnDrawGizmos()
        {
                EditorDrawBorderGizmos();
        }

        protected void EditorDrawBorderGizmos()
        {

        }
#endif
…


绘制边框,我们采用GizmosDrawWireCube(Vector3 center, Vector3 size)方法。从中我们看出,需要中心点与大小。而计算他们主要还是靠Grid组件获取Cell的世界坐标方法(world position),所以我们添加Grid组件。


        private Grid m_Grid;

        public Grid grid
        {
            get
            {
                if (m_Grid == null)
                {
                    m_Grid = GetComponent<Grid>();
                }
                return m_Grid;
            }
        }


先来看一张坐标系的图。


3 - 17计算BorderCenter


你要知道的是,Grid获取Cell的中心世界坐标,所以左下角要减去Cell Size的一半;同样的,右上角要加上Cell Size的一半;而且widthheight是指Cell的数量,你还要乘以Cell Size才是他们的真正长与宽。


        public Vector3 halfCellSize
        {
            get { return grid.cellSize / 2f; }
        }


完整的EditorDrawBorderGizmos()


        protected void EditorDrawBorderGizmos()
        {
            Color old = Gizmos.color;

            GUIStyle textStyle = new GUIStyle();
            textStyle.normal.textColor = m_EditorBorderColor;

            // 获取边框左下角与右上角的世界坐标
            Vector3 leftDown = grid.GetCellCenterWorld(leftDownPosition) - halfCellSize;
            Vector3 rightUp = grid.GetCellCenterWorld(rightUpPosition) + halfCellSize;

            // 绘制左下角Cell与右上角Cell的Position
            Handles.Label(leftDown, (new Vector2Int(leftDownPosition.x, leftDownPosition.y)).ToString(), textStyle);
            Handles.Label(rightUp, (new Vector2Int(rightUpPosition.x, rightUpPosition.y)).ToString(), textStyle);

            if (mapRect.width > 0 && mapRect.height > 0)
            {
                Gizmos.color = m_EditorBorderColor;

                // 边框的长与宽
                Vector3 size = Vector3.Scale(new Vector3(width, height), grid.cellSize);

                // 边框的中心坐标
                Vector3 center = leftDown + size / 2f;

                // 绘制边框
                Gizmos.DrawWireCube(center, size);
            }

            Gizmos.color = old;
        }


其中有一些新出现的东西:

  • Handles.Label:在Scene面板中绘制文字;
  • m_EditorBorderColor:边框颜色。

这使得我们可以改变边框颜色,不至于让颜色被淹没。再来额外添加一些需要用到的变量,来控制是否需要绘制Gizmos,并修改OnDrawGizmos()


#if UNITY_EDITOR
        [Header("Editor Gizmos")]
        public bool m_EditorDrawGizmos = true;
        public Color m_EditorBorderColor = Color.white;
        public Color m_EditorCellColor = Color.green;
        public Color m_EditorErrorColor = Color.red;

        private void OnDrawGizmos()
        {
            if (m_EditorDrawGizmos)
            {
                EditorDrawBorderGizmos();
            }
        }
…
#endif


好了,来看看我们的效果。


3 - 18绘制Border效果图


需要注意的是(0, 1)不是世界坐标,而是右上角CellPosition


这样我们就绘制完成了边框,既能改变颜色,又能在Inspector面板控制是否显示它,绘制的时候也知道是不是越界了。但是,RectInt的宽与高是可以为负数的,这就不对了。接下来,我们来限定它并在Scene面板中绘制一些信息。


3        在Scene面板中绘制地图信息(Information)


这部分工作,我们在Editor中进行,所以在Editor文件夹下创建新文件MapGraphEditor.cs


3.1      限定宽与高


这里我们限定宽与高都不能低于2


        public MapGraph map
        {
            get { return target as MapGraph; }
        }

        public override void OnInspectorGUI()
        {
            DrawDefaultInspector();

            // 检测地图长宽是否正确,如果不正确就修正
            if (map.mapRect.width < 2 || map.mapRect.height < 2)
            {
                RectInt fix = map.mapRect;
                fix.width = Mathf.Max(map.mapRect.width, 2);
                fix.height = Mathf.Max(map.mapRect.height, 2);
                map.mapRect = fix;
            }
        }


DrawDefaultInspector()是用来绘制本来的Inspector面板。我们暂时并不需要自定义Inspector面板,所以使用它。


3.2      绘制地图信息


Scene面板中绘制GUI是需要在UnityOnSceneGUI()方法中进行。而且在这之中,要在Handles.BeginGUI()Handles.EndGUI()中间进行绘制。还要创建GUI块。


            // Scene面板左上角显示信息
            Handles.BeginGUI();
            {
                Rect areaRect = new Rect(50, 50, 200, 200);
                GUILayout.BeginArea(areaRect);
                {
                    // 你的GUILayout代码
                }
                GUILayout.EndArea();
            }
            Handles.EndGUI();


再来创建一个绘制两个横向Label方法供我们使用。


        protected void DrawHorizontalLabel(string name, string value, GUIStyle style = null, int nameMaxWidth = 80, int valueMaxWdith = 120)
        {
            EditorGUILayout.BeginHorizontal();
            if (style == null)
            {
                EditorGUILayout.LabelField(name, GUILayout.MaxWidth(nameMaxWidth));
                EditorGUILayout.LabelField(value, GUILayout.MaxWidth(valueMaxWdith));
            }
            else
            {
                EditorGUILayout.LabelField(name, style, GUILayout.MaxWidth(nameMaxWidth));
                EditorGUILayout.LabelField(value, style, GUILayout.MaxWidth(valueMaxWdith));
            }
            EditorGUILayout.EndHorizontal();
        }


完成后的OnSceneGUI()


        protected virtual void OnSceneGUI()
        {
            if (!map.m_EditorDrawGizmos)
            {
                return;
            }

            GUIStyle textStyle = new GUIStyle();
            textStyle.normal.textColor = map.m_EditorCellColor;

            // Scene面板左上角显示信息
            Handles.BeginGUI();
            {
                Rect areaRect = new Rect(50, 50, 200, 200);
                GUILayout.BeginArea(areaRect);
                {
                    // 你的GUILayout代码
                    DrawHorizontalLabel("Object Name:", map.gameObject.name, textStyle);
                    DrawHorizontalLabel("Map Name:", map.mapName, textStyle);
                    DrawHorizontalLabel("Map Size:", map.width + "x" + map.height, textStyle);
                    DrawHorizontalLabel("Cell Size:", map.grid.cellSize.x + "x" + map.grid.cellSize.y, textStyle);
                }
                GUILayout.EndArea();
            }
            Handles.EndGUI();
        }


完成后的效果图:


3 - 19绘制信息


我们的Scene面板看起来不错。


那么又有新问题来了,如果地图非常的大,比如128*128,那绘制中间的时候,也不知道我们绘制到第几个Cell了,好吧,我们继续改造它。


4        在Scene面板中绘制鼠标(Mouse)所在Cell


回到我们的MapGraph.cs这次使用OnDrawGizmosSelected()方法来绘制,只有选中地图时才显示,添加新方法。


        private void OnDrawGizmosSelected()
        {
            if (m_EditorDrawGizmos)
            {
                EditorDrawCellGizmos();
            }
        }

        protected void EditorDrawCellGizmos()
        {
        }


首先,我们要知道鼠标位置是指Scene面板中的,而不是Game面板中,而且游戏也没有在运行,所以不能用Input.mousePosition,而应该使用Event.current.mousePosition


            Event e = Event.current;
            Vector2 mousePosition = e.mousePosition;


其次,同样的理由,转换成世界坐标时,不能使用Camera.main(场景中也可能就没有Camera),而应该是Scene面板的Camera


最后,Event所获取的mousePosition是从屏幕左上角(Left Up)开始的,而Camera是从屏幕左下角(Left Down)开始的。所以转换世界坐标时,不能直接使用。


            // 获取当前操作Scene面板
            SceneView sceneView = SceneView.currentDrawingSceneView;

            /// 获取鼠标世界坐标:
            /// Event是从左上角(Left Up)开始,
            /// 而Camera是从左下角(Left Down),
            /// 需要转换才能使用Camera的ScreenToWorldPoint方法。
            Vector2 screenPosition = new Vector2(e.mousePosition.x, sceneView.camera.pixelHeight - e.mousePosition.y);
            Vector2 worldPosition = sceneView.camera.ScreenToWorldPoint(screenPosition);

            // 当前鼠标所在Cell的Position
            Vector3Int cellPostion = grid.WorldToCell(worldPosition);
            // 当前鼠标所在Cell的Center坐标
            Vector3 cellCenter = grid.GetCellCenterWorld(cellPostion);


接下来绘制我们选中的Cell,同样使用了Gizmos.DrawWireCube


            GUIStyle textStyle = new GUIStyle();
            textStyle.normal.textColor = m_EditorCellColor;
            Gizmos.color = m_EditorCellColor;

            Handles.Label(cellCenter - halfCellSize, (new Vector2Int(cellPostion.x, cellPostion.y)).ToString(), textStyle);
            Gizmos.DrawWireCube(cellCenter, grid.cellSize);


3 - 20绘制Cell


绘制是绘制出来了,可以却又有新问题了,有些时候过好一阵子才会跟着鼠标渲染,这是由于这些方法都不是每帧运行的。接下来,我们来修正这一问题。


5        每帧刷新Scene面板(Update Scene)


回到MapGraphEditor.cs中,我们添加一个方法。


        /// <summary>
        /// 立即刷新Scene面板,这保证了每帧都运行(包括Gizmos)。
        /// 如果在OnSceneGUI或Gizmos里获取鼠标,需要每帧都运行。
        /// </summary>
        protected void UpdateSceneGUI()
        {
            HandleUtility.Repaint();
        }


HandleUtility.Repaint()方法来刷新Scene面板。


我们在OnSceneGUI()调用它:


        protected virtual void OnSceneGUI()
        {
            if (!map.m_EditorDrawGizmos)
            {
                return;
            }

            GUIStyle textStyle = new GUIStyle();
            textStyle.normal.textColor = map.m_EditorCellColor;

            // Scene面板左上角显示信息
            Handles.BeginGUI();
            {
                Rect areaRect = new Rect(50, 50, 200, 200);
                GUILayout.BeginArea(areaRect);
                {
                    // 你的GUILayout代码
                    DrawHorizontalLabel("Object Name:", map.gameObject.name, textStyle);
                    DrawHorizontalLabel("Map Name:", map.mapName, textStyle);
                    DrawHorizontalLabel("Map Size:", map.width + "x" + map.height, textStyle);
                    DrawHorizontalLabel("Cell Size:", map.grid.cellSize.x + "x" + map.grid.cellSize.y, textStyle);
                }
                GUILayout.EndArea();
            }
            Handles.EndGUI();

            // 立即刷新Scene面板
            UpdateSceneGUI();
        }


这样,在渲染Scene面板时,同时再次刷新它,就保证了每帧都运行。


6        判断Cell是否在地图内与修改绘制Cell


我们已经几乎完成MapGraph.cs的编写,还缺少判断选择的点是不是在地图内,在将来游戏中也是需要的。在RectInt中已经有这个方法。


        /// <summary>
        /// 地图是否包含Position
        /// </summary>
        /// <param name="position"></param>
        /// <returns></returns>
        public bool Contains(Vector3Int position)
        {
            return mapRect.Contains(new Vector2Int(position.x, position.y));
        }


接下来在EditorDrawCellGizmos()方法中,修改绘制Cell,让Scene面板更人性化。


修改前代码:


            GUIStyle textStyle = new GUIStyle();
            textStyle.normal.textColor = m_EditorCellColor;
            Gizmos.color = m_EditorCellColor;

            Handles.Label(cellCenter - halfCellSize, (new Vector2Int(cellPostion.x, cellPostion.y)).ToString(), textStyle);
            Gizmos.DrawWireCube(cellCenter, grid.cellSize);


修改后代码:


                /// 绘制当前鼠标下的Cell边框与Position
                /// 如果包含Cell,正常绘制
                /// 如果不包含Cell,改变颜色,并多绘制一个叉
                if (Contains(cellPostion))
                {
                    GUIStyle textStyle = new GUIStyle();
                    textStyle.normal.textColor = m_EditorCellColor;
                    Gizmos.color = m_EditorCellColor;

                    Handles.Label(cellCenter - halfCellSize, (new Vector2Int(cellPostion.x, cellPostion.y)).ToString(), textStyle);
                    Gizmos.DrawWireCube(cellCenter, grid.cellSize);
                }
                else
                {
                    GUIStyle textStyle = new GUIStyle();
                    textStyle.normal.textColor = m_EditorErrorColor;
                    Gizmos.color = m_EditorErrorColor;

                    Handles.Label(cellCenter - halfCellSize, (new Vector2Int(cellPostion.x, cellPostion.y)).ToString(), textStyle);
                    Gizmos.DrawWireCube(cellCenter, grid.cellSize);

                    // 绘制Cell对角线
                    Vector3 from = cellCenter - halfCellSize;
                    Vector3 to = cellCenter + halfCellSize;
                    Gizmos.DrawLine(from, to);
                    float tmpX = from.x;
                    from.x = to.x;
                    to.x = tmpX;
                    Gizmos.DrawLine(from, to);
                }


最后来看一下我们的成果:


3 - 21初步完成地图编辑器


这样我们就初步完成了地图编辑器。记得在MapGraph的“Prefab”上添加我们的MapGraph.cs


7        在菜单中创建我们的地图(Createin Menu)


我们虽然已经有Prefab,但如果在其它项目使用,还要重新添加脚本。我们来创建一个选项直接建立MapGraph,打开MapGraphEditor.cs文件,添加方法。


        [MenuItem("GameObject/SRPG/Map Graph", priority = -1)]
        public static MapGraph CreateMapGraphGameObject()
        {
            GameObject mapGraph = new GameObject("MapGraph", typeof(Grid));
            GameObject tilemap = new GameObject("Tilemap", typeof(Tilemap), typeof(TilemapRenderer));
            tilemap.transform.SetParent(mapGraph.transform, false);
            Selection.activeObject = mapGraph;
            return mapGraph.AddComponent<MapGraph>();
        }


这只是一个例子,你可以自己选择添加的物体与代码,并设置他们。


3 - 22菜单中创建MapGraph