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

相关文章

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

在不同系统间迁移Python程序的方法与教程

《在不同系统间迁移Python程序的方法与教程》本文介绍了几种将Windows上编写的Python程序迁移到Linux服务器上的方法,包括使用虚拟环境和依赖冻结、容器化技术(如Docker)、使用An... 目录使用虚拟环境和依赖冻结1. 创建虚拟环境2. 冻结依赖使用容器化技术(如 docker)1. 创

Spring Boot整合log4j2日志配置的详细教程

《SpringBoot整合log4j2日志配置的详细教程》:本文主要介绍SpringBoot项目中整合Log4j2日志框架的步骤和配置,包括常用日志框架的比较、配置参数介绍、Log4j2配置详解... 目录前言一、常用日志框架二、配置参数介绍1. 日志级别2. 输出形式3. 日志格式3.1 PatternL

CentOS系统Maven安装教程分享

《CentOS系统Maven安装教程分享》本文介绍了如何在CentOS系统中安装Maven,并提供了一个简单的实际应用案例,安装Maven需要先安装Java和设置环境变量,Maven可以自动管理项目的... 目录准备工作下载并安装Maven常见问题及解决方法实际应用案例总结Maven是一个流行的项目管理工具

基于Python开发PPTX压缩工具

《基于Python开发PPTX压缩工具》在日常办公中,PPT文件往往因为图片过大而导致文件体积过大,不便于传输和存储,所以本文将使用Python开发一个PPTX压缩工具,需要的可以了解下... 目录引言全部代码环境准备代码结构代码实现运行结果引言在日常办公中,PPT文件往往因为图片过大而导致文件体积过大,

开启mysql的binlog日志步骤详解

《开启mysql的binlog日志步骤详解》:本文主要介绍MySQL5.7版本中二进制日志(bin_log)的配置和使用,文中通过图文及代码介绍的非常详细,需要的朋友可以参考下... 目录1.查看是否开启bin_log2.数据库会把日志放进logs目录中3.查看log日志总结 mysql版本5.71.查看

使用DeepSeek API 结合VSCode提升开发效率

《使用DeepSeekAPI结合VSCode提升开发效率》:本文主要介绍DeepSeekAPI与VisualStudioCode(VSCode)结合使用,以提升软件开发效率,具有一定的参考价值... 目录引言准备工作安装必要的 VSCode 扩展配置 DeepSeek API1. 创建 API 请求文件2.

C#实现系统信息监控与获取功能

《C#实现系统信息监控与获取功能》在C#开发的众多应用场景中,获取系统信息以及监控用户操作有着广泛的用途,比如在系统性能优化工具中,需要实时读取CPU、GPU资源信息,本文将详细介绍如何使用C#来实现... 目录前言一、C# 监控键盘1. 原理与实现思路2. 代码实现二、读取 CPU、GPU 资源信息1.

在C#中获取端口号与系统信息的高效实践

《在C#中获取端口号与系统信息的高效实践》在现代软件开发中,尤其是系统管理、运维、监控和性能优化等场景中,了解计算机硬件和网络的状态至关重要,C#作为一种广泛应用的编程语言,提供了丰富的API来帮助开... 目录引言1. 获取端口号信息1.1 获取活动的 TCP 和 UDP 连接说明:应用场景:2. 获取硬

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma