返回目录
第三章 绘制地图
四 初步完善地图编辑器(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()两个Unity的Callback中使用,而Handles范围就广一些,而且功能更强大。我们需要在Scene中绘制Cell的Position,所以使用两种混用的方法。你还需要知道,Handles是在UnityEditor的命名空间中,在MonoBehaviour中需要在#if UNITY_EDITOR与#endif之间使用。
OnDrawGizmos():无论是否被选中,都在Scene面板中渲染;OnDrawGizmosSelected():只有选中状态下,才在Scene面板中渲染。
我们先在MapGraph.cs中创建这一领域。
#if UNITY_EDITORusing UnityEditor;#endif…#if UNITY_EDITORprivate void OnDrawGizmos(){EditorDrawBorderGizmos();}protected void EditorDrawBorderGizmos(){}#endif…
绘制边框,我们采用Gizmos的DrawWireCube(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计算Border的Center
你要知道的是,Grid获取Cell的中心世界坐标,所以左下角要减去Cell Size的一半;同样的,右上角要加上Cell Size的一半;而且width与height是指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的PositionHandles.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)不是世界坐标,而是右上角Cell的Position。
这样我们就绘制完成了边框,既能改变颜色,又能在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是需要在Unity的OnSceneGUI()方法中进行。而且在这之中,要在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的PositionVector3Int 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