3D沙盒游戏开发日志2——网格和建筑物放置系统

2024-02-17 05:20

本文主要是介绍3D沙盒游戏开发日志2——网格和建筑物放置系统,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

日志

沙盒游戏的灵魂当然是足够高的建筑自由度来打造自己的世界,所以我就先来制作一个初级的建筑系统。观察各个沙盒游戏(饥荒,我的世界)等,他们的建筑物放置都是以网格为单位的而不是精确的浮点数坐标,
我想原因无非是节省内存上的开销并且给玩家提供更好的游戏理解(只需要记住几格就好),所以在制作建造系统
前需要先制作网格

世界网格

首先思考哪些物品是以网格为坐标单位的,人物的移动肯定不是,我们自己建造的建筑和地图生成的建筑物肯定是,游戏中的某些生物也是以网格为单位表现行为的,还有我们生成世界时也是以网格为单位规划世界大小的。
网格肯定不能过大,至少要小于所有的建筑物,并且存储、计算、表示都要简单不然就失去了使用它的意义,所以我决定就直接取整数作为网格坐标,每个网格的大小也就是1 * 1

public class GridPos
{public short x;public short y;public short z;public Vector3 Pos{get => new Vector3(x, y, z);}public static GridPos GetGridPos(Vector3 pos){GridPos gridPos = new GridPos();gridPos.x = (short)pos.x;gridPos.y = (short)pos.y;gridPos.z = (short)pos.z;return gridPos;}
}

然后就是建筑物,每个建筑物应该有一套自己的信息,占几个网格等

//json数据存储
public struct BuildingStats
{//xz单位为网格public byte length;//xpublic byte width;//zpublic float height;//y
}
/// <summary>
/// 记录一个建筑物存储的和运行时的所有信息
/// </summary>
public struct BuildingInfo
{public GridPos center;public BuildingStats stats;
}

因为后期我们的建筑物数据肯定会很多,所以最好的方法是把打表规定好的建筑物信息BuildingStats转为json存在硬盘中,需要时再去读取。而BuildingInfo则是我们在运行时的建筑物数据,它包括除了建筑物存储信息以外的一个网格坐标。
有了这些基础,我们就可以开始制作一个建筑系统

建筑系统

按照规矩,建筑是角色的能力,所以把代码放在新脚本ConstructionController中。还需要一个组件Constructable来控制建筑物本身。
先分析下每个建筑被建造出来需要几个阶段

/// <summary>
/// prebuild:未放置阶段,跟随鼠标移动,实时检测是否可放置
/// building:放置阶段,播放动画等,需要一段时间,可以被打断
/// postbuild:放置结束,物体与人物控制分离
/// </summary>
public enum ConstructionState
{PreBuild, Building, PostBuild
}

逻辑是这样的,ConstructionController只负责配置好第一阶段,产生一个空物体挂载有Constructable,并生成真正的建筑物作为其子物体,然后三阶段都会由Constructable执行,最后ConstructionConstroller听取Constructable产生的建筑结束回调事件(成功或失败)

void FixedUpdate()
{if(Input.GetKeyDown(KeyCode.C) && !inConstructionMode){TryConstruct();inConstructionMode = true;}
}

inConstructMode是用来控制在尝试放置一个建筑物期间不能再进行放置。

void TryConstruct()
{Ray ray = viewController.thirdPersonCam.ScreenPointToRay(Input.mousePosition);RaycastHit raycastHit;//注意获取某一层layermask的方法//Raycast中的layermask不是nametolayer得到的int值//nametolayer得到的是某一层的index,而此处需要的是一个32位数代表32个层的状态LayerMask layerMask = 1 << LayerMask.NameToLayer("ground");if(Physics.Raycast(ray, out raycastHit, float.PositiveInfinity, layerMask.value)){//temp,应该是从json文件中加载对应的模型数据BuildingInfo temp = new BuildingInfo();temp.center = GridPos.GetGridPos(transform.position);temp.stats = new BuildingStats();temp.stats.length = 6;temp.stats.width = 6;temp.stats.height = 2;//生成prebuildingGameObject preBuilding = new GameObject("preBuilding");preBuilding.transform.position = raycastHit.point + Vector3.up * temp.stats.height / 2;preBuilding.transform.rotation = Quaternion.identity;GameObject realBuilding = Instantiate(building, preBuilding.transform);realBuilding.transform.localPosition = Vector3.zero;realBuilding.transform.rotation = Quaternion.identity;//添加constructable来继续控制后续的建造Constructable target = preBuilding.AddComponent<Constructable>();target.info = temp;target.constructFinishCallback += OnConstructionFinish;}
}

