当前位置: 首页 > web >正文

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

最终效果

在这里插入图片描述

文章目录

  • 最终效果
  • 前言
    • 实战
    • 1、素材
    • 2、新增建筑物形状单元脚本
    • 2、建筑物模型脚本
    • 3、创建不同形状的模型预制体
    • 4、在场景视图可视化建造网格
    • 5、新增ScriptableObject创建不同的建筑数据
    • 6、建筑预览类和建筑实体类
    • 7、实现网格建造
  • 源码
  • 参考
  • 专栏推荐
  • 完结

前言

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

  • 【unity实战】实现2D3D游戏通用,类似《我的世界》《七日杀》《星露谷物语》的Unity 网格建造方块放置堆叠系统(附带源码)
    在这里插入图片描述

  • 【用unity实现100个游戏之4】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)
    在这里插入图片描述

  • 【【unity实战】实现一个放置3d物品网格建造装修系统(附项目源码)
    在这里插入图片描述

  • 【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的位置并转换为Listreturn 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视图绘制网格Gizmovoid 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,如果学习过程中遇到任何问题,也欢迎你评论私信找我。

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

http://www.xdnf.cn/news/17686.html

相关文章:

  • 【实用案例】录音分片上传的核心逻辑和实现案例【文章附有代码】
  • Godot ------ 平滑拖动03
  • SpringBoot 自动配置核心机制(面试高频考点)
  • Orange的运维学习日记--38.MariaDB详解与服务部署
  • JavaEE 初阶第十七期:文件 IO 的 “管道艺术”(下)
  • 《范仲淹传》读书笔记与摘要
  • 使用frp内网穿透实现远程办公
  • 基于AI量化模型的比特币周期重构:传统四年规律是否被算法因子打破?
  • Python(9)-- 异常模块与包
  • AI Coding 概述及学习路线图
  • Elasticsearch Node.js 客户端的安装
  • 【功能测试】软件集成测试思路策略与经验总结
  • FFmpeg - 基本 API大全(视频编解码相关的)
  • 【数据结构】深入理解顺序表与通讯录项目的实现
  • leetcode-hot-100 (图论)
  • CobaltStrike的搭建和使用
  • 爬虫与数据分析实战
  • 【09-神经网络介绍2】
  • 一文读懂 C# 中的 Lazy<T>
  • 第10节 大模型分布式推理典型场景实战与架构设计
  • Godot ------ 平滑拖动02
  • Apache Ignite 核心组件:GridClosureProcessor解析
  • C# 异步编程(计时器)
  • Python: configparser库 ini文件操作库
  • 使用MAS(Microsoft Activation Scripts)永久获得win10专业版和office全套
  • Edit Distance
  • react中父子数据流动和事件互相调用(和vue做比较)
  • GO学习记录三
  • 基于MongoDB/HBase的知识共享平台的设计与实现
  • 【Dv3Admin】菜单转换选项卡平铺到页面