0%

核心玩法框架设计思路(Fishing Elite)

核心玩法框架是基于“属性、行为、条件”三大设计元,融合帧同步的联机实时同步思路、状态同步的业务逻辑维护思路、ECS的属性管理和快照思路,以支持快速迭代、支持可视化编辑工具和弱编码工具为主要特点的联机实时对决框架。

一、核心玩法框架内容简介

1.1 项目现状

  • 较为紧张的开发时间、高频的迭代需求
  • 高创新需求:需求难以具体描述、且常常需要反复尝试和调整
  • 高度基于物理、拟真、随机性的游戏演算过程
  • 分布式无状态的服务器基础框架、低服务器开发支持
  • 中重度的拟真玩法和在线对决

1.2 项目需求

  • 一定程度的工具化,提升迭代速度
  • 工具化需要更好的支持创新
  • 更好的拟真性封装实现
  • 基于客户端的核心玩法演算
  • 完整的实时对决机制

1.3 框架功能

  • 一套面向策划的可视化脚本工具
    • 特点:低理解成本、高灵活性、易于操作
    • 功能:快速简便的尝试和实现新的设计想法,进而有效提高产品的迭代速度
  • 纯客户端的核心玩法业务
    • 特点:由客户端维护状态、计算数值;服务器仅协助处理核心玩法中的重要事件或数据(如回合结算、关卡刷新)
    • 功能:实现全部的核心玩法功能
  • 一整套联网实时对决方案
    • 特点:实现联机客户端间的近似同步,对服务器仅依赖消息转发
    • 功能:实时联机同步、断线重连、录像回放、防作弊、……

二、游戏业务框架设计思路

从需求出发?——如何整理框架设计思路
“你这个框架,主要解决了什么问题?”

2.1 思维方式1:从需求逐步抽象成定义

需求分层:以某个用户想装修一栋房子用于居住为例

  • 用户想要书房有舒缓的灯光和放书的柜子,想要在游戏房有R.G.B.和手办柜,……(具象的、细节的)
  • 用户想要在不同房间有风格各异的装潢。(笼统总结,带有方向指引的)
  • 用户想要丰富的居住体验,最大化发挥各个房间的不同价值。(Lv.1:具体总结、指引设计方向)
  • 用xxx的预算,完成一个能够支持每个房间采用不同解决方案,满足不同居住需求的总装修设计,并实际落地。(Lv.2:带有限定词的总结和方向指引——用户去哪了?其实限定词就描述了用户,而描述用户常常是需求总结中最容易忽视的一点——此时可以让其它描述也剥离用户视角:用户想要什么→我要干什么)
  • 本装修设计思路是以多样性和实用性为主目标,对不同预算提出合适的解决方案,为满足用户具体需求提出实际解决方案的一种工程指导。(Lv3:对工作目标和结果的定义)

需求分层:以UI框架为例

  • 我们需要解决UI上常见的异步时序/资源管理/状态代码冗余等等问题,支持UI动画/界面分层等等功能。(具象的、细节的)
  • 我们需要简化UI功能的代码编写、统一UI功能上下文规范、简化工作流。(Lv.1:具体总结、指引设计方向)
  • 为中轻度游戏项目量身定制一套最大程度降低成本、提升效率的UI框架。(Lv.2:带有限定词的总结和方向指引)
  • MRCV框架是针对游戏客户端业务功能开发,对显示层和数据层设计中的具体问题提出解决方案,并以简化外层工作、提升迭代速度为主目标的设计模式框架。MRCV框架作用于代码、资源、设计和所有相关工作流。MRCV依赖于设计的合理抽象。MRCV适用于任意复杂度的项目和功能。(Lv3:框架的目标和定义、作用域、作用条件。)(MRCV框架设计介绍.pptx )

总结

框架设计需要先结合需求、现状、资源等等,综合的形成整体“印象”,然后抽象出最接近这一”印象”的一个“定义”,再最终找出能符合”定义”的一个”设计”。

