最终效果
文章目录
前言
之前我已经做过不少网格建造相关的实战案例:
但是他们都是仅仅针对规则的矩形建筑,如果是不规则的比如T形、L形、+形建筑要怎么做呢?本文就来实现一下。
实战
1、素材
https://assetstore.unity.com/packages/3d/props/furniture/furniture-free-low-poly-3d-models-pack-260522
2、新增建筑物形状单元脚本
这个脚本为空就行,我们什么都需要做
using UnityEngine;
//建筑物形状单元
public class BuildingShapeUnit : MonoBehaviour
{
}
2、建筑物模型脚本
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
// 建筑物模型脚本
public class BuildingModel : MonoBehaviour
{
[SerializeField] private Transform wrapper;
// 公开的旋转角度属性,获取wrapper的Y轴欧拉角
public float Rotation => wrapper.eulerAngles.y;
// 存储建筑物形状单元
private BuildingShapeUnit[] shapeUnits;
private void Awake()
{
// 获取所有子物体中的BuildingShapeUnit组件
shapeUnits = GetComponentsInChildren<BuildingShapeUnit>();
}
// 旋转方法,接收旋转步长参数
public void Rotate(float rotationStep)
{
// 在Y轴上旋转wrapper物体
wrapper.Rotate(new Vector3(0, rotationStep, 0));
}
// 获取所有建筑物单元的位置
public List<Vector3> GetAllBuildingPositions()
{
// 使用LINQ查询所有shapeUnits的位置并转换为List
return shapeUnits.Select(unit => unit.transform.position).ToList();
}
}
3、创建不同形状的模型预制体
下面是参考
这里我们使用BuildingShapeUnit,而不是直接按名称获取建筑物形状单元,是因为获取对象中的组件比搜索文本更快。
4、在场景视图可视化建造网格
新增建筑网格单元格类,用于管理单个网格单元的状态
// 建筑网格单元格类,用于管理单个网格单元的状态
public class BuildingGridCell
{
}
新增建筑系统主控制器
// 建筑系统主控制器
public class BuildingSystem : MonoBehaviour
{
public const float CellSize = 1f; // 每个网格单元的大小
}
新增建筑网格系统,用于管理建筑在网格上的放置
using System.Collections.Generic;
using UnityEngine;
// 建筑网格系统,用于管理建筑在网格上的放置
public class BuildingGrid : MonoBehaviour
{
[SerializeField]
private int width; // 网格的宽度(X轴方向格子数量)
[SerializeField]
private int height; // 网格的高度(Z轴方向格子数量)
private BuildingGridCell[,] grid; // 二维数组存储所有网格单元格
// 初始化网格
private void Start()
{
// 根据设定的宽高创建网格
grid = new BuildingGridCell[width, height];
// 初始化每个网格单元格
for (int x = 0; x < grid.GetLength(0); x++)
{
for (int y = 0; y < grid.GetLength(1); y++)
grid[x, y] = new BuildingGridCell();
}
}
// 在Scene视图绘制网格Gizmo
void OnDrawGizmos()
{
Gizmos.color = Color.yellow;
// 参数无效时不绘制
if (BuildingSystem.CellSize <= 0 || width <= 0 || height <= 0)
return;
Vector3 origin = transform.position;
// 绘制横向网格线(Z轴方向)
for (int y = 0; y <= height; y++)
{
Vector3 start = origin + new Vector3(0, 0.01f, y * BuildingSystem.CellSize);
Vector3 end = origin + new Vector3(width * BuildingSystem.CellSize, 0.01f, y * BuildingSystem.CellSize);
Gizmos.DrawLine(start, end);
}
// 绘制纵向网格线(X轴方向)
for (int x = 0; x <= width; x++)
{
Vector3 start = origin + new Vector3(x * BuildingSystem.CellSize, 0.01f, 0);
Vector3 end = origin + new Vector3(x * BuildingSystem.CellSize, 0.01f, height * BuildingSystem.CellSize);
Gizmos.DrawLine(start, end);
}
}
}
配置,效果
5、新增ScriptableObject创建不同的建筑数据
using UnityEngine;
//建筑数据
[CreateAssetMenu(menuName = "Data/Building")]
public class BuildingData : ScriptableObject
{
// 字段序列化并封装为属性(可在Inspector中编辑但外部只能读取)
[field:SerializeField]
public string Description { get; private set; } // 建筑描述文本
[field:SerializeField]
public int Cost { get; private set; } // 建筑造价/成本
[field:SerializeField]
public BuildingModel Model { get; private set; } // 关联的建筑模型
}
6、建筑预览类和建筑实体类
建筑预览类,用于在放置建筑前显示预览效果
using System.Collections.Generic;
using UnityEngine;
// 建筑预览类,用于在放置建筑前显示预览效果
public class BuildingPreview : MonoBehaviour
{
// 预览状态枚举
public enum BuildingPreviewState
{
POSITIVE, // 可放置状态
NEGATIVE // 不可放置状态
}
[SerializeField] private Material positiveMaterial; // 可放置状态材质
[SerializeField] private Material negativeMaterial; // 不可放置状态材质
// 当前预览状态(默认NEGATIVE)
public BuildingPreviewState State { get; private set; } = BuildingPreviewState.NEGATIVE;
public BuildingData Data { get; private set; } // 关联的建筑数据
public BuildingModel BuildingModel { get; private set; } // 建筑模型实例
private List<Renderer> renderers = new(); // 所有渲染器组件缓存
private List<Collider> colliders = new(); // 所有碰撞体组件缓存
// 初始化预览
public void Setup(BuildingData data)
{
Data = data;
// 实例化建筑模型
BuildingModel = Instantiate(data.Model, transform.position, Quaternion.identity, transform);
// 获取所有渲染器和碰撞体
renderers.AddRange(BuildingModel.GetComponentsInChildren<Renderer>());
colliders.AddRange(BuildingModel.GetComponentsInChildren<Collider>());
// 禁用所有碰撞体(预览状态不需要物理碰撞)
foreach (var col in colliders)
{
col.enabled = false;
}
SetPreviewMaterial(State); // 设置初始材质
}
// 设置预览材质
private void SetPreviewMaterial(BuildingPreviewState newState)
{
// 根据状态选择材质
Material previewMat = newState == BuildingPreviewState.POSITIVE ? positiveMaterial : negativeMaterial;
// 更新所有渲染器材质
foreach (var rend in renderers)
{
Material[] mats = new Material[rend.sharedMaterials.Length];
for (int i = 0; i < mats.Length; i++)
mats[i] = previewMat;
rend.materials = mats;
}
}
// 改变预览状态
public void ChangeState(BuildingPreviewState newState)
{
if (newState == State) return;
State = newState;
SetPreviewMaterial(State);
}
// 旋转预览模型
public void Rotate(int rotationStep)
{
BuildingModel.Rotate(rotationStep);
}
}
建筑实体类
using UnityEngine;
// 建筑实体类
public class Building : MonoBehaviour
{
public string Description => data.Description; // 建筑描述(从数据读取)
public int Cost => data.Cost; // 建筑成本(从数据读取)
private BuildingModel model; // 建筑模型实例
private BuildingData data; // 建筑数据
// 初始化建筑
public void Setup(BuildingData data, float rotation)
{
this.data = data;
// 实例化模型并设置初始旋转
model = Instantiate(data.Model, transform.position, Quaternion.identity, transform);
model.Rotate(rotation);
}
}
分别新增空物体,挂载脚本,配置成预制体
7、实现网格建造
修改建筑网格单元格类
// 建筑网格单元格类,用于管理单个网格单元的状态
public class BuildingGridCell
{
// 当前单元格上放置的建筑引用
private Building building;
// 设置当前单元格的建筑
public void SetBuilding(Building building)
{
this.building = building; // 将传入的建筑赋值给当前单元格
}
// 检查当前单元格是否为空
public bool IsEmpty()
{
return building == null; // 如果building为null则表示单元格为空
}
}
修改建筑网格系统
// 在网格上放置建筑
public void SetBuilding(Building building, List<Vector3> allBuildingPositions)
{
// 遍历建筑所有单元位置
foreach (var p in allBuildingPositions)
{
// 将世界坐标转换为网格坐标
(int x, int y) = WorldToGridPosition(p);
// 在对应网格单元格设置建筑
grid[x, y].SetBuilding(building);
}
}
// 世界坐标转网格坐标
private (int x, int y) WorldToGridPosition(Vector3 worldPosition)
{
// 计算相对于原点的网格坐标(X轴)
int x = Mathf.FloorToInt((worldPosition - transform.position).x / BuildingSystem.CellSize);
// 计算相对于原点的网格坐标(Z轴,对应网格Y)
int y = Mathf.FloorToInt((worldPosition - transform.position).z / BuildingSystem.CellSize);
return (x, y);
}
// 检查是否可以建造
public bool CanBuild(List<Vector3> allBuildingPositions)
{
foreach (var p in allBuildingPositions)
{
(int x, int y) = WorldToGridPosition(p);
// 检查是否超出网格范围
if (x < 0 || x >= width || y < 0 || y >= height)
return false;
// 检查单元格是否已被占用
if (!grid[x, y].IsEmpty())
return false;
}
return true;
}
修改建筑系统主控制器
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
// 建筑系统主控制器
public class BuildingSystem : MonoBehaviour
{
public const float CellSize = 1f; // 每个网格单元的大小
// 可配置的建筑数据(在Inspector中设置)
[SerializeField] private BuildingData buildingData1; // 建筑类型1数据
[SerializeField] private BuildingData buildingData2; // 建筑类型2数据
[SerializeField] private BuildingData buildingData3; // 建筑类型3数据
[SerializeField] private BuildingPreview previewPrefab; // 建筑预览预制体
[SerializeField] private Building buildingPrefab; // 建筑实体预制体
[SerializeField] private BuildingGrid grid; // 建筑网格系统
private BuildingPreview preview; // 当前预览实例
private void Update()
{
Vector3 mousePos = GetMouseWorldPosition(); // 获取鼠标世界坐标
if (preview != null)
{
// 处理建筑预览状态
HandlePreview(mousePos);
}
else
{
// 按键1-3创建不同建筑的预览
if (Input.GetKeyDown(KeyCode.Alpha1))
{
preview = CreatePreview(buildingData1, mousePos);
}
else if (Input.GetKeyDown(KeyCode.Alpha2))
{
preview = CreatePreview(buildingData2, mousePos);
}
else if (Input.GetKeyDown(KeyCode.Alpha3))
{
preview = CreatePreview(buildingData3, mousePos);
}
}
}
// 处理建筑预览逻辑
private void HandlePreview(Vector3 mouseWorldPosition)
{
preview.transform.position = mouseWorldPosition; // 跟随鼠标位置
// 获取建筑所有单元位置
List<Vector3> buildPositions = preview.BuildingModel.GetAllBuildingPositions();
// 检查是否可以建造
bool canBuild = grid.CanBuild(buildPositions);
if (canBuild)
{
// 对齐到网格中心
preview.transform.position = GetSnappedCenterPosition(buildPositions);
preview.ChangeState(BuildingPreview.BuildingPreviewState.POSITIVE);
// 鼠标左键放置建筑
if (Input.GetMouseButtonDown(0))
{
PlaceBuilding(buildPositions);
}
}
else
{
preview.ChangeState(BuildingPreview.BuildingPreviewState.NEGATIVE);
}
// R键旋转建筑
if (Input.GetKeyDown(KeyCode.R))
{
preview.Rotate(90);
}
}
// 计算对齐到网格中心的坐标
private Vector3 GetSnappedCenterPosition(List<Vector3> allBuildingPositions)
{
// 获取所有单元的X/Z坐标
List<int> xs = allBuildingPositions.Select(p => Mathf.FloorToInt(p.x)).ToList();
List<int> zs = allBuildingPositions.Select(p => Mathf.FloorToInt(p.z)).ToList();
// 计算中心点并对齐网格
float centerX = (xs.Min() + xs.Max()) / 2f + CellSize / 2f;
float centerZ = (zs.Min() + zs.Max()) / 2f + CellSize / 2f;
return new Vector3(centerX, 0, centerZ);
}
// 放置建筑
private void PlaceBuilding(List<Vector3> buildingPositions)
{
// 实例化实际建筑
Building building = Instantiate(buildingPrefab, preview.transform.position, Quaternion.identity);
building.Setup(preview.Data, preview.BuildingModel.Rotation);
// 在网格上注册建筑
grid.SetBuilding(building, buildingPositions);
// 清除预览
Destroy(preview.gameObject);
preview = null;
}
// 获取鼠标在世界空间的位置(Y=0平面)
private Vector3 GetMouseWorldPosition()
{
//Plane.Raycast比传统的Physics.Raycast检测性能好
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
Plane groundPlane = new Plane(Vector3.up, Vector3.zero);
if (groundPlane.Raycast(ray, out float distance))
{
return ray.GetPoint(distance);
}
return Vector3.zero;
}
// 创建建筑预览
private BuildingPreview CreatePreview(BuildingData data, Vector3 position)
{
BuildingPreview buildingPreview = Instantiate(previewPrefab, position, Quaternion.identity);
buildingPreview.Setup(data);
return buildingPreview;
}
}
配置
效果,按键盘按键1、2、3切换建造不同的建筑物体,按R可以旋转建筑
源码
https://gitee.com/unity_data/unity-grid-construction-system
参考
https://www.youtube.com/watch?v=VEisdNlIvyU
专栏推荐
完结
好了,我是向宇
,博客地址:https://xiangyu.blog.csdn.net,如果学习过程中遇到任何问题,也欢迎你评论私信找我。
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!