【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月前
|
图形学 开发者 UED
Unity游戏开发必备技巧:深度解析事件系统运用之道,从生命周期回调到自定义事件,打造高效逻辑与流畅交互的全方位指南
【8月更文挑战第31天】在游戏开发中,事件系统是连接游戏逻辑与用户交互的关键。Unity提供了多种机制处理事件,如MonoBehaviour生命周期回调、事件系统组件及自定义事件。本文介绍如何有效利用这些机制,包括创建自定义事件和使用Unity内置事件系统提升游戏体验。通过合理安排代码执行时机,如在Awake、Start等方法中初始化组件,以及使用委托和事件处理复杂逻辑,可以使游戏更加高效且逻辑清晰。掌握这些技巧有助于开发者更好地应对游戏开发挑战。
92 0
|
3月前
|
图形学 C# 开发者
Unity粒子系统全解析:从基础设置到高级编程技巧,教你轻松玩转绚丽多彩的视觉特效,打造震撼游戏画面的终极指南
【8月更文挑战第31天】粒子系统是Unity引擎的强大功能,可创建动态视觉效果,如火焰、爆炸等。本文介绍如何在Unity中使用粒子系统,并提供示例代码。首先创建粒子系统,然后调整Emission、Shape、Color over Lifetime等模块参数,实现所需效果。此外,还可通过C#脚本实现更复杂的粒子效果,增强游戏视觉冲击力和沉浸感。
137 0
|
3月前
|
开发者 图形学 前端开发
绝招放送:彻底解锁Unity UI系统奥秘,五大步骤教你如何缔造令人惊叹的沉浸式游戏体验,从Canvas到动画,一步一个脚印走向大师级UI设计
【8月更文挑战第31天】随着游戏开发技术的进步,UI成为提升游戏体验的关键。本文探讨如何利用Unity的UI系统创建美观且功能丰富的界面,包括Canvas、UI元素及Event System的使用,并通过具体示例代码展示按钮点击事件及淡入淡出动画的实现过程,助力开发者打造沉浸式的游戏体验。
79 0
|
3月前
|
图形学
Unity动画☀️Unity动画系统Bug集合
Unity动画☀️Unity动画系统Bug集合
|
5月前
|
存储 JSON 关系型数据库
【unity实战】制作unity数据保存和加载系统——大型游戏存储的最优解
【unity实战】制作unity数据保存和加载系统——大型游戏存储的最优解
136 2
|
3月前
|
图形学 C#
超实用!深度解析Unity引擎,手把手教你从零开始构建精美的2D平面冒险游戏,涵盖资源导入、角色控制与动画、碰撞检测等核心技巧,打造沉浸式游戏体验完全指南
【8月更文挑战第31天】本文是 Unity 2D 游戏开发的全面指南,手把手教你从零开始构建精美的平面冒险游戏。首先,通过 Unity Hub 创建 2D 项目并导入游戏资源。接着,编写 `PlayerController` 脚本来实现角色移动,并添加动画以增强视觉效果。最后,通过 Collider 2D 组件实现碰撞检测等游戏机制。每一步均展示 Unity 在 2D 游戏开发中的强大功能。
144 6
|
2月前
|
测试技术 C# 图形学
掌握Unity调试与测试的终极指南:从内置调试工具到自动化测试框架,全方位保障游戏品质不踩坑,打造流畅游戏体验的必备技能大揭秘!
【9月更文挑战第1天】在开发游戏时,Unity 引擎让创意变为现实。但软件开发中难免遇到 Bug,若不解决,将严重影响用户体验。调试与测试成为确保游戏质量的最后一道防线。本文介绍如何利用 Unity 的调试工具高效排查问题,并通过 Profiler 分析性能瓶颈。此外,Unity Test Framework 支持自动化测试,提高开发效率。结合单元测试与集成测试,确保游戏逻辑正确无误。对于在线游戏,还需进行压力测试以验证服务器稳定性。总之,调试与测试贯穿游戏开发全流程,确保最终作品既好玩又稳定。
79 4
|
3月前
|
图形学 缓存 算法
掌握这五大绝招,让您的Unity游戏瞬间加载完毕,从此告别漫长等待,大幅提升玩家首次体验的满意度与留存率!
【8月更文挑战第31天】游戏的加载时间是影响玩家初次体验的关键因素,特别是在移动设备上。本文介绍了几种常见的Unity游戏加载优化方法,包括资源的预加载与异步加载、使用AssetBundles管理动态资源、纹理和模型优化、合理利用缓存系统以及脚本优化。通过具体示例代码展示了如何实现异步加载场景,并提出了针对不同资源的优化策略。综合运用这些技术可以显著缩短加载时间,提升玩家满意度。
89 5
|
2月前
|
前端开发 图形学 开发者
【独家揭秘】那些让你的游戏瞬间鲜活起来的Unity UI动画技巧:从零开始打造动态按钮,提升玩家交互体验的绝招大公开!
【9月更文挑战第1天】在游戏开发领域,Unity 是最受欢迎的游戏引擎之一,其强大的跨平台发布能力和丰富的功能集让开发者能够迅速打造出高质量的游戏。优秀的 UI 设计对于游戏至关重要,尤其是在手游市场,出色的 UI 能给玩家留下深刻的第一印象。Unity 的 UGUI 系统提供了一整套解决方案,包括 Canvas、Image 和 Button 等组件,支持添加各种动画效果。
114 3
|
2月前
|
设计模式 存储 人工智能
深度解析Unity游戏开发:从零构建可扩展与可维护的游戏架构,让你的游戏项目在模块化设计、脚本对象运用及状态模式处理中焕发新生,实现高效迭代与团队协作的完美平衡之路
【9月更文挑战第1天】游戏开发中的架构设计是项目成功的关键。良好的架构能提升开发效率并确保项目的长期可维护性和可扩展性。在使用Unity引擎时,合理的架构尤为重要。本文探讨了如何在Unity中实现可扩展且易维护的游戏架构,包括模块化设计、使用脚本对象管理数据、应用设计模式(如状态模式)及采用MVC/MVVM架构模式。通过这些方法,可以显著提高开发效率和游戏质量。例如,模块化设计将游戏拆分为独立模块。
149 3