【unity实战】使用unity制作一个类似Rust的3D生存建造建筑系统(附项目源码)
最编程
2024-02-23 13:24:35
...
using System.Collections.Generic;
using UnityEngine;
public class BuildingManager : MonoBehaviour
{
[Header("建筑物对象列表")]
[SerializeField] private List<GameObject> floorObjects = new List<GameObject>(); // 地板建筑物的列表
[SerializeField] private List<GameObject> wallObjects = new List<GameObject>(); // 墙壁建筑物的列表
[Header("建筑设置")]
[SerializeField] private SelectedBuildType currentBuildType; // 当前选中的建筑类型
[SerializeField] private LayerMask connectorLayer; // 连接器所在的层
[Header("鬼影设置")]
[SerializeField] private Material ghostMaterialValid; // 鬼影建筑物有效时的材质
[SerializeField] private Material ghostMaterialInvalid; // 鬼影建筑物无效时的材质
[SerializeField] private float connectorOverlapRadius = 1f; // 寻找附近连接器的检测半径
[SerializeField] private float maxGroundAngle = 45f; // 地面的最大可放置倾斜角度
[SerializeField] private float placementDistance = 20f; // 放置距离
[Header("内部状态")]
[SerializeField] private bool isBuilding = false; // 是否正在建造中
[SerializeField] private int currentBuildingIndex; // 当前建造物的索引
private GameObject ghostBuildGameobject; // 鬼影建筑物对象
private bool isGhostInValidPosition = false; // 鬼影建筑物是否在有效位置
private Transform ModelParent = null; // 模型父对象的Transform
[Header("拆除设置")]
[SerializeField] private bool isDestroying = false; // 是否在拆除建筑物
private Transform lastHitDestroyTransform; // 上次点击的拆除目标的Transform
private List<Material> lastHitMaterials = new List<Material>(); // 上次点击的拆除目标的材质列表
private void Update()
{
// 遍历数字1到7
for (int i = 1; i <= 7; i++)
{
// 检查是否按下对应的数字键
if (Input.GetKeyDown(KeyCode.Alpha0 + i))
{
select(i);
}
}
if (Input.GetKeyDown(KeyCode.X)) // 按下X键切换拆除模式
isDestroying = !isDestroying;
if (isBuilding && !isDestroying) // 如果正在建造且不在拆除状态
{
ghostBuild(); // 显示鬼影建筑物
if (Input.GetMouseButtonDown(0)) placeBuild(); // 点击鼠标左键放置建筑物
}
else if (ghostBuildGameobject) // 如果没有在建造且鬼影建筑物存在,则销毁鬼影建筑物对象
{
Destroy(ghostBuildGameobject);
ghostBuildGameobject = null;
}
if (isDestroying) // 如果在拆除状态
{
ghostDestroy(); // 显示拆除的鬼影效果
if (Input.GetMouseButtonDown(0)) destroyBuild(); // 点击鼠标左键拆除建筑物
}
}
//选择建造测试
void select(int number)
{
isBuilding = !isBuilding;
if (number == 1)
{
currentBuildingIndex = 0;
currentBuildType = SelectedBuildType.floor;
}
if (number == 2)
{
currentBuildingIndex = 0;
currentBuildType = SelectedBuildType.wall;
}
if (number == 3)
{
currentBuildingIndex = 1;
currentBuildType = SelectedBuildType.wall;
}
if (number == 4)
{
currentBuildingIndex = 2;
currentBuildType = SelectedBuildType.wall;
}
if (number == 5)
{
currentBuildingIndex = 3;
currentBuildType = SelectedBuildType.wall;
}
}
private void ghostBuild()
{
GameObject currentBuild = getCurrentBuild(); // 获取当前建筑物类型
createGhostPrefab(currentBuild); // 创建鬼影建筑物
moveGhostPrefabToRaycast(); // 将鬼影建筑物移动到光线投射点
checkBuildValidity(); // 检查建筑物的有效性
}
//创建鬼影建筑物
private void createGhostPrefab(GameObject currentBuild)
{
if (ghostBuildGameobject == null) // 如果鬼影建筑物对象不存在,则创建
{
ghostBuildGameobject = Instantiate(currentBuild);
ModelParent = ghostBuildGameobject.transform.GetChild(0); // 获取模型父对象的Transform
ghostifyModel(ModelParent, ghostMaterialValid); // 设置模型为鬼影材质
ghostifyModel(ghostBuildGameobject.transform); // 设置建筑物为鬼影材质
}
}
//将鬼影建筑物移动到光线投射点
private void moveGhostPrefabToRaycast()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, placementDistance)) // 光线投射检测
{
ghostBuildGameobject.transform.position = hit.point; // 将鬼影建筑物移动到光线投射点
}
}
//检查建筑物的有效性
private void checkBuildValidity()
{
Collider[] colliders = Physics.OverlapSphere(ghostBuildGameobject.transform.position, connectorOverlapRadius, connectorLayer); // 检测鬼影建筑物附近的连接器碰撞体
if (colliders.Length > 0) // 如果有连接器碰撞体
{
ghostConnectBuild(colliders); // 连接鬼影建筑物到连接器上
}
else // 如果没有连接器碰撞体
{
ghostSeparateBuild(); // 鬼影建筑物与连接器分离
if (isGhostInValidPosition) // 如果鬼影建筑物在有效位置
{
Collider[] overlapColliders = Physics.OverlapBox(ghostBuildGameobject.transform.position, new Vector3(2f, 2f, 2f), ghostBuildGameobject.transform.rotation); // 检测鬼影建筑物周围是否与其他物体重叠
foreach (Collider overlapCollider in overlapColliders)
{
if (overlapCollider.gameObject != ghostBuildGameobject && overlapCollider.transform.root.CompareTag("Buildables")) // 如果与其他可建造物体重叠,则设置鬼影建筑物为无效状态
{
ghostifyModel(ModelParent, ghostMaterialInvalid);
isGhostInValidPosition = false;
return;
}
}
}
}
}
// 连接鬼影建筑物到连接器上
private void ghostConnectBuild(Collider[] colliders)
{
Connector bestConnector = null;
foreach (Collider collider in colliders) // 遍历连接器碰撞体
{
Connector connector = collider.GetComponent<Connector>();
if (connector && connector.canConnectTo) // 如果连接器存在且可连接
{
bestConnector = connector;
break;
}
}
if (bestConnector == null || currentBuildType == SelectedBuildType.floor && bestConnector.isConnectedToFloor || currentBuildType == SelectedBuildType.wall && bestConnector.isConnectedToWall) // 如果没有找到合适的连接器或者当前建筑类型与连接器不匹配,则设置鬼影建筑物为无效状态
{
// 如果建筑无法连接或连接不合法,则将建筑模型设为不可放置的材质
ghostifyModel(ModelParent, ghostMaterialInvalid);
isGhostInValidPosition = false;
return;
}
snapGhostPrefabToConnector(bestConnector); // 将鬼影建筑物对齐到连接器上
}
//将鬼影建筑物对齐到连接器上
private void snapGhostPrefabToConnector(Connector connector)
{
Transform ghostConnector = findSnapConnector(connector.transform, ghostBuildGameobject.transform.GetChild(1)); // 查找鬼影建筑物中对应的连接器
ghostBuildGameobject.transform.position = connector.transform.position - (ghostConnector.position - ghostBuildGameobject.transform.position); // 将鬼影建筑物移动到连接器位置
if (currentBuildType == SelectedBuildType.wall) // 如果当前建筑类型为墙壁,则调整鬼影建筑物的旋转角度
{
Quaternion newRotation = ghostBuildGameobject.transform.rotation;
newRotation.eulerAngles = new Vector3(newRotation.eulerAngles.x, connector.transform.rotation.eulerAngles.y, newRotation.eulerAngles.z);
ghostBuildGameobject.transform.rotation = newRotation;
}
// 将建筑模型设为可放置的材质
ghostifyModel(ModelParent, ghostMaterialValid);
isGhostInValidPosition = true;
}
// 鬼影建筑物与连接器分离
private void ghostSeparateBuild()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
if (currentBuildType == SelectedBuildType.wall) // 如果当前建筑类型为墙,则将建筑模型设为不可放置的材质
{
ghostifyModel(ModelParent, ghostMaterialInvalid);
isGhostInValidPosition = false;
return;
}
if (Vector3.Angle(hit.normal, Vector3.up) < maxGroundAngle) // 如果当前建筑类型为地板且法线与y轴夹角小于最大可放置角度,则将建筑模型设为可放置的材质
{
ghostifyModel(ModelParent, ghostMaterialValid);
isGhostInValidPosition = true;
}
else // 否则,将模型修改为不可放置的材质
{
ghostifyModel(ModelParent, ghostMaterialInvalid);
isGhostInValidPosition = false;
}
}
}
// 查找鬼影建筑物中对应的连接器
private Transform findSnapConnector(Transform snapConnector, Transform ghostConnectorParent)
{
// 查找鬼影建筑预制体中与连接器相对应的连接器
ConnectorPosition oppositeConnectorTag = getOppositePosition(snapConnector.GetComponent<Connector>());
foreach (Connector connector in ghostConnectorParent.GetComponentsInChildren<Connector>())
{
if (connector.connectorPosition == oppositeConnectorTag)
{
return connector.transform;
}
}
return null;
}
// 查找鬼影建筑预制体中与连接器相对应的连接器
private ConnectorPosition getOppositePosition(Connector connector)
{
// 获取连接器的相反位置
ConnectorPosition position = connector.connectorPosition;
// 如果当前建筑类型是墙且连接点的父级类型是地板,则返回底部连接点
if (currentBuildType == SelectedBuildType.wall && connector.connectorParentType == SelectedBuildType.floor)
return ConnectorPosition.bottom;
// 如果当前建筑类型是地板、连接点的父级类型是墙且连接点位置为顶部
if (currentBuildType == SelectedBuildType.floor && connector.connectorParentType == SelectedBuildType.wall && connector.connectorPosition == ConnectorPosition.top)
{
// 如果连接点所在物体的Y轴旋转角度为0(即朝向为正面),则返回离玩家最近的连接点
if (connector.transform.root.rotation.y == 0)
{
return getConnectorClosestToPlayer(true);
}
else
{
// 否则返回离玩家最近的连接点
return getConnectorClosestToPlayer(false);
}
}
// 根据连接点位置返回相反的连接点位置
switch (position)
{
case ConnectorPosition.left:
return ConnectorPosition.right;
case ConnectorPosition.right:
return ConnectorPosition.left;
case ConnectorPosition.bottom:
return ConnectorPosition.top;
case ConnectorPosition.top:
return ConnectorPosition.bottom;
default:
return ConnectorPosition.bottom;
}
}
// 获取距离玩家最近的连接点
private ConnectorPosition getConnectorClosestToPlayer(bool topBottom)
{
Transform cameraTransform = Camera.main.transform;
// 如果topBottom为true,根据玩家位置返回顶部或底部连接点
if (topBottom)
{
return cameraTransform.position.z >= ghostBuildGameobject.transform.position.z ? ConnectorPosition.bottom : ConnectorPosition.top;
}
else
{
// 否则,根据玩家位置返回左侧或右侧连接点
return cameraTransform.position.x >= ghostBuildGameobject.transform.position.x ? ConnectorPosition.left : ConnectorPosition.right;
}
}
// 修改模型为鬼影材质
private void ghostifyModel(Transform modelParent, Material ghostMaterial = null)
{
// 如果提供了鬼影材质,将模型的材质设为鬼影材质
if (ghostMaterial != null)
{
foreach (MeshRenderer meshRenderer in modelParent.GetComponentsInChildren<MeshRenderer>())
{
meshRenderer.material = ghostMaterial;
}
}
else
{
// 否则,禁用模型的碰撞器
foreach (Collider modelColliders in modelParent.GetComponentsInChildren<Collider>())
{
modelColliders.enabled = false;
}
}
}
// 获取当前建筑对象
private GameObject getCurrentBuild()
{
switch (currentBuildType)
{
case SelectedBuildType.floor:
return floorObjects[currentBuildingIndex];
case SelectedBuildType.wall:
return wallObjects[currentBuildingIndex];
}
return null;
}
// 放置建筑
private void placeBuild()
{
// 如果鬼影模型存在且在有效位置上
if (ghostBuildGameobject != null & isGhostInValidPosition)
{
// 在鼠标指针位置实例化新的建筑物并销毁鬼影模型
GameObject newBuild = Instantiate(getCurrentBuild(), ghostBuildGameobject.transform.position, ghostBuildGameobject.transform.rotation);
Destroy(ghostBuildGameobject);
ghostBuildGameobject = null;
isBuilding = false;
// 更新新建筑物的连接点
foreach (Connector connector in newBuild.GetComponentsInChildren<Connector>())
{
connector.updateConnectors(true);
}
}
}
//显示拆除的鬼影效果
private void ghostDestroy()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
if (hit.transform.root.CompareTag("Buildables")) // 判断是否是可建造的对象
{
if (!lastHitDestroyTransform) // 如果上一次点击的建筑物为空,则记录当前点击的建筑物
{
lastHitDestroyTransform = hit.transform.root;
lastHitMaterials.Clear();
// 获取建筑