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

发布于:2025-08-13 ⋅ 阅读:(19) ⋅ 点赞:(0)

最终效果

在这里插入图片描述

前言

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

但是他们都是仅仅针对规则的矩形建筑,如果是不规则的比如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,如果学习过程中遇到任何问题,也欢迎你评论私信找我。

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


网站公告

今日签到

点亮在社区的每一天
去签到