最终效果
前言
其实3d物品建造装修系统之前就已经做过了,感兴趣的可以去看看:手搓一个网格放置功能,及装修建造种植功能
但是它有一些缺点,比如网格是自己绘制的,使用起来可能比较麻烦,所有这里分享另一种更加简单的方法。就是使用tilemap,可以省略自己绘制复杂网格的时间,但是缺点可能就是玩家无法在游戏界面看到网格的具体位置,当然,实现功能千千万万,选择自己喜欢的就行。
绘制开始场景
在平台上放置tilemap,并配置对应参数
简单绘制,效果
素材
可以寻找下载你喜欢的模型,导入到项目中
这里我推荐个地址
https://sketchfab.com/Cytiene/collections/great-downloadable-models-6304c532e52649f59de0de234edcb91f
开始
新增可放置对象脚本PlaceableObject ,暂时什么都不做
public class PlaceableObject : MonoBehaviour { }
所有模型物品都挂载脚本,并给模型添加碰撞体
新增BuildingSystem,定义一个建筑系统的脚本
public class BuildingSystem : MonoBehaviour { public static BuildingSystem current; public GridLayout gridLayout; private Grid grid; [SerializeField] private Tilemap mainTilemap; // 地图的Tilemap组件 [SerializeField] private TileBase whiteTile; // 白色方块的TileBase public GameObject prefab1; // 预制体1 public GameObject prefab2; // 预制体2 private PlaceableObject objectToPlace; // 当前要放置的对象 private void Awake() { current = this; grid = gridLayout.gameObject.GetComponent<Grid>(); // 获取网格组件 } //测试切换不同模型物品 private void Update() { if (Input.GetKeyDown(KeyCode.A)) { InitializeWithObject(prefab1); } else if (Input.GetKeyDown(KeyCode.B)) { InitializeWithObject(prefab2); } } // 工具方法:将鼠标位置转换为世界坐标系下的位置 public static Vector3 GetMouseWorldPosition() { // 从相机发出一条射线,将鼠标位置转换为世界坐标系下的位置 Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); // 如果射线碰撞到物体,则返回碰撞点的世界坐标 if (Physics.Raycast(ray, out RaycastHit raycastHit)) { return raycastHit.point; } else // 否则返回零向量 { return Vector3.zero; } } // 将坐标对齐到网格上 public Vector3 SnapCoordinateToGrid(Vector3 position) { Vector3Int cellPos = gridLayout.WorldToCell(position); // 将世界坐标转换为网格单元坐标 position = grid.GetCellCenterWorld(cellPos); // 获取网格单元中心点的世界坐标 return position; } //初始化放置物体 public void InitializeWithObject(GameObject prefab) { // 将物体初始位置设为网格对齐的原点 Vector3 position = SnapCoordinateToGrid(Vector3.zero); // 在初始位置实例化物体 GameObject obj = Instantiate(prefab, position, Quaternion.identity); // 获取PlaceableObject组件并添加ObjectDrag组件 objectToPlace = obj.GetComponent<PlaceableObject>(); // 获取可放置物体组件 obj.AddComponent<ObjectDrag>(); // 添加拖拽组件 } }
新增ObjectDrag,定义一个物体拖拽的脚本,注意物品移动要有碰撞体,不然拖拽不会生效
public class ObjectDrag : MonoBehaviour { private Vector3 offset; // 鼠标按下时物体和鼠标之间的偏移量 // 当鼠标按下时记录偏移量 private void OnMouseDown() { offset = transform.position - BuildingSystem.GetMouseWorldPosition(); } // 当鼠标拖动时移动物体并对齐到网格上 private void OnMouseDrag() { Vector3 pos = BuildingSystem.GetMouseWorldPosition() + offset; transform.position = BuildingSystem.current.SnapCoordinateToGrid(pos); } }
挂载脚本配置
效果,按AB生成不同的物品,点击物品可以进行拖拽
当然,你也可以修改ObjectDrag,直接使用Update方法,让物品一直跟随鼠标移动
// 每帧更新建筑物的位置 private void Update() { Vector3 pos = BuildingSystem.GetMouseWorldPosition() + offset; transform.position = BuildingSystem.current.SnapCoordinateToGrid(pos); }
放置
修改PlaceableObject
public class PlaceableObject : MonoBehaviour { // 是否已经放置 public bool Placed { get; private set; } // 物体占据的格子数 public Vector3Int Size { get; private set; } // 物体碰撞器的四个顶点(本地坐标系) private Vector3[] Vertices; private void Start() { // 获取物体碰撞器的四个顶点 GetColliderVertexPositionsLocal(); // 计算物体占据的格子数 CalculateSizeInCells(); } // 获取物体碰撞器的四个顶点 private void GetColliderVertexPositionsLocal() { BoxCollider b = gameObject.GetComponent<BoxCollider>(); Vertices = new Vector3[4]; Vertices[0] = b.center + new Vector3(-b.size.x, -b.size.y, -b.size.z) * 0.5f; Vertices[1] = b.center + new Vector3(b.size.x, -b.size.y, -b.size.z) * 0.5f; Vertices[2] = b.center + new Vector3(b.size.x, -b.size.y, b.size.z) * 0.5f; Vertices[3] = b.center + new Vector3(-b.size.x, -b.size.y, b.size.z) * 0.5f; } // 计算物体占据的格子数 private void CalculateSizeInCells() { Vector3Int[] vertices = new Vector3Int[Vertices.Length]; for (int i = 0; i < vertices.Length; i++) { // 将物体顶点从本地坐标系转换到世界坐标系 Vector3 worldPos = transform.TransformPoint(Vertices[i]); // 将世界坐标系中的位置转换成格子坐标系中的位置 vertices[i] = BuildingSystem.current.gridLayout.WorldToCell(worldPos); } // 计算物体占据的格子数 Size = new Vector3Int( Mathf.Abs(vertices[0].x - vertices[1].x), Mathf.Abs(vertices[0].y - vertices[3].y), 1 ); } // 获取物体的起始位置(左下角的格子位置) public Vector3 GetStartPosition() { return transform.TransformPoint(Vertices[0]); } // 放置物体 public virtual void Place() { // 删除物体拖拽组件 ObjectDrag drag = gameObject.GetComponent<ObjectDrag>(); Destroy(drag); // 标记物体已经放置 Placed = true; // TODO:触发放置事件 } }
修改BuildingSystem
private void Update() { //。。。 //放置测试 if (Input.GetKeyDown(KeyCode.Space)) { if (CanBePlaced(objectToPlace)) // 检查物体是否可以放置 { objectToPlace.Place(); // 放置物体 Vector3Int start = gridLayout.WorldToCell(objectToPlace.GetStartPosition()); // 将世界坐标转换为格子坐标 TakeArea(start, objectToPlace.Size); // 将物体所占据的区域填充为白色瓦片 } else { Destroy(objectToPlace.gameObject); // 物体无法放置,销毁物体 } } else if (Input.GetKeyDown(KeyCode.Escape)) { Destroy(objectToPlace.gameObject); // 按下 Esc 键,销毁物体 } } //获取一个区域内的瓦片信息数组 private static TileBase[] GetTilesBlock(BoundsInt area, Tilemap tilemap) { TileBase[] array = new TileBase[area.size.x * area.size.y * area.size.z]; int counter = 0; foreach (var v in area.allPositionsWithin) { Vector3Int pos = new Vector3Int(v.x, v.y, 0); array[counter] = tilemap.GetTile(pos); // 获取指定位置上的瓦片 counter++; } return array; } //检查物体是否可以放置在指定位置 private bool CanBePlaced(PlaceableObject placeableObject) { BoundsInt area = new BoundsInt(); area.position = gridLayout.WorldToCell(placeableObject.GetStartPosition()); // 将世界坐标转换为格子坐标 area.size = placeableObject.Size; // 获取物体所占据的格子大小 TileBase[] baseArray = GetTilesBlock(area, mainTilemap); // 获取该区域内的瓦片数组 foreach (var b in baseArray) { if (b == whiteTile) // 如果有白色瓦片,表示物体无法放置 { return false; } } return true; // 没有白色瓦片,可以放置物体 } //在指定区域填充为白色瓦片 public void TakeArea(Vector3Int start, Vector3Int size) { mainTilemap.BoxFill(start, whiteTile, start.x, start.y, start.x + size.x, start.y + size.y); // 将指定区域填充为白色瓦片 }
效果,物体重叠会直接销毁物品
旋转物体
修改PlaceableObject
//旋转 public void Rotate() { transform.Rotate(eulers: new Vector3(0, 90, 0)); // 绕 Y 轴顺时针旋转 90 度 // 交换长宽并限制高度为 1 Size = new Vector3Int(Size.y, Size.x, 1); // 旋转顶点数组 Vector3[] vertices = new Vector3[Vertices.Length]; for (int i = 0; i < vertices.Length; i++) { vertices[i] = Vertices[(i + 1) % Vertices.Length]; // 将顶点数组顺时针旋转 } Vertices = vertices; // 更新顶点数组 }
修改BuildingSystem,调用
private void Update() { //。。。 //按回车旋转物体 if (Input.GetKeyDown(KeyCode.Return)) { objectToPlace.Rotate(); } }
效果
扩展优化
1. 绘制地图边界,确保放置物品在指定区域内工作
在建筑区域周围绘制瓷砖边框,这将增加图块地图边界效果,并确保放置物品在指定区域内工作。
2. 让模型所占面积大小更加准确
现在TileMap每格网格比较大,为了让模型所占面积大小更加准确,可以适当缩小Grid的比例
效果
3. 隐藏白色瓦片指示区域
实际使用我们肯定不想看到白色瓦片所显示的指示区域,我们可以关闭Tilemap Renderer,或者修改TileMap颜色透明的为0
效果
最终效果
其他
后续其他内容我就不继续完善了,留给大家自己去发挥,比如
- 添加一些放置特效、动画、音效
- 删除功能
- 无法放置显示红色,未放置显示蓝色
- 显示可放置物品UI,切换物品
- 等等。。。
源码
https://gitcode.net/unity1/3dplacesystem