找回密码
 立即注册
查看: 335|回复: 0

游戏引擎应用-Unreal Engine的Advanced Locomotion ...

[复制链接]
发表于 2022-8-25 20:08 | 显示全部楼层 |阅读模式
0. 前言

第一次搞动画,也不能算第一次搞动画,应该是第一次用Unreal的动画工具,还是挺有意思的,不过Unreal给的这些工具对程序员来说也是大fen级别的使用体验。(凌晨了不想拖,所以前面和后面写的都不是很详细,中间部分对于新手确实值得看一看,哪天有空了再修修补补梳理一下)
注:可能存在不正确的理解,望指正。
1. 整体逻辑

浏览动画蓝图的层次结构,对于我来说最好从上到下,因为下面的状态机的启动是依赖上层的调用,看一个系统理应从最上层的调用开始看,所以我的叙述也是从输入到输出的,通过看一个看见触发的一系列反应理解系统。如果直接看动画的状态机,反而弄得一知半解。
1.1 重要文件简概

ALS_Base_CharacterBP:是该项目创建的角色基类,继承于Character,其中定义了对各种Input的处理(PlayerInputGraph),以及对各种Event的处理(Event Graph),把整个系统都容纳进来了。部分逻辑需要使用Character的东西,比如说一些状态的Event。
ALS_AnimMan_CharacterBP:是该项目创建的角色子类,继承于ALS_Base_CharacterBP,Overlay的主要部分。
ALS_AnimBP:是该项目创建的动画蓝图,作用在ALS_AnimMan_CharacterBP的Mesh上,具体位于Mesh的Animation->Anim Class,这样输入与输出就链接起来了。
2. 当Space按下的时候,发生了什么

第二章重点在于从头到尾的触发逻辑。
按下Space,在ProjectSettings中Mapping到了JumpAction,整体算是一个比较复杂的逻辑,但是对于动画入门来说,还是需要知道从输入到动画显示一系列的来龙去脉,关于ALS的分析文章很多,所以这篇是我给自己的笔记,记录下自己怎么理解一个新系统,反正写只看文档会很难看,想看可以边看蓝图边看文档。
2.1 输入被接收(ALS_Base_CharacterBP->PlayerInputGraph)

Space按下之后,系统就需要先拿到Space产生的Event即InputAction JumpAction,开始执行我们下面需要处理的逻辑,首先这个部分分为Pressed和Released两个逻辑,先看简单的Released逻辑,下面这个问题可以解决。
明明Jump应该是一个瞬发逻辑,为什么需要Released逻辑?
因为这个是其父类Character留给做跳跃键按下时间增加跳跃高度的一个方法,用里面的JumpkeyHoldTime可以做到按键时长监控,不过ALS项目并没有这么做,所以在用Character的时候,Jump并不是瞬发逻辑。
再看Pressed逻辑,最开始是第一个Switch(选Movement Action是None通过),第二个Swicth(选Meovement State不是Mantling通过),这两个Switch说明了多个动作执行期间可以增加跳跃提高流畅度的可能性,现在用Switch将他们挡住,说明了也有机会用另一个分支求解,这样的设计增加了程序的可扩展性!
2.2 响应第一个分支(ALS_Base_CharacterBP->Ragdoll End)

这说明了什么,我们的Space可以响应从Ragdoll状态爬起来,而我们现在要查看的就是这部分的逻辑,其实官方给的很清楚,不过很可惜,我们并不能用Space获取这个动态的全部逻辑(因为部分逻辑在Ragdoll触发/更新中),好在各个方法与变量的名字比较清楚,可以让我们至少明白逻辑。
①用MainAnimInstance获取发起GetUp瞬间的人物姿态,其C++代码如下,当然我现在还不能够理解C++部分的骨骼动画体系,现在先Mark一下,ALS用的命名是RagdollPose
void UAnimInstance::SavePoseSnapshot(FName SnapshotName) {
        FAnimInstanceProxy& Proxy = GetProxyOnGameThread<FAnimInstanceProxy>();
        if(USkeletalMeshComponent* SkeletalMeshComponent = GetSkelMeshComponent()) {
                // 我们可以用SnapshotName获得SkeletalMeshComponent
                Proxy.SavePoseSnapshot(SkeletalMeshComponent, SnapshotName);       
        }
}
②用RagdollOnGround判断当前Ragdoll状态是否在地面上在地面上
如果此时我们的角色不在地面上,说明此时我们的角色还在Falling状态,那就将Ragdoll系统现在计算出来的速度放在角色身上,就不强行播放起身动画了,而是直接恢复人物Falling的正常状态。
设置的角色状态设置在了CharacterMovementComponent是UE自带的类型,之前在RagdollStart的时候用SetMovementMode设置成为了None,现在需要重置回来,这个部件与人物运动应该有关。
如果此时我们的角色在地面上,ASL提供了一个从地上起立的动画系统,调用Montage Play的时候,会获取起身的动画资源,并且这个时候有朝向判断输入,GetGetUpAnimation是Function->Ragdoll System的抽象函数,由其子类ALS_AnimMan_Character重写,可以在重写函数界面看到基本的处理方式,这些链接到输出的部分都是AnimMontage,其调用方式是,AnimMontage会在动画编辑器中设置Slot,然后我们可以将AnimMontage放在Slot中,再讲Slot放入AnimGraph,在【调用AnimMontage的时候,会从AnimGraph的输出逆向检索到我们放置的Slot,然后再逆回来进行动画计算。】(需要Check,在将Slot BaseLayer输入去除之后,发现除了BaseLayer Slot中的动画能够播放,其他正常的跑动动画都没有了,说明这部分的逻辑一是将前面处理的动画传递过去;二是在Slot在被需求时,截断前面的动画)



截断的尝试

