jquave 发表于 2023-1-17 13:30

图形引擎实战:战斗同步分享


同步概念: 状态同步与帧同步

本篇主要分享同步战斗逻辑,主要会包括技能逻辑、攻击检测、移动、碰撞等内容;网络同步按大类来分一般会非为状态同步和帧同步两种;
帧同步的战斗逻辑在客户端运行,所有操作指令通过服务器转发到各个连接的客户端,然后客户端按照一定的逻辑帧帧率进行演算,保证输入的指令并且保证计算的结果一致就能保证同步,其中的难点在于定点数碰撞检测以及随机数统一等;
状态同步的战斗逻辑在服务端运行, 客户端则只做表现,所有的客户端将操作上传到服务器由服务器计算结果再将角色状态下发同步给各客户端;
这里介绍act demo中所用到的状态同步方案,通讯协议方面考虑实时性所以选择的是UDP,ProtoBuf作为序列化协议;
碰撞检测

因为想要实现3D的act体验,而不像早期mmo站桩输出的感觉,所以在检测角色的移动和攻击检测会用到3D碰撞检测,考虑过在服务端跑Physx物理引擎,但实际上考虑到只需要实现简单几何体例如长方体,球体,圆柱体等间的相交检测就能达到想要的效果而且不用完整跑性能消耗较高的物理引擎;所以放弃了在服务端使用Physx的想法;
在碰撞检测中最常用也相对简单的就是OBB(Oriented bounding box)间的相交检测,2D的两个OBB间的相交需要判断两个OBB在所有轴上的投影都发生重叠,在3D中更为复杂但是基本思路一致;


有了碰撞检测之后,我们只需要在移动时检测角色碰撞框是否与周围其他玩家的碰撞框之间是否有相交即可;在检测攻击时也类似,检测攻击框与其他玩家的碰撞框之间是否有相交;
可视化的调试窗口:

开发服务器端时因为看不到直观的演算结果和反馈,所以调试时会有些许困难;由此想到可以在服务端使用图形渲染API例如DX来将地面以及角色的碰撞框、攻击框绘制出来,既可以实时的看到状态变化也能够对比与客户端的差异;在demo中我使用DXapi来绘制角色碰撞框和攻击框,用不同颜色来代表不同的状态;


定时器:

为了保证逻辑演算的准确性,服务器上也需要类似客户端FixedUpdate的固定帧更新;那么就需要一个高精度的定时器,初版的demo是在.net平台编写的,尝试了System.Windows.Forms.Timer 、System.Threading.Timer 与System.Timers.Timer等方式实际测试精度均不可靠。后经查阅发现使用win32api中的CreateTimerQueueTimer函数可以实现毫秒级的定时器,这样一来服务器就可以按固定的帧率进行演算了;
CreateTimerQueueTimer(
            // Timer handle
            out phNewTimer,
            // Default timer queue. IntPtr.Zero is just a constant value that represents a null pointer.
            IntPtr.Zero,
            // Timer callback function
            callbackDelegate,
            // Callback function parameter
            pParameter,
            // Time (milliseconds), before the timer is set to the signaled state for the first time.
            // 定时器第一次设置为信号状态之前的时间(毫秒)。
            dueTime,
            // Period - Timer period (milliseconds). If zero, timer is signaled only once.
            //Period - 计时器周期(毫秒)。 如果为零,则计时器仅发出一次信号。
            period,
            (uint)Flag.WT_EXECUTEINIOTHREAD);
RootMotion:

在游戏的客户端中,我们可以通过打开动画的RootMotion开关,以获得角色移动时完全贴合动画位移的效果,避免实际速度与动画中的位移不匹配而造成的“滑步”。既然我们是状态同步,那么我们的位移实际上是在服务端演算,角色正常的走跑等匀速移动比较好处理,但是战斗动作中大多数都不是简单的匀速,如何在同步中也还原这些位移呢。很多游戏会给技能动作配置位移的曲线,在使用技能时通过这些曲线去贴近动作的位移,由此想到动画文件本身就是按造曲线存储的,与其去编辑这些曲线贴近动作,为什么不直接读取动画文件的位移呢。
服务器去读取那么复杂的动画文件显然不合适,我们只需要RootMotion的效果,那么就只需要读取动画中Root节点的位移曲线而已,在unity中打开Animation窗口,再打开对应的动画文件并将窗口切换到Curves,就能看到动画中存储的曲线信息,可以看到类似Animaator.RootT的节点就代表动画中的根节点位移信息。


这里我写了一个简单的工具,可以在unity中读出动画文件的根节点曲线信息,并按想要的帧率存储成byte文件,供服务器读取;可以通过以下步骤获得root曲线:
①获得动画文件所有的曲线:
            EditorCurveBinding[] curveBindings = AnimationUtility.GetCurveBindings(animationClip);
