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

相关文章

Python实现网格交易策略的过程

《Python实现网格交易策略的过程》本文讲解Python网格交易策略,利用ccxt获取加密货币数据及backtrader回测,通过设定网格节点,低买高卖获利,适合震荡行情,下面跟我一起看看我们的第一... 网格交易是一种经典的量化交易策略,其核心思想是在价格上下预设多个“网格”,当价格触发特定网格时执行买

深度解析Nginx日志分析与499状态码问题解决

《深度解析Nginx日志分析与499状态码问题解决》在Web服务器运维和性能优化过程中,Nginx日志是排查问题的重要依据,本文将围绕Nginx日志分析、499状态码的成因、排查方法及解决方案展开讨论... 目录前言1. Nginx日志基础1.1 Nginx日志存放位置1.2 Nginx日志格式2. 499

PyQt5 GUI 开发的基础知识

《PyQt5GUI开发的基础知识》Qt是一个跨平台的C++图形用户界面开发框架,支持GUI和非GUI程序开发,本文介绍了使用PyQt5进行界面开发的基础知识,包括创建简单窗口、常用控件、窗口属性设... 目录简介第一个PyQt程序最常用的三个功能模块控件QPushButton(按钮)控件QLable(纯文本

Linux系统中查询JDK安装目录的几种常用方法

《Linux系统中查询JDK安装目录的几种常用方法》:本文主要介绍Linux系统中查询JDK安装目录的几种常用方法,方法分别是通过update-alternatives、Java命令、环境变量及目... 目录方法 1:通过update-alternatives查询(推荐)方法 2:检查所有已安装的 JDK方

Linux系统之lvcreate命令使用解读

《Linux系统之lvcreate命令使用解读》lvcreate是LVM中创建逻辑卷的核心命令,支持线性、条带化、RAID、镜像、快照、瘦池和缓存池等多种类型,实现灵活存储资源管理,需注意空间分配、R... 目录lvcreate命令详解一、命令概述二、语法格式三、核心功能四、选项详解五、使用示例1. 创建逻

游戏闪退弹窗提示找不到storm.dll文件怎么办? Stormdll文件损坏修复技巧

《游戏闪退弹窗提示找不到storm.dll文件怎么办?Stormdll文件损坏修复技巧》DLL文件丢失或损坏会导致软件无法正常运行,例如我们在电脑上运行软件或游戏时会得到以下提示:storm.dll... 很多玩家在打开游戏时,突然弹出“找不到storm.dll文件”的提示框,随后游戏直接闪退,这通常是由于

基于Python开发一个图像水印批量添加工具

《基于Python开发一个图像水印批量添加工具》在当今数字化内容爆炸式增长的时代,图像版权保护已成为创作者和企业的核心需求,本方案将详细介绍一个基于PythonPIL库的工业级图像水印解决方案,有需要... 目录一、系统架构设计1.1 整体处理流程1.2 类结构设计(扩展版本)二、核心算法深入解析2.1 自

使用Python构建一个高效的日志处理系统

《使用Python构建一个高效的日志处理系统》这篇文章主要为大家详细讲解了如何使用Python开发一个专业的日志分析工具,能够自动化处理、分析和可视化各类日志文件,大幅提升运维效率,需要的可以了解下... 目录环境准备工具功能概述完整代码实现代码深度解析1. 类设计与初始化2. 日志解析核心逻辑3. 文件处

golang程序打包成脚本部署到Linux系统方式

《golang程序打包成脚本部署到Linux系统方式》Golang程序通过本地编译(设置GOOS为linux生成无后缀二进制文件),上传至Linux服务器后赋权执行,使用nohup命令实现后台运行,完... 目录本地编译golang程序上传Golang二进制文件到linux服务器总结本地编译Golang程序

Linux系统性能检测命令详解

《Linux系统性能检测命令详解》本文介绍了Linux系统常用的监控命令(如top、vmstat、iostat、htop等)及其参数功能,涵盖进程状态、内存使用、磁盘I/O、系统负载等多维度资源监控,... 目录toppsuptimevmstatIOStatiotopslabtophtopdstatnmon