【unity实战】在Unity中建立不规则模型的网格建造系统(附计划源码)

最终效果

在这里插入图片描述

前言

之前我已经做过不少网格建造相关的实战案例:

但是他们都是仅仅针对规则的矩形建筑,如果是不规则的比如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


专栏推荐

地址
【unity游戏开发入门到精通——C#篇】
【unity游戏开发入门到精通——unity通用篇】
【unity游戏开发入门到精通——unity3D篇】
【unity游戏开发入门到精通——unity2D篇】
【unity实战】
【制作100个Unity游戏】
【推荐100个unity插件】
【实现100个unity特效】
【unity框架/工具集开发】
【unity游戏开发——模型篇】
【unity游戏开发——InputSystem】
【unity游戏开发——Animator动画】
【unity游戏开发——UGUI】
【unity游戏开发——联网篇】
【unity游戏开发——优化篇】
【unity游戏开发——shader篇】
【unity游戏开发——编辑器扩展】
【unity游戏开发——热更新】
【unity游戏开发——网络】

完结

好了,我是向宇,博客地址:https://xiangyu.blog.csdn.net,如果学习过程中遇到任何问题,也欢迎你评论私信找我。

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!
在这里插入图片描述

posted @ 2025-08-13 11:33  wzzkaifa  阅读(15)  评论(0)    收藏  举报