原神Like:大世界项目底层构建全流程

原神Like:大世界项目底层构建全流程

尝试去了解提瓦特大陆的本质。


这份文档并不是一份用于构建大世界底层的百宝书。每个项目都有自己的需求和苦衷,他人经验往往只能隔靴搔痒。这份文档更多是记录和沉淀自己在过去一年做的事情,成为自己前进河流里的一颗确定的石子,同时还能让别人有所启发的话,就再好不过。

标题虽然起的很大,为此做的事情也很多,但人并不是因为做了伟大的事情而伟大,而是因为想把事情做得更伟大而伟大。希望下次有机会做事的时候,可以把事情做得更好一点。


底层和规划的奠基期


金字塔和倒金字塔

底层基建不可能是凭空推进的,一定会有一份“我们要做什么样的世界”的基础规划。但是在项目初期,很可能没有人可以解答“我们要做什么”的问题,同时每个人对于“应该怎么做”都存在着路径依赖。无论多么想马上动手做事情,一定要在初期和做规划的人大致核对好预期,例如世界构成、整体循环、内容模块和场景规模等,基于这些基本预期去设计底层框架。

如果规划是正着搭金字塔,那底层就是倒着建金字塔。为了不让规划成为“空中楼阁”,底层要能够支持规划在一定程度上推倒重来,因为项目总需要进行试错。金字塔的底部要比顶层宽,但是要宽出多少试错空间,就要看项目的具体考量了。


充分了解项目已有资源

开始从底层搭建金字塔时,往往已经有了做好的一堆材料,它们有些需要复用,有些实在用不了。为了能把事情做的更高更远,在更长的时间内保持稳定不变成屎山,需要充分了解项目现况,包括已有功能、已有资源、各个模块开发进度及现有管线等。

一个新的底层,是为了承载更多人做内容的空间,如果一开始没有考虑兼容性,而是做了一堆全新的看似好用的功能,项目成员的移植成本会非常大,最后开发效率可能还不如之前。


底层与规划的着眼点

底层是为了项目的未来而制作的。说是这么说,但项目一定会需要第一个demo切片来验证这套规划是否可以量产,如果demo的体验不行,那未来压根就不存在了。出于对长期生产的考虑,底层功能需要做到可拓展、可继承和模块化,但一定要尽可能支持第一个demo切片的生产。

功能最终是为了服务于设计而存在的。如果沉迷于设计完美的功能,那还不如让玩家去游玩功能更完美的虚幻5。


底层的设计方向

在去年的工作里,基于对项目现有情况的思考,并出于“希望能做的事情越多越好”的美好愿望,在底层搭建工作的伊始,设立了几个基本方向。

  1. 编辑器-服务器-客户端流程

    策划使用编辑器配置数据,实际运行时先由服务器下发数据,再由客户端根据服务器下发与策划配置生成游玩对象。

  2. 场编和玩法信息json数值化

    基于1,为了能实现编辑器编辑与多人合作,需要将场编和玩法信息全部json数值化保存。对于场编信息,点位可以存为世界坐标xyz,而区域可以看作由多个xyz和高度数值而绘制的区域。在场编数值化的基础上,将玩法信息(例如插件、玩法属性等)同样加以数值化,这样就能尽量使用json数值来承载关卡的全部数据。

  3. 分布式逻辑

    对于一个世界,由世界承载整个世界通用的逻辑,每个玩法对象承载自己的逻辑,而包含多个玩法对象的玩法单元则承载让该玩法得以成立的逻辑。逻辑承载体以世界-玩法单元-玩法对象的标准进行划分,尽量避免在一个世界逻辑里写死多个玩法的做法。

  4. 世界的划分标准

    在3里,提到了“世界”的概念。出于连续制作多个箱庭的预期,当时用世界作为新的关卡概念,它意味着一个场景分区块流送加载、关卡内容由服务器保存状态的可游玩箱庭


