在做一个策略类的游戏时,需要实现一个基地的功能,功能并不是太复杂,默认只能显示场景(45度视角)的一部分,然后通过移动场景(地形)查看场景中的其他部分,当点击建筑时可以拖动场景中的建筑到一定地方!
最终效果如下:
第一步:先布局好场景界面,如图:
下面我们先把地表的网格显示出来,这儿用的是 Unity3D 自带的透明顶点 Shader,暂时没有想到好的解决办法,如下图:
接着我们设置主摄像机的旋转视角为45度,我们现在可以看到场景里面有两个摄像机,另一个摄像机的目的是为了当我们拖动对象时可以始终保持被拖动的对象被优先渲染,如下图:
另外我们需要保证另一个摄像机(BuildingCamera)的旋转、位置、缩放要与主摄像机相同,并且保证 Depth 要大于主摄像机的 Depth:
下面我们新建立一个层,主要用于显示被拖动的对象,如图:
我们再添加一个 Tag 为 Drag 的标记,主要用来检测拖动的对象,如图:
到现在,另外我们还需要确保 Grids 与 Buildings 的位置、旋转、缩放保持一致,主要目的是为了计算单位统一:
下面我们设置 Plane 与 Small、Large、Middle 对象的 Tag 为 Drag,因为 Plane 是放置,Small、Large、Middle 对象是可拖动对象,如图:
到这儿场景基本上布置完毕,现在我们需要编写代码来实现了,首先我们给建立一个 C# 类,取名 SceneGrid.cs 文件,代码如下:
using UnityEngine; using System.Collections.Generic; public class SceneGrid : MonoBehaviour { public int gridRows; public int gridCols; public Dictionary<Vector3, int> gridList; void Awake() { this.gridList = new Dictionary<Vector3, int> (); float beginRow = -this.gridRows * 0.5f + 0.5f; float beginCol = -this.gridCols * 0.5f + 0.5f; Vector3 position = Vector3.zero; for (int rowIndex = 0; rowIndex < this.gridRows; rowIndex ++) { for (int colIndex = 0; colIndex < this.gridCols; colIndex ++) { position = new Vector3(beginRow + rowIndex, 0f, beginCol + colIndex); this.gridList.Add(position, 0); } } } /// <summary> /// 更新格子状态 /// </summary> /// <param name="positionList">Position list.</param> /// <param name="status">If set to <c>true</c> status.</param> public void SetGrid(Vector3[] positionList, bool status) { foreach (Vector3 position in positionList) { if(this.gridList.ContainsKey(position)) { this.gridList[position] = (status == true ? 1 : 0); } } } /// <summary> /// 能否可以放置 /// </summary> /// <returns><c>true</c> if this instance can drop the specified positionList; otherwise, <c>false</c>.</returns> /// <param name="positionList">Position list.</param> public bool CanDrop(Vector3[] positionList) { foreach (Vector3 position in positionList) { if(!this.gridList.ContainsKey(position)) return false; if(this.gridList[position] != 0) return false; } return true; } }
然后我们把 SceneGrid.cs 挂载到 Plane 对象上,如图:
然后我们继续创建一个 C# 类,取名:SceneBuilding.cs,代码如下:
using UnityEngine; using System.Collections; public class SceneBuilding : MonoBehaviour { public int buildingWidth = 1; public int buildingHeight = 1; }
接着我们把 SceneBuilding.cs 依次添加到 Small、Large、Middle 对象上,如图:
我们再添加一个 C# 类,取名:SceneController.cs,这个类比较重要,场景的主要逻辑都在这个类当中,代码如下:
using UnityEngine; using System.Collections; public class SceneController : MonoBehaviour { /// 鼠标枚举 enum MouseTypeEnum { LEFT = 0 } // 拖动建筑枚举 enum BuildingLayerEnum { BUILDING = 8 } /// <summary> /// 水平移动速度 /// </summary> public float horizontalSpeed = 10f; /// <summary> /// 垂直移动速度 /// </summary> public float verticalSpeed = 10f; /// <summary> /// 滚轮速度 /// </summary> public float mouseScrollSpeed = 10f; /// <summary> /// 拖动状态判断 X 坐标 /// </summary> public float moveOffsetX = 1f; /// <summary> /// 拖动状态判断 Y 坐标 /// </summary> public float moveOffsetY = 1f; /// <summary> /// 主摄像机 /// </summary> public Camera mainCamera; /// <summary> /// 拖动建筑显示层 /// </summary> public Camera buildingCamera; /// <summary> /// 建筑容器对象,要跟 表格容器对象在相同位置,缩放、旋转都要相同 /// </summary> public Transform buildingsObject; /// <summary> /// 场景格子 /// </summary> public SceneGrid sceneGrid; /// <summary> /// 鼠标状态 /// </summary> private bool mousePressStatus = false; /// <summary> /// 鼠标 X 坐标 /// </summary> private float mouseX; /// <summary> /// 鼠标 Y 坐标 /// </summary> private float mouseY; /// <summary> /// 滚轮数据 /// </summary> private float mouseScroll; /// <summary> /// 建筑信息 /// </summary> private SceneBuilding sceneBuilding; /// <summary> /// 拖动的建筑对象 /// </summary> private GameObject moveObject; /// <summary> /// 移动对象的位置信息 /// </summary> private Vector3 movePosition; /// <summary> /// 移动偏移数据 /// </summary> private Vector3 moveOffset; /// <summary> /// 最后一次对象位置列表 /// </summary> private Vector3[] prevPositionList; /// <summary> /// 射线碰撞位置 /// </summary> private Vector3 hitPosition; void Update() { // 按下鼠标、轴 if (Input.GetMouseButtonDown((int)MouseTypeEnum.LEFT)) { this.mousePressStatus = true; // 如果有选中的建筑信息 if(this.sceneBuilding != null) { // 重置建筑信息对象 this.sceneBuilding = null; } // 检测鼠标点击区域是否是建筑对象 this.sceneBuilding = PhysisUtils.GetTByMousePoint<SceneBuilding> (this.mainCamera); // 如果是建筑对象 if (this.sceneBuilding != null) { this.prevPositionList = GridUtils.GetPostionList(this.sceneBuilding.transform.localPosition, this.sceneBuilding.buildingWidth, this.sceneBuilding.buildingHeight); this.sceneGrid.SetGrid(this.prevPositionList, false); } } // 松开鼠标、轴 if (Input.GetMouseButtonUp ((int)MouseTypeEnum.LEFT)) { bool dropStatus = false; this.mousePressStatus = false; // 销毁拖动对象 if(this.moveObject != null && this.sceneBuilding != null) { Vector3 targetPosition = this.moveObject.transform.localPosition; Destroy(this.moveObject); this.moveObject = null; if(this.CanDrop(ref this.hitPosition)) { Vector3[] positionList = GridUtils.GetPostionList(targetPosition, this.sceneBuilding.buildingWidth, this.sceneBuilding.buildingHeight); if(this.sceneGrid.CanDrop(positionList)) { dropStatus = true; this.sceneGrid.SetGrid(positionList, true); this.sceneBuilding.transform.localPosition = targetPosition; }else{ Debug.Log("不能放置"); } } } if(!dropStatus) if(this.prevPositionList != null) this.sceneGrid.SetGrid(prevPositionList, true); this.prevPositionList = null; } // 如果鼠标在按住状态 if (this.mousePressStatus) { this.mouseY = this.horizontalSpeed * Input.GetAxis ("Mouse X"); this.mouseX = this.verticalSpeed * Input.GetAxis ("Mouse Y"); // 当超过一定的偏移坐标,才视为拖动建筑 if((Mathf.Abs(this.mouseX) >= this.moveOffsetX || Mathf.Abs(this.mouseY) >= this.moveOffsetY) && this.sceneBuilding != null) { // 创建一个新的建筑对象 if(this.moveObject == null) { // 设置建筑信息的屏幕坐标 this.movePosition = this.mainCamera.WorldToScreenPoint(this.sceneBuilding.transform.position); // 设置建筑信息的坐标偏移值 this.moveOffset = this.sceneBuilding.transform.position - this.mainCamera.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, this.movePosition.z)); this.moveObject = (GameObject)Instantiate(this.sceneBuilding.gameObject); this.moveObject.name = this.sceneBuilding.gameObject.name; this.moveObject.tag = null; this.moveObject.layer = (int)BuildingLayerEnum.BUILDING; this.moveObject.transform.parent = this.buildingsObject; Destroy(this.moveObject.GetComponent<SceneBuilding>()); Destroy(this.moveObject.GetComponent<BoxCollider>()); this.moveObject.transform.localPosition = this.sceneBuilding.gameObject.transform.localPosition; } } // 如果移动摄像机 if(this.sceneBuilding == null) { Vector3 rotationVector = Quaternion.Euler(this.mainCamera.transform.eulerAngles) * new Vector3(this.mouseY, 0f, this.mouseX); rotationVector.y = 0f; this.mainCamera.transform.localPosition -= rotationVector * Time.deltaTime; }else { // 如果移动的是建筑 if(this.moveObject != null) { if(this.CanDrop(ref this.hitPosition)) { this.hitPosition -= this.moveOffset; Vector3 currentLocalPosition = this.buildingsObject.transform.InverseTransformPoint(this.hitPosition); currentLocalPosition.x = (int)currentLocalPosition.x - 0.5f; currentLocalPosition.z = (int)currentLocalPosition.z - 0.5f; if(this.sceneBuilding.buildingWidth % 2 == 0) { currentLocalPosition.x += 0.5f; } if(this.sceneBuilding.buildingHeight % 2 == 0) { currentLocalPosition.z += 0.5f; } currentLocalPosition.y = 0f; // 设置对象跟随鼠标 this.moveObject.transform.localPosition = currentLocalPosition; } } } } // 鼠标滚轮拉近拉远 this.mouseScroll = this.mouseScrollSpeed * Input.GetAxis ("Mouse ScrollWheel"); if (this.mouseScroll != 0f) { this.mainCamera.transform.localPosition -= new Vector3(0f, mouseScroll, 0f) * Time.deltaTime; } this.buildingCamera.transform.localPosition = this.mainCamera.transform.localPosition; } /// <summary> /// 能否放置 /// </summary> /// <returns><c>true</c> if this instance can drop the specified position; otherwise, <c>false</c>.</returns> /// <param name="position">Position.</param> private bool CanDrop(ref Vector3 position) { Ray ray = this.mainCamera.ScreenPointToRay(Input.mousePosition); RaycastHit raycastHit = new RaycastHit(); if(Physics.Raycast(ray, out raycastHit)) { if(raycastHit.collider.tag == "Drag") { position = raycastHit.point; return true; } } return false; } }
然后把 SceneController.cs 挂载到 SceneController 对象上,并且设置好相关属性,如图:
代码中,提取出了一个计算表格所占格子的类,取名 :GridUtils.cs,代码如下:
using UnityEngine; using System.Collections; public class GridUtils { /// <summary> /// 获取格子所在的位置列表 /// </summary> /// <returns>The postion list.</returns> /// <param name="transformPosition">Transform position.</param> /// <param name="buildingWidth">Building width.</param> /// <param name="buildingHeight">Building height.</param> public static Vector3[] GetPostionList(Vector3 transformPosition, int buildingWidth, int buildingHeight, int gridRows = 10, int gridWidth = 10) { Vector3 localPosition = new Vector3 (transformPosition.x, 0f, transformPosition.z); localPosition.x -= buildingWidth * 0.5f; localPosition.z += buildingHeight * 0.5f; Vector3[] positionList = new Vector3[buildingWidth * buildingHeight]; for (int rowIndex = 0; rowIndex < buildingWidth; rowIndex ++) { for (int colIndex = 0; colIndex < buildingHeight; colIndex ++) { positionList[rowIndex * buildingHeight + colIndex] = new Vector3(localPosition.x + rowIndex + 1f - 0.5f, 0f, localPosition.z - colIndex - 1f + 0.5f); } } return positionList; } }
到这儿一切都完成了,现在我们运行一下看看效果吧