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

文章目录

  • 最终效果
  • 前言
  • 绘制开始场景
  • 素材
  • 开始
  • 放置
  • 旋转物体
  • 扩展优化
    • 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.mfbz.cn/a/187344.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vue框架学习笔记——事件处理

文章目录 前文提要事件处理的解析过程样例代码如下&#xff1a;效果展示图片&#xff1a;v-on:click"响应函数"v-on:click简写形式响应函数添加响应函数传参占位符"$event"注意事项 前文提要 本人仅做个人学习记录&#xff0c;如有错误&#xff0c;请多包…

1、windows10系统下Qt5.12.0与卸载

一、安装包下载 1、Qt社区下载 https://download.qt.io/archive/qt/5.12/5.12.10/qt-opensource-windows-x86-5.12.10.exe 2、百度网盘下载 链接&#xff1a;百度网盘 请输入提取码 3、Qt官网下载&#xff1a; Try Qt | 开发应用程序和嵌入式系统 | Qt 二、安装提示 下…

android实战项目之二十二---如何快速APP中集成支付宝和微信支付功能

效果图 实现方案 jcenter 集成方式 implementation com.xgr.easypay:EasyPay:2.0.5 // 基类库&#xff0c;必选 implementation com.xgr.easypay:wechatpay:2.0.5 // 微信支付&#xff0c;可选 implementation com.xgr.easypay:alipay:2.0.5 // 支付宝支付&#xff0c;可…

vivado产生报告阅读分析21

其他命令选项 • -of_objects <suggestion objects> &#xff1a; 启用特定建议的报告。在此模式下运行时 &#xff0c; report_qor_suggestions 不会生成新建议。此命令可快速执行 &#xff0c; 读取 RQS 文件后 &#xff0c; 此命令可用于查看其中包 含的建议。其…

[JVM] 京东一面~说一下Java 类加载过程

系统加载 Class 类型的文件主要三步&#xff1a;加载->连接->初始化。连接过程又可分为三步&#xff1a;验证->准备->解析。 通过全限定名来加载生成 class 对象到内存中&#xff0c;然后进行验证这个 class 文件&#xff0c;包括文件格式校验、元数据验证&#xf…

从零开始学习管道:进程通信的概念,特点和示例

&#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;Linux &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 本博客主要内容通过进程通信的概念&#xff0c;引入管道&#xff0c;实…

Leetcode201. 数字范围按位与

Every day a Leetcode 题目来源&#xff1a;201. 数字范围按位与 最直观的解决方案就是迭代范围内的每个数字&#xff0c;依次执行按位与运算&#xff0c;得到最终的结果&#xff0c;但此方法在 [left, right] 范围较大的测试用例中会因超出时间限制而无法通过&#xff0c;因…

linux开启apache服务

vim /etc/apache2/ports.conf 键盘输入i 进入插入编辑模式&#xff0c;修改apache2默认监听端口号为8080 &#xff0c;编辑好后&#xff0c;按Esc键“&#xff1a;wq!” 保存退出。&#xff08;注&#xff1a;端口也可以不修改&#xff09; 在终端输入“/etc/init.d/apache2 …

【JVM】一篇通关JVM垃圾回收

目录 1. 如何判断对象可以回收1-1. 引用计数法1-2. 可达性分析算法1-3. 四种引用强引用软引用弱引用虚引用终结器引用 2. 垃圾回收算法3. 分代垃圾回收4. 垃圾回收器5. 垃圾回收调优 1. 如何判断对象可以回收 1-1. 引用计数法 引用计数法 只要一个对象被其他变量所引用&…

qgis添加arcgis的FeatureServer

左侧浏览器-ArcGIS要素服务器-新建连接 http://sampleserver6.arcgisonline.com/arcgis/rest/services/ 展开-双击即可

通过ros系统中websocket中发送sensor_msgs::Image数据给web端显示(三)

