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

news2025/1/2 2:43:07

文章目录

  • 最终效果
  • 前言
  • 绘制开始场景
  • 素材
  • 开始
  • 放置
  • 旋转物体
  • 扩展优化
    • 1. 绘制地图边界,确保放置物品在指定区域内工作
    • 2. 让模型所占面积大小更加准确
    • 3. 隐藏白色瓦片指示区域
  • 最终效果
  • 其他
  • 源码
  • 参考
  • 完结

最终效果

在这里插入图片描述

前言

其实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://www.youtube.com/watch?v=rKp9fWvmIww&t=567s

完结

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

好了,我是向宇,https://xiangyu.blog.csdn.net

一位在小公司默默奋斗的开发者,出于兴趣爱好,于是最近才开始自习unity。如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1250597.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

论文阅读——DDeP(cvpr2023)

分割标签耗时且贵&#xff0c;所以常常使用预训练提高分割模型标签有效性&#xff0c;反正就是&#xff0c;需要一个预训练分割模型。典型的分割模型encoder部分通过分类任务预训练&#xff0c;decoder部分参数随机初始化。作者认为这个方法次优&#xff0c;尤其标签比较少的情…

代码随想录算法训练营第四十四天|57. 爬楼梯、322.零钱兑换、279. 完全平方数

KamaCoder 57. 爬楼梯 题目链接&#xff1a;题目页面 (kamacoder.com) 这道题使用完全背包来实现&#xff0c;我们首先考虑的是总的楼梯数&#xff0c;因此dp数组大小为n 1 &#xff0c;其意义是&#xff0c;在n阶时有多少种方法爬到楼顶&#xff0c;因此&#xff0c;当前n状…

【腾讯云云上实验室-向量数据库】用向量数据库——实现高效文本检索功能

文章目录 前言Tencent Cloud VectorDB 简介Tencent Cloud VectorDB 使用实战申请腾讯云向量数据库腾讯云向量数据库使用步骤腾讯云向量数据库实现文本检索 结论和建议 前言 想必各位开发者一定使用过关系型数据库MySQL去存储我们的项目的数据&#xff0c;也有部分人使用过非关…

【spring(五)】SpringMvc总结 SSM整合流程

目录 一、SpringMVC简介&#xff1a; 二、SpringMVC快速入门&#xff1a; 三、SpringMVC bean的管理&#xff1a;⭐ ①配置bean ②扫描bean 四、SpringMVC配置类&#xff1a;⭐ 五、SpringMVC 请求与响应 六、SpringMVC REST风格 七、SSM整合 异常处理&#xff1a; 八、…

Windows核心编程 进程遍历与文件加密

目录 进程的遍历 CreateToolhelp32Snapshot Process32First Process32Next 文件加密 使用openssl库进行DES加密 进程的遍历 什么是快照&#xff1a;虚拟中的快照&#xff1a; 我们在分析病毒&#xff0c;分析木马的时候&#xff0c;不能在真机分析&#xff0c;在虚拟机中…

【多线程】-- 02 线程创建之实现Runnable初识多线程并发问题

多线程 2 线程创建 2.2 实现Runnable接口 【学习提示】查看JDK帮助文档 定义MyRunnable类实现Runnable接口实现run()方法&#xff0c;编写线程执行体创建线程对象&#xff0c;调用start()方法启动线程 package com.duo.demo01;//创建线程方式二&#xff1a;实现Runnable接…

分布式事务,一致性理论, 两阶段提交(2PC), 三阶段提交(3PC),Seata分布式事务方案

文章目录 分布式事务&#xff1a;1、一致性理论2、两阶段提交&#xff08;2PC&#xff09;3、三阶段提交&#xff08;3PC&#xff09;4、Seata分布式事务方案 上一篇降到了 分布式锁&#xff0c;先来和大家聊一聊分布式事务&#xff0c; 分布式锁的链接如下&#xff1a; http…

位图的详细讲解

位运算操作符&#xff1a;或&#xff0c;与&#xff0c;异或&#xff0c;按位取反。 操作符 |两个中有一个是一则为一&两个都是一则为一^相同为零&#xff0c;不同为一~零变成一&#xff0c;一变成零 什么是位运算符: 位运算是直接对整型数据的二进制进行运算。 位图概念…

物流公司打印用什么软件,佳易王物流运单打印管理系统软件下载