流送加载底层构建


什么是流送加载

streaming一词最早起源于视频。为了优化观众收看长视频的体验,视频网站不希望长视频在打开页面时就一次性全部加载好,这会导致长时间的loading。于是网站将长视频切分为一段一段分别加载,观众在快看完一段时加载下一段。这种加载就像小溪流水,是一波一波推送的,所以被叫做streaming。

上面的小溪流水概念,在游戏里其实是类似的。对于常规3D世界游戏,当玩家处于当前区域时,对周围区域的内容其实是无感的。我们可以将玩家眼中的内容分成三层范围:

  1. 可见且可达:能够看到,且马上就能到达。这个范围里的对象要保持玩法逻辑的全量激活。
  2. 可见不可达:能够看到,但不能马上到达。这个范围里的对象要保证可见的美观性,但玩法逻辑可以轻量化。
  3. 不可见不可达:不能够看到或只能在很远的尺度上看到,并且不能马上到达。这个范围内可以卸载玩法对象,只保证场景的可识别度。

当玩家在3D世界里不断移动时,这三层范围会以玩家为中心不断移动,就像玩家身上套了一层九宫格在走路。最常规的整体思路是,先划分好地块的基本单位,然后根据每类玩法对象的可见和可达诉求,单独设计LOD(对应可见)和玩法逻辑加卸载(对应可达)方案。

如此这样,玩家在一个地块内移动时,每类玩法对象都以三层范围规则进行加卸载;当玩家位于地块相接处时,下一个地块会接替上一个地块变成可见且可达的状态。


结合项目需求设计

上面描述了个相当理想的环境,实际上打开UE的World Composition就能马上动手做起来。但项目是不可能说换引擎就换引擎的,更何况哪怕换了引擎,也不可能直接使用原生功能,为了后续开发还是要根据项目需求来设计。

基于去年的工作,在早有预期场景无法做太大的情况下,并没采用原神自动切割地块的方案,而是希望进行人为划分,即自己划分好不同的区块以及衔接方式,然后给每个区块设置三层trigger来对应三层范围。

在资源有限的情况下,就要用设计来达成更好的体验。当然再好的设计,也是需要执行验证的。


demo验证和实装

像流送加载这种既影响功能开发,又需要美术配合调整生产管线的,不能等到实际场景开始做了才隆重推出,必须在很早就进行demo验证。进行验证的资源完全不需要是正式资源,找点压力爆表的多面模型放进来即可。

需要注意的是,既然是做流送加载功能的策划,那内心肯定有对于“箱庭该长什么样”的理解。基于这个理解,demo验证时就可以制作出一个大致白盒进行验证,而且反过来也可以为规划侧提供制作方向。

既然做功能的人需要了解规划,那么在验证功能时,就顺便亲手验证下规划到底行不行。


世界信息数值化的含义


用point和trigger描述世界

想象一个常规3D游戏的关卡,上面有由怪物和机关组成的关卡玩法,还有进入某些区域才会触发的特殊事件。如果要亲手制作这样一个关卡,你需要摆哪些东西进去?

首先把最简单的怪物放到场上,配好AI和数值。然后把机关摆在对应位置,再写好机关之间的玩法逻辑。最后对于这些区域事件,需要先摆好触发trigger,然后在触发逻辑里进行一系列判定,最后执行对应的事件。

在上面的处理中,怪物和机关是用point来描述世界位置的,而区域是用trigger来描述世界位置的。trigger可以看作由一组point和一个高度数值组成的区域,所以这些场编信息都可以用数值化的方式处理。


场景和关卡的数值化

如果将世界看作场景+关卡,既然关卡可以做到数值化,场景信息同样可以进行数值化。虽然场景数值化的功能开发,最直接原因是老旧引擎不支持prefab嵌套等基础功能,但既然都要进编辑器,最好基于同一套基础功能(point和trigger)进行数值化。