思考:“状态机与行为树,哪个更好?” “MVC和ECS,哪个更适合UI框架?”
设计的逻辑闭环:细节的用户需求 → 对用户和需求的笼统印象 → 对设计本身的定义 → 是否能实现每个细节用户需求,是否能满足用户体验,发挥用户价值 → 随着用户发挥价值,产生新的需求。

(顺时针)基于设计形成的逻辑闭环,用户与设计间是螺旋上升的关系

思考:这种思维方式,只能指导我们如何从需求出发完成一个框架设计。但我们又如何从框架设计反推用户的使用方式,推进用户发挥新的价值?

2.2 思维方式2:统合框架用户与设计者间的主要矛盾

参考:康德—先天综合判断

从用户需求到设计理论的矛盾
囿于程序框架设计的表现形式,程序框架设计总结必须是基于先天判断(理论驱动)的,即独立于经验和感官印象,具备普遍性、必然性的;而用户需求常常是基于后天判断(用户驱动)的,即基于感官印象或实践经验,不具备必然性的。我们首先需要通过理论抽象和普适原则的归纳,将来自感官印象和实践经验的后天判断提炼为不受个别案例限制的先天判断,确立其在程序框架设计中的普遍适用性和必然有效性。

从框架设计到框架使用的矛盾
囿于程序框架设计的功能效用,程序框架设计总结必须是基于综合判断(创造性的)的,即附加性质的、扩充性质的、基于经验认知的;而框架使用者常常需要基于分析判断(解释性的)的结论,即说明性的、必然性的、理论可靠的。我们必须通过精确的逻辑演绎和对细节的严密审查,将基于经验和创造性的综合判断精炼为清晰、明确并具有可验证性的分析判断,以便框架使用者能够理解和信赖这些结论在实际应用中的真实有效性。

总结
可以看到:框架设计者与他的主要客户(面向的客户、框架使用者)是存在根本矛盾的。因此,框架设计要服务用户,就必须要先剥离用户

思考:如何统合不同思维方式间的矛盾?
可能的答案:从更高抽象层面寻找共性,或有限辩证式的寻找解决方案;批判性的沟通和互相学习;合理妥协,求同(目标)存异(细节);螺旋式的探索和实践(但要注意成本)

三、核心玩法框架 - 设计思路

3.1 核心玩法的“印象”

见上文 1.1 项目现状 和 1.2 项目需求

3.2 核心玩法的“定义”

核心玩法框架针对基于大量复杂演算的玩法,为其实时在线同步和过程回放,提供工程实践上的解决方案;
它还作用于程序代码编写和策划设计实现相关工作流,以提高配置迭代速度、增加配置灵活性为主目标;
核心玩法框架定制给希望成本更低、不严格要求精度和品质、需要更快速迭代、更灵活创新的场景。

四、核心玩法框架 - 设计要素

4.1 实时在线对决

引子

要实现在线同步,至少需要实现哪些功能?

  • 快照
    • 游戏可被序列化和反序列化
  • 回放
    • 游戏逻辑执行是稳定可再现的
      要实现更好的在线同步,需要实现哪些功能?
  • 帧一致性
    • 依据各端间约定好的步长(step),保证各端在每一帧执行时,都已经准备好了所有所需内容。
    • 全端等待?延迟端亏损?亏损补偿?
  • 矫正
    • 游戏内的所有属性可被随时修改到另一个值,并不影响业务逻辑
  • 预测(快进)
    • 游戏运行不依赖未知内容
  • ……

帧同步是如何实现复杂玩法的在线同步的?(常见于MOBA)(马里奥建造2)

这类游戏约定游戏帧率(步长),在每一步中收集所有玩家的操作,并下发;各个客户端都依据这同一操作进行游戏,通过内部业务逻辑设计保证输入相同时,输出一定一致。
可总结:将游戏逻辑视为一个保证输入输出的黑盒子,将玩家输入视为唯一输入入口;操作和游戏业务逻辑解耦了。

参考:细谈网络同步在游戏历史中的发展变化(上)云风的 BLOG: lockstep 网络游戏同步方案
参考:经典帧同步网络卡顿现象:马造2的卡比能多恶心?马里奥制造2

为什么我们不适用帧同步

  • 帧同步的开发和维护成本极高。
  • 帧同步对网络稳定性的要求高。

