UE4移动组件详解(三)——RootMotion与特殊移动模式的实现思路

五.特殊移动模式的实现思路


        这一章节不是详细的实现教程,只是给大家提供常见游戏玩法的一些设计思路,如果有时间的话也会考虑做一些实现案例。如果大家有什么特别的需求,欢迎提出来,可以和大家一起商讨合理的解决方案。

5.1 二段跳,多段跳的实现


        其实4.14以后的版本里面已经内置了多段跳的功能,找到Character属性JumpMaxCount,就可以自由设置了。当然这个实现的效果有点简陋,只要玩家处于Falling状态就可以进行下一次跳跃。实际上常见的多段跳都是在上升的阶段才可以执行的,那我们可以在代码里加一个条件判断当前的速度方向是不是Z轴正方向,还可以对每段跳跃的速度做不同的修改。具体如何修改,前面3.2.1小结已经很详细的描述了跳跃的处理流程,大家理解了就能比较容易的实现了。

5.2喷气式背包的实现


        喷气式背包表现上来说就是玩家可以借助背包实现一个超高的跳跃,然后可以缓慢的下落,甚至是飞起来,这几个状态是受玩家操作影响的。如果玩家不操作背包,那肯定就是自然下落了。

        首先我们分析一下,现有的移动状态里有没有适合的。比如说Fly,如果玩家进入飞行状态,那么角色就不会受到重力的影响,假如我在使用喷气背包时进入Flying状态,在不使用的时候切换到Falling状态,这两种情况好像可以达到效果。不过,如果玩家处于下落中,然后缓慢下落或者几乎不下落的时候,玩家应该处于Flying还是Falling?这时候突然切换状态是不是会很僵硬?

        所以,最好整个过程是一个状态,处理上也会更方便一些。那我们试试Falling如何?前面的讲解里描述了Falling的整个过程,其实就是根据重力不断的去计算Z方向的速度并修改玩家位置(NewFallVelocity函数)。重写给出一个接口MyNewFallVelocity来覆盖NewFallVelocity的计算,用一个开关控制是否使用我们的接口。这样,现在我们只需要根据上层逻辑来计算出一个合理的速度即可。可以根据玩家的输入操作(类似按键时间燃料值单位燃料能量)去计算喷气背包的推动力,然后将这个推动力与重力相加,再应用到MyNewFallVelocity的计算中,基本上就可以达到效果了。

        当然,真正做起来其实还会复杂很多。如果是网络游戏,你要考虑到移动的同步,在客户端角色是Simulate的情况下,你需要在SimulateTick里面也处理NewFallVelocity的计算。再者,可能还要考虑玩家在水里应该怎么处理。

5.3 爬墙的实现


        爬墙这个玩法在游戏里可以说是相当常见了。刺客信条,虐杀原形,各类武侠轻功甚至很多2D游戏里面也有类似的玩法。
在UE里面,由于爬墙也是一个脱离重力的表现,而且离开墙面玩家就应该进入下落状态,所以我们可以考虑借助Flying来实现。基本思路就是:

        创建一个新的移动模式 爬墙模式
        在角色执行地面移动(MoveAlongFloor)的时候,一旦遇到前面的障碍,就判断当前是否能进入爬墙状态
        检测条件可以有,障碍的大小,倾斜度甚至是Actor类型等等。
        如果满足条件,角色就进入爬墙状态,然后根据自己的规则计算加速度与速度,其他逻辑仿照Flying处理
        修改角色动画,让玩家看起来角色是在爬墙(这一部分涉及动画系统,内容比较多)
        这样基本上可以实现我们想要的效果。不过有一个小问题就是,玩家的胶囊体方向实际还是竖直方向的,因此碰撞与动画表现可能有一点点差异。如果想表现的更好,也可以对整个角色进行旋转。