地块和地块下的各个物体都有点位信息,可以使用point进行保存;不同类型的物体加卸载配置可以直接存为数值;而用于触发整个地块的三层范围加卸载的区域,则使用trigger进行保存。


世界与实体的结构设计


将世界进行模块化

一个生意盎然的世界里,会带有自然的昼夜交替和天气表现。不同区域的人文和怪物不同,同时不同的天气和昼夜也会带来不同的表现。角色行走在世界里,脚步会溅起水花,武器打到石头上会把石头弹走,而发出的声音会让怪物进入警戒状态。

在上面这段过程里,事件和事件之间虽然是因果关系,但这些事件并不属于同一个模块。昼夜和天气是两个世界通用系统,它们之间可以互相影响,但不存在父子关系。策划可以在不同区域设计并布置不同的NPC和怪物,这些游戏对象在接到昼夜和天气系统的变化事件时会触发特定逻辑。

脚步溅起水花是角色脚步和地面信息之间的交互导致,而武器把石头弹走是攻击数据触发了石头的物理表现。在石头的弹飞表现里会播放音频事件,这一事件被监听音频事件的怪物监听到,从而走了警戒的逻辑。

世界的功能是可以且需要被模块化的。模块之间可以互相通信,但尽量减少A模块直接操控B模块状态的设计。模块可以被使用一套基建的其他世界直接复用,并给各个世界提供自定义的配置空间。


实体的唯一性(GUID)

如果我在世界A1放了一个怪物B1,然后在复用世界A1场景的世界A2的同样位置放了一个怪物B2,那B1和B2是一个实体吗?如果我在世界A1杀死了怪物B1,过一天后B1再次刷新,那B1和再次刷新的B1是一个实体吗?

对于上面的问题,B1和B2不是一个实体,而B1和再次刷新的B1是一个实体。

实体的唯一性用来做服务器存档和校验。为了存档玩家在不同情况下的世界状态,每个需要保存状态的实体都要在服务器侧创建对象进行存储。实体的状态会发生改变和刷新,但同一个实体不可能同时存在于多个世界里。

为了确认实体的唯一性,需要使用Globally Unique Identifier即GUID进行标识,就像给每个从编辑器中新创建出来的实体分发身份证。当实体有了唯一指定GUID,就可以非常方便地进行世界间甚至跨世界通信,服务器侧也会对拥有GUID的玩法实体创建对象并存储状态。


World-Group-Entity

在世界和实体之间还缺少了一个单位。想想由一组机关和怪物构成的玩法,玩家要修复机关后水闸才能转动,而修复机关的时候,会刷出一波怪来干扰玩家,玩家如果坚持修复机关,一定会被攻击玩家的怪物打断。如果玩家在修复完成前就退出,希望能够回档到“开始修复机关前”的状态,即玩家需要重新交互开始修复机关,而这一波怪会再次刷新。

在这个玩法里,修复机关和水闸的逻辑由各自玩法实体承载,这波怪物也是提前布置在场景里等待激活的对象。但缺少了一个地方承载“机关修复到中途时激活怪物,且修复未完成时下线会导致状态回档”的逻辑,或者说没有东西能显示出这些对象属于同一个玩法。

想象一个充满气泡的盒子,盒子是世界,而气泡是一组玩法。玩家在气泡和气泡之间移动,可能在气泡完成后才离开,也有可能在气泡破碎的中途就离开。我们现在有了盒子,现在需要一个气泡单位,而它可以被称为Group。

Group内包含了多个服务器存档的玩法对象。它用于描述和实现多个玩法机关之间的关系,从而形成一个组玩法。将玩法以组为单位打包后,它不仅更加符合策划的直觉,同时可以以组为单位进行激活、反激活和竞争占用。

Group是《原神》里一个相当重要的功能,最终地实现方案就是照着《原神》思路做的。在要解决的问题相似的情况下有的放矢地进行参考不一定是坏事。


