4.2 游戏场景 塔防游戏的场景通常比较简单,就像一个棋盘格,可以在上面摆放防守单位,并专门留给敌人一条通道。在这个实例中,我们使用二维数组来表示场景中的格子,每个格子只有两种状态,允许摆放防守单位或不允许。 打开本书光盘目录chapter04_TD_Start内的Unity工程,在这个工程中,已经预先提供了本游戏所需要的模型、动画、音效等资源,如图4-1所示。 图4-1 塔防游戏场景 这是一个使用Terrain制作的简单场景,场景中间绘制了一条通道,敌人将从通道的左侧出发,目的地是通道右侧的房子,它是我们的基地。在通道以外的地方我们可以设置防守单位进攻敌人。 创建脚本GridNode.cs: using UnityEngine; using System.Collections; //定义场景信息 [System.Serializable] public class MapData { public enum FieldTypeID { // 可以放置防守单位 GuardPosition, //不可以放置防守单位 CanNotStand, } //默认可以放置防守单位 public FieldTypeID fieldtype = FieldTypeID.GuardPosition; } public class GridNode : MonoBehaviour { public MapData _mapData; // 显示一个图标 void OnDrawGizmos() { Gizmos.DrawIcon(this.transform.position, "gridnode.tif"); } } 这个脚本首先定义了一个MapData类,它只有一个fieldtype属性,用来标识场景中单元格的状态,可以放置防守单位或不可以。因为MapData类并不是继承自MonoBehaviour,所以我们在类的定义之前添加了一个[System.Serializable]属性,保证它会被序列化,可以在编辑器中设置它的初值。 在GridNode类中,它只包含一个MapData属性,并使用OnDrawGizmos函数在编辑器中画出自身图标。 创建空游戏体,并为其指定GridNode.cs脚本,然后将其Tag设为“gridnode”。这个游戏体将作为设置单元格信息的节点,如图4-2所示,在后面我们将根据Tag的名字来查找它。 图4-2 节点 创建脚本GridMap.cs: using UnityEngine; using System.Collections; public class GridMap : MonoBehaviour { static public GridMap Instance = null; // 是否显示场景信息 public bool m_debug = false; // 场景的大小 public int MapSizeX = 32; public int MapSizeZ = 32; // 一个二维数组用于保存场景信息 public MapData[,] m_map; void Awake() { Instance = this; // 初始化场景信息 this.BuildMap(); } // 创建地图 [ContextMenu("BuildMap")] public void BuildMap() { //创建二维数组 m_map = new MapData[MapSizeX, MapSizeZ]; for (int i = 0; i < MapSizeX; i++) { for (int k = 0; k < MapSizeZ; k++) m_map[i, k] = new MapData(); } // 获得所有Tag为gridnode的节点 GameObject[]nodes=(GameObject[])GameObject.FindGameObjectsWithTag("gridnode"); foreach (GameObject nodeobj in nodes) { //获得节点 GridNode node = nodeobj.GetComponent<GridNode>(); Vector3 pos = nodeobj.transform.position; //如果节点的位置超出场景范围,则忽略 if ((int)pos.x >= MapSizeX || (int)pos.z >= MapSizeZ) continue; //设置格子的属性 m_map[(int)pos.x, (int)pos.z].fieldtype = node._mapData.fieldtype; } } //绘制场景信息 void OnDrawGizmos() { if (!m_debug || m_map == null) return; // 线条的颜色 Gizmos.color = Color.blue; // 绘制线条的高度 float height = 0; // 绘制网格 for (int i = 0; i < MapSizeX; i++) { Gizmos.DrawLine(new Vector3(i, height, 0), new Vector3(i, height, MapSizeZ)); } for (int k = 0; k < MapSizeZ; k++) { Gizmos.DrawLine(new Vector3(0, height, k), new Vector3(MapSizeX, height, k)); } // 改为红色 Gizmos.color = Color.red; for (int i = 0; i < MapSizeX; i++) { for (int k = 0; k < MapSizeZ; k++) { //在不能放置防守区域的方格内绘制红色的方块 if (m_map[i,k].fieldtype == MapData.FieldTypeID.CanNotStand) { Gizmos.color = new Color(1, 0, 0, 0.5f); // 绘制红色的方块 Gizmos.DrawCube(new Vector3(i + 0.5f, height, k + 0.5f), new Vector3(1, height + 0.1f, 1)); } } } } } Awake函数和Start函数类似,都会在对象实例化后自动调用,但它会早于Start函数,我们在这里初始化所有的地图信息。 在BuildMap函数中,首先创建保存场景信息的二维数组,默认每个单元格都可以摆放防守单位。然后在当前场景中查找到所有Tag名为gridnode的游戏体,将其属性赋予和它位置相同的单元格。 在使用new为一个数组分配大小后,如果数组中的元素是对象,还需要对数组中的每个对象再使用一次new才能使用。 在BuildMap函数前面有一个[ContextMenu("BuildMap")]属性,添加它之后,可以在编辑状态下执行BuildMap。 在OnDrawGizmos函数中,我们使用线断绘制出场景中的单元格,并将不能放置防守单位的单元格绘制为红色,这个功能主要是帮助我们预览场景单元格的状态。在OnDrawGizmos中绘制的图案并不会出现在最后的游戏中。 创建一个空游戏体,并为其指定GridMap.cs脚本。在Inspector窗口设置场景的大小,选中Debug,然后选择右上角的齿轮按钮,选择【BuildMap】,这个选项是我们在GridMap中定义的,如图4-3所示。 图4-3 自定义的菜单选项 在使用BuildMap选项之后,在场景中可以预览单元格的状态,它们都是在GridMap.cs的OnDrawGizmos函数中绘制的,如图4-4所示。 图4-4 预览单元格 在场景中的通道上摆放节点,并将节点的属性设为CanNotStand,表示该节点所在单元格不能摆放防守单位,如图4-5所示。 图4-5 设置节点属性 重新使用BuildMap功能,通道中摆放节点的位置都被绘制为红色,如图4-6所示。 图4-6 不能摆放防守单位的区域