动画同步
unity/c++ mmo大型多人在线游戏开发系列教程,本节代码在此。
在之前的实现里,我们已经可以让每个玩家互相看见对方的移动了,但仍缺少了移动的动画表现, 本节我们就把缺少的动画加上。

动画状态
Unity的动画状态机对我们来说是个黑盒,我们仅能通过设置动画状态机的变量来改变动画的状态,因此,动画的同步也就只能同步这些动画的状态变量。
就我们主角在用的那个动画状态机,里面主要是四个变量:
水平移动速度: Speed(float)
是否在地面: Grounded(bool)
跳跃: Jumping(bool)
下落: Falling(bool)
目前simulate
的角色跟主角采用的是同一个动画状态机,因此,只需要把这些变量也同步就好。
对于布尔类型的变量,我们可以把它们打包到一个整型变量中,能省点空间:
public class PlayerController : MonoBehaviour
{
public int GetMoveMode()
{
int result = 0;
if (Grounded)
{
result |= 1;
}
if (IsJumping)
{
result |= 2;
}
if (IsFalling)
{
result |= 4;
}
if (IsFlying)
{
result |= 8;
}
return result;
}
}
public class NetworkComponent : MonoBehaviour
{
private void UpdateMoveMode(int mode)
{
IsJumping = false;
IsFalling = false;
Grounded = false;
IsFlying = false;
if ((mode & 1) != 0)
{
Grounded = true;
}
if ((mode & 2) != 0)
{
IsJumping = true;
}
if ((mode & 4) != 0)
{
IsFalling = true;
}
if ((mode & 8) != 0)
{
IsFlying = true;
}
_anim.SetBool("Grounded", Grounded);
_anim.SetBool("Jumping", IsJumping);
_anim.SetBool("Falling", IsFalling);
_anim.SetBool("Flying", IsFlying);
}
}
而对于Speed
,本身我们就有在同步位置、速度之类的信息,就不再需要处理了。
更新时机
动画的状态同步过来了,什么时候更新呢?
目前我们有三种移动同步的方式,直接设置、插值、预测。直接设置最简单,收到即更新就好。插值的话则要延时到开始处理这个数据包时再更新。至于预测,也直接是收到即更新,毕竟预测动画的状态出错的几率很大,倒不如直接用服务器的值:
public void SyncMovement(ServerMovePack serverMovePack)
{
if (SyncMovementMode == ESyncMovementMode.Direct)
{
// 直接设置,在ApplyMovementFromServer中直接应用Mode
ApplyMovementFromServer(serverMovePack);
}
else if (SyncMovementMode == ESyncMovementMode.Interpolate)
{
// 插值模式,延迟到serverMovePack开始处理时再应用Mode
_interpolateMovements.Enqueue(serverMovePack);
}
else
{
// ...
// 预测模式同样直接应用Mode
UpdateMoveMode(serverMovePack.Mode);
}
}
插值模式下的速度计算
在插值模式下,一开始我是这么算速度的:
// 速度 = 位移 / 插值时长
_curVelocity = (_endPos - _startPos) / MovementSyncInterval;
其中插值时长是固定的一个值,跟服务端同步的间隔大小一样。也就是说,取两次服务端同步数据间的位移,除以同步间隔,得出平均速度。
但是,当应用到动画状态机后,会发现动画偶尔会突变,例如autonomous
一直在跑,但是simulate
跑的过程中可能突然切了一下到普通走动动画,然后又切回跑的动画。
原因是,目前服务端其实并没有实时在计算更新角色的位置,角色的位置都是直接取autonomous
上传的值。服务器在MovementSyncInterval
内不一定就能稳定收到来自autonomous
总计MovementSyncInterval
的更新数据包。因此authority
的状态在MovementSyncInterval
时间内发生的状态变化并不一定就等于MovementSyncInterval
,可能多了,也可能少了。
这就最终导致上面的计算方式,位移的计算部分是错的,导致动画偶尔发生突变。
在当前服务器还没自己实时计算更新的情况下,我们只能做一些妥协,让simulate
看起来流畅一些,autonomous
在上传位置时附带了时间戳,simulate
端的速度计算改为:
float realInterval = serverMovePack.TimeStamp - _lastSimulateTimeStamp;
_curVelocity = (_endPos - _startPos) / realInterval;
_lastSimulateTimeStamp = serverMovePack.TimeStamp;
但注意,这个速度只会应用到动画上,simulate
物理的移动速度还是用最上面的计算方法,毕竟就是硬性要求在MovementSyncInterval
内到达_endPos
。因此虽然细看之下可能会发 现滑步之类的情况,但这已经比动画突变要好很多了。
代码整理
现在autonomous
的移动逻辑放在PlayerController
中,而simulate
的移动逻辑则在NetworkComponent
中,对一些同时处理autonomous
和simulate
的代码来说就会有点难受,例如IK:
private void OnAnimatorIK(int layerIndex)
{
if (!UseFootIK)
return;
// 要分别处理autonomous和simualte
if (_controller != null)
{
if (_controller.IsFalling || _controller.IsJumping) return;
if (_controller.GetSpeed() > 0) return;
}
else
{
if (_networkComp.IsFalling || _networkComp.IsJumping) return;
if (_networkComp.GetSpeed() > 0) return;
}
// ...
}
虽然两者的移动逻辑并不一样,但移动的状态是共享的,因此现在抽出一个CharacterMovement
组件,把共公的状态都塞里面。本身是可以把autonomous
和simulte
的移动逻辑也都塞里面的,但两者的相关性不大,并且塞一起那就太长了,不好看,所以又单独抽了一个SimulateMovement
,依赖于CharacterMovement
。搞完之后,PlayerController
和NetworkComponent
就没剩多少东西了,也挺好。
小结
到目前为止,我们实现了一个算是可用的移动同步方案,剩下可以优化的点是:
- 作弊检测,目前服务端是完全信任客户端的数据,但是人心险恶啊,如何检测数据的合法性,以及检测到非法数据时如何回退客户端是个问题。
- 服务端实时位置计算,通常的作弊检测只是校验一下速度之类,再进一步的方案是,服务端也实时计算,这么一来检测会更准确,同时也可以做一些服务端预测,在没收到客户端的数据时也能跑下去。
但这些并不有趣,暂且先放一边,下一节我们来写战斗,给角色加几个技能玩一下。