5.4 爬梯子的实现


        梯子是竖直方向的,所以玩家只能在Z轴方向产生速度与移动,那么我们直接使用Walking状态来模拟是否可以呢?很可惜,如果不加修改的话,Walking里面默认只有水平方向的移动,只有遇到斜面的时候才会根据斜面角度产生Z轴方向的速度。那我这里给出一个建议,还是使用Flying。(Flying好像很万能)

        玩家在开始爬一个梯子的时候,首先要把角色的Attach到梯子上面,同时播放响应的动画来配合。一旦玩家爬上了梯子,就应该进入了特殊的 爬梯子状态。这个状态仔细想想,其实和前面的爬墙基本上相似,不同的就是爬梯子的速度,而且玩家可以随时停止。

        随时停止怎么做?两个思路:

        参考Walking移动的计算,计算速度CalcVelocity的时候使用自定义的摩擦系数Friction以及刹车速度(这两个值都设置大一些)
        当玩家输入结束后,也就是Accceleration=0的时候,直接设置速度为0,不执行CalcVelocity
        另外,要想让爬梯子表现的进一步好一些。看起来是一格一格的爬,就需要特殊的控制。玩家每次按下按钮的时候,角色必须完整的执行一定位移的移动(一定位移大小就是每个梯子格的长度)。这里可以考虑使用根骨骼位移RootMotion,毕竟动画驱动下比较容易控制位移,不过根骨骼位移在网络条件差的情况下表现很糟。
        还有一个可以进一步优化的操作,就是使玩家的手一直贴着梯子。这个需要用IK去处理,UE商城里面有一个案例可以参考一下。

六.RootMotion


RootMotion概述


        RootMotion,根骨格位移,属于移动组与动画系统相结合的一个部分,表示角色的整体运动(包括物理)是由动画来驱动的。

        一般来说,在大部分游戏的应用里面,玩家的移动与动画是分开的。移动系统只负责处理玩家的位置与旋转,动画系统只做对应的动画表现,只要移动的速度合适就可以与动画做到完美的匹配,也就是说,动画播放的位置(即Mesh的位置)是由角色移动来驱动的(UE4里面,动画是由胶囊体的位置数据来驱动)。这样的好处之一就是解耦,移动与动画之间不需要紧密的联合,只关注自己的内容即可。

        没有移动数据配合的动画
        但是,有一些复杂的移动是很难模拟的,比如UE4官方给出的例子:一个举着锤子的人向前挥舞,一开始速度比较慢,中间挥舞时由于角色控制不住速度会很快,到最后锤子落地时,速度又变的很慢,角色会踉跄的走两步。

        实际上,无论使用哪种方法,我们都很难找到一种可以处理所有类似表现的通用模拟方案。那么我们不模拟可以么?就让玩家动画播放到很远的位置再移动胶囊体呢?也不行,因为这样的话,如果中间有墙面,角色的动画就会因为没有碰撞而穿模过去。最理想的方法就是就是交给美术去做一个带有位移的动画,玩家的位置完全交给动画去处理,不同的动画可以有不同的移动表现。(还有一种通过曲线去处理的方法,不过两者其实是类似的)

        因此,我们需要有RootMotion来应对部分复杂的动画,可以让角色的移动位置与动画完美匹配。

        3A游戏里面几乎都会用到Rootmotion

        RootMotion操作与测试
        概念介绍完后,不妨动手测试一下。我们可以直接下载官方的ContentExample并打开Animation Level的案例1.9进行测试。可以看到绿色Character开启了Rootmotion,胶囊体会一直跟随动画移动,而红色Character没有开启Rootmotion所以胶囊体没有发生位移,动画由于没有任何碰撞穿了过去。

        打开其角色蓝图与动画蓝图,简单看一下蓝图上的处理逻辑。

        1.通过事件先设置其MovementMode为Walk,然后找到其AnimInstance转换为动画蓝图并执行事先定义好的事件——PlayRootMotionExample

        2.PlayRootMotionExample位于动画蓝图,只是执行了一个Montage的播放。该Montage是由一个带位移的动画合成的,动画由动画设计师在Maya里面制作并将导出的.fbx文件导入到UE4里面。同时,需要让Montage的slot插槽设置为FullBody的并在动画图表里面播放该slot

        3.只导入动画资源是不够的,需要做一些配置。首先,角色的根骨格应该在原点且无旋转的。其次,可以看到在动画序列(资源)蓝图的左边有一个RootMotion的选项栏,要勾选上EnableRootmotion。最后,在动画蓝图的右边AnimPreviewEditor(动画预览编辑器)的下面有一个RootMotionMode,选择RootMotionFromMontagesOnly。

