最近做了一个包含播报制战斗的活动,活动大致是玩家一起挑战某个世界boss,直至将boss消灭,按伤害量进行奖励。该活动的战斗是播报类型的,即战斗过程是完全无操作,玩家只是要看完整个完整的战斗过程。
很显然,在玩家一开始进行战斗时,战斗结果已经确定了,客户端只是在接收到服务端发来的该场战斗的数据后,像播“视频”般将整场战斗播放出来。在此总结分析下战斗部分的代码。
与战斗相关的UML类图:
Main为活动的核心类,拥有很多模块的引用,如声音模块,网络通讯模块,资源加载模块,窗口管理模块,这些在上面的UML类图里没有一一列出来。CombatVideo(战斗播放)和StateTask(状态机)则是与战斗最为相关的模块。
状态机:
通常来说,拥有较多状态表现且状态经常改变的模块,适合设计成状态机模式。
针对播报战斗而言,有进入战斗Loading,请求战斗初始数据,初始化战斗面板UI,加载战斗资源,播放出场动画,请求战斗结果数据,播放战斗,退出战斗等状态,且某些状态需要符合一定条件后才可以转变成下一个状态,如向服务端请求战斗结果,只有当接收到服务端的数据后,才能进入播放战斗的状态。因此,很适合以状态机模式来设计。
战斗模块的状态转换还是非常简单的,基本是线性的,CombatCallState->CombatWaitingState->CombatLoadingState->SCombatResLoadedState->CombatSpiritOutState->CFightRoundState,不涉及复杂的状态变换。
由StateTask按顺序执行各个State的execute方法,当某一State的工作完成后,调用StateTask的pushTask接口,新增一个新的State。
战斗播放:
拿到后台返回的一推战斗数据后,如何将其表现成一场完整的战斗过程,这是整个战斗系统的核心。
核心类:
CombatVideo:持有CombatVideoPlayer,Screen,VideoConverter,负责了整个战斗播放的控制工作
CombatVideoPlayer:战斗播放器,接收Segment数据,负责战斗播放。
CombatData:服务端的战斗数据。
VideoConverter:将服务端的CombatData转换为Segment数据。
Segment:战斗播放的最小的单位数据,由CombatVideoPlayer控制播放。Segment的粒度细分到:受到buff、近程攻击、远程攻击、换宠、逃跑等。持有对Screen的引用,是实际逻辑的实现类,什么意思?就是整个舞台什么时候做什么事,做什么逻辑,是由Segment来控制的,因此称Segment为“实际逻辑的实现类”。用剧本片段来比喻Segment是最恰如其分的。
以换宠这个Segment为例:
1、先在舞台相应位置create一个宠物球特效
2、特效播放完后,UI(血条,图标等)信息更新为新宠物的信息
3、在舞台相应位置create新宠物,播放从小变大的帧动画
4、宠物播放idle动画,UI添加buff信息。
当上面四个过程都完毕后,类本身派发ON_SEG_END事件,表明该Segment已经播放完毕。
可以说,一场完整的战斗,所有的细节都是细分成Segment的。当客户端收到该轮战斗的CombatData时,根据其类型下发给相应的Segment处理,进而将冷冰冰的战斗数据,变成有血有肉的视觉表现。CombatVideoPlayer则负责播放一帧帧的Segment,串联成一场战斗。
Screen:战斗场景的容器类,持有对战斗Sprite,特效,UI的引用。
CombatVideoPlayer负责按序执行处理队列里的Segment数据,调用Segment的start方法,并监听VideoEvent.ON_SEG_END事件,每个Segment执行完毕后,派发VideoEvent.ON_SEG_END事件。CombatVideoPlayer监听到VideoEvent.ON_SEG_END事件后,就处理下一个Segment数据。直到处理完所有的Segment数据。
Segment可细分为不同类型的Segment,ActionChangeSpiritSegment、ActionFightSegment、ActionStartSegment分别对应换宠、打击、进场等动作片段。各Segment通过接口控制Screen的Spirit进行各种动作表现,如宠物进场/退场、宠物打击/受击等。