因为这个游戏的所有建筑物都是在一个平面上,不存在楼梯之类的东西,所以我可以直接检测鼠标点击的地面,这里要注意一个关于射线检测layermask的问题,已经注释在代码中了
现在我们还没有打表记录建筑物数据,也没有相关的数据读取脚本,所以先临时配置一个建筑物做测试(以后应该是从文件中读取一个BuildingStats)。PreBuilding就是那个空物体,它附带有一个trigger boxcollider和iskinemic rigidbody。boxcollider表示的是一个以格子为单位的占地区域,rigidbody是为了让它能与其他建筑物发生碰撞(因为其他静止的建筑物并不带有rigidbody)。值得一提的是我在这里曾遇到了些困难
一开始我尝试将这个表示占地区域的碰撞体作为建筑物的某个空子物体,并为该物体添加rigidbody,后来我发现这将检测不到碰撞。我们都知道只要双方有一方有rigidbody就能发生碰撞,并且含有rigidbody的父物体可以检测到来自子物体collider的碰撞,但是只有挂载rigidbody物体的脚本能检测到,父物体的脚本并不能。
也就是说想要监听到碰撞或触发事件的几个条件是:

1.双方至少一方有rb
2.每一方自己或者子物体要有collider,所有自己和子物体collider的事件都会收到
3.监听脚本必须和rb挂载在同一个物体上

我们将空物体放在建筑物应当在的位置然后把建筑物的本地坐标置0,后面放置结束时直接detach子物体就可以了。注意我们前面height是使用float存储而非byte,因为网格是平面的,在竖直方向上我们不应该以网格为计量单位,而应该使用模型真正的高度。
接下来我们可以看看Constructable是如何完成三个阶段的

private List<Material> originalMats;//建筑物原本的mat
private List<Collider> collidersInTrigger;//检测到碰撞的物体(用于确定是否可以放置)void Init()
{constructionController = FindObjectOfType<ConstructionController>();viewController = FindObjectOfType<ViewController>();collidersInTrigger = new List<Collider>();originalMats = new List<Material>();foreach(MeshRenderer mr in GetComponentsInChildren<MeshRenderer>()){originalMats.Add(mr.material);}
}
void Awake()
{Init();
}

首先我们需要记录所有的材质,因为我们之后要替换整个物体的材质来指示该位置是否可以放置(绿色或红色)并表示这是预放置阶段,在真正放置后再将材质替换回去。决定某个位置是否可放置的重要因素是该位置是否有物体,我们使用碰撞检测记录一个collider列表,当列表为空时即为可放置。此外我们需要viewcontroller来帮助完成跟随鼠标移动的功能,constructcontroller中存储了两种预放置材质。

void Start()
{BoxCollider collider = gameObject.AddComponent<BoxCollider>();SetCoveredCollider(info, collider);//设置collider信息foreach(Collider col in GetComponentsInChildren<Collider>()){col.isTrigger = true;}//设置了刚体才能检测到与其他建筑物的碰撞(因为其他建筑物没有刚体)Rigidbody rb = gameObject.AddComponent<Rigidbody>();rb.isKinematic = true;rb.useGravity = false;
}

然后我们需要配置碰撞检测,记住真正的建筑物现在是我们的子物体,我们用网格配置一个新碰撞体来做碰撞检测而不是使用子物体已有的碰撞体。

/// <summary>
/// 通过BuildingInfo配置一块不可放置区域碰撞体
/// </summary>
/// <param name="buildingInfo"></param>
/// <param name="collider"></param>
static void SetCoveredCollider(BuildingInfo buildingInfo, BoxCollider collider)
{collider.center = Vector3.zero;collider.size = new Vector3(buildingInfo.stats.length, buildingInfo.stats.height, buildingInfo.stats.width);collider.isTrigger = true;
}

我们按照表中数据配置好碰撞体,然后将所有碰撞体都设为trigger,并添加rb。