②通过曲线名称获得Root曲线(XYZ三轴)
foreach (var curveBinding in curveBindings)
            {
                var curveName = curveBinding.propertyName;
                if (curveName.StartsWith("RootT.x"))
                {
                  curveX = AnimationUtility.GetEditorCurve(animationClip, curveBinding);
                  continue;
                }
}
③按造一定的帧率通过curveX.Evaluate(time)计算出每帧的位置并存储导出;
当播放技能或者特殊动作时,就可以按帧读取动画曲线文件中的位置信息,并将其赋值给角色的根节点。
下面是显示效果,在客户端的平滑处理后(客户端未作预测,位移完全由服务器同步下发),同步的位移效果与单机状态的rootmotion效果一致;


角色数据类型与逻辑:
服务器端的角色基类,包含了以下主要数据和逻辑:
①Transform位置及方向,因为一般角色不会有X轴和Z轴的旋转,所以我将方向简化为Y轴的弧度值,便于传输和计算;
②ActorState角色状态,用于状态机处理指令;
③AnimPlayer动画播放器,用来按帧执行动画计算位移和触发效果等;
④RoleData角色数据,包含技能组、血量、移速等等由配置表读出的数据;
⑤CurrentOrder当前指令,由客户端上传的当前帧指令;
逻辑处理的顺序问题,在处理所有角色逻辑时,先执行角色操作指令还是先执行攻击检测也会导致表现上的不同;假如先执行指令再执行攻击检测的情况,那么如果闪避、招架等指令和攻击的指令同时到达时,那么就更利于防御或者闪避的成功;反之则更容易击中。
我在demo中的每一帧的逻辑执行顺序为:
处理玩家指令->位移、动画计算->攻击检测->发送同步数据
需要注意的是,这里的每一步都是执行完所有玩家的当前步骤再执行下一步,而不是一次执行完一位玩家的四个步骤;
同步的消息设置:

首先登录和登出的时候会先同步玩家自己的角色信息,还有场景内所有其他玩家的角色信息,供客户端创建场景内的玩家actor。然后还要同步给场景内所有玩家自己的登录消息,让其他玩家创建自己的actor。同理登出的时候也要将登出的消息同步给场景内所有玩家。消息类似于下面:
message LoginRes
{
bool result = 1;
PlayerInfo myPlayer = 2;
repeated PlayerInfo otherPlayers = 3;
}
message PlayerOnlineSync
{
bool inOrOut = 1;
PlayerInfo player = 2;
}
然后便是发送频繁的客户端操作指令和服务端的同步指令,客户端的操作指令比较简单只包括操作类型、方向和技能ID就可以,服务端的指令包含角色id、指令类型、技能id、方向以及位置;消息类似于下面:
message PlayerOrder
{
OrderType order = 1;
float direction = 2;
uint32 skillId = 3;
}
message ServerOrder
{
int32 actorId = 1;
OrderType order = 2;
uint32 skillId = 3;
float direction = 4;
Pos transform = 5;
}
技能数据配置:

技能数据通过Excel进行配置,然后通过工具转换为服务器可读的byte文件,在服务器端建立配置表管理器。
配置表的结构:


服务器回传状态时同时也会有技能施放的信息,客户端不用管动作切换的问题,只需要按配置表读取对应的动画名称,在播放对应的动画片段即可;
这里是客户端的动画状态机,几乎没有连线:




客户端:

①平滑
因为服务器回传的状态数据的频率肯定远低于实际运行的画面帧率,如果只是将角色的位置设置到同步的位置上,那么角色移动就会很”卡顿“,于是就需要平滑处理;这里我尝试将服务器同步的位置作为终点,在渲染帧中,使用lerp、movetowards或者smoothdamp等平滑插值的方式去设定当前位置;这样的做法简单有效,但是会造成位置表现落后于实际位置一段时间,这个时间就是插值的时间;
更加有效的方式就是客户端预测,在移动的指令输入后,客户端直接响应操作进行移动,等服务器的数据同步回来后再进行校验,结果以服务器为准。
②自动锁定敌人
在初版的demo中操作时,由于是3d视角,总会出现打不到瞄不准的情况出现,所以加了锁敌的逻辑。原理十分简单,用physx.overlap的方式检测周围是否有其他玩家的碰撞体(这个碰撞只用于锁敌,没有参与角色控制),锁定与自己面朝方向最接近的,当然也可以设定夹角范围和距离。

欢迎加入我们!
感兴趣的同学可以投递简历至:CYouEngine@cyou-inc.com

FeastSC 发表于 2023-1-17 13:37

有无demo地址分享[赞同]

闲鱼技术01 发表于 2023-1-17 13:40

[爱][爱][爱]
页: [1]
查看完整版本: 图形引擎实战:战斗同步分享