我们的Montage在Anim Graph->Base Layer中,在触发起身的时候,会发现只有Slot中出来动画流,这是因为在Montage Play调用的时候,参数Stop All Montages被设置成为了True。经过多次混合,到了AnimGraph输出前,Ragdoll Override的部分,这个部分是将Ragdoll与正常姿势过渡的地方,其中Ragdoll Blend Time是指从Default Pose转到Ragdoll Pose混合过渡的时间,Default Blend Time同理,这里按理来说Ragdoll Blend Time应该是0,才不会有连续X会出现闪烁的BUG。(小BUG别给我逮住了)同时可以打开Ragdoll States看内部的结构。
这部分的总结:按下Space之后
Default Pose:如果我们死亡状态在空中,那么不播放Montage,也就是不会截断BaseLayer前面计算的姿态,Default Pose为空中的正常状态;如果我们死亡状态在地上,那么播放Montage,Default Pose输出我们起身的Montage
Ragdoll Pose:而Ragdoll Pose输出我们之前①中瞬间记录的RagdollPose。
从逻辑上来说,在Ragdoll Override我们就实现了死亡状态复活状态的过渡,并且通过这个例子可以知道空中可以按X,老好玩了。


③重新将控制权给玩家,启动胶囊体碰撞,并且把Mesh碰撞与Ragdoll物理模拟解除,这样就恢复了。
2.3 响应第二个分支(ALS_Base_CharacterBP->PlayerInputGraph)

第二个分支稍微复杂一点,如果前面一个分支看懂了,这个部分就很容易,首先检测Movement State的状态,因为现在咱们是可以跳的,但是我们还有一个可以爬墙的逻辑,也是由Jump控制,所以现在需要判断。
2.3.1 角色在空中(ALS_Base_CharacterBP->Mantle Check)

空中当然是不能跳的!虽然不能跳,我们还是可以处理是否能够攀爬,进入Mantle前,给了MantleCheck函数一个Settings,这个Settings区分了在地面上能够够到的墙面高度以及在空中能够够到的墙面高度(因为地面上可以起跳一下,所以够到的肯定高一点),注意,在空中的Settings是Falling Trace Settings。
前面蓝图扭曲很复杂,其实就是利用Settings计算出我们需要的碰撞胶囊体,用自己的胶囊体原本应该在的位置,做一个Z轴方向的偏移和缩放,并且按照移动的速度方向(GetPlayerMovementInput)给出Start和End不同的位置,一前一后,根据这个胶囊体碰撞计算出碰撞点和碰撞法线。但是这里有一个BUG(小BUG别给我逮住了),如果我们人不动,那么就没法用移动输入来判断了,那么在空中只按Jump的逻辑永远不会触发!其实用角色看的方向就可以解决这个问题。这里我参考的是其Debug Shape中画视锥的方法。这样就可以用空格,在空中不用方向键也能触发攀爬!



修改为朝向决定胶囊体的位置,而不是移动输入

按照前面获得的落点位置以及法向量,可以求出我们要判断的界面大概的位置,然后再发出一个球形碰撞体,利用这个碰撞体去检测地面,关于Trace可以去查阅官方文档。这个时候提取到了Hit Component



我的Debug,白色是Start,黑色是End,红色是计算出的碰撞位置

即使判断到了有一个阻挡物,还需要判断这个阻挡物能够提供足够的站立空间,判断空间还用了一个Trace,这个Trace就是在上去的位置从头到脚扫一遍,如果这个部分没有任何碰撞,那么就说明可以上去(注意是没有碰撞,所以在判断的时候用NOR),并且此时将



判断可站立空间的Trace,绿色是Start,紫色是End,蓝色箭头是Transform的Location和变成Rotation的法线表现,这个时候Transform是世界坐标下的