状态同步是如何实现复杂玩法的在线同步的?(常见于MMORPG)

这类游戏的主要游戏计算都被放在服务端,即客户端仅负责输入,服务端负责了业务逻辑和具体的输出。通过这种方式,所有客户端都必定能收到完全一致的、经过服务器验证的状态。
但这种同步常常容易出现:较差的打击感和反馈感(延迟)、高服务器负载、……。

参考:细谈网络同步在游戏历史中的发展变化(中)

延伸:基于状态的设计模式

以状态机为例:复杂的状态切换维护、上下文与状态的耦合问题、状态间的上下文依赖问题等,均成为状态机难以维护的理由——不能很好的支持开闭原则。
从领域驱动设计的角度分析:状态仅仅是区分业务逻辑领域的一个标志位,状态模式作为一种行为型设计模式,其主要目标是分离行为模块,而非管理和维护状态。

思考:状态是不是一种属性?由属性驱动逻辑是否算是一种数据驱动?
参考:领域驱动设计:参考书《领域驱动设计 软件核心复杂性应对之道》
参考:从4万行代码降到1.8万,腾讯视频竟然用DDD做架构重构?
参考:一文读懂:领域驱动设计DDD

为什么我们不适用状态同步

  • 状态同步对服务器开发要求高。
  • 状态同步无法较好的还原游戏细节。

ECS是如何实现复杂玩法的在线同步的?(守望先锋、喷射战士)

E+C:将业务逻辑模块分割成了最小逻辑元或数据元(C),而每个实体(E)由多个C组合而成,游戏由所有实体组合而成。因此,只要C是可序列化和反序列化的,整个游戏就是可以快照的。
S:系统模块(S)将细碎散乱的C合理的管理起来,并依照规划好的策略刷新它们。因此,只要S中的逻辑策略是稳定可再现的,整个游戏就是可以回放的。
可总结:数据与逻辑的解耦(状态和行为的解耦)

参考:云风的 BLOG: 浅谈《守望先锋》中的 ECS 构架
参考:经典ECS卡顿现象:【Splatoon 3】自产搞笑合集(1:05~1:32间)

延伸:什么叫属性与逻辑解耦?

if (a > 10) b = 3; 这行代码意味着属性a与属性b有耦合。
if (a > 10) BLogic(); 这行代码意味着属性a与逻辑b有耦合。

思考:可否定义”逻辑单元”为”一批属性的耦合集”?

延伸:分层框架与平铺框架

UI框架有很强的分层需求(MRCV框架复盘.pptx )
数据驱动的框架常常是平铺框架

思考:在MRCV框架中是如何处理分层UI元素与平铺式Proxy之间的矛盾的?有什么更好的解决方案?
思考:为什么说数据驱动的框架常常是平铺式框架?数据层的设计会用哪些技巧来更好的平铺?

为什么我们不适用ECS

  • 我们并没有大量同质化实体(E)
  • 我们并没有大量可复用逻辑或数据(C)
  • 我们的业务逻辑并没有复杂到需要有专门的系统来进行管理(S)

总结:我们的工作重点在哪里?能借鉴哪些思路?

  • 低开发和维护成本 → 使用状态同步而非帧同步
  • 分模块的分工效率 → 使用按领域划分的平铺模式
  • 低服务器开发成本 → 不能让服务器负责状态同步计算
  • 高迭代效率、高debug支持、高度工具化 → 借鉴组件模式,高度实现热插拔
  • 较好(而非完美)的还原游戏细节 → 属性同步机制
  • 完美的还原游戏重要节点 → 行为(状态)同步机制
  • 低网络要求 → 较好的回放、快照、预测、回滚、矫正机制

4.2 工具化

(编辑中)(工业化与工具.pptx )

参考:低代码逻辑编排观:PlayMaker

艺术家出身的开发者创作会更喜欢 Playmaker,Playmaker 是一种发散型思维,散乱的做,做到哪里想到哪里,随心所欲,发现 Bug 再去纠正;这在前期的创作中非常重要,我没有去花大的精力考虑那些需要注重的程序先后关系,而把精力完全花在了创意上面。