RootMotionMode有四个选项,含义如下:

        NoRootMotionExtraction 表示不会处理其动画内含的Transform数据
        IgnoreRootMotion 表示会解压出来Transform数据但是不应用
        RootMotionFromEverything 表示解析所有含有Rootmotion动画资源都可以去解析其Transform数据并参与到整个动画系统的权重计算
        RootMotionFromMontagesOnly 表示只取蒙太奇的Transform数据作为Rootmotion的计算

        另外,值得注意的是,该系统是支持网络同步的。当我们开启多人模式的时候,表现依旧流畅,这也是这篇文章的分析重点之一。

        理论上AnimSequence, Blendspace, AnimMontage都可以支持网络同步,但是这样在网络环境下会有几个问题,比如提高动画同步的复杂度、影响游戏性能、提高预测难度、容易破坏动画表现等,所以一般我们要关闭Montage以外的同步,将Root Motion Mode设置为Root Motion From Montages Only。如果开启了动画资源的Rootmotion但是不设置RootMotionMode会有卡顿的表现。

        实际上,虽然Rootmotion支持Montages的网络同步,但是由于其预测难度远大于普通移动,在网络环境不稳定的情况下,表现是相当糟糕的。因此,在多人游戏的时候,要适度的使用Rootmotion。

        至于同步的具体细节以及为什么会这样,后面会有详细的分析。

        (Youtube上有很多关于Rootmotion的教学视频,这里给出一个从零开始搭建并根据玩家输入控制的案例)

        UE4中RootMotion相关概念理解
        前面从概念和使用上比较详细的做了讲解,下面我们开始深入源码。首先,不妨先思考一下Rootmotion的实现,其基本思路还是挺简单的,就是移动组件每帧不断的去读取动画数据里面的移动Transform,然后应用到实际的胶囊体上面。当然,具体实现起来可能与我们所想的有所差异,这里并不是直接获取坐标并设置。而且,涉及到的数据与类非常多,逻辑也比较复杂,我们需要同时理解动画系统以及移动组件的实现原理。

        这里先给出涉及到的相关概念:

        【动画系统相关】

        USkeletalMeshComponent:对应一个包含骨骼动画的Mesh模型,可以以组件的形式Attach到一个玩家身上
        UAnimInstance:动画实例,其实就是C++版本的动画蓝图,每个SkeletalMeshComponent组件都会有一个UAnimInstance Class类型的配置项以及一个UAnimInstance类型的指针,我们动画的大部分逻辑都是在这里处理的。
        FAnimInstanceProxy:动画实例代理,属于多线程优化动画系统的核心对象。他可以分担动画蓝图的更新工作,将部分动画蓝图的任务分配到其他线程去做。
FAnimMontageInstance:Montage动画实例,播放时会在动画实例UAnimInstance里面存储一个FAnimMontageInstance数组用于记录该实例的所有Montage。

        另存一个Map记录当前激活的Montage

        另外,还有一个特殊的FAnimMontageInstance来记录当前正常播放含有RootMotion的那个动画

        FRootMotionMovementParams:用于累积计算Rootmotion的自定义结构体,可以临时存储当前帧的RootmotionTransform数据。FAnimMontageInstance保存一个用于临时缓存Rootmotion数据的成员变量

        FRootMotionExtractionStep:由于一个蒙太奇动画里面可能有多个动画序列组成,所以在提取RootMotion数据的时候需要记录当前所在的片段以及具体位置,FRootMotionExtractionStep就是将这些数据封装并整合成一个结构体
