【unity实战】实现一个放置3d物品建造装修系统(附项目源码)

简介: 【unity实战】实现一个放置3d物品建造装修系统(附项目源码)

最终效果

前言

其实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

参考

【视频】https://www.youtube.com/watch?v=rKp9fWvmIww&t=567s

目录
相关文章
|
2天前
|
存储 JSON 关系型数据库
【unity实战】制作unity数据保存和加载系统——大型游戏存储的最优解
【unity实战】制作unity数据保存和加载系统——大型游戏存储的最优解
11 2
|
2天前
|
图形学
【unity实战】Unity中基于瓦片的网格库存系统——类似《逃离塔科夫》的库存系统(下)
【unity实战】Unity中基于瓦片的网格库存系统——类似《逃离塔科夫》的库存系统
7 0
|
2天前
|
图形学 容器
【unity实战】Unity中基于瓦片的网格库存系统——类似《逃离塔科夫》的库存系统(上)
【unity实战】Unity中基于瓦片的网格库存系统——类似《逃离塔科夫》的库存系统
5 0
|
2天前
|
图形学
【制作100个unity游戏之27】使用unity复刻经典游戏《植物大战僵尸》,制作属于自己的植物大战僵尸随机版和杂交版3(附带项目源码)
【制作100个unity游戏之27】使用unity复刻经典游戏《植物大战僵尸》,制作属于自己的植物大战僵尸随机版和杂交版3(附带项目源码)
13 2
|
2天前
|
图形学
【制作100个unity游戏之28】花半天时间用unity复刻童年4399经典小游戏《黄金矿工》(附带项目源码)
【制作100个unity游戏之28】花半天时间用unity复刻童年4399经典小游戏《黄金矿工》(附带项目源码)
10 0
|
2天前
|
图形学
【制作100个unity游戏之29】使用unity复刻经典游戏《愤怒的小鸟》(完结,附带项目源码)(上)
【制作100个unity游戏之29】使用unity复刻经典游戏《愤怒的小鸟》(完结,附带项目源码)
10 2
|
2天前
|
图形学
【制作100个unity游戏之27】使用unity复刻经典游戏《植物大战僵尸》,制作属于自己的植物大战僵尸随机版和杂交版2(附带项目源码)
【制作100个unity游戏之27】使用unity复刻经典游戏《植物大战僵尸》,制作属于自己的植物大战僵尸随机版和杂交版2(附带项目源码)
8 1
|
2天前
|
存储 JSON 图形学
【unity实战】制作unity数据保存和加载系统——小型游戏存储的最优解
【unity实战】制作unity数据保存和加载系统——小型游戏存储的最优解
6 0
|
2天前
|
图形学
【制作100个unity游戏之29】使用unity复刻经典游戏《愤怒的小鸟》(完结,附带项目源码)(下)
【制作100个unity游戏之29】使用unity复刻经典游戏《愤怒的小鸟》(完结,附带项目源码)(下)
8 0
|
2天前
|
存储 JSON 关系型数据库
【制作100个unity游戏之27】使用unity复刻经典游戏《植物大战僵尸》,制作属于自己的植物大战僵尸随机版和杂交版13(完结,附带项目源码)
【制作100个unity游戏之27】使用unity复刻经典游戏《植物大战僵尸》,制作属于自己的植物大战僵尸随机版和杂交版13(完结,附带项目源码)
9 0