美术资源规范
模型
模型分为低模(1500-2000面)、高模(6000-10000面)两种规格,战场单位统一使用低模,但在合体技等镜头动画表演使用高模。主角模型是由头、上衣、武器、下装4部分组成的,游戏中通过网格、贴图合并成1个完整模型进行展示,这样可以实现部件换装。非主角模型比较简单,直接加载完整模型。
挂点
高低模都有头、脚、血量、受击、左右手、左右脚等挂点,高模相比低模额外多了表情挂点(下文解释挂点作用)。
材质球
高模跟低模使用不一样的材质球。低模全身只用了一种材质球,而高模用了两种材质球,脸部和身体分别是不同材质球,脸部材质球实现了uv动画用于做表情变化。高低模的身体材质球都实现了描边,高模额外开启了自阴影。高模贴图为256x256大小png图片,低模为128x128大小png图片,贴图都是宽高相等的POT尺寸,这样Android/IOS可以分别使用ECT2/PVRTC压缩格式。
表情实现
人物表情是通过shader uv动画实现的,索引0-3从分别对应下面贴图的4个表情。由于shader是项目TA编写输出的,要让动作美术能够控制表情变化,我们定了个表情挂点位置映射索引的规则,表情挂点x轴数值除以100向下取整即为索引,动作美术在动画时间轴里只需要编辑表情挂点的位置,通过程序转换设置shader参数,就能控制表情变化。
动画
战斗单位的动画状态机具有非常多的状态,有多达60+多个动画,但常用动画只有其中几个,所以战斗单位不会在进入战场时一次性加载所有动画,默认只加载站立、受击、奔跑、死亡等4种动画。其他动画则每回合按需加载,我们会按角色预先存储动作和对应资源路径的配置表,需要用到的动作查表获取路径加载资源,作为AnimationClip加载到RuntimeAnimatorController上。另外,像受击浮空等动画还需要处理好依赖,相关的过渡动画也要一并加载。
技能
技能是使用Flux编辑器制作出来的,通过时间轴上创建多个Sequance轨道来组成一段技能表演,每个Sequance脚本负责1种表现,如角色移动、播放特效等,Sequence脚本共同作用就能表现出一段技能。1个技能最终生成动作、音频共2个Prefab。1个战斗单位拥有的技能也非常多,不会在进入游戏时一次性加载,也是每回合按需加载要用到的技能。
Buff
Buff相比技能表现要简单,因为最多只有添加、持续、触发、移除等4个阶段需要做表现,每个buff prefab挂相应的4个脚本,配置特效资源,人物动作,替换材质即可。
美术资源流水线
主角模型与非主角模型的资源提交规范稍有不同,但制作流水线基本是一样的。美术提交包括模型fbx、动作fbx、动画机,材质球、贴图等资源,通过工具脚本进行资源检查、预处理,生成预制件到指定目录。
图解战斗框架
一套战斗框架其实包括了很多内容,一篇文章难以讲清所有细节。不过笔者尝试画图总结了战斗按功能划分的各个模块,希望尽量讲清基本模块的内容,模块之间的关系,从而在宏观层面了解战斗系统。
骨架——战斗状态机
架构图紫色部分为PlayMaker脚本集合,如果将战斗框架理解为人,那PlayMaker状态机就是人的骨架,它串联了整个战斗流程。妖尾战斗采用了PlayMaker插件可视化编辑整个战斗流程,这样易于编辑,追踪整个战斗流程,直观地将战斗分成始化、表演、指令选择三大战斗状态。各战斗状态基本为线性流程,战斗状态之间则通过全局事件进行转移。
另外一点是,我们希望尽量用lua实现战斗逻辑,PlayMaker插件原生只支持C#,为了支持Lua,我们实现了继承C#状态机行为基类(FsmStateAction)的子类,该类负责驱动Lua脚本,Lua脚本实现跟FsmStateAction类同样的接口和行为,这样就可以用Lua编写状态机逻辑了,代码基本实现如下:
namespace HutongGames.PlayMaker.Actions
{
public class LuaFsmStateAction : FsmStateAction
{
public string luaFileName = "";
private LuaTable _luaTable;
private LuaFunction luaOnEnter = null;
private LuaFunction luaOnExit = null;
private LuaFunction luaOnUpdate = null;
public override void OnEnter()
{
if (_luaTable == null && !string.IsNullOrEmpty(luaFileName))
{
LuaSupport.DoFile(luaFileName);
LuaFunction luaFunction = LuaSupport.lua.GetFunction(luaFileName + ".create");
if (luaFunction != null)
{
_luaTable = luaFunction.Invoke<LuaFsmStateAction, LuaTable>(this);
luaFunction.Dispose();
}
if (_luaTable != null && _luaTable.IsAlive)
{
luaOnEnter = _luaTable.GetLuaFunction("OnEnter");
luaOnExit = _luaTable.GetLuaFunction("OnExit");
luaOnUpdate = _luaTable.GetLuaFunction("OnUpdate");
}
else
{
Debug.LogError("Cannot find lua class " + luaFileName);
Finish();
return;
}
}
SafeCall(luaOnEnter);
}
public override void OnExit()
{
SafeCall(luaOnExit);
}
public override void OnUpdate()
{
SafeCall(luaOnUpdate);
}
private void SafeCall(LuaFunction func)
{
if (func != null && func.IsAlive)
{
func.Call();
}
}
}
}
大脑——战斗控制器
战斗的核心管理器就是架构图底下蓝色部分的战斗控制器,它是战斗系统的大脑。战斗控制器负责接收协议数据,驱动战斗逻辑。
战斗控制器有两种方式接收数据输入。对于通常的联网战斗,底层网络层接收后台协议数据,再传输给战斗控制器。妖尾还在新账号进入游戏时,设计了一场战斗用于展示关键剧情,这场战斗则是离线模拟战斗。我们单独实现了模拟战斗控制器,它负责根据策划配表生成模拟协议数据,传输给战斗控制器驱动战斗逻辑。
协议设计
整个战斗流程的协议设计如下图所示,可以分为战场初始化,等待加入战场,战前表演,回合选招,回合表演,战斗结束等6个阶段。战斗控制器收到不同的协议包切换PlayMaker状态,进而改变战斗流程。
一场战斗是由一组连续的协议数据组成的。如果由于客户端卡顿,切出后台等原因,出现前一个协议包还未处理表现完,下一个协议包已经到了,忽略协议包不处理,或者粗暴切断当前逻辑,直接处理下一个协议包都是不可取的,可能导致战斗表现异常。因此战斗控制器设计了协议缓存队列,用于缓存顺序处理协议数据,然而缓存队列并不是简单地顺序处理数据就能万事大吉了,如果不加以考虑处理断线重连的情况,就会碰到像战斗进度严重延迟,甚至卡死等情况。
断线重连
战斗控制器的一大要务就是处理好战斗中的断线重连,恢复并修正战斗流程。简单来看,战斗中断线重连有两大类情况:断线重连后战斗已结束;断线重连后战斗未结束。
第一种情况比较简单,断线重连的登陆包带有玩家是否处于战斗中的标志位,如果当前不处于战斗中,前台却仍处于战斗场景中,则清掉所有战斗协议缓存,执行退出战斗的逻辑。
第二种情况则要细分多种情况讨论。一般断线重连后,战斗协议缓存队列可能存有多个战斗协议,需要确认协议数据是否仍为原来那场战斗的。简单判断原则就是,如果队列中收到初始战场包,且其战场ID与之前协议不同,可以认为断线重连回来后已开始了另一场新战斗,旧战斗数据已失效,直接清出缓存,开始处理表现新战斗。
接着考虑断线回来后还在原战斗的情况,战斗设计上断线重连必然会收到初始战场包,战场包带有当前战斗阶段的标志位,根据标志位即可还原战场状态:标志位为战前表演,回合表演阶段,该客户端马上发送表演结束Req,等待服务端通知下回合开始,避免拖慢战斗进度;标志位为回合选招阶段,客户端切为选招界面,并根据阶段开始时间戳修正剩余选招时间。
肉身——战斗资源
战斗资源理所当然就是战斗系统的肉身了。管理资源的难点在于合理加载卸载,如人有四肢五官,协调越好,运动性能越强,越节省体力。
资源管理策略
战斗场景 | 登录预加载 | 常驻内存 |
全屏背景图 | 根据场景切换 | 常驻一张图 |
战斗HUD | 高配登录预加载; 低配入场预加载 | 高配常驻内存; 低配战后卸载 |
功能模块UI | 战中即时加载 | 出战斗卸载 |
通用特效 | 高配登录预加载; 低配入场预加载 | 常驻内存 |
己方模型 | 进战斗预加载 | 高配缓存到下一场战斗,无命中则战后卸载; 低配战后卸载 |
敌方模型 | 进战斗预加载 | 战后卸载 |
骨骼动画 | 入场加载基本动画, 其余动画按需回合加载 | 战后卸载 |
技能 | 回合按需加载 | 缓存一回合,不命中则淘汰 |
Buff | 回合按需加载 | 战后卸载 |
上图简述了战斗系统涉及的主要资源及加载缓存策略,一言蔽之,就是既要体面,又要节约。
我们希望游戏体验尽量流畅,在社区场景遭遇战斗时能秒切进入战斗,所以:
- 最基本的战斗场景和全屏图资源,高低配机都会预加载好并常驻内存;
- 本着节约原则,其他资源尽量做到不影响体验,用时加载,不用时战后卸载。
- 骨骼动画,技能,Buff等资源每回合根据表演内容再按需加载;
- 模型方面可以预判像玩家自己,队友等己方模型经常复用,高配机会缓存至下一场战斗,无命中再战后卸载,战斗HUD也会常驻内存;低配机资源比较紧张,还是用战后即时卸载的策略。
资源使用策略
另外,一场战斗表现少说也会涉及数十个资源的异步加载,如果每处表演逻辑都要异步等待资源加载回调,很容易导致回调地狱。因此战斗状态机特意将资源加载,资源使用划分成两个阶段。每回合等待表演所需资源全部异步加载完毕,才能进入到表演阶段,表演逻辑按同步方式使用资源即可。由于资源加载粒度细分到以回合为单位来加载,实测资源加载等待并不会影响战斗表演的流畅体验。
资源ab打包策略
讲到资源管理,ab打包是个绕不开的话题。ab打包粒度越细,包数量越多,IO压力大;ab打包粒度越粗,资源越冗余,包体,热更新资源量都会变大,说到底是平衡的艺术。
战斗场景 | 单独打包 |
全屏背景图 | 每张图单独打包 |
战斗HUD | HUD集合打包,HUD上的动态小图标按类别集合打包 |
功能模块UI | 按模块集合打包 |
通用特效 | 所有通用特效集合打包 |
主角模型 | 每个主角各个模型部件单独打包,各个骨骼动作单独打包 |
非主角模型 | 每个角色为单位打包 |
技能 | 每个技能单独打包,技能引用资源分普通技能,合体技两类,再按角色为单位打包 |
Buff | 所有Buff集合打包 |
简单罗列了战斗相关资源的ab打包策略,原则上是尽量按资源使用耦合程度划分打包,可能一起使用的资源,打包到一起,如果资源过多,就要进一步拆分ab包。再者,做好提前设计,确保打包策略在未来资源量堆起来后仍能适用。比如,主角模型的模型部件,骨骼动作非常多且在未来很有可能新增,可以每个资源单独打包;非主角模型模型,骨骼动作数量相对固定,就能以角色单位打包。规划好ab打包策略后,跟美术约定好规则来提交资源目录及资源,就能编写工具根据配表,不同目录执行不同的ab打包策略。
战斗表演
战斗表演大体分为技能和Buff两类表演。技能是有开始结束的一段表现,小到普通攻击,大到多人合体技,都是技能表演;Buff则是附在单位上的持续性状态表现,如人物的中毒,封印状态表现。
技能表演
正如前面的框架图里提到妖尾战斗有很多表演脚本,可综合对角色,UI,场景,镜头,节奏做全方位的调度控制,从而表现一段技能。伽吉鲁和蕾比两个角色的合体技是非常有代表性的一段技能表演,涵盖了对很多技能脚本的应用,简单举例讲解这个合体技的实现,就可以了解技能是怎么编辑,表演的。下图是合体技的游戏表现:
总体来看,这个合体技由镜头动画+技能打击两部分构成,这两部分都是在同一条时间轴通过脚本组合运用编辑出来的,最后生成一个合体技预制件。
图中红色部分是镜头动画实现脚本:
- 动画挂靠脚本带有Animator组件,控制相机和角色的位置旋转。
- 动画挂靠脚本带有相机挂点,启用场景镜头动画相机并放到挂点下
- 动画挂靠脚本带有角色挂点,生成对应角色高模实例,放到挂点下,并播放合体技动作
- 动画挂靠脚本每帧根据角色表情挂点更新角色表情
- 特写角色特效脚本实例生成包括角色底下的IRON文字特效,光影粒子特效,速度线UI特效等,放置到相对镜头相机指定位置播放
- 线光源设置脚本修改战斗环境光
- 替换材质脚本替换伽吉鲁模型材质
- 相机径向模糊脚本启用相机后处理特效
不难看出镜头动画的主要逻辑是由动画挂靠脚本实现的,主要镜头,角色走位调度由美术实现Animator进行控制。视镜头动画的复杂效果,可能会堆多一些特写特效脚本同步播放,丰富画面效果。
镜头动画播放完,紧接着就是绿色部分脚本,配合完成技能释放:
- 移动脚本控制伽吉鲁跑到战场中央
- 播放特效,动作脚本控制伽吉鲁做出打击表现
- 受击脚本控制目标做出受击动作,扣血反馈
- 震屏脚本,角色抖动脚本配合着加强打击感
- 回位脚本控制伽吉鲁回到自己的站点
技能释放需要由更多的脚本组合完成,一般不需要美术产出很多资源,利用一些简单攻击特效,配置角色走位,动作,受击,镜头控制就能做出漂亮打击感的技能。
Buff表演
Buff表演相比技能表演更简单,容易编辑,实现。每种Buff都可以分为Buff添加,Buff持续,Buff触发,Buff移除4个阶段,视需求自由决定每个阶段是否有具体表现,Buff编辑器只需配置每个阶段的特效,人物动作,替换材质即可。下图是反击Buff的游戏表现,4个阶段都有特效表现。当然,也存在一些Buff是设计成完全无表现的。
结语
至此本文就结束了,主要还是就美术资源,资源管理,协议交互,战斗表演做了些介绍,内容并没有涵盖整个战斗系统,不过已是战斗系统核心设计内容,特此记录,也希望能提供一些经验借鉴。