FSlotAnimationTrack:组成当前Montage插槽片段的集合,下图里面有两个FSlotAnimationTrack。

        FAnimTrack:一个FAnimTrack表示一个动画轨道,每个轨道可以由一个或多个Animsequence片段构成,下图的动画轨道就由三个动画序列构成

        FRepRootMotionMontage: Character身上用于同步RootMotion相关信息的属性,包括UAnimMontage、执行位置、旋转、速度等
        FSimulatedRootMotionReplicatedMove:在Character上以数组的形式存储,记录最近一段时间每帧的Rootmotion信息,用于服务器到Simulated客户端的数据同步
        以下几个属于非常规性的Rootmotion,常规的Rootmotion不会使用到:

        FRootMotionSource:广义上的Rootmotion,本质上没有具体的动画数据,通过模拟力产生每帧的Transform信息。比如说玩家受击产生位移,如果是使用普通的受力属于物理影响,同步效果比较差。在移动组件里面集成FRootMotionSource,就可以使用类似Rootmotion的方式非常流畅处理角色的移动,同时还可以兼顾网络同步。
        FRootMotionSourceGroup: containing active root motion sources being applied to movement ,包含了一组RootmotionSource的结构体。同一时刻可能有多个不同的力(或者说RootmotionSource)作用于玩家,移动组件可以根据权重优先级等混合出一个合理的移动位置。
FRootMotionServerToLocalIDMapping:用于同步匹配客户端和服务器上面FRootMotionSourceGroup里面不同的RootmotionSource。

        【移动组件相关】

        UE4里面移动的能力是被封装到一个组件里面的(组件式设计,大部分功能都被封装到不同的组件里),与真正的玩家角色分离,不同的角色可以配置不同的组件并设置各自的参数从而实现不同的移动效果

        ACharacter:玩家角色,可以以一个人物角色的表现存在于游戏世界中,默认包含了一个骨骼Mesh组件、一个角色移动组件、一个胶囊体组件、一个箭头指示组件,有基本的移动、物理碰撞、模型显示的功能。只有使用ACharacter,我们才能完整的使用Rootmotion所有的相关功能

        UMovementComponent:移动组件的基类,只包含基本的移动位置处理
        UCharacterMovementComponent:提供一套完整的角色移动的解决方案,包括行走、游泳、飞行等状态,网络同步,插值优化,Rootmotion等