五、核心玩法框架 - 设计简介

5.1 设计元

设计元是框架的基本组成原子

属性

整个游戏都是由属性(或称数据)组成的。属性是可序列化和反序列化的。属性可以驱动行为的产生。
属性的设计为类似帧同步的实时同步机制、回放机制提供了土壤。

行为

行为是一种数据。行为是业务逻辑启动的钥匙。行为数据的生命可能是瞬间的,也可能是有限持续性的。
行为的设计为类似状态同步的实时同步机制、回放机制提供了土壤。

业务逻辑单元

业务逻辑单元是一段自解释的逻辑,它是属性的修改器,它描述了属性间的耦合关系。
业务逻辑单元的设计反映了领域边界的制定,为平铺式设计和热插拔提供了土壤。

条件

条件是行为的触发器。条件由最简洁的业务逻辑单元构成,条件和行为共同组成属性和逻辑的解耦器。
条件虽然包含逻辑,但在这里也被视为一种数据。

实体

实体是一系列数据和业务逻辑单元的聚合。
实体的设计为热插拔提供了土壤,并更好的梳理框架分层。

数据

数据是一系列静态属性的聚合。
数据层的设计为工具化提供了土壤。

组成核心玩法框架的设计元

5.2 设计原则

设计原则是指导设计元如何有机结合的

组件化、平铺式

我们的核心玩法阶段繁多、而功能模块间耦合偏少。组件化、平铺式的设计最大的特点是碎片化;而碎片化的设计虽然不利于处理复杂耦合关系,但利于业务逻辑维护和迭代效率,是很适合我们核心玩法的。
业务领域自解释
如何划分每个设计碎片?我们从领域驱动设计的思路出发,并额外添加一项要求:每一个碎片必须能自解释,即自驱动运行(狭义)、无(轻)依赖、无(松)耦合。这样不仅支持了每个业务领域的热插拔、还省去了管理器等复杂设计模块。
属性分层
动态属性/静态属性;临时属性/存档属性
编辑器属性/实体持久化属性/功能临时属性

工程倾向

工程倾向是基于业务环境分析,在框架的冗余区域内求索,合理利用框架设计中的伸缩性、延展性的一种工程实践方案。

主要业务逻辑在C#编写

  • 主要业务逻辑多数依赖Update,且有很多物理/拟真的算法。在C#写更加方便且性能友好。
  • 在C#层可以方便的统合基于SO的编辑工具(静态属性)和业务逻辑。
  • 基于:行为的设计、业务逻辑单元的解耦、属性的全局性质,Lua可以轻松的观测和修改C#层的业务逻辑执行方式。只要对运行期行为数据进行截取/修改/观测,即可实现热更需求。

关于领域间的互相访问和调用

按照框架设计本意,为维持SOLID原则(主要是开闭原则)、维持属性与逻辑解耦,原则上实体之间是不能互相访问的。业务逻辑中需要访问其它实体的属性时,只能通过属性系统(FishingPropertyWrapper)访问:这样访问到的属性是可靠的、只读的。而业务逻辑无权写外部属性,更无权调用其它实体的业务逻辑单元。
但在实际工程实践中,因为业务领域间的边界会随着需求变化而变化、扩充或融合;在这个动态迭代的背景下,业务逻辑单元间的耦合本身就是不可避免的。受开发资源和时间限制,把所有逻辑耦合都用行为解耦是过于麻烦的甚至是不现实的。同时只要我们有意识的处理业务逻辑耦合,耦合所产生的问题就是可控的、可预期的。因此,工程实践上允许实体间的互相访问、非异步业务逻辑间的有限互相调用

管理器(Manager/System)与封装器(Wrapper)

