Blog版本:
https://dorgon.horizon-studio.net/?p=2044
前言
写出一套好用好维护的程式码相信会是所有程式设计师无意识中追求的目标,而把自己的
功能包装成Plugin是一个非常直接达成这个目的的手段,因为设计的过程会强迫把自己置
更高、更抽象的位置往下俯瞰整个游戏系统并定位出自己的功能应该在哪个位置。
只是,并不是任何的程式码都适合放进Plugin中。因此接下来我的目的主要是想根据自己
过去的经验,分享一些自己在写各种Plugin时的大原则。虽然这些想法不一定适用于所有
的情境,应该也会有许多我没考虑到的状况,但基本上也就只是目前在我有限的经验中现
阶段的想法。如果有人有不一样的观点也欢迎一起讨论,或许可以碰撞出一些东西也说不
定。
顺带一提,这边的Plugin讨论的主要是CodePlugin,其他类型的Plugin,例如Asset Only
或BP Only,并不在接下来的讨论范围内,除非我有另外指明。
目前我在Unreal Marketplace已经有上架六项产品,内容包括Quest、Interact、
Dialogue以及Tween,有兴趣的可以参考:
https://www.unrealengine.com/marketplace/en-US/profile/horizon-studio
另外我也会不定期的在我的FB页面上分享一些开发日志,通常是跟游戏技术相关的话题,
有兴趣的欢迎追踪:https://www.facebook.com/dorgonresearches
本文开始
Unreal中的程式码我们可以非常粗爆的大致分层如下:
EngineModule->EnginePlugin->GamePlugin->GameModule->GameFeaturePlugin。其中
EngineModule、EnginePlugin我们顶多修修bug不可能做太大的修改;GameModule则是看
专案团队的风格会有不同的构成方式,我们等一下再回来聊这部份;而GamePlugin跟
GameFeaturePlugin则是我们接下来要讨论的重点。
EnginePlugin 、GamePlugin相对直观,大家应该不会有什么疑惑,但GameFeaturePlugin
这个概念或许就有人没听过了。这个功能是5.0所引入的最大变革之一,对程式相关职能
的人而言,特别是写Gameplay相关的,我觉得其重要性甚至更胜于Nanite跟Lumen,因为
相关机制会从根本性上的改变了我们程式码的架构方式以及思考模式。
就设计理念看来,他是属于DLC概念的实现,跟Patch不同,Patch需要盖掉原本的内容,
而DLC必须要做到“热插拔”的机制。 就传统UE4 project,我们要达到DLC的这项要求非
常麻烦,因为我们只能对“Content-Only”plugin包出dlc包,而哪些dlc需要开启或关闭
则需要你在BaseGame中做出一些可以管理的设计。在GameFeature Plugin 机制出现后,
我们总算不需要做这些劳力活了。注:这边的BaseGame指的是 ${PROJECT_ROOT}/Source
下面所有的Runtime GameModule以及${PROJECT_ROOT}/Content下面所有会打包进去的
Asset。
有些人会困惑于我们建出的 GameFeature Plugin 没办法跨专案共用,但这个理解并没有
错,基本上GameFeature Plugin需要依赖于BaseGame而存在,因此我们也可以理解成他是
专属于project的Plugin。若你的功能想要跨专案共用,你要写的是一般的GamePlugin而
不是GameFeature。
在理解了GamePlugin跟GameFeaturePlugin的不同点之后,让我们先来思考一下,Plugin
到底是什么?就字面上来看的话,形象一点解释,他就像是一个插头,把这个插头插进我
们的专案之后就能够提供各种我们游戏专案会需要的功能。虽然Plugin不像DLC需要做到
“热插拔”,但大家期望的是功能的插入跟移除是相对简单的。因此每个GamePlugin在设
计上要能做到独立运作,也就是说在设计上只能依赖于Engine或EnginePlugin,依赖于其
他同层级的GamePlugin会明显造成游戏专案的使用障碍。移除简不简单这件事取决定
Plugin的功能与细部设计这边先不展开讨论,但对于功能的插入而言,应该没有人会希望
开启某个Plugin前还需要去阅读文件看清楚有哪些相依Plugin需要下载吧?若我们设计的
Plugin有依赖于其他同层级的GamePlugin的情形发生,可能要思考看看哪些功能要下放到
GameModule给专案自己实作,哪些功能需要整合进同一个GamePlugin中。
由上面的讨论我们知道,Plugin在本质上就是对于能独立运作功能的封装,而这些功能来
自于各种需求各异的不同专案,因此Plugin在设计上必须要放在一个更抽象的位置以满足
不同专案的需求。因此我不建议在Plugin中实现任何的框架机制,因为那个是Engine该做
的事;若只是薄薄一层的实作,主要目的只是更改某些Engine的默认设定的话,那个则是
专案GameModule该做的事,我们不该假设我们的设定会比Engine的默认更通用;若是觉得
Engine的某些Function实作有bug或不够完美,所以想继承下来做override来做
walkaround,那个也是专案该做的事,我们可以写一些文档分享给其他团队说明为什么我
们想做这些修改,而不是写成某个Plugin要求大家先继承某个class再做某些事,因为那
不一定适用于所有的专案,再次强调,我们不该假设我们的实作会比Engine的实作更具有
通用性,若你觉得你的目的是修掉Engine Bug,你该做的是上Github送PullRequest给
Engine。
那么到底什么东西适合写成一个Plugin,最好的判断方法是:先假设把这个Plugin拿到
UnrealMarketplace上卖时,我们是否能够明确的指出这个Plugin想解决的是什么问题?
受众是谁、有多少人可能会需要这个功能……等等类似的问题,若无法回答这些问题,代
表相关功能的想法还不够成熟,建议先拉回GameModule中等待各种需求的磨练。
那么在把功能写在GameModule时我们该注意什么呢?就我所知,根据专案团队的风格会有
不同的构成方式,有人喜欢使用一个大module放入所有的功能、也有人喜欢依功能的性质
导入一些模组化的设计,但不可否认的是,由于这边的程式码非常接近Gameplay、更迭会
非常快速,有可能一个游戏企画的改变导致某个系统在瞬间变成一个没用的东西。基于这
种变动频繁的特性,因此有些人会觉得不需要在GameModule的层级上浪费时间进行游戏逻
辑的模组化设计,反正就是想办法用最快的方法将功能做出来,不要因为模组化而导入的
各种限制而拖慢了组装系统的时间。不过我自己倒是有不同的观点,在GameModule这个层
级做模组化反而是我们用来对抗变化的手段,良好的模组化设计应当要考虑到当你想把某
个功能移除时需要耗费多少的力气,理想状况是直接把该Module移除就行,不过这部份就
属于不同系统的实作细节了,就不再往下讨论。除此之外,我认为模组化的练习可以帮助
我们慢慢的把相对不成熟的想法粹练出来。我们不需要一开始就用Plugin的高度在思考问
题,在个人经验不足或需求还不明确的情况下很容易陷入寸入难行的地步;相对的在
GameModule层级进行设计的话,我们可以先以更贴近专案的需求的方式来思考怎么进行抽
象化的设计。
结论
以上写了这么多,大致下可以简单用以下几点描述:
1. 每个GamePlugin在设计上要能做到独立运作,也就是说在设计上只能依赖于
Engine或EnginePlugin。
2. 不建议在Plugin中实现任何的框架机制,因为那个是Engine该做的事,不该假设
我们的设定会比Engine的默认更通用。
3. 功能不要急着写成Plugin,若你无法回答写这个Plugin想解决的是什么问题、受
众是谁时,先把功能放回GameModule磨练。