实体设计:玩法机关的本质


结合“上下文”进行抽象

单独将实体抽出来加以说明,是因为玩法实体功能设计成什么样对于制作关卡来说真的非常重要。在说长不长的玩法制作生涯里,体验了各式各样的开发方式后,个人有个很深的体会是:

关卡机关应当把逻辑和表现解耦,由逻辑驱动表现,同时逻辑是一堆功能模块的组合。关卡机关会以状态为单位进行切换,每个状态有着不同表现和逻辑。

针对“逻辑是一堆功能模块的组合”这一期望,玩法功能的制作不再以类为最小单位,而是以模块为最小单位。一个关卡机关可以通过多个模块组合出来,相似功能通过模块自定义参数进行扩展,而不开发一个功能极为相似的新模块。

针对“关卡机关会以状态为单位进行切换”这一期望,可以想象有一个宝箱被用在怪物玩法里,它初始是锁定状态,在玩法完成后解锁,玩家交互后将其打开。宝箱在每个状态下的表现和逻辑都不同,且状态之间存在特定的切换关系。同时,如果玩家退出重进,服务器也会以这个状态为准去存储该玩法实体。


关卡实体的整体结构

基于上面的设计预期,一个关卡实体的整体结构会是:

通过配置不同的模型资源,让机关拥有对应表现;机关拥有基本的配置参数,例如唯一名称、Tag列表、LOD模板和逻辑脚本。同时,机关可以挂载任意多个插件,每个插件提供对应功能,可以在机关逻辑里调用、监听或被执行。

机关可以配置多个状态,每个状态对应一种表现和逻辑;在机关在状态之间切换时触发不同的逻辑,形成完整的玩法功能。

在上面的思路里,一个交互后打开的宝箱,可以看作拥有两个状态的机关。在状态1下,宝箱动画机为关闭状态,上面挂载一个可以监听角色以指定角度进出的范围插件,玩家进入时,如果宝箱处于状态1,就会注册交互选项,退出时关闭交互选项。如果玩家按下交互选项,就会马上关闭交互选项,同时发送事件由宝箱自身监听到。一旦宝箱监听到交互事件,就会将自身的状态切换到状态2。

切换状态2后,会触发宝箱监听切换到状态2的事件。此时会触发动画trigger打开宝箱,发放掉落奖励,延迟一段时间后将自己销毁。


宝箱类和宝箱实例的关系

在上面的例子里,我们已经在脑内生产了一个宝箱类。接着我们会马上发现,如果我想在一个世界里摆放10个宝箱,这些宝箱的逻辑和表现其实是完全一致的,只是掉落奖励有所不同。那我们可不可以使用同一个类,但是只修改掉落ID,从而达到批量摆设和修改的目标?

我们已经生产好的宝箱类是模板,而放在场上的10个宝箱,则是通过覆写生成的宝箱实例。实例会拥有GUID,且保存于世界数据内;但宝箱类只是模板,它不拥有GUID,也不存在于任何世界。

日后我们希望整体调整宝箱的外观或开启表现时,只需要修改类,就会被所有实例继承,除非该实例已经进行过覆写。


举出制作的说服案例

读了上面的内容,你很容易就会发现,上面的功能设计思路基本来自于虚幻的actorBP。但作为一个全新功能,我们当然不能以“虚幻就是这么做的!”为理由要求开发。

一个全新的功能,对于之前用惯老功能的人来说会更方便吗?项目近期制作和规划的玩法内容,用这个功能制作会更便利吗?这个功能是否可以支持后续长远的迭代和扩展?

理清功能思路固然重要,和他人沟通确认好处和可行性更加重要。做功能是为了现在和未来的设计而服务,而不是闭门造车。


后续功能扩展做法

基于上面的框架,这个功能的后续拓展做法是:

  1. 增加机关类的基础属性。需要提供默认值,保证之前的机关不出问题。
  2. 增加插件。
  3. 增加逻辑节点。