在核心玩法框架设计中,是没有“管理器”的概念的。因为所有的属性和行为(数据)都是开放可读的、所有业务逻辑模块都是自解释的,所以它们各自独立存在于框架环境中,并不需要受某个统一逻辑管理。
而为了提升具体开发的便捷性,还是需要一个统一出入口协助访问那些宛如一盘散沙的属性、行为,甚至其它实体。这就是本次核心玩法框架中的封装器”Wrapper”。
封装器是作为一种工具类存在的,它主要有以下作用:

  • 按字典的形式,统一存储框架内所有属性/行为/实体的引用,方便查询和访问,也提供筛选等高级功能。
  • 为基础数据单元封装额外接口:如为行为数据单元(FishingBehaviour)额外封装运行中行为类(FishingRuntimeBehaviour),从而分离编辑器逻辑和运行期逻辑、分离静态数据和动态引用。
  • 为所有数据或引用提供唯一更新顺序。

生命周期与状态与逻辑切片

核心玩法框架中的业务逻辑单元,及其聚合体(领域或实体),在本质上是没有生命周期的:它只有初始化、销毁;其中的Update方法仅仅是一种逻辑触发入口,而非生命周期的概念。它在本质上也没有状态的概念:即使有,状态也只作为一种驱动业务逻辑改变的属性或数据而已。
而作为替代,所有的业务逻辑单元都有”开始”和”结束”的概念。得益于行为的设计,所有的业务逻辑都可以被行为数据简单的管理:我们可以通过对行为数据的截取/修改/观测等等,得知并修改业务逻辑单元的开始、运行和结束。这也是另一种意义上的生命周期,这种生命周期更精细,更简洁。

另一种思考方式:一文带你入门面向切面编程(AOP)

5.4 必要工具

必要工具是在不破坏框架设计基础上,让框架与需求有机结合的润滑剂。必要工具的设计常常是拿来主义的。

计时器

计时器管理了整个核心玩法所有属性、行为、条件、业务逻辑单元的更新周期,它部分替代了Unity本身的Update方法。计时器可以支持游戏跳转/重开/倍速/追帧等各种时间相关功能。

思考:计时器管理了业务逻辑单元的更新周期,而渲染单元的更新周期应当如何管理?如果我们希望更少的执行逻辑计算,但不影响渲染流畅性,应该怎么设计?

加载器

加载器是一个工具类,它负责所有实体的加载和卸载,但它不管理实体。
现在核心玩法的加载器是数据驱动的,它会随时响应外部数据的变化并更换核心玩法内使用的实体。

计算器(随机数、浮点数)

要实现”输入相同时,输出一定相同”的逻辑黑盒子,必须保证业务逻辑内所有计算使可控的。因此我们需要自己包装专门的随机方法、浮点数计算方法。
现在我们的随机方式是使用一个初始随机种子,随每次随机计算递增;随机计算是基于线性同余法和二进制打乱实现的,可以保证较高随机性,较低均匀性,较长周期性。

输入

输入是核心玩法框架之外的一个单独模块。核心玩法框架只是受输入的数据驱动而已。
所有输入会被严格转换为数据,从而支持在线对决、回放等等功能。

序列化与反序列化

存档和读档是帧同步游戏的重点之一;而ECS设计更是让游戏存档可以精细分块。我们需要通过自包装的序列化和反序列化工具实现至少以下功能:

  • 游戏存档和读档(如:在游戏的第五秒存档,并在有需要时还原回第五秒的游戏数据)
  • 分领域的存档和读档(如:在某条鱼计算出错时,主机单独存档某条鱼,让另一个客户端重置这条鱼的数据)
  • 存档对比和矫正(另一个客户端重置这条鱼的数据时,用不易被用户察觉的方式缓动过去)
  • 加密和压缩(让存档文件尽可能小,又要有可读性的工具支持)

得益于平铺式的全局属性设计,我们只要把所有属性都序列化,就可以形成游戏存档。读档时再把属性创建回来即可。如果我们是分层式的属性管理,光是描述属性层级关系就已经很困难了。

因为过度的平铺化,核心玩法运行中,行为数据、属性数据等数据类,会被高频率大量创建、高频率重复访问和修改。因此我们需要对这些数据单元实施精细的池管理。

六、核心玩法框架 - 具体设计

框架设计图
联机方案设计
防作弊方案设计
客户端服务器通讯简图
属性框架简图

附:真实性AI设计思路

Q&A

如何处理逻辑时序

如何管理属性

AI的设计方向

如何防作弊

鱼之间的关系——实体的复杂互相访问问题