先看看最终效果
配置连接点
配置不同状态不同颜色的材质
连接器控制
public class Connector : MonoBehaviour { [Header("连接器位置")] public ConnectorPosition connectorPosition; [Header("连接器所属建筑类型")] public SelectedBuildType connectorParentType; [Header("是否可以连接地面")] private bool canConnectToFloor = true; [Header("是否可以连接墙壁")] private bool canConnectToWall = true; [HideInInspector] public bool isConnectedToFloor = false; // 是否连接到地面 [HideInInspector] public bool isConnectedToWall = false; // 是否连接到墙壁 [HideInInspector] public bool canConnectTo = true; // 是否可以连接其他建筑 //在场景中绘制连接器的可视化表示 private void OnDrawGizmos() { // 根据 canConnectTo 变量设置颜色 Gizmos.color = isConnectedToFloor ? (isConnectedToFloor ? Color.red : Color.blue) : (!isConnectedToWall ? Color.green : Color.yellow); // 在连接器位置绘制一个圆形表示连接状态 Gizmos.DrawWireSphere(transform.position, transform.lossyScale.x / 2f); } // 更新连接器和附近的其他连接器的连接状态 public void updateConnectors(bool rootCall = false) { Collider[] colliders = Physics.OverlapSphere(transform.position, transform.lossyScale.x / 2f); // 获取连接范围内的所有碰撞体 isConnectedToFloor = !canConnectToFloor; isConnectedToWall = !canConnectToWall; foreach (Collider collider in colliders) { // 忽略自身的碰撞体 if (collider.GetInstanceID() == GetComponent<Collider>().GetInstanceID()) { continue; } //忽略处于非激活状态的碰撞体 if (!collider.gameObject.activeInHierarchy) { continue; } if (collider.gameObject.layer == gameObject.layer) { // 获取相邻连接器的信息 Connector foundConnector = collider.GetComponent<Connector>(); if(!foundConnector) continue; // 如果相邻连接器是地面连接器,则更新 isConnectedToFloor 为 true if (foundConnector.connectorParentType == SelectedBuildType.floor) isConnectedToFloor = true; // 如果相邻连接器是墙壁连接器,则更新 isConnectedToWall 为 true if (foundConnector.connectorParentType == SelectedBuildType.wall) isConnectedToWall = true; // 如果是根调用,则继续递归更新相邻连接器的状态 if (rootCall) foundConnector.updateConnectors(); } } // 根据连接状态更新 canConnectTo 的值 canConnectTo = true; if (isConnectedToFloor && isConnectedToWall) { canConnectTo = false; } } } // 连接器位置枚举 [System.Serializable] public enum ConnectorPosition { left, // 左侧 right, // 右侧 top, // 顶部 bottom // 底部 }
配置,注意xyz轴朝向不要弄错了,以此来定连接器位置
建造系统代码
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 <= 8; 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 = 1; currentBuildType = SelectedBuildType.floor; } if (number == 3) { currentBuildingIndex = 0; currentBuildType = SelectedBuildType.wall; } if (number == 4) { currentBuildingIndex = 1; currentBuildType = SelectedBuildType.wall; } if (number == 5) { currentBuildingIndex = 2; currentBuildType = SelectedBuildType.wall; } if (number == 6) { 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(); // 获取建筑物的所有 MeshRenderer 组件的材质,并添加到材质列表 foreach (MeshRenderer meshRenderer in lastHitDestroyTransform.GetComponentsInChildren<MeshRenderer>()) { lastHitMaterials.Add(meshRenderer.material); } // 将建筑物设置为鬼影材质 ghostifyModel(lastHitDestroyTransform.GetChild(0), ghostMaterialInvalid); } else if (hit.transform.root != lastHitDestroyTransform) // 如果当前点击的建筑物与上次不同,则重置上一次点击的建筑物 { resetLastHitDestroyTransform(); } } else // 如果点击的不是可建造的对象,则重置上一次点击的建筑物 { resetLastHitDestroyTransform(); } } } //重置上一个选中的建筑的材质 private void resetLastHitDestroyTransform() { int counter = 0; foreach (MeshRenderer meshRenderer in lastHitDestroyTransform.GetComponentsInChildren<MeshRenderer>()) { meshRenderer.material = lastHitMaterials[counter]; counter++; } lastHitDestroyTransform = null; } //拆除建筑物 private void destroyBuild() { if (lastHitDestroyTransform) { // 禁用建筑物的所有连接点 foreach (Connector connector in lastHitDestroyTransform.GetComponentsInChildren<Connector>()) { connector.gameObject.SetActive(false); connector.updateConnectors(true); } Destroy(lastHitDestroyTransform.gameObject); // 销毁建筑物 isDestroying = false; lastHitDestroyTransform = null; } } } // 建筑类型枚举 [System.Serializable] public enum SelectedBuildType { floor,//地面 wall//墙 }
配置
记得修改建筑的Tag
效果
数字键盘1~7切换建筑
按x进行拆除建筑模式,点击鼠标左键拆除建筑
源码
https://gitcode.net/unity1/3dbuildsystem
参考
https://www.youtube.com/watch?v=IYUhB97FqXo