1. 程式人生 > >Unity3D 基地實現(攝像機移動、拖動建築等)

Unity3D 基地實現(攝像機移動、拖動建築等)

在做一個策略類的遊戲時,需要實現一個基地的功能,功能並不是太複雜,預設只能顯示場景(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;
	}
}

到這兒一切都完成了,現在我們執行一下看看效果吧,用到的 PhysisUtils.cs(物理檢測相關的類在另外的文章有提到)

原文地址:http://www.omuying.com/article/34.aspx#sourcecode