关于移动组件,可以到我的另一篇文章里《Exploring in UE4》移动组件详解[原理分析]去查看,里面详细的描述了各种移动细节、同步流程、插值优化等

        Rootmotion单机执行流程与原理
        动画数据初始化:

        【对于动画蓝图里面的动画数据】

        1.绑定动画蓝图的Character进入场景时就已经开始了各种动画数据相关的初始化(UAnimInstance::InitializeAnimation),随后通过UpdateAnimation不断的更新动画蓝图里面的逻辑,同时把一部分逻辑交给了FAnimInstanceProxy处理。

        【对于非动画蓝图里面的Montage数据】

        2.一般是玩家手动触发Montage的播放,通过USkeletalMeshComponent找到对应的AnimInstance并执行UAnimInstance::Montage_Play

        3.创建一个FAnimMontageInstance并进行相关的初始化,开始真正的播放蒙太奇

        4.判断蒙太奇是否带有Rootmontion,是的话将赋值给RootMotionMontageInstance,用于后续的ACharacter::IsPlayingNetworkedRootMotionMontage判断

        5.Montage初始化之后就会在后续每帧执行的AnimInstance::UpdateAnimation里面参与计算了。在通常情况下,只要我们的动画的更新方式不选择EVisibilityBasedAnimTickOption::OnlyTickPoseWhenRendered(有四种),Montage都是会参与更新的,更新逻辑在UAnimInstance::UpdateMontage里面

        移动组件PerformMovement :

        Prepare RootMotion阶段

        1.移动组件在TickComponent的PerformMovement里,先判断其是否处于RootMotion的状态。满足下面两个条件之一即可先做一些Rootmotion相关数据的处理和清理:

        a.CurrentRootMotion里面是否有ActiveRootMotionSources(即前面提到的非常规性的Rootmotion)
        b.通过当前角色身上的Mesh组件函数USkeletalMeshComponent::IsPlayingRootMotion判断其是否处于Rootmotion状态

        2.再次进行判断来确保是否要更新动画系统。如果上面的b条件再次成立,就开始做准备工作了,即更新动画执行TickCharacterPose(TickPose默认在Mesh组件里面会每帧都去调用的,这里由于其Tick时机与Mesh组件不同所以需要强制执行一次),这里面会调用UAnimInstance::UpdateAnimation更新动画,并在MontageInstance->Advance(DeltaSeconds, RootMotionParams, bUsingBlendedRootMotion)将当前Montage位移信息更新到AnimInstance的成员变量ExtractedRootmotion里面。

        那么这里可能会有一个疑问,Montage动画里面的Transform数据具体是怎么解析出来的呢?答案都在UAnimMontage::ExtractRootMotionFromTrackRange这个函数里面。我们需要将这一帧动画播放前的播放位置以及当前Montage执行到的位置作为参数传递进去,Montage就会从当前的轨道SlotTrack里面获取到所有AnimSequence片段(也就是下面堆栈图中的FAnimSegment),判断当前Montage处于哪一个AnimSequence片段并解析出对应的FRootMotionExtractionStep(该对象其实只是封装了对应的AnimSequence以及这一帧播放的起始位置)。

        最后,根据对应的AnimSequence以及当前播放的起始位置计算出RootMotion的DeltaTransform,需要注意的是这里面得到的是一个位移而不是一个位置。(通过UAnimSequence::GetBoneTransform获取)
        3.假如我们现在已经将Montage的信息提取到ExtractedRootmotion里面,但是动画蓝图里面的AnimSequence、BlendSpace等动画数据怎么一起参与计算呢?在概念理解里面我们提到了多线程优化的核心类FAnimInstanceProxy,默认情况下,动画蓝图里面的大部分更新逻辑都被放到了FAnimInstanceProxy里面,无论是否是多线程,我们都需要通过FAnimInstanceProxy::TickAssetPlayerInstances去处理相关逻辑,并在这里根据权重去将所有的动画资源的Rootmotion数据提取到ExtractedRootMotion里面。

        如果RootMotionMode为RootMotionFromEverything,那么我们在主线程Tick的时候就会立刻去更新TickAssetPlayerInstances,这样是为了能及时获取到每一帧的Rootmotion信息。

        主线程更新
        如果RootMotionMode是其他模式,那么TickAssetPlayerInstances就会被放到其他线程里面取执行。

        其他线程更新
        同理,如果RootMotionMode为RootMotionFromEverything,在Proxy更新完毕后,我们需要及时地根据权重将所有参与计算的资源数据提取到ExtractedRootMotion里面。

        如果RootmotionMode不为RootMotionFromEverything,那么我们可以在FParallelAnimationCompletionTask里面去更新Rootmotion相关数据(不过下面的堆栈仍然是在主线程执行的)

        应用阶段:

        1.TickPose之后,如果角色处于IsPlayingRootMotion状态就会去执行ConsumeRootMotion消耗掉Animinstance在前面阶段产生的ExtractedRootmotion,其实也就是将前面得到的ExtractedRootmotion数据复制到新的变量RootMotion里面并清空ExtractedRootmotion

        2.得到的新的Rootmotion数据会先根据ACharacter的AnimRootMotionTranslationScale进行缩放调整,同时把其相关数据拷贝到移动组件的成员变量 RootMotionParams 里面

        3.对RootMotionParams 进行局部到世界的坐标转换

        4.执行移动模拟,也就是将前面得到的Transform应用到移动组件里面。这里会先根据当前的Rootmotion的DeltaTransform以及deltaTime算出一个速度AnimRootMotionVelocity进行模拟,具体逻辑在StartNewPhysics里(包括PhysWalking、Flying等),这里面不同的移动状态都会判断Rootmotion进而处理速度。注意:这一步并不会更新其Rotation

        5.模拟结束,读取RootMotionParams的Transform来更新Rotation

        6.清除移动组件的成员RootMotionParams里面的数据

        有一点需要强调一下,Rootmotion是逐渐积累的,也就是说每次我们得到的Transform都是当前Tick时间内其移动的位移,而不是指一个特定的坐标。Accumulate函数就是表示将当前传入的DeltaTransform赋值到我们当前记录的数据上。