逻辑载体:如何让机关执行


为什么需要蓝图

在上面的例子中,无论是世界逻辑,还是实体逻辑,都频繁提到“逻辑放在世界上/放在实体上”。那这个放到底是什么放法,或者说采用什么方式编写游戏逻辑?

基于项目对于图形化的期望,最后采用了蓝图方案。蓝图顾名思义就是虚幻那一套,多入口,事件监听,生命周期,这里不作过多介绍。蓝图逻辑和工具最好都由项目组自己开发,擅自使用第三方插件,甚至直接使用官方功能,很容易导致后续无法维护和拓展。


分布式逻辑开发

现在我们拥有了世界框架,制作了初步的机关功能,并以组玩法为单位划分了Group,可以向World-Group-Entity绑定蓝图逻辑,从而实现各类玩法功能了。在上文有提过,期望逻辑尽量实现分布式,即Entity(单个玩法机关)、Group(一组玩法机关)和World(整个世界)能够分别负责各自的逻辑,为此需要不同类型的蓝图和蓝图节点。具体节点要根据项目需求来开发,但希望尽量遵守这一原则。


Extra:让策划写代码的坏处

让策划写代码就像直接用官方功能来做项目一样,贪图了一时的敏捷开发,后续维护要付出大量人力。一个会发生人员流动的大型项目,很难保证制作人员能够不出Bug。而让策划直接写代码的做法,在开发内容越来越复杂的发展下,最终会达到一个“这个脚本Bug只有策划看得懂”的巅峰。

程序希望“从策划手里保护代码”,更是希望“从代码手里保护策划”。一套更加贴近项目开发的自创蓝图,虽然本质上依然是写脚本,但可以尽量减少低级的代码错误,同时让策划的工作更加顺畅。

策划不断提高自己的代码能力是没问题的,作为个人兴趣也非常值得鼓励。但一个大型项目希望以这种方式持续开发七年的话,需要谨慎考虑这项目在七年后会变成什么样子。


关卡编辑器:图形化和协同合作


产品设计的原则

编辑器在一般项目会由关卡和技术策划一起完成,作为非pro,仅列举个人在实际制作中的评估标准,仅供参考。

  1. 功能易读:将一个新功能放进关卡编辑器后,可以不依靠注释直接看出它是做什么的。
  2. 功能可找:在我要用某个功能但暂时忘了在哪里的时候,可以依靠布局的分类逻辑去找到。
  3. 提供常用操作:诸如复制贴贴、批量删除、展示备注等功能,最好在功能上线前就一并制作,减少不方便操作给人带来的麻烦。
  4. 展示配置流程:许多功能都需要一系列操作才能完成配置。对于这类功能,请以“用户并不清楚完整流程”为假设,在编辑器里就按照用户的每一步骤给出下一步骤的操作,让其自然而然完成配置的全部流程。
  5. 提供Debug方式:在用户使用编辑器配置完内容后,需要进行跑测,这时编辑器要提供出有价值的Debug信息,让用户进行快速调试。

整体布局和编辑流程

最终开发的编辑器布局参考了原神,以世界为编辑单元,以功能模块为不同页签,用户可以对一个世界的不同功能进行任意编辑。

在世界和功能的基础上,考虑到多人合作和内容分类的问题,还制作了内容分类,可以将一个世界下同一类的内容按照不同分类进行区分。


数值划分和编辑器处理

在最上面提到,希望这套基建可以形成编辑器-服务器-客户端的工作流程。显而易见的,编辑器起码会产出三套数据,分别是编辑器、服务器和客户端。编辑器的每一步操作会产出的数据不同,也会有不同的自动化处理。

这一点请根据项目具体进度和开发需求进行调整,无需赘言。


编辑器开发管线

假设编辑器已经做好,现在关卡想往里面加入新的功能,要如何进行开发?

