游戏框架的一些思考

从永航来到祖龙之后,负责开发一款帧同步的游戏框架。整个开发过程中碰到了各种各样的问题,好在团队很给力,总监技术非常强,很多问题大多都被解决了。
在整个过程中,自己又加深了对局内框架的理解,这里挑几点我认为最重要的列举出来。

1 数据黑板、面向对象、组件编程

之前在永航《星际火线》项目开发过程中碰到了一系列OO编程的问题,其中最主要的有两个:一是多人协同开发时,经常会不注意接口的抽象化,经常基类最终成为一个大的接口,大家都在用;二是复杂跨逻辑数据很难传递。
前一个问题说白了是团队能力问题,其实也一定程度上说明OO本身的抽象比较困难。而后一个问题,则非常讨厌,我甚至认为这是OO本身的一些缺陷。
举个例子,需求是:角色开枪发出子弹攻击到敌人,计算敌人掉血。其中子弹的伤害公式同时来自于角色属性、枪、子弹、子弹飞行距离、敌人属性、玩法等。伤害公式在不同模式下还可能不尽相同。按照OO思想,我们很容易想到实现一个Damage类,并让上面每个模块实现一个TransmitDamge(Damage)的接口,这样每次开枪的时候,需要按照Player, Weapon, Bullet, Movepattern, Enemy, GameMode的顺序,依次传入伤害公式需要的各个模块的属性,最终在碰撞敌人后,计算出结果。在这种结构下,Damage需要耦合所有相关的模块,好吧,只有Damage,感觉还行。现在多了一种需求,枪械或者玩家可以觉醒,觉醒后伤害公式和之前获得的所有参数都有可能发生变化,举个例子:Player某个觉醒可能会导致其他任何一个模块的数值变化,比如会导致Enemy的防御减少。最可怕的是,策划提出,有40把枪,每把枪有3种觉醒行为。于是你发现,基本上你要耦合所有模块并且暴露这些模块原来封装好的数据(比如Movepattern的初始速度,觉醒后需要修改了,于是需要暴露出来。当然,你可以提供回调,也是茫茫多的回调)。这个时候,OO数据和逻辑封装一起就出现了一个问题:

想访问某个OO对象的数据,就必须由该对象提供数据访问接口,外界访问时,必须先拿到该对象的实例(很难统一数据访问接口,因为接口应该是面向行为的)

那么,耦合则在所难免。
现在项目组的技术总监是服务器出身,对于MVC的框架理解很深。我们在探讨过程中发现,MVC中model层的抽象,恰恰是将数据的访问完全解耦逻辑。总监的一句话,启发了我:

数据永远不怕耦合

于是,局内逻辑直接抽象出Model层(KVDB),就解决了上面的问题。
再加上使用类Unity的Object-Component方式,就形成了一种对象-组件-数据的框架。继续深入了解后发现,业界其实已经存在一套类似的框架:ECS。
思考到这,后续就不用过多累述了,有兴趣的读者直接查阅ECS框架即可(OverWatch就是这个框架)。这里给出两个参考资料。
守望先锋的ecs框架(感谢强哥):http://www.gad.qq.com/article/detail/28682
开源的ecs框架(感谢楠哥): https://github.com/sschmid/Entitas-CSharp/wiki
当然,具体问题具体分析,我们最终没有完全选择ECS模式,毕竟框架是否适合,还是要先从需求出发,这个世界上永远没有银弹。

2 行为树

这次实现框架的重点之一,就是希望用行为树统一局内主要战斗逻辑:包括技能、AI、场景等等。当前阶段,行为树主要应用于技能部分。
在经过了最初的一些摸索之后,行为树的强大已经体现了出来。在逻辑范围内,主要通过condition和action的组合,基本上能够完成绝大部分的逻辑功能。
行为树的文章比较多了,这里主要参考腾讯的Behaviac,自己实现了一套。去掉了一些不常用的冗余功能,同时针对项目特点(可恢复帧同步框架,需要定点数和数据黑板支持),增加了很多没有的功能模块。
一般情况下,对于每个对象,行为树是其一个组件,例如角色包含若干个SkillBtAgent以及一个AIBtAgent。这里存在两个问题:

  1. BT是树状结构,创建BT最好的办法是一边解析一边创建,这就导致必须在战斗期间创建BT节点,很难动态缓存(可以使用prototype模式clone节点,但仍然需要战斗期间clone)
  2. 如果每一个实例都存在一个BTAgent,则会生成大量BT节点,内存和时间消耗都会很大。

经过思考,我们参考ECS模式中的Service,将BT变成了一种静态服务。这样,每一个对象,只是将其上下文扔进BT,然后经过各个节点更新上下文,从而改变了对象行为。由于KVDB的存在,跨模块访问对象数据只需知道访问该对象的key即可,所以BT访问对象数据非常方便。