先用Switch判断是不是在空中,如果在空中就将攀爬状态(MantleType)设置为FallingCatch;如果在地面上,将之前碰撞检测平面的球位置与人物目前位置相减,就可以将我们这次的攀爬分为HighMantle、LowMantle。
到我们正式启动播放的地方了,这个部分相较于整体更加复杂。(ALS_Base_CharacterBP->MantleStart
⑤-①使用MantleType获得我们攀爬需要的状态参数,获取逻辑在GetMantleAsset中,不过ALS_Base_Character的这个函数是处理非常简单,我们在这里获得的值都是在ALS_AnimMan_Character中重写的,里面细分了很多种情况,返回的值可以在Default中看见。获得状态参数之后,利用Lerp将高度转化为动画的参数(不同的高度,消耗的时间、起始位置不一样)。
⑤-②将之前的Transform放到局部坐标系下,方法很简单,将Hit Component的世界坐标转换作用到Transform上,现在的Transform就是相对于Hit Component的了。
⑤-③将角色的Transform和记录的Mantle Transform做减法,可以获得两个Transform的差距,差距为角色到目标地点。
⑤-④计算动画起始差距,差距在动画开始到目标地点之间,可以从上面蓝色箭头看到,这一步是反解上面Vector到Rotation的逻辑,所以这个Vector就是归一化的蓝色向量指示方向,而这个Starting Offset就是为了让角色不在平面方向上进入墙体所设置的宽度,可以手动将中间的点乘Starting Offset Y值改为130或者更高,测试一下就明白了。


⑤-⑤禁止角色移动功能。
⑤-⑥配置Timeline的播放参数,并且播放Timeline,Timeline本质上是一个计时器,用这个计时器提取我们位置校准曲线的参数,就可以控制角色位置属性,同时因为控制位置和动画播放是分开的,所以其运动曲线需要匹配动画的节奏、速度,也就需要调整Timeline的播放速度,把PlayFromStart去除可以看见角色在原地做爬墙动作。



驱动的Timeline提供给Mantle Update执行的Tick以及Mantle End的触发条件

⑤-⑦开始播放Montage,和下蹲一样,会找到Montage的Slot并且执行播放。(Check Valid是个好习惯!)
我们现在知道了如果要播放这个Montage,其实要经过几个步骤,第一,将人物从现在的位置移动到动画开始的位置,第二,将人物放在动画开始的位置时播放Montage动画,第三,播放Montage动画的同时,我们要执行与之适配的跳跃位移,这样动作才能看起来正确且连贯。都到这里了,看下Update里面是怎么执行这三个操作的
播放爬梯的途中到底经历了什么(ALS_Base_CharacterBP->MantleUpdate
需要注意的是,上面我们说了这个动画已经被分为了三个步骤,但是其实只有两个动画线路,就是角色位置动画起始位置动画起始位置(Mantle Animated Start Offset)动画结束位置(Mantle Target)。Update是在动画期间随Tick被调用的函数。
⑥-①我们需要在Update期间更新Mantle Target也就是动画结束的位置。
为什么需要在Update期间更新结束位置,不应该在动画开始的时候,攀爬的结束点已经确定了么?
角色攀爬的物体不一定是固定不动的,如果攀爬的物体一直在运动,那么我们之前捕获到的Hit Component的Transform也会改变,我们用Hit Component计算出来的相对结束位置也会改变,所以动态更新是为了适配攀爬移动物体。
⑥-②利用之前设定的Timeline获取位置校准曲线提供的矫正值,这几个矫正值就是下面几个混合操作需要的Lerp参数,很容易理解。
⑥-③没搞明白前面的逻辑是什么意思,不就是纯纯的用MantleAnimatedStartOffsetLocation么,进源代码看一下,除了Rotation其他的部分都是用的普通Lerp,Rotation用了四元数Lerp,但是Rotation也没拆开啊,所以实在没看懂这里在做什么,我自己改成了就只用Animated的格式,看起来也没什么问题;后面倒是没什么问题,把这里处理完毕的Offset给最终位置加上再Lerp,不过这部分逻辑真不值得恭维,最后三次线性Lerp,用处不是特别大。而且我把XYCorrection关了、左边的Lerp逻辑盖了也没有一点问题,总的来说,这里就是一个Lerp。



这坨逻辑在搞什么飞机?

⑥-④将计算好的Transform设置到角色身上,这样就可以有正确的位置。
结束的时候操作(ALS_Base_CharacterBP->MantleEnd
很简单,将CharacterMovement的状态置回去。
不过这还没有结束,我们分析的始终都是在ALS_Base_CharacterBP上的,别忘了,AnimMan是子类,他也可以对这几个函数进行重写,可以发现Start、End、GetMantleAsset函数被重写了,其中加入了地面上开始的低攀爬不会收手上拿着的东西的逻辑,可以试试看。
至此,我们第二个逻辑在空中的响应就做好了,甚至还改了几个BUG,第二个逻辑在地上的逻辑还是在Input中,首先要判断是否存在运动逻辑,如果我们现在在控制方向,那么就要检查是不是可以攀爬,因为在地上攀爬和空中攀爬的参数配置是不一样的,如果不能,再看看是不是蹲着,如果没有蹲着就执行跳跃逻辑。
哇尼玛的,蓝图是真坑啊,双击一下Interface变成function接收不到正确的Interface实现了,还没法看见Interface走了,真的**玩意,我还以为上面什么改着改着改错了,Holy。
2.3.2 角色在地上的逻辑——跳跃(ALS_Base_CharacterBP->Mantle Check)

可以看见,在Input先调用父类(Character)的Jump逻辑(这部分的Jump逻辑也可以看我之前的文章),Jump逻辑触发Press缓存,在Tick中执行CheckJumpInput,调用OnJumped这样一个BlueprintNativeEvent,在ALS_Base_CharacterBP->Event OnJumped处理这个事件,这里有一个速度判断,如果角色速度太低,那么我们将不允许他空中旋转,这里Last Velocity Rotation代表的是速度关于X轴的角度Rotation,所以角色速度高的话,就可以从self的Rotation转到速度的Rotation,至于合不合理,另说,这样的做法还是有参考价值的。最后调用了BPI Jumped,这是ALS_Animation_BPI提供的Interface,在ALS_AnimBP有实现。
Event BPI Jumped触发之后,首先将Jumped状态置为True,然后通过人物速度设置动画的播放速率Jump Play Rate,等待0.1s之后将Jumped状态置为False。
注意这里和之前不一样的是,Jumped的动画播放速率是动画播放速率,而不能等同于从跳起到落下的人物运动速率,之前那可以是因为用Timeline做了动画与角色移动的匹配,不清楚可以自己尝试修改一下这个速率。而为什么这里不用呢?是因为用了这里混合了Unreal自带的UCharacterMovementComponent中的DoJump方法,由CheckJumpInput调用,其内部执行了在跳跃的时候Z轴的Velocity修改,并且将MovementState变成Failling,这是一个好系统的设计方法吗?我不觉得,不过现在得看别人的。
!!!所以按照这里的论述这里还补充了一个正文中没提到的事实,也就是调用Jump之后,进入Tick触发的CheckJumpInput,里面的DoJump方法修改了MovementMode(注意这个变量是Character Movement自带的),并且在ALS_Base_CharacterBP的Event Graph中接受到了MovementMode的改变,触发了Event OnMovementModeChanged,并且调用函数,通过MovementMode的修改去修改这个项目的新Enum变量MovementState(一定要和MovementMode区分开),然后又调用StateChanged的接口,实现在了EventGraph中,判断新State和旧State是不是一样的,后面的逻辑主要在OnMovementStateChanged,执行了旋转,并且进入空中状态时执行UnCrouch。
我大概懂了,他就是想把普通的Movement全部和Unreal那套东西胶起来,真无敌了。
直到这里,我们还是没有看见之前一直能看见的MontagePlay,但是Jump的动画逻辑就是如此执行完毕了。执行跳跃动画和之前的Slot动画不同的是,跳跃动画不在Slot中,是利用动画自动机实现播放。其实很多文章都是专门介绍AnimGraph这个部分的,所以我觉得,在这里不需要我过多介绍什么,还是按照流程来吧。
AnimGraph上层看起来很庞大,其实本身逻辑还是挺清晰的,先只看LayerBlending的部分,LayerBlending使用了三个生成一个混合的Layer产出了一个Post Layering,这里我们先用感性的想法看一下整体逻辑。
①LayerBlending:将三个输出的Layer混合起来,点开看这个状态机内部似乎很复杂,其实逻辑挺清晰的
Inputs:将接受到的输入Cache一下。
Make Dynamic Additives from Base Layer:计算BaseLayerInput到BasePosesInput的“距离”,也就是其表示的Additive,这样我们就可以将不同的距离Blend到OverlayLayer上,这样OverlayLayer提供不同姿态的基础Pose,可以获得正确的骨骼偏移,相当于Pose = BasePose + OffsetPose,那么我们计算出OffsetPose,就可以应用到不同的BasePose中。
后面的混合部分应该还需要一点骨架的知识。
②BasePoses:这里面有两个Sequence Evaluator,应该是根据其Explicit Time,然后将Sequence里面这个时间节点的Pose输出,而这两个ALS_N_Pose和ALS_CLF_Pose都是很简短的不变的Sequence,这样设置Time为0就等于提取两个固定的Pose了。
③OverlayLayer:这个里面主要是处理我们不同姿态的情况下的基础Pose,计算方法和BasePoses相似,但是要复杂一点,可以点进去看一下其混合逻辑,这里不做过多说明,因为不是Jump的范畴(当然也不能说完全和Jump无关,不过是分离的两个体系)
④BaseLayer:这是动画自动机的大头部分,但是到Jump可能只是冰山一角的部分。
从后往前看,OutputPose前面就是一个Slot,这个Slot就是播放攀爬等操作的地方,如果使用这个Slot就会截断前面的计算,也等于说前面的计算直接忽略(目前还是不知道底层的Animation Graph实现方法,不过这个可能是一个比ALS更大的活,Holy);再向前看,是一个惯性化节点,是提供给变换混合惯性化混合请求的节点;再向前就是Main Movement State


状态机,呃应该不用赘述,简单就是输入、状态转移、输出,可以看见Jump的逻辑就在这里面,所以这也是我为什么说只需要看冰山一角的原因。而我们要从Grounded到Jump需要经过MovementState和InAir,可以浅看一下条件,这两个都是导线,只传输,不输出;Jump和Grounded都是可以输出的节点。
Grounded -> MovementState:MovementState != Grounded
MovementState -> InAir:MovementState == InAir
InAir -> Jump(Priority 1):Jumped
InAir -> Fall(Priority 2):MovementState == InAir
在之前的叙述中,我们已经满足了这些条件,并且Jumped还是一个开了整整0.1s的信号!所以按照状态机的理论,我们能够在触发跳跃的时候到达正确的状态机节点Jump。



Modify Curve一定不要想成和输入的Pose有什么关系,Pose对于他来说只是一个执行流,而这个节点真正执行的是将下面两个Curve的值设置一下,不过感觉这个也不是Curve形式存在的东西,更像是变量的替代物。可能这里面只能用这个?还是为了扩展性设计了这个?原项目中Base Pose N应该是1

这个ModifyCurve到底是怎么执行的,还是一个很大的问题,从调试的结果上来看,Modify Curve执行的仍然是一个Blend的方法,就是这里置1不是立刻将Curve变成1,而是通过混合曲线变成1,我们可以来看一下为什么需要将这两个Curve置位。
当然,这个Curve因为会有多方面的混合影响,可能我的观察也不是非常全面,但是在写下下面的东西之前,我已经做了将近快3天的Debug查看了,可能内容比价杂乱,敬请指正。(注:如果要用C++调试,随便建立一个C++类就可以了)
推测的Curve运行逻辑:从调试AnimInstance的C++代码可以看出来,在FAnimInstanceProxy:: UpdateCurvesToEvaluationContext中,其Curve修改逻辑是将Context中的值全部赋值给能够在Mapping中找到FName的Curve,那么修改状态应该是保存在Context里面的。并且其修改逻辑应该近似脉冲的方式,Curve是一直存在的,如果不给他Modify Curve的信号,那么他就会回到0的位置,只有这种方法能够解释所有的Modify Curve都没有对应的置零方法,部分Curve在Anim Asset中存在置零,部分Curve是没有在Anim Asset中出现过的,所以我认为在AnimGraph进行计算的时候,Context在整条链路收集并处理数据,Curve的数量也是动态更新的,完全有可能利用Context做到上面的功能。 经过添加了一个新的Curve测试,发现确实是这样的。
Pose:里面保存的是骨骼数据(还需要仔细看其数据结构),类型为FCompactPose,在ALS_AnimBP->AnimGraph->BasePoses中,如果将节点Blend Multi的Normalize Alpha选项关闭,并且将Desired Alpha 0设置为0.8,可以看见模型变大了好多!并且这个变大效应会在翻滚、爬墙等状态中失效。这个是一个很好的理解Pose中的数据以及AnimGraph ->LayerBlending的方式,首先变大的原因用Toggle Pose Watch可以看到,骨骼变小了!这个是因为我们关闭了Normalize,0.8的数据直接作用到了骨骼上,让他缩小了,然后在LayerBlending中,BasePose作为被减的部分变小,让Additive部分变大了,在将Additive加到Overlay部分,所以整体看起来就变大了,在那些部分失效是因为部分Anim Asset强制部分Curve的值变化,而这些Curve正好是LayerBlending中需要的。同样这个部分可以看出,这种设计方式的强大,利用BasePose在很大变化的情况下能够表现出正确的Animation。
蓝图是真的很难Debug,而且这样用Curve真的是合理的么,影响因素也太多了,不容易看。
如果理解了我上面说明的Curve运行逻辑,那么可以知道,像Base Pose N这种除了蹲下站立切换时没有被调用的Curve,在各种状态时需要即用即改(但按理来说,这怎么都应该是状态参数做的事情,而且只能在状态修改的时候更改!)。Base Pose N本意是Normal状态下的Pose占比,这个Curve使用的很广,可以在AnimBP中搜索一下就知道,主要是用来区分蹲下与站立的主Pose状态,
设置的位置也很多:这也是不合理的地方,主要分布在MainMovementStates的非Gorund部分以及MainGroundStates的Standing部分,可以总结为,所有MainMovementStates中非CLF的部分,还外加两个Anim Asset(ALS_N_to_CLF/ALS_CLF_to_N)。所以从他自己的设计哲学来说,如果CLF的解耦在MainMovementState中,可能更加能够用MovementState表达这个状态?但是之前用MovementMode把自己限制住了,在前面说过,状态先通过MovementMode控制,再通过MovementState控制,所以只好把Ground作为一个MovementState,其细分逻辑只能往MainGroundedStates下放,也合理吧。
读取的位置也很多:各个Overlay都有自己的站立动作,以表示不同的特征(受伤、男女等等),所以每个Overlay内部都有一个表示其站立动作蹲下动作的Blend节点,就和BasePoses差不多。



其中一个使用Base Pose N的地方,并且可以看到这里直接使用的Base Pose N是一个Float,是因为在Update的时候,各个Curve的值都会保存下来,所以参与后续计算的值都是相同的。

这里看完之后,再向前走就简单了。首先是一个混合,看混合的B节点,是利用两个变量实现从跳跃落下重、轻落地动画的过渡,这里的Light和Heavy动画提取也是和之前一样提取其中一帧位置初始设定好的动画;再向前是一个Apply Additive(注意和Blend的区别),下面的Additive是由两个参数控制的动画,可以点开这个动画,他自身就是设定为了一个Additive动画,其设置了Additive Settings为ALS_N_FallLoop,然后给这个设置了五个标准点,通过坐标混合动画强度反馈出去,这个动画的含义是空中侧身位置。


再往前就是Jump的状态机,状态机内Entry先通过一个Entry节点,可是为什么要通过一个Entry节点呢?我只能横竖看出一个不能用导线,果然
A conduit ( Conduit ) cannot be used as the entry node for a state machine. To enable this, check the 'Allow conduit entry states' checkbox for StateMachine. Warning, if a valid entry state cannot be found at runtime then this will generate a reference pose!


这里的处理方法还是挺精华的,看的出来作者确实希望动画流畅,这里的状态机就是左脚起跳和右脚起跳会有一个脚的位置的判断,如果播放跳跃已经播放完了,那么就进Jump Loop,同时慢慢进入Flail(这个是由转换规则控制的,Jump Loop -> Flail设置了1s的插值)。
第二个问题,跳跃是怎么结束的?和之前一样,解铃还须系铃人,之前我们的MovementMode在UE5的源文件中被修改了,然后影响到了MovementState,从而引导状态机的移动,在落地的时候,最开始的触发逻辑也是如此,在UCharacterMovementComponent::SetPostLandedPhysics触发了MovementMode修改这里就不赘述整个过程了,放调用栈在这里看


Landed触发之后,就是蓝图部分的Landed触发,其中包括Event触发MovementState的修改等等,这些部分影响接下来的动画工作。下面的蓝图有两个方式回去,但是系统却选择的蓝色边,虽然Jump->Land是没有优先级的,但是Land(导线)->Land却是有优先级的,这种优先级分离的设计也挺恶心的说实在话,如果把Land(导线)里面的条件改掉,就可以让Jump从红色边走回来。之后Land的动画细节已经被上面的东西都囊括了,所以就不赘述了,感觉写的也很详细了。


至此,一个Space按下的逻辑,从头到尾哪一步的处理,Unreal的隐式规则,输入到输出,基本上是全覆盖了,很复杂,但回看也很简单,不过至此,我们所理解的东西还不足以形成系统,了解系统还需要了解系统运行的参数与状态。
3. 状态与参数

这部分对于一个系统来说非常重要,其主要分布在各个蓝图的Input/Update阶段
3.1 ALS_Base_CharacterBP

TickGraph中的各项,其中Draw Debug Shapes是对这个Tick中计算出来部分参数的可视化;Update Grounded Rotation等是根据状态调整角色Rotation方式。


3.1.1 Set Essential Values

Acceleration:现在的Velocity和Previous Velocity的差值除以Delta Time计算;
Speed:定义为速度的XY轴长度,不考虑Z轴;
Is Moving:Speed > 1;
Last Velocity Rotation:在Is Moving生效的前提下,速度XY投影与X轴的Rotation;
Movement Input Amount:从Character获得当前的Acceleration(此处的加速度定义为键盘中的输入比率,应该是Movement Component里面自己的一套东西,这里只是用来判断是否存在移动输入),通过比Max计算出的比率;
Has Movement Input:Movement Input Amount > 0(通常判断是否有移动输入的变量);
Last Input Velocity Rotation:在Has Movement Input的前提下,获取Movement Acceleration方向的X轴Rotation(所以Character获取的Acceleration就是Input的期望速度方向,大小其实有就行);
Aim Yaw Rate:现在的Yaw和Previous Aim Yaw的差值除以Delta Time计算(其实就是Yaw的速度);
更多的参数在Get Essential Values中,因为部分参数只需要一个Get就可以获得,作者就写在获取总Values里面了,比如说Velocity。
3.1.2 Cache Values

Previous Velocity:保存这次Tick的Velocity给下次计算;
Previous Aim Yaw:同理;
3.1.3 Update Character Movement

要理解这个部分的状态修改,需要先理解在Player Input Graph部分能够控制的部分状态,这些状态的定义可以在ALS_***中找到,都是Enum类型。
MovementMode(Walking、Navmesh Walking、Falling):之前分析跳跃的时候说过了,这个是与Character Movement Component胶水起来的部分,其改变一般是在UE源代码中。
Desired Stance(Standing、Crouching):按键输入期望的Stance状态。
Desired Gait(Walking、Running、Sprinting):按键输入期望的Gait状态,这里在实现的时候因为Desired Gait的存在,所以没法把状态保持下来,也就是说Walking切换会被Sprinting的触发给改变。
Desired Rotation Mode(Velocity Direction、Looking Direction、Aiming):这三个是表示Controller的视角旋转时,人物应该怎么表现,这个默认是修改后直接给Rotation Mode设置,不过我觉得大可不必这样,为什么呢?因为既然是一份Desired缓存,直接激活赋值岂不是违背了设计的原则?Desired作为一个期望备份,还在第一人称和第三人称转视角的时候被使用了,我觉得这个设计就蛮好的,不Delay可能是因为存在Bug?
上面的状态都是Desired状态,相当于一个Input缓冲机制,这个设计方式挺好的,可以实现Desired状态持久化,以及Desired与表现分离,在表现的时候用多个条件+Desired判断最终的表现结果,最后情况合适又可以恢复到Desired的状态(在设计的时候Sprinting就应该作为一个条件,而Desired Gait就是Walking、Running,这样就可以实现Walking、Running的状态恢复)。
Allowed Gait(状态、环境允许的Gait):这里面逻辑还挺复杂的,从Stance、Rotation Mode、Gait以及Can Sprint判断现在能够允许的Gait。
Actual Gait(状态实际能够接受的Gait):这个是在Allowed Gait基础上,通过Movement Settings(其中Setting组以Stance×Gait为划分,一共六组)提取出状态能够达到的速度,利用当前速度与最大速度去限制实际上目前能够表现的Gait。
这里还更新了Gait与Movement Settings,设置了Character Movement Component中的Max Walk Speed、Max Walk Speed Crouched、Max Acceleration、Braking Deceleration Walking、Ground Friction。
3.1.4 xxxRotation

这里不说状态,说一下Rotation的操作,运动的Rotation可以通过运动速度方向和当前速度方向进行插值得到中间状态的Rotation,但是这个项目还有一个无运动的Rotation,是通过AddActorWorldRotation实现的属性更改,后面直接赋值给Target Rotation,并且这是Tick调用的,所以可以利用Curve形成动画,这个Curve(RotationAmount)也许和脚部运动的Curve有关系。
3.1.5 HUD

部分状态与参数在HUD上修改,比如说Overlay就是在OverlayStateSwitcher上修改并广播事件的,不过这些逻辑已经很解耦了,不用看大概也能理解。
3.2 ALS_AnimMan_Character

主要是一个UpdateHeldObject通过Overlay的修改影响角色手上握持的物品,并且这个类能够根据Overlay提供给Base中不同的Slot动画。
3.3 ALS_AnimBP

这个部分也主要是在Update中,并且内容十分丰富,毕竟是要支持一整个状态机的状态及参数,IK的部分放在后面专门讨论。



图看不清打开自己的工程

3.3.1 Update Character Info

将之前在Base出算出来的Essential Values和States拿过来。
3.3.2 Update Aiming Values

Smoothed Aiming Rotation:将上一Tick的Smoothed Aiming Rotation和3.3.1提取到的Aiming Rotation按照Config里面的参数插值。
(Smoothed) Aiming Angle:将计算Aiming Rotation与Actor Rotation之间的差值,并且只保留俯仰角和偏航角。
Aim Sweep Time:计算得到的俯仰角转化为0~1的参数,利用这个参数提取俯仰角变换动画里面的Pose。
Spine Rotation:将Aiming Angle的X部分(也就是Yaw的部分)除以躯干转动骨骼的数量,然后每段按等分执行他自己的那一份,就可以看见躯干部分人物躯干呈现自然的扭转。
Left/Right/Forward Yaw Time:这三个部分就是将Yaw映射到动画取Pose的时间,用一个简单的线性映射做好的。



Spine Rotation的效果,每段扭转一部分,就形成了紫绿红黄颜色的骨骼过渡,结果就是腰部躯干呈现自然的旋转效果

这部分基本上涉及到了Aiming的所有动画细节,其参数也在AnimGraph中间黄色的部分全部有体现。
3.3.3 Update Layer Values

设置了许多曲线参数,把Curve的值放在变量上,直接赋值的部分是
Base Pose N:BasePose_N
Base Pose CLF:BasePose_CLF
Spine Add:Layering_Spine_Add
Head Add:Layering_Head_Add
Arm L Add:Layering_Arm_L_Add
Arm R Add:Layering_Arm_R_Add
Hand R:Layering_Hand_R
Hand L:Layering_Hand_L
Arm L LS(LocalSpace):Layering_Arm_L_LS
Arm R LS:Layering_Arm_R_LS
非直接赋值的部分
Enable Aim Offset:Lerp(1,0,Mask_AimOffset)
Enable Hand IK L:Lerp(0,Enable_HandIK_L,Layering_Arm_L)
Enable Hand IK R:Lerp(0,Enable_HandIK_R,Layering_Arm_R)
Arm L MS(MeshSpace):1-Floor(Layering_Arm_L_LS)
Arm R MS:1-Floor(Layering_Arm_R_LS)
基本上涉及到了动画Sequencer中存在的大部分Curve了。
3.3.4 Do While Not Moving

这个部分是处理原地动作的,Rotate in Place是Aim或者第一人称的行为,Turn in Place是第三人称中LookingDirection时扭头过大的行为,这里面处理的时候用了一个计时器和时间对比,实现了扭头过大一段时间才会触发Turn的操作,Dynamic Transition是IK行为后面分析。
注意Do When Starting To Move,清除了Do While Not Moving所有的状态,前面ML Do While就像一个锁存器似的,哈人。
3.3.5 Update Movement Values

Velocity Blend:将Velocity转换到角色空间,求出前后左右的比率存下来,再与上一个时刻的Velocity Blend进行Blend。
Diagonal Scale Amount:是IK的一个属性值
Relative Acceleration Amount:计算出相对加速度的比率,这个也是角色空间的,所以可以变成LRFB,里面计算的时候还考虑了速度与加速度点乘是否大于0,不然就不用Max Acceleration,而是用Max Braking Deceleration。
Lean Amount:将Relative Acceleration Amount计算出来的由加速度导致的倾斜和上一个时刻的Lean Amount进行Blend。
Walk Run Blend:在Locomotion Cycles中,判断移动动画是走还是跑。
Stride Blend:在Locomotion Cycles中,设置移动动画的步长,根据Weight_Gait选择是用跑步步长曲线还是走路步长曲线(这个Lerp有个Bias,是因为Weight_Gait是1表示Walk,2表示Run,3表示Sprint),根据BasePose_CLF选择是下蹲步长曲线还是上面计算出来的步长曲线。
Standing Play Rate:在Locomotion Cycles中,设置移动动画的播放速率,根据速度和预设的跑动动画速度的比率求出动画应该播放的速率,比如说动画一秒跑100米,现在我们的移动速度一秒跑200米,那么动画的播放速度就要加倍才能匹配,同时播放速率也要匹配步长与Mesh大小。
Crouching Play Rate:同理
3.3.6 Update Movement Rotation

Movement Direction(Forward、Back、Left、Right):通过Gait和Rotation Mode控制只能前进的几个状态,其他的状态根据速度与视角方向的夹角计算方位(这个案例来说Velocity Blend的方法更合理,果然项目大了,一个目的几百种实现,数不清的方便参数不用都不奇怪了),然后去求目前的方位,这个求法挺有意思,甚至觉得很牛。
F/B/L/RYaw:没明白是干什么的,关了感觉没什么差别?
3.3.7 Update In Air Values

Fall Speed:Z轴速度
Land Prediction:预测到地面的一个Curve,大费周章,整的,还整了一个Trace,逻辑和之前攀爬差不多,细节的比如Start和End以及Mask_LandPrediction和之前会不一样。
Lean Amount:空中的部分,之前的Ground的,计算方法还是一样。
3.3.8 Update Ragdoll Values

经典布娃娃又来了
Flail Rate:原来布娃娃的时候还是可以放动画的,不过Ragdoll控制比动画要大,在空中可以看的很明显,这里系统设定速度越大播放越快(获取速度的方式可能要注意一下?)
至此,我们介绍了重要文件中,绝大部分与动画有关的状态及参数,这有助于我们理解整个系统,至少,能够帮助我们理解一个动画架构应该怎么设计,但是ALS里面有部分内容确实是可以修修补补的。
4. 动画状态机深入

还有很多动画我们没有分析到,但是利用上面的参数已经可以顺畅地看Anim Graph中,IK前面的所有流程了,当然后续可能会分析一点比较特殊的动画,这里还是先从IK开始分析,毕竟这也是现代计算机动画很重要的一个部分。
4.1 IK(Inverse Kinematics)

早些年上计算机动画的时候就学过,不过也只记得一个计算IK的时候一个Trick,那就是限制两个骨骼共线了。不过Unreal Engine里面的IK并不需要我们知道IK怎么求解,还是先会使用为重要,目前还没看出手部IK的作用,先从脚部IK分析一下其运行逻辑。
4.1.1 Update Foot IK

这就是我们上一章节没有分析的Update数据部分,因为左脚与右脚是对称的,所以我们先只看左脚的逻辑。
①Set Foot Locking:这一步使用了两个曲线Enable_FootIK_LFootLock_L,脚部的IK Curve都是绑定在动画Sequencer里面的Curve,骨骼设定的是ik_foot_l,下面三个变量是传入Reference在里面被修改。
①-①进入函数,首先检测Enable是否激活,如果没有激活就可以不做。
①-②首先将Foot Lock Curve的值提取出来,设置为Foot Lock Curve Value
①-③Current Foot Lock Alpha的赋值过程只有Foot Lock Curve Value到1或者下降才能触发赋值,这样在上升阶段就不会赋值,也就是不能采取Blend in的过程,要么直接黏住,要么慢慢解开;这里慢慢解开的意思就是,人物在从IK固定状态移走时,保证脚部不会滑动,如果不在Lock Alpha降低的时刻执行某些特殊的操作,角色脚部就会平滑移动。
①-④直接黏住的逻辑,如果我们Curve达到峰值,那么说明要黏住了,此时获取骨架上的Socket位置与旋转,写入Current Foot Lock Location/Rotation,也就是重置了我们Lock锁定的点。值得注意的是,这里设置的位置与旋转是局部坐标系下的。
①-⑤Current Foot Lock Alpha大于0的时候,需要绑定,根据之前说的此时既可能是马上绑定的逻辑,也可以是绑定慢慢解除的逻辑,在慢慢解除的过程中,项目使用了一个特殊的逻辑来处理让角色脚部减少滑动感,这就需要函数Set Foot Lock Offset。(注意,这里在处理的时候,会因为Lock的存在导致一直被锁住,所以疯狂轻触移动键,就会因为一直Lock,但是角色位置一直在变,出现腿被Lock住身体在移动的状态, 这里Lock的原因是Locomotion States有Quickstop触发逻辑,而这个逻辑里面是将两条腿锁住的,并且左腿无法达到①-④重置条件,所以在快速连按的情况下,Lock一直存在,并且不重置,所以身体即使做很小的偏移,叠加起来就大了,个人认为解决这个问题的思路主要在如何走出第一步,如果预留一个动身时间不走第一步,这个动身状态可以直接转换运动状态,而不是将起身状态作为参数写在Movement Animation里面,这样脚部在启动状态就不会移动;如果尝试过就知道,这个短时间内身体的移动基本上看不出来,但是因为触发了快速停止,所以会有调整脚部姿态的动画,这是完全没有必要的,不过设计上肯定就没有内嵌简洁)



Lock导致的BUG

①-⑤-补充:根据之前的逻辑,Set Foot Lock Offset是计算偏移,来保证我们能够拿到移动前的锚点,也就是说这里计算的Offset在触发重置之前都会累积
①-⑤-①先用CharacterMovement判断IsMovingOnGround(注意,这里的判断是用MovementMode),如果在地面上,那么计算旋转的差值。
①-⑤-②将Velocity和DeltaTime相乘获得短时间内的位移,再获得Mesh骨架的世界旋转,逆变换到局部坐标系,Location Difference就是局部坐标系下的位移。
①-⑤-③Local Location累积Offset,并且这个Offset发生在局部坐标系,还需要考虑Rotation带来的影响。
①-⑤-④Local Rotation累积Offset
②Set Foot Offsets:这里传入的参数和上面的类似,就不赘述了,这个函数主要是找到如何让脚部完美贴合地面的Offset,等于说就是找到锚点。
②-①同样是Check Enable,不然就重置Offset变量。
②-②取定足部的点为脚IK的XY和Root的Z,定义为IK Floor Location,这样可以保证XY的位置正确并且Z的高度是正常站姿平地贴地位置,姑且称为自然状态,在Config中设定的IK Trace Distance Above/Below Foot调整Trace Start和End,这里的Trace使用的Line,意思应该是这个点上有能站的就行,不考虑整个脚掌;然后计算Foot上的IK点在自然状态下的位置,同时用Impact point/normal计算在此时的地面上Foot上的IK点应该在的位置,相减就得到了Location的Offset目标,定义为Current Location Target,这样计算是和骨骼位置有关的;利用法向量计算绕XY轴旋转时候的Target Rotation Offset(脚绕Z轴旋转,也就是Yaw,会变得比较奇怪)
②-③对Location插值,目标点在上面还是在下面,这里有不同的插值速度,还是挺细的。
②-④对Rotation插值。
③Set Pelvis IKOffset,因为脚的位置改变了,相应的盆骨位置也需要改变,并且盆骨处也会有适当的位移。
③-①盆骨的位置作者认为应该以低的脚为参照移动,这样可以看做整根腿做了偏移,而高的脚则需要修改膝盖部分的逻辑,这是IK解算可以得到的。
③-②插值,设置为Pelvis Offset



白色是骨骼点,红色是Offset点,淡蓝色是Impact Point,黑色箭头是Impact Normal


4.1.2 IK解算

解算部分在Anim Graph的Foot IK部分,里面使用的是Transform Bone和Two Bone IK
Transform Bone就是设置骨骼点的Transform,这些都是上面Update计算出来的。
Two Bone IK的运行逻辑,按我的理解是:
IKBone是我们需要操作的真实骨骼点起点,Two Bone IK是自动沿着骨骼找两节骨骼与三个骨骼点,进行操作,其中中间节点需要IK解算位置。
Effector是设置起点的移动,在上面的更新中就是Offset部分,这个部分设置了起点的Transform,驱动了脚踝的旋转以及脚底位置的高度。
Joint Target是设置中间节点的趋进位置,所以可以看见Modify Knee Targets里面是用Const Transform套在虚拟的膝盖节点上,套上去之后是一个前弯的效果,如果把这个节点换到后面去,膝盖就会后弯了。
解算部分的总体逻辑就是:
Apply Foot Locking,把虚拟脚掌锁在自然状态
Apply Foot + Pelvis Offsets,计算骨骼节点偏移,其中盆骨是真实骨骼,Foot是虚拟骨骼,提供给后面Two Bone IK Transform
Modify Knee Targets,给Two Bone IK提供中间关节贴近目标,现在就是膝盖前驱,用的固定的Transform,毕竟脚前屈也就那么点角度了
Apply IK to Feet,调用Two Bone IK解算
至此脚部IK就结束了。
4.2 其他细节

个人觉得动画机并不是重点,重点是这个项目对动作的解耦与耦合,所以动画机就是看看实现,转化规则都不难,主要是看顶层框架的设计。
4.2.1 手部IK

这个部分绑定逻辑在ALS_AnimMan_Character上,绑定时链接骨骼Socket,手部IK就可以根据Socket为起点进行解算,当然链接的逻辑以及他找中间节点的逻辑也需要多考察一下。
同时Overlay部分的设计思路非常好,感觉这种解耦方式非常值得学习。
4.2.2 AnimNotify

动画的转换规则还可以发出Notify事件,之前说过的QuickStop就是通过这个逻辑发出来的,这个逻辑可以实现很多小的Transition功能,不过确实容易搞出一些BUG。
5. 总结

制作动画时,动画与角色位移分离,那么需要给动画适配的移位参数,Timeline控制移位参数,但如果总这样设计会很麻烦,并且也只能支持PlayMontage,爬墙就是用的这个逻辑。
不能用变量的视角看Curve,而应该用动态的眼光看Curve,信号,其实就是信号,要的时候就给脉冲,不过可能还有其他的方法可以控制动画状态,Curve确实得有全局观之后才能看得明白。
状态机设计的困难点并不是囊括所有的状态与状态转移,而是高效、准确地集合真正具备相同特征的Movement,这样做出来的耦合和解耦才是利于扩展的,所以在设计动画的时候需要有全局的视野,特别是要首先考虑所有的交互情况,才能将行为分层分级。只要System设计好了,细节想怎么添加怎么添加,这就是这个系统的包容性。
动画设计感觉还是比较庞大的工作,这样一个System涉及的东西实在是太多,而且只能用蓝图实现,参数零星分散确实对新手不友好。
就这样吧,收获颇多。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-25 06:48 , Processed in 0.098715 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表