另外,上面还提到了一个Character身上的配置AnimRootMotionTranslationScale。由于动画师制作的动画长度是固定的,而游戏里面的需求是变化的,我们能保证每次角色移动的距离就是动画师设计的那样么?不能,比如玩家翻越一个障碍,障碍的大小不可能是严格一致的,所以我们需要一个参数去做适配调整,这个参数就AnimRootMotionTranslationScale,可以通过函数SetAnimRootMotionTranslationScale设置。

        RootMotion的同步
        目前的引擎中,Rootmotion只支持Montage的同步,这里分析的也只是基于Montage的同步流程。同步分为Simulated客户端以及Autonomous客户端两种情况,也就是说你显示屏上其他的玩家与你本地控制的玩家执行Rootmotion的同步流程是有差异的,这与移动组件的实现密切相关。如果对Simulated或者Autonomous不懂,可以参考《Exploring in UE4》关于网络同步的理解与思考[概念理解]。

        Simulated客户端同步
        (没有收到服务器同步数据时与服务器的执行逻辑大致相同,只不过在SimulateTick里触发)

        动画 Montage初始化:(同步只支持Montage)

        1.服务器本地先触发执行MontagePlay并赋值给RootMotionMontageInstance

        2.Simulated客户端在服务器触发MontagePlay后,通过属性回调随后触发MontagePlay

        注意:这里第二步是需要开发者自己处理的,一般来说应该是服务器执行后修改某个属性,然后这个属性的回调函数触发客户端去执行MontagePlay,二者执行有一个短暂的网络延迟。

        移动组件 SimulatedTick:

        1.移动组件执行Tick ,UCharacterMovementComponent::SimulatedTick

        2.如果当前玩家的Mesh对应的AnimScriptInstance→RootMotionMode为RootMotionFromMontagesOnly(也就是说其他三种ERootMotionMode不支持网络同步),触发Rootmotion在Simulated客户端的同步操作

        3.TickCharacterPose,从动画当前位置里面解析出DeltaTransform,用AnimInstance上的ExtractedRootMotion提取出来。具体细节可以参考上面的单机流程

        4.根据CharacterOwner->GetAnimRootMotionTranslationScale()设置RootMotion的Scale并提取到移动组件的RootMotionParams里面

        5.调用SimulateRootMotion转换到世界坐标,计算Rootmotion的速度并开始调用StartNewPhysics进行模拟,与单机版本不同,其模拟流程都在函数SimulateRootMotion里面处理。这里有一点需要特别注意,由于Simulated客户端通过网络同步,可能因为网络波动而卡顿,所以这该函数只会先更新胶囊体的位置(bEnableScopedMovementUpdates为true即可),而不更新Mesh的位置,Mesh需要通过本地的Tick去平滑,其逻辑在下面会进一步描述。

        6.模拟完毕后,如果Rotation不为默认值FQuat::Identity就会通过MoveUpdatedComponent修改,清除临时提取的RootMotionParams的相关数据

        上面提到的是客户端模拟的流程,不过既然是同步,当然需要客户端与服务器交互了

        注意:玩家身上有一个FRepRootMotionMontage RepRootMotion记录了每帧服务器的Rootmotion的运行信息,包括当前帧的坐标、旋转、Montage、速度、执行的位置等。这个属性是同步的(而且只在播放Rootmotion的时候同步),在服务器播放Rootmotion的时候每帧都会通过ACharacter::PreReplication处理这些数据并发送给Simulated客户端