在行为树的实现细节方面,需要重点考虑的一件事情是,事件性触发如何处理。因为行为树的各个节点更新是从Root一层一层嵌套Tick下去的,所以理论上讲,行为树所有的节点,都应该是Tick执行。对于Condition来说,总有一种类Event的Condition,是外界发生了某一瞬间的事情,如玩家释放的按钮,发生碰撞等等,这些事件无法直接通过Tick进行判断。
我们给出的一种方法是,当某种事件发生时,由该事件所在的模块,写入某一标志位,Condition查询该标志位,在下一帧模块更新时,复位该标志位。对于某些事件发生,需要出发一个持续行为的时候,我们将Condition换成PreCondition,当且仅当进入某个节点时才进行判断,一旦进入节点后,就不再判断。这样,既保证了事件本身的影响仅在一帧内,又满足了事件可以出发持续性的行为。

3 状态机真正功能

希望用行为树统一局内之后,一段时间一直纠结状态机是否需要存在。因为对象的属性都是存储在KVDB中,所以不需要状态机中的“状态”,又因为行为树通过condition-action表达了所有的逻辑,所以不需要状态机中的“机”。
然而,经过一些尝试,我发现状态机除了上面的两个概念以外,还隐藏了一个重要的概念:
状态机的进入和离开是成对出现的
这就形成了一种机制,某种状态,一定同时存在一个进入事件和一个离开事件,成对出现可以保证角色状态的可恢复性。举个例子,某个技能,在释放时不能移动,释放完成后可以移动,我们用行为树构造时,可以写成以下的样子:

condition : onButtonUp
action : setMove false
action : releaseBullet 5
action : setMove true

正常情况下,上面设置移动的方法没有太多问题。但是,当新增了一条打断技能的需求时,就会发现,打断逻辑无法确定是否执行setMove true的行为。因为之前的技能有可能执行到任何一个位置,如果不管之前状态贸然setMove true,又可能会将其他模块的属性冲掉(比如打断时玩家已经死亡,而死亡是无法移动的,这时通过打断逻辑设置可移动无疑是错误的)。
所以,状态机的存在,保证了enterState时候设置上的状态,一定可以在leaveState的时候清除掉。这里可能也存在一个更高层的编程方法论:
谁创建,谁操作,谁管理
这个方法论简单的说,也就是每一个对象都需要有明确的管理者,去管理生存周期。

4 管理生存周期

在进行局内细节设计的时候,一定要注意生存周期和管理的明确化,要求必须所有的对象,都有明确的管理者。我们使用的是Object-Component结构,对象与组件、对象与对象、组件与组件之间通过消息进行通信。这种框架本身耦合的非常松,不注意对象之间关系的话,很容易造成对象一创建,到处找不到的情况。
最典型的例子是,角色释放技能,被另外一个角色打断,此时应同时清除掉所有该技能对应的特效、子物体、碰撞盒甚至停止音乐等等。这就要求技能释放者应时刻了解自己创建了哪些东西,并且框架本身必须提供明确的清除接口。

5 对象属性,buff、装备及其他

角色身上的属性是一个比较有意思的模块。初看这个,感觉不就是一条一条的变量吗,还至于形成一个模块吗。仔细分析才会发现,问题不是那么简单。
一般情况,存在两种属性:数值属性和布尔属性。数值属性如攻防血,布尔属性如是否可以移动、是否可瞄准、是否可被攻击等。
属性的复杂主要在于,不同模块都可以对同一属性进行写入,这个时候如果仅仅用一个变量来代表则会导致模块间的重叠。举个例子,玩家被击晕的时候无法移动,被冰冻的时候也无法移动,假设在a时间,先后中了一个3秒的击晕和一个5秒的冰冻,这时,两个模块同时将属性movable置为false。接下来,在第3秒,由于击晕时间已到,则击晕模块把movable置为true,然而,此时5秒的冰冻并没有结束,显然movable应该是false。上面使用了两个buff作为例子,当然,实际中,buff是可以通过自身管理避免这种问题,但是,属性之间的叠加很容易也发生在不同模块之间。
我们解决的思路是,各个模块维护各个模块自身的同名属性值,同时对象还维护一个叠加属性值。当模块进行写入操作时,遍历其他所有模块对应的同名属性值,计算出结果,更新叠加值。当某些逻辑需要取属性值时,则直接读取叠加值。这样,既保证了模块与模块之间的独立,又保证了读取属性的效率。
对于布尔型属性,可以用整型+mask进行存储,对于数值型属性,可以用list+index存储。当然,不管是哪一种类型属性,使用k-v对应的map都是一个更灵活的选择。
有了这种针对属性的数据结构,你会发现buff、装备等,只需要构造相对应影响属性的key即可。

6 帧同步编程注意事项

主要就以下几点:

  1. 保证定点数一致性
  2. 保证遍历的顺序一致性
  3. 保证随机一致性
  4. 保证玩家操作不在逻辑世界中
  5. 保证玩家操作之前发生的随机事件不在逻辑世界中
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值