物流公司打印用什么软件&#xff0c;佳易王物流运单打印管理系统软件下载 软件特色&#xff1a; 1、功能实用&#xff0c;操作简单&#xff0c;不会电脑也会操作&#xff0c;软件免安装&#xff0c;已内置数据库。 2、物流开单打印&#xff0c;可以打印两联单或三联单&#x…

X-RAY POC编写

POC(Proof of Concept) - 利用证明 POC&#xff0c;Proof of Concept&#xff0c;意思是 利用证明。这个短语会在漏洞报告中使用&#xff0c;漏洞报告中的POC则是一段说明或者一个攻击的漏洞介绍&#xff0c;使得读者能够确认这个漏洞是真实存在的。 EXP(Exploit) - 漏洞利用…

GDPU 数据结构 天码行空11

文章目录 数据结构实验十一 图的创建与存储一、实验目的二、实验内容三、【实验源代码】&#x1f37b; CPP版&#x1f37b; c 语言版&#x1f37b; java版 四、【实验结果】五、【实验总结】 数据结构实验十一 图的创建与存储 一、实验目的 1、 理解图的存储结构与基本操作&a…

【STM32】新建工程

学习来源&#xff1a;[2-2] 新建工程_哔哩哔哩_bilibili 目前STM32的开发主要有基于寄存器的开发方式、基于标准库也就是库函数的方式和基于HAL库的方式。本学习是基于库函数的方式。&#xff08;各种资料去百度云下载&#xff09; 1 建立工程文件夹 Keil中新建工程&#xf…

高性能Mysql第三版学习(一)

学习目标&#xff1a; 高性能Mysql第3版 学习内容&#xff1a; MySQL架构与历史Mysql基座测试服务器性能Schema与数据类型优化创建高性能的索引查询性能优化Mysql高级特性Explain 学习时间&#xff1a; 周一至周五晚上 9点—晚上10点周六晚上9点-10点周日晚上9 点-10点 学习…

公众号留言功能还有可能开放吗?

为什么公众号没有留言功能&#xff1f;2018年2月12日&#xff0c;TX新规出台&#xff1a;根据相关规定和平台规则要求&#xff0c;我们暂时调整留言功能开放规则&#xff0c;后续新注册帐号无留言功能。这就意味着2018年2月12日号之后注册的公众号不论个人主体还是组织主体&…

初出茅庐的小李博客之C语言必备知识共用体

C语言必备知识共用体 共用体是一种构造数据类型&#xff0c;有时候也称之为联合体。 它的用途&#xff1a; 使几个不同类型的变量共占一段内存。 共用体举例 union 共用体名 { 类型标识符 成员名;类型标识符 成员名; };union data //共用体名字是data{ int i; …

面试题:什么是自旋锁?自旋的好处和后果是什么呢?

文章目录 什么是自旋自旋和非自旋的获取锁的流程 自旋锁的好处AtomicLong 的实现实现一个可重入的自旋锁示例自旋的缺点适用场景 什么是自旋 “自旋”可以理解为“自我旋转”&#xff0c;这里的“旋转”指“循环”&#xff0c;比如 while 循环或者 for 循环。“自旋”就是自己…

初学剪辑者找视频素材就上这6个网站

视频剪辑必备的6个素材网站&#xff0c;高清无水印&#xff0c;还可以免费下载&#xff0c;无版权限制&#xff0c;赶紧收藏起来&#xff01; 1、菜鸟图库 https://www.sucai999.com/video.html?vNTYxMjky 菜鸟图库网素材非常丰富&#xff0c;网站主要以设计类素材为主&#…

宝塔 Linux 面板安装一个高大上的论坛程序 —— Flarum

这个是很早搭建的版本,基于宝塔面板,比较复杂,如果想要简单的搭建方法,可以参看咕咕新写的这篇: 【好玩的 Docker 项目】10 分钟搭建一个高大上的论坛程序 购买腾讯云轻量应用服务器 待补充 登录服务器 待补充 BBR 加速脚本 BBR 加速脚本: BASH cd /usr/src &…

XML映射文件

<?xml version"1.0" encoding"UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace"org.mybatis.example.BlogMapper&q…

Chatbot开发三剑客:LLAMA、LangChain和Python

聊天机器人&#xff08;Chatbot&#xff09;开发是一项充满挑战的复杂任务&#xff0c;需要综合运用多种技术和工具。在这一领域中&#xff0c;LLAMA、LangChain和Python的联合形成了一个强大的组合&#xff0c;为Chatbot的设计和实现提供了卓越支持。 首先&#xff0c;LLAMA是…