数组TArray RootMotionRepMoves;存储了服务器发来中的所有Rootmotion的数据,前面提到的RepRootMotion在客户端的回调函数里会被提取数据并添加到RootMotionRepMoves数组里面。

        7.客户端模拟移动后会开始根据服务器的Rootmotion信息开始校验,执行函数ACharacter::SimulatedRootMotionPositionFixup。这里会先判断客户端是否要用这个RootMotionRepMoves数组里面的数据(条件:小于0.5秒,在同一个Section片段,非循环Montage,服务器落后于客户端的位置最接近客户端的那个)

        8.如果寻找到满足条件的数据RepRootMotion,就会先通过ACharacter::RestoreReplicatedMove按照服务器传递的数据修改本地角色的坐标与旋转。随后,按照当前服务器的位置以及客户端的位置执行ExtractRootMotionFromTrackRange实现Rootmotion的回滚,这里只是为了得到一个回滚的结果。

        9.最后,根据deltaposition、playrate算出一个deltatime,再根据回滚结果LocalRootMotionTransform进行一次本地模拟SimulateRootMotion。

        10.模拟之后调用SmoothCorrection进行平滑处理。前面提到SimulateRootMotion只会更新胶囊体位置而不更新Mesh的位置,就是为了在这里进行平滑。平滑的逻辑大概就是客户端记录一个ClientData数据去记录当前的Mesh偏移以及服务器的时间戳,在随后的每帧的Tick里面,不断的更新Offset偏移,让其逐渐为0,当偏移为0的时候Mesh就和胶囊体完全重合完成了平滑。具体的逻辑可以参考我的另一篇文章《Exploring in UE4》移动组件详解[原理分析]。

        Autonomous客户端的同步流程
        Montage初始化:

        1.客户端本地先执行Montage播放,通过RPC通知服务器播放

        2.服务器收到RPC触发MontagePlay并赋值给RootMotionMontageInstance

        (当然,这里也可以采用类似simulated客户端的方式,先在服务器播放,通过属性回调触发Autonomous客户端进行播放)

        移动组件 ReplicateMoveToServer(由于逻辑重复,这里会对一些步骤做简化):

        1.类似上面的simulated客户端执行TickComponent,不过这里每帧触发的函数不是SimulatedTick而是ReplicateMoveToServer。随后执行PerformMovement里面提取Rootmotion的信息(执行TickCharacterPose)

        2.根据deltatime与DeltaTransform计算速度,调用StartNewPhysics进行Rootmotion移动模拟

        3.更新Rotation,清除临时提取的RootMotionParams的相关数据。将本次移动的相关数据存放到FSavedMove_Character里面,记录这次移动并存储到SavedMoves数组里面(用于回滚等)

        4.执行CallServerMove,将本地计算的数据发送到服务器,调用RPC函数ServerMove

        (这里如果遇到了PendingMove执行优化移动的情况,则需要执行特殊的RPC ServerMoveDualHybridRootMotion)

        服务器处理:

        5.服务器根据客户端信息执行MoveAutonomous重新模拟计算,并根据结果判断客户端移动是否合法。

        6.随后,在服务器执行UNetDriver::ServerReplicateActors的同步时,发送ACK(ClientAckGoodMove)或者Adjust(SendClientAdjustment)。如果这时候服务器正在播放Montage且发现客户端数据有问题,就会执行ClientAdjustRootMotionSourcePosition进行纠正。否则,就会执行正常的纠正逻辑。

        7.客户端如果收到ClientAdjustRootMotionSourcePosition信息,首先会根据服务器传递来坐标等信息先更新移动组件的数据(执行ClientAdjustPosition),随后根据服务器传递的Montage Position来直接设置本地Montage动画的Position。

        Autonomous客户端的同步流程其实也很复杂,建议先看一下官方文档或者我前面提到的文章来做一个大体的理解。

        梳理上面的同步逻辑,其实我们发现RootMotion本质上走的还是移动组件的处理流程,只不过其移动数据是从动画里面提取的。而且很明显的可以看出,Rootmotion只支持Montage的同步,其他的模式根本走不进这套流程(IsPlayingNetworkedRootMotionMontage),其他模式也不会从动画蓝图Proxy里面提取相关的数据(FAnimInstanceProxy::TickAssetPlayerInstances)。

        前面我们提到,之所以这么做根本原因是动画系统(或者说动画状态机)的同步是复杂而困难的,目前虚幻引擎通用的动画同步方法是客户端与服务器各自维护一个状态机以及几个同步的属性值,然后通过这些属性的判断来同步动画,这里的动画状态机并没有同步。一旦动画系统复杂起来,各个状态的间的切换与转变都会变得极为复杂,由于网络环境的不稳定,状态机的同步需要非常严密的设计与处理(当然,这也并不是说做不了),最好把每一帧的状态与触发事件都同步过去,再进一步做更多额外的校验工作。所以,我们看到很多游戏的动画除了基本的状态外,其他很多都是通过Montage来同步的,Montage之间可以打断而且独立,所以同步起来相对容易。

        另外,因为Rootmotion本质上就是为了提高表现效果才使用的方案,单机模拟都比较困难,对于还需要预测的网络同步就更困难了,或者说除非是常规的线性运动的Rootmotion,其他的不规则的运动几乎无法预测。而如果不预测,我们就很难应对网络抖动,一旦网络一卡整个表现就不再流畅。所以,网络环境不好的情况下,Rootmotion的表现是相当差的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值