Unity3D 基地实现(摄像机移动、拖动建筑等)

2016年01月07日 12:14 2 点赞 1 评论 更新于 2025-11-21 19:40

在开发策略类游戏时,常常需要实现基地功能。此功能不算复杂,默认仅显示场景(45 度视角)的一部分,玩家可通过移动场景(地形)查看其他部分,点击建筑后还能将其拖动到指定位置。

最终效果

此处可插入最终效果的截图或相关演示视频链接。

实现步骤

场景界面布局

首先,对场景界面进行布局。这里使用 Unity3D 自带的透明顶点 Shader 显示地表网格,目前尚未找到更好的解决方案。

场景界面布局

摄像机设置

  1. 主摄像机视角:将主摄像机的旋转视角设置为 45 度。此时场景中有两个摄像机,另一个摄像机(BuildingCamera)的作用是在拖动对象时,确保被拖动对象优先渲染。 主摄像机视角设置
  2. 摄像机同步:保证 BuildingCamera 的旋转、位置、缩放与主摄像机相同,并且其 Depth 值大于主摄像机的 Depth 值。

层与标记设置

  1. 新建层:创建一个新层,用于显示被拖动的对象。 新建层
  2. 添加标记:添加一个标记为“Drag”的 Tag,用于检测拖动的对象。 添加标记
  3. 设置对象 Tag:将 Plane 以及 Small、Large、Middle 对象的 Tag 设置为“Drag”,其中 Plane 用于放置,Small、Large、Middle 对象为可拖动对象。 ![设置对象 Tag](此处可插入设置对象 Tag 的截图)

代码实现

1. SceneGrid.cs

创建一个名为 SceneGrid.cs 的 C# 类,用于管理场景中的格子状态。将其挂载到 Plane 对象上。

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](此处可插入将 SceneGrid.cs 挂载到 Plane 对象上的截图)

2. SceneBuilding.cs

创建一个名为 SceneBuilding.cs 的 C# 类,用于定义建筑的宽度和高度。将其依次添加到 Small、Large、Middle 对象上。

using UnityEngine;
using System.Collections;

public class SceneBuilding : MonoBehaviour
{
public int buildingWidth = 1;
public int buildingHeight = 1;
}

![添加 SceneBuilding.cs](此处可插入将 SceneBuilding.cs 添加到 Small、Large、Middle 对象上的截图)

3. SceneController.cs

创建一个名为 SceneController.cs 的 C# 类,该类包含场景的主要逻辑。将其挂载到 SceneController 对象上,并设置好相关属性。

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.cs 挂载到 SceneController 对象上并设置属性的截图)

4. GridUtils.cs

提取一个名为 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;
}
}

运行测试

完成上述步骤后,运行项目,查看最终效果。

通过以上步骤,我们实现了 Unity3D 基地的摄像机移动和拖动建筑功能。在实际开发中,可根据需求对代码和场景进行进一步优化和扩展。

作者信息

洞悉

洞悉

共发布了 3994 篇文章