表演的艺术,妖尾回合制战斗系统客户端设计

时间:2019-11-12
本文章向大家介绍表演的艺术,妖尾回合制战斗系统客户端设计,主要包括表演的艺术,妖尾回合制战斗系统客户端设计使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

妖尾历经几年开发,终于在今年6月底顺利上线,笔者从2017年初参与开发,主要负责妖尾战斗系统开发。战斗作为游戏的核心玩法系统,涉及很多技术点,希望能借几篇文字,系统性总结MMORPG战斗系统的开发经验。
本文主要从宏观层面总结回合制游戏战斗的美术资源规范,系统框架设计和主要技术点,比如断线重连,技能表演等。

系列博文传送门:
记录战斗记录你,详解妖尾战斗录像系统

美术资源规范

  • 模型

模型分为低模(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 回合按需加载 战后卸载

上图简述了战斗系统涉及的主要资源及加载缓存策略,一言蔽之,就是既要体面,又要节约。

我们希望游戏体验尽量流畅,在社区场景遭遇战斗时能秒切进入战斗,所以:

  1. 最基本的战斗场景和全屏图资源,高低配机都会预加载好并常驻内存;
  2. 本着节约原则,其他资源尽量做到不影响体验,用时加载,不用时战后卸载。
  3. 骨骼动画,技能,Buff等资源每回合根据表演内容再按需加载;
  4. 模型方面可以预判像玩家自己,队友等己方模型经常复用,高配机会缓存至下一场战斗,无命中再战后卸载,战斗HUD也会常驻内存;低配机资源比较紧张,还是用战后即时卸载的策略。
  • 资源使用策略

另外,一场战斗表现少说也会涉及数十个资源的异步加载,如果每处表演逻辑都要异步等待资源加载回调,很容易导致回调地狱。因此战斗状态机特意将资源加载,资源使用划分成两个阶段。每回合等待表演所需资源全部异步加载完毕,才能进入到表演阶段,表演逻辑按同步方式使用资源即可。由于资源加载粒度细分到以回合为单位来加载,实测资源加载等待并不会影响战斗表演的流畅体验。

  • 资源ab打包策略

讲到资源管理,ab打包是个绕不开的话题。ab打包粒度越细,包数量越多,IO压力大;ab打包粒度越粗,资源越冗余,包体,热更新资源量都会变大,说到底是平衡的艺术。

资源 打包策略
战斗场景 单独打包
全屏背景图 每张图单独打包
战斗HUD HUD集合打包,HUD上的动态小图标按类别集合打包
功能模块UI 按模块集合打包
通用特效 所有通用特效集合打包
主角模型 每个主角各个模型部件单独打包,各个骨骼动作单独打包
非主角模型 每个角色为单位打包
技能 每个技能单独打包,技能引用资源分普通技能,合体技两类,再按角色为单位打包
Buff 所有Buff集合打包

简单罗列了战斗相关资源的ab打包策略,原则上是尽量按资源使用耦合程度划分打包,可能一起使用的资源,打包到一起,如果资源过多,就要进一步拆分ab包。再者,做好提前设计,确保打包策略在未来资源量堆起来后仍能适用。比如,主角模型的模型部件,骨骼动作非常多且在未来很有可能新增,可以每个资源单独打包;非主角模型模型,骨骼动作数量相对固定,就能以角色单位打包。规划好ab打包策略后,跟美术约定好规则来提交资源目录及资源,就能编写工具根据配表,不同目录执行不同的ab打包策略。

战斗表演

战斗表演大体分为技能和Buff两类表演。技能是有开始结束的一段表现,小到普通攻击,大到多人合体技,都是技能表演;Buff则是附在单位上的持续性状态表现,如人物的中毒,封印状态表现。

  • 技能表演

正如前面的框架图里提到妖尾战斗有很多表演脚本,可综合对角色,UI,场景,镜头,节奏做全方位的调度控制,从而表现一段技能。伽吉鲁和蕾比两个角色的合体技是非常有代表性的一段技能表演,涵盖了对很多技能脚本的应用,简单举例讲解这个合体技的实现,就可以了解技能是怎么编辑,表演的。下图是合体技的游戏表现:

总体来看,这个合体技由镜头动画+技能打击两部分构成,这两部分都是在同一条时间轴通过脚本组合运用编辑出来的,最后生成一个合体技预制件。

图中红色部分是镜头动画实现脚本:

  1. 动画挂靠脚本带有Animator组件,控制相机和角色的位置旋转。
  2. 动画挂靠脚本带有相机挂点,启用场景镜头动画相机并放到挂点下
  3. 动画挂靠脚本带有角色挂点,生成对应角色高模实例,放到挂点下,并播放合体技动作
  4. 动画挂靠脚本每帧根据角色表情挂点更新角色表情
  5. 特写角色特效脚本实例生成包括角色底下的IRON文字特效,光影粒子特效,速度线UI特效等,放置到相对镜头相机指定位置播放
  6. 线光源设置脚本修改战斗环境光
  7. 替换材质脚本替换伽吉鲁模型材质
  8. 相机径向模糊脚本启用相机后处理特效

不难看出镜头动画的主要逻辑是由动画挂靠脚本实现的,主要镜头,角色走位调度由美术实现Animator进行控制。视镜头动画的复杂效果,可能会堆多一些特写特效脚本同步播放,丰富画面效果。

战斗系统运用了几个透视相机,按相机深度由低到高分别是:

  • 战斗背景图相机
  • 战斗单位名字相机
  • 战斗UI相机
  • 战斗主相机
  • 战斗镜头动画相机
  • 战斗镜头动画UI相机

镜头动画播放完,紧接着就是绿色部分脚本,配合完成技能释放:

  1. 移动脚本控制伽吉鲁跑到战场中央
  2. 播放特效,动作脚本控制伽吉鲁做出打击表现
  3. 受击脚本控制目标做出受击动作,扣血反馈
  4. 震屏脚本,角色抖动脚本配合着加强打击感
  5. 回位脚本控制伽吉鲁回到自己的站点

技能释放需要由更多的脚本组合完成,一般不需要美术产出很多资源,利用一些简单攻击特效,配置角色走位,动作,受击,镜头控制就能做出漂亮打击感的技能。

  • Buff表演

Buff表演相比技能表演更简单,容易编辑,实现。每种Buff都可以分为Buff添加,Buff持续,Buff触发,Buff移除4个阶段,视需求自由决定每个阶段是否有具体表现,Buff编辑器只需配置每个阶段的特效,人物动作,替换材质即可。下图是反击Buff的游戏表现,4个阶段都有特效表现。当然,也存在一些Buff是设计成完全无表现的。

结语

至此本文就结束了,主要还是就美术资源,资源管理,协议交互,战斗表演做了些介绍,内容并没有涵盖整个战斗系统,不过已是战斗系统核心设计内容,特此记录,也希望能提供一些经验借鉴。

原文地址:https://www.cnblogs.com/leoin2012/p/11831170.html