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

相关文章

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

解读GC日志中的各项指标用法

《解读GC日志中的各项指标用法》:本文主要介绍GC日志中的各项指标用法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、基础 GC 日志格式(以 G1 为例)1. Minor GC 日志2. Full GC 日志二、关键指标解析1. GC 类型与触发原因2. 堆

使用Python绘制3D堆叠条形图全解析

《使用Python绘制3D堆叠条形图全解析》在数据可视化的工具箱里,3D图表总能带来眼前一亮的效果,本文就来和大家聊聊如何使用Python实现绘制3D堆叠条形图,感兴趣的小伙伴可以了解下... 目录为什么选择 3D 堆叠条形图代码实现:从数据到 3D 世界的搭建核心代码逐行解析细节优化应用场景:3D 堆叠图

SpringBoot开发中十大常见陷阱深度解析与避坑指南

《SpringBoot开发中十大常见陷阱深度解析与避坑指南》在SpringBoot的开发过程中,即使是经验丰富的开发者也难免会遇到各种棘手的问题,本文将针对SpringBoot开发中十大常见的“坑... 目录引言一、配置总出错?是不是同时用了.properties和.yml?二、换个位置配置就失效?搞清楚加

linux重启命令有哪些? 7个实用的Linux系统重启命令汇总

《linux重启命令有哪些?7个实用的Linux系统重启命令汇总》Linux系统提供了多种重启命令,常用的包括shutdown-r、reboot、init6等,不同命令适用于不同场景,本文将详细... 在管理和维护 linux 服务器时,完成系统更新、故障排查或日常维护后,重启系统往往是必不可少的步骤。本文

Python中对FFmpeg封装开发库FFmpy详解

《Python中对FFmpeg封装开发库FFmpy详解》:本文主要介绍Python中对FFmpeg封装开发库FFmpy,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录一、FFmpy简介与安装1.1 FFmpy概述1.2 安装方法二、FFmpy核心类与方法2.1 FF

MySQL 打开binlog日志的方法及注意事项

《MySQL打开binlog日志的方法及注意事项》本文给大家介绍MySQL打开binlog日志的方法及注意事项,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要... 目录一、默认状态二、如何检查 binlog 状态三、如何开启 binlog3.1 临时开启(重启后失效)

基于Python开发Windows屏幕控制工具

《基于Python开发Windows屏幕控制工具》在数字化办公时代,屏幕管理已成为提升工作效率和保护眼睛健康的重要环节,本文将分享一个基于Python和PySide6开发的Windows屏幕控制工具,... 目录概述功能亮点界面展示实现步骤详解1. 环境准备2. 亮度控制模块3. 息屏功能实现4. 息屏时间

Mac系统下卸载JAVA和JDK的步骤

《Mac系统下卸载JAVA和JDK的步骤》JDK是Java语言的软件开发工具包,它提供了开发和运行Java应用程序所需的工具、库和资源,:本文主要介绍Mac系统下卸载JAVA和JDK的相关资料,需... 目录1. 卸载系统自带的 Java 版本检查当前 Java 版本通过命令卸载系统 Java2. 卸载自定

Python实例题之pygame开发打飞机游戏实例代码

《Python实例题之pygame开发打飞机游戏实例代码》对于python的学习者,能够写出一个飞机大战的程序代码,是不是感觉到非常的开心,:本文主要介绍Python实例题之pygame开发打飞机... 目录题目pygame-aircraft-game使用 Pygame 开发的打飞机游戏脚本代码解释初始化部