通过ros系统中websocket中发送sensor_msgs::Image数据给web端显示(三) 不使用base64编码方式传递 #include <ros/ros.h> #include <signal.h> #include <sensor_msgs/Image.h> #include <message_filters/subscriber.h> #include <message_filter…

Linux的gcc,gdb基础

执行详解: 1)如何执行 路径可执行文件名 或者 路径可执行文件名 & (将进程放到后台执行); 可以把可执行文件放到 /usr/bin 就可以省略路径了; 思考:为什么? ps :/usr/bin ps,ls,pwd (先了解,后期写项目就知道为什么了) 2)两步执行与一步执行 a.可以三步合为一步,即…

中职组网络安全-FTPServer20221010.img(环境+解析)

任务环境说明&#xff1a; √服务器场景&#xff1a;FTPServer20221010.img √服务器操作系统&#xff1a;未知&#xff08;关闭链接&#xff09; √FTP用户名&#xff1a;attack817 密码&#xff1a;attack817 1.分析attack.pcapng数据包文件&#xff0c;通过分析数据包attack…

MIT6.824-Raft笔记:Raft初探、副本间log时序

从宏观角度说明raft在程序中的作用&#xff0c;和客户端的关系&#xff0c;以及多个副本之间的关系&#xff1b;从微观角度说明多个副本之间raft对日志处理的流程。 1. Raft 初探 宏观角度说明raft在程序中的作用&#xff0c;和客户端的关系&#xff0c;以及多个副本之间的关…

群晖(Synology)NAS 存储池修复需要的时间

群晖&#xff08;Synology&#xff09;NAS 存储池的处理可以说是非常耗时的。 根据官方文档的说明和算法&#xff1a; 一个 10TB 的存储池修复将会差不多 24 个小时。 如果你更换硬盘后对存储池进行处理的话&#xff0c;通常需要等上个几天时间吧。 群晖&#xff08;Synology…

Tabular特征选择基准

学术实验中的表格基准通常是一小组精心选择的特征。相比之下,工业界数据科学家通常会收集尽可能多的特征到他们的数据集中,甚至从现有的特征中设计新的特征。为了防止在后续的下游建模中过拟合,数据科学家通常使用自动特征选择方法来获得特征子集。Tabular特征选择的现有基准…

JVM 内存分析工具 MAT及实践

线程分析工具 MAT 官网下载地址&#xff1a;http://www.eclipse.org/mat/downloads.php mat百度网盘链接&#xff1a;&#xff08;速度更快&#xff09; 链接&#xff1a;https://pan.baidu.com/s/1tMp8MQIXuPtg9zBgruO0Ug?pwdjqtv 提取码&#xff1a;jqtv jdk17 百度网盘链接…

HCIP-八、路由引入

八、路由引入 实验拓扑实验需求及解法1.配置所有设备的IP地址。2.R1/2/3/4运行OSPF3.R3/4/5运行IS-IS4.在R3/4上将OSPF 1引入IS-IS5.在R3/4上将IS-IS引入OSPF6.路径优化 实验拓扑 实验需求及解法 本实验模拟OSPF与IS-IS互联的网络环境&#xff0c;完成以下需求&#xff1a; 1…

vivado产生报告阅读分析22

“ Advanced ”选项卡 “ Advanced ” &#xff08; 高级 &#xff09; 选项卡如下图所示。 在“ Advanced ”选项卡中提供了以下字段 &#xff1a; • “ Report ” &#xff08; 报告 &#xff09;&#xff1a; 选中“ Advanced ”选项卡中的“ Cells to Analyze ” &…

Flink Flink中的分流

一、什么是分流 所谓“分流”&#xff0c;就是将一条数据流拆分成完全独立的两条、甚至多条流。也就是基于一个DataStream&#xff0c;定义一些筛选条件&#xff0c;将符合条件的数据拣选出来放到对应的流里。 二、基于filter算子的简单实现分流 其实根据条件筛选数据的需求…
最新文章