void FixedUpdate()
{if(constructionState == ConstructionState.PreBuild){FollowMousePos();/*foreach(var item in collidersInTrigger){Debug.Log(item.name);}*/if(collidersInTrigger.Count == 0)//可以放置{GetComponentInChildren<MeshRenderer>().material = constructionController.preBuildSuccessMat;if(Input.GetMouseButtonDown(0)) PlaceBuilding();} else GetComponentInChildren<MeshRenderer>().material = constructionController.preBuildFailMat;}
}
/// <summary>
/// prebuilding跟随鼠标位置移动
/// </summary>
void FollowMousePos()
{Ray ray = viewController.thirdPersonCam.ScreenPointToRay(Input.mousePosition);RaycastHit raycastHit;LayerMask layerMask = 1 << LayerMask.NameToLayer("ground");if(Physics.Raycast(ray, out raycastHit, float.PositiveInfinity, layerMask.value)){GridPos buildPos = GridPos.GetGridPos(raycastHit.point + Vector3.up * info.stats.height / 2);transform.position = buildPos.Pos;                }
}

跟随鼠标很简单,射线获取位置,取格子坐标,更新坐标。

/// <summary>
/// 开始放置建筑物
/// </summary>
void PlaceBuilding()
{constructionState = ConstructionState.Building;constructionState = ConstructionState.PostBuild;//将建筑物替换回原材质MeshRenderer[] mrs = GetComponentsInChildren<MeshRenderer>();for(int i = 0; i < originalMats.Count; ++i){mrs[i].material = originalMats[i];}//恢复建筑物的colliderforeach(Collider col in GetComponentsInChildren<Collider>()){col.isTrigger = false;}transform.DetachChildren();constructFinishCallback?.Invoke(true);Destroy(gameObject);
}

现在还没有制作放置的过程和动画等,所以先设置为是直接完成。

void OnConstructionFinish(bool result)
{inConstructionMode = false;
}

完成后解除constructioncontroller的放置模式。

最终效果

遗留问题

我们这次是使用一个cube进行测试,但实际的模型往往复杂的多,要在表格里配置他们的height并不是件容易的事,而且稍有偏差建筑就会出现“浮空”的情况,所以下次或者之后的制作中会尝试解决这个问题,可能是通过unity自动生成碰撞体适配高度或者添加重力来完成。

这篇关于3D沙盒游戏开发日志2——网格和建筑物放置系统的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/716780

相关文章

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

基于人工智能的图像分类系统

目录 引言项目背景环境准备 硬件要求软件安装与配置系统设计 系统架构关键技术代码示例 数据预处理模型训练模型预测应用场景结论 1. 引言 图像分类是计算机视觉中的一个重要任务,目标是自动识别图像中的对象类别。通过卷积神经网络(CNN)等深度学习技术,我们可以构建高效的图像分类系统,广泛应用于自动驾驶、医疗影像诊断、监控分析等领域。本文将介绍如何构建一个基于人工智能的图像分类系统,包括环境

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

无人叉车3d激光slam多房间建图定位异常处理方案-墙体画线地图切分方案

墙体画线地图切分方案 针对问题:墙体两侧特征混淆误匹配,导致建图和定位偏差,表现为过门跳变、外月台走歪等 ·解决思路:预期的根治方案IGICP需要较长时间完成上线,先使用切分地图的工程化方案,即墙体两侧切分为不同地图,在某一侧只使用该侧地图进行定位 方案思路 切分原理:切分地图基于关键帧位置,而非点云。 理论基础:光照是直线的,一帧点云必定只能照射到墙的一侧,无法同时照到两侧实践考虑:关

Hadoop企业开发案例调优场景

需求 (1)需求:从1G数据中,统计每个单词出现次数。服务器3台,每台配置4G内存,4核CPU,4线程。 (2)需求分析: 1G / 128m = 8个MapTask;1个ReduceTask;1个mrAppMaster 平均每个节点运行10个 / 3台 ≈ 3个任务(4    3    3) HDFS参数调优 (1)修改:hadoop-env.sh export HDFS_NAMENOD

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

活用c4d官方开发文档查询代码

当你问AI助手比如豆包,如何用python禁止掉xpresso标签时候,它会提示到 这时候要用到两个东西。https://developers.maxon.net/论坛搜索和开发文档 比如这里我就在官方找到正确的id描述 然后我就把参数标签换过来