首先要和客户端服务器确定好功能的具体方案,以及客户端和服务器的通信方式。在确定好功能后,策划需要最终确认下数据结构。在功能开发好后,以上面确定的数据结构,和负责编辑器的同学确定图形化方案,即如何使用编辑器配置出这些数据。最后功能上线,积极聆听来自内部的反馈和喷,做出后续的调整和修改。


开发中的查漏补缺

上面的流程看似简单,做起来依然会遇到很多具体情况,有些甚至要部分返工进行处理。在下次开发时,最好从一开始就考虑好后续方案。例如多版本开发、编辑器校验、客户端校验、服务器校验、客户端服务器数值不一致的处理、多人开发文件合并、多版本文件合并和定时转表推送等。


迷思:编辑器前的合作方式


交互地图工具案例

早在这一项目前,我就经常产生如下迷思。我个人打游戏的时候,经常用到很多网友自发维护的Interactive Map来查看地图信息,大家可以在上面打标签和写备注,让人了解到整个关卡的全貌。

既然打游戏都能有这样的工具,那么做游戏不可以有吗?个人在之前的开发里经常遇到“不知道对方做了什么,甚至不知道有谁在做这个”的情况,是否可以通过工具来降低沟通成本?

在《艾尔登法环》的成功后,阅读了「信息地图」开发辅助工具如何影响《艾尔登法环》的开放地图设计?这篇文章,要说不心动是不可能的。如果能拥有这样的工具,当然最重要的是一个愿意共同创造世界的团队,那么做出理想中的项目,似乎也不是不可能。


接入工作流的方式

首先说一句,玩法数据想直接从这种工具里刷入游戏项目根本是不可能的。换句话说,能做到这件事的工具也有,但它的名字叫关卡编辑器,而不是交互地图工具。交互地图工具是后验的,是等到世界里制作了内容后,才会体现在交互地图上,给更多的成员看到世界的当前状态。

它接入世界制作工作流的时间点,主要分为两个:

  1. 策划开始配置前:此时世界可能还不存在,或者仅有基本资源。策划可以直接在地图上放置点位,传达自己对于布设的预期,和其他策划沟通。
  2. 策划开始配置后:此时世界上已经有了玩法数据,并根据配置进度不断增加。这时玩法数据自动化传到交互地图工具侧,工具会自动刷出当前世界的配置进度。

底层工作的交付标准


可以脱离负责人独立行走

  1. 各模块的负责人要在功能完成后留下KM,并持续维护至功能转手。
  2. 接手人可以get到编辑器和功能的设计理念,可以按照思路做,也可以修正思路,但尽量不要“知其不可为而为之”。
  3. 后续新功能接入时,尽量在当前框架上拓展,尽量避免修改到老功能导致之前内容失效的问题。

可以满足当初的规划开发

就像最开始所说,功能是为了满足当前的规划而开发的。在功能开发的过程中,规划也已经正进行着内容切片的验证。尽量不要闭门造车,多了解规划的内容和开发进度,看看是不是自己觉得有意思的内容。

否则,给一个自己都觉得无趣的项目做轮子,做到最后容易陷入虚无。


大家满意,才是真的满意

就像最开始所说,功能不仅是要满足当前的规划,还要服务于未来的开发。这个项目打算做多少年,到了那时候是否会产出屎山?留在项目里的成员在用新的工具时,不可避免地会产生各种迷惑和问题,有些不便之处会随着时间愈加放大。底层不是一时交付就算完成,请尽量考虑未来的自己与大家,维护这套轮子越走越远,起码拖延它陷入屎山的速度。


原神Like:大世界项目底层构建全流程

https://uynad.github.io/2023/10/31/ldesign/20231101-truth-of-level/

作者

UyNad

发布于

2023-11-01

更新于

2024-08-07

许可协议

CC BY-NC-SA 4.0

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×