功能需求
- 根据瞄准仰角改变人物上半身姿势;
- 可以在走路中换弹;
- 枪朝向瞄准的方向;
- 上半身在各方向移动中保持稳定,同时有轻微晃动;
- 据枪时左手跟随枪,且左手位置根据不同枪变化;
- 枪绑定的物体在双手、背之间的切换;
- 约束开启和关闭(如跑步)状态平滑过渡;
1.改变瞄准仰角
这个功能有多种方案实现,总体可以分为几类:
1.改变腰的旋转;
2.改变手臂和头的旋转;
3.腰、双臂、头的旋转都改变;
一类方案:
只要转动腰一个骨骼,比较简单;
使用这种方案的游戏:
尘白禁区
二类方案:
转动3个骨骼,比较复杂(因为改变仰角时双臂的旋转是一个极不规则的旋转,写代码使用正向动力学控制基本不可能,要么用IK,要么动画混合);
使用这种方案的游戏:
原神
武装突袭1
因为枪相对头和第一人称相机的位置不固定,第一人称看到的枪的位置和远近也不固定,仰视时枪可能和头、第一人称相机穿模。
三类方案:
基本上只能用动画混合树。
例子:绝地求生、和平精英、荒野行动。仰角比较小时转动手腕,达到一个仰角后转动胳膊和腰。
转动Spine(简单但是僵硬)
animator.SetBoneLocalRotation()
需要把相应层打开IK Pass,在OnAnimatorIK()里执行animator.SetBoneLocalRotation(),可以直接改变某个骨骼的旋转。这似乎是官方推荐的方法:
Quaternion.AngleAxis()输入的轴是物体的局部坐标系的,所以要获得Spine坐标系下人物x轴的向量。Quaternion.AngleAxis()返回一个旋转变化,它乘给spine.localRotation。这里angleX是仰角,是个会变大变小的值,怎么能做变化量?
public Animator animator;
HumanBodyBones boneToLookUpDown=HumanBodyBones.Spine;
Transform spine;
[Range(-90,90)]
public float angleX=0;
void Start(){
animator = GetComponent<Animator>();
spine=animator.GetBoneTransform(boneToLookUpDown);
}
void OnAnimatorIK(){
Quaternion spineRot=Quaternion.AngleAxis(angleX,spine.InverseTransformDirection(transform.right));
animator.SetBoneLocalRotation(boneToLookUpDown,spine.localRotation*spineRot);
}
这里animator.SetBoneLocalRotation()之前我打印了一下spine.localEulerAngles,结果是:
animator.SetBoneLocalRotation()之前spine的旋转一直不变,会被动画系统掰回原来的旋转,所以animator.SetBoneLocalRotation(xxx,spine.localRotation*deltaRot);里面虽然有spine的当前旋转,但这个旋转一直是人物姿势被改变前的旋转,这句其实是设置spine的局部绝对旋转。
如果不运行,只在编辑模式看一下效果,可以把人物的旋转归零,选Global,Rotate Tool,直接旋转Spine骨骼。
效果
约束:RotationConstraint(或AimConstraint)
在Spine添加旋转约束,Source是控制仰角的物体。
预览人物端枪的动画。
在旋转约束点Is Activate,组件会计算出Spine当前的旋转偏移。点Lock。
在端枪状态的动画里加上开启旋转约束的关键帧,其他状态不开启。
这个方案也满足了需求3。
在LateUpdate()里修改Spine的旋转
动画系统是在Update()和LateUpdate()之间生效的,在LateUpdate()里直接用transform的API也可以修改骨骼的变换。
AvatarMask+一维混合树(双臂和腰都可以转动)
在3d动画软件里做出人物仰视俯视的动画,然后用一维混合树。
如果不会自己做,Mixamo有一个叫Rifle Aiming Idle的动画,里面有Aim Height参数,可以调出仰视俯视瞄准的动画。
持枪状态在body层是一个移动的二维混合树,arm层是控制仰角的一维混合树。
注意:如果不同方向移动的动画Hips的旋转不一样,此时只用AvatarMask会让不同方向移动时上半身偏转,也需要在Spine或Chest加约束。
第一人称效果(枪相对头的位置变化了,所以看到枪的位置不一样):
问题:这个方案必须用AvatarMask覆盖上半身,如果走路动画的Hips的旋转在晃动,那么上半身也会跟着晃动。可以把走路动画改成Hips旋转不晃动的,然后走路时上半身和双臂都一动不动,动作更僵硬。
使用IK(转动双臂和头)
使用IK需要面对的问题是IKGoal的父级Axis绑定在哪里。绑定在根节点,双手就没有走路时的晃动效果;我们需要的是Axis的位置跟随上半身,旋转和瞄准方向一致。可以绑定在Chest,并使用旋转约束只让Axis的位置跟随Chest。
轴-枪、双手IK
人物改变仰角的时候枪是绕右肩旋转的,所以在瞄准姿势在人物根节点下右肩的位置放一个节点作为转轴Axis。
把枪挂载到Axis下,在枪下面建左手右手的IKGoal节点。枪应该使Axis在枪托尾部的位置。IKGoal的位置和旋转需要运行后手调。
Axis前方很远的地方建一个target,作为头看向的目标。
配置双手IK:
public Transform rightHandIK,leftHandIK;
void HandsIK(){
animator.SetIKPosition(AvatarIKGoal.RightHand,rightHandIK.position);
animator.SetIKPositionWeight(AvatarIKGoal.RightHand,1);
animator.SetIKRotation(AvatarIKGoal.RightHand,rightHandIK.rotation);
animator.SetIKRotationWeight(AvatarIKGoal.RightHand,1);
animator.SetIKPosition(AvatarIKGoal.LeftHand,leftHandIK.position);
animator.SetIKPositionWeight(AvatarIKGoal.LeftHand,1);
animator.SetIKRotation(AvatarIKGoal.LeftHand,leftHandIK.rotation);
animator.SetIKRotationWeight(AvatarIKGoal.LeftHand,1);
}
配置头看向:
public Transform target;
void HeadLookIK(){
animator.SetLookAtPosition(target.position);
animator.SetLookAtWeight(1);
}
需要手调的变量有:
1.Axis的位置;
2.枪的位置;
3.双手IK的位置和旋转;
效果
第一人称效果:仰视的时候枪穿到了相机的Near clip plane后面,没有显示全。
看上去好像不错。这个方案的问题:
对于枪绑定右手时无需特别处理的动画:持枪跑步、换弹、收枪、取枪的动画,要么给枪K帧(增加动画工作量)。要么在做这些动作时还改成枪绑定右手(增加程序工作量);
轴-IK-双手-枪
这种架构左手和枪没有约束关系,可能出现左手脱离护木的问题。
使用IK方案的问题:
1.换弹动画需要关闭IK,然后无法改变仰角,如果做第一人称,换弹时可能看不到双手的动画
2.保持枪指向瞄准方向
如果把枪绑定在右手,枪的局部旋转极有可能是个不规则的值,且在移动中手部的旋转可能变化。不适合直接由右手控制枪的旋转。可以给枪的容器节点加旋转约束,由指向瞄准方向的物体控制它的旋转。
3.上半身在移动中保持稳定
方法就是给spine或chest加旋转约束。在1.里已经讨论过。
4.左手跟随枪
如果枪绑定在右手上,那么左手就要使用IK跟随枪。否则就要用双手跟随枪的架构。
5.枪跟随的物体在右手、左手、背之间转换
要么使用动画事件,要么给枪的容器节点加Parent Constraint,把上述物体加入Source,由动画剪辑的关键帧控制各Source的权重。这个方法的好处是:
1.代码量减少;
2.预览动画就能看到效果,动画事件则必须运行才能看到。
状态机设计
射击游戏的动作状态比较多。一般RPG游戏都有静止、走路、跑步、跳跃状态,即使增加攻击、技能,也只是增加几个状态。而射击游戏如果人物空手、持步枪都有静止、走路、跑步、跳跃,状态数就增加了一倍。如果加上持手枪,状态又增加一倍,还要考虑站、蹲、趴……为了避免状态数量太恐怖,射击游戏一般都采用上下身动画叠加的设计。上半身不管是空手、持冷兵器、持手枪、持步枪,下半身都是同一套站静止、站移动、蹲静止、蹲移动、跳。这样上半身和下半身形成一个“状态矩阵”,叠加出很多状态。
使用上下半身叠加的架构下,不使用叠加的全身动作该放在哪?
有些动作要上下半身叠加,有些动作是全身动作,比如跑步、攀爬、死亡、趴着……
全身动画放置的几种方案:
1.放在Base层,上半身放None;
2.在这两层上面加一个全身层。
方案1导致身体层和双臂层出现了很多同名的状态,状态机的状态几乎增加了一倍。如果把一些None状态合并,又可能导致上半身状态机难读。
这里的“全部状态都使用叠加”就是空手时也像拿枪那样半侧身,而不是空手时面向前方。这种方案的下半身不区分是空手、步枪、手枪等,上半身则不区分站、蹲。而跑步是特例,跑步为了效果自然不使用上下半身叠加,所以上半身有多少种武器状态就有多少种跑步,跑步动画放在Base layer,而且上半身还要留出相应的None状态。也就是说跑步在上下半身层都要有状态。
但是这种空手姿势太战术了,如果想做有角色扮演的游戏,这样和别人对话比较奇怪。空手状态还是应该正对前方,这样空手在Base层单独有一个二维混合树,拿武器站着有一个半侧身的混合树,蹲着一个混合树。上半身层空手状态为None,拿武器状态只区分哪类武器,不区分站和蹲。
方案2:在这两层上面单独建一层WholeBody
Trunk&LowerBody层只考虑人物的躯干和腿有什么状态。需要的状态有noGunStanding(身体向正前方站立)、gunStanding(身体向右偏45度站立)、crouching(拿枪和不拿枪共用一个躯干&下半身姿势)、NoGun Jump、Gun Jump(允许玩家边跳边开枪、换弹);
后来我又把跳跃改成全身动作了。因为边跑边跳跃时WholeBody层进入一下None状态又到跑步状态,出现在空中跑步的情况,然后我把全身层进入跑步的条件加了一个onGround,不会在跳跃中跑步了,又遇到了新问题:跳跃是靠触发器进入的,允许边跳跃边瞄准射击意味着一个触发器同时触发两层的转换,如果稍有不同步就只能触发一层,出现异常状态。最终决定只在全身层做一个跳跃状态。
arm层只考虑人物双臂有什么状态。有noGunIdle、rifleIdle、rifleReload、putAwayRifle、takeOutRifle、rifleRunning;
WholeBody层有所有用到全身的状态。和一个不覆盖下层的None(motion)。有running、prone、goProne、getUp、climbing;
跑步到底要不要上下半身叠加?
然后我发现我以为的“跑步是全身动作,不需要上下半身叠加”又想错了。跑步中需要支持取枪收枪,这是需要上下半身叠加的。
跑步中跳跃
跑步中跳跃时,如果跑步放在Base层,上半身放None的设计,若不做处理,跑步中跳跃上半身还是跑步状态。下半身可以依靠状态机状态间的互斥不做处理,但为了上半身层,跳跃时需要把running设false。这就是跳跃打断了跑步,running不能只在玩家按下抬起shift时设置,看了一些模板,跑步参数是每帧都设置的,设置前判断有没有进行可能打断跑步的行为(换弹、射击、瞄准、跳跃)。
动画状态机的一个简化技巧:动作状态到空闲状态无需设置到所有空闲状态的转换,只需转换到其中一个,然后靠空闲状态中间的转换。但是空闲状态到动作状态是所有空闲状态都要设置转换。例如蹲着Crouching到捡东西PickUp,捡完后无需设置PickUp到Crouching的转换,只需有PickUp到Standing的转换,再通过控制身体高度的参数从Standing转换到Crouching。
动作条件的判断/动作之间的关系
有很多动作做之前需要先判断是否满足条件,一些动作可以打断另一些动作。主要是跑步、换弹、射击、瞄准之间的关系。需要回答几个问题:
- 两个动作能不能同时进行?
- 对于不能同时进行的,一个动作进行中输入另一个,是阻止后来动作还是后来动作打断前一个动作?
- 如何实现条件判断或者打断动作?
射击和跑步
二者的关系可以是
- 跑步中禁止射击;
- 射击打断跑步,进入走路状态;
换弹和跑步
二者的关系可以是
- 跑步中禁止换弹;
- 换弹打断跑步,进入走路状态;
换弹打断跑步
-
定义字段bool reloading,换弹动画中设为true,结束时设为false,可以通过关键帧、动画事件、状态机行为脚本设置;
-
Update()里设置跑步的动画参数,在条件里加入&&!reloading;
-
状态机里加入跑步到换弹的转换;
瞄准和跑步
二者的关系可以是
- 跑步中禁止换弹;
- 换弹打断跑步,进入走路状态;
射击和换弹
二者都不能打断对方,必须等对方完成后才能开始。
瞄准和换弹
换弹优先级高于瞄准,换弹可打断瞄准,瞄准必须等换弹后。
瞄准和射击
二者可以同时进行
为执行一个动作添加条件判断
无非2种方法:
- 写一个判断函数,里面写入需要满足的各种条件,执行前判断;
- 定义一个bool字段,让其他各部分设置它,执行前判断;
在状态机同一层的动作的互斥,本身也是一种限制。但如果不能同时进行的动作在状态机不同层,就必须另外加限制。
怎么知道换弹(一个trigger触发的、hasExitTime结束的动画)完成了没有
要知道一个trigger触发的动画是否结束,无非是定义一个bool,在动画开始时设true,结束时设false,读取这个bool即可。关键帧、动画事件、状态机行为脚本都可以实现。3种方法各有特点:
- 关键帧:给换弹动画第一帧和快结束时打个true,最后一帧打false。如果给一个属性打了关键帧,这个属性就会每一帧都被动画系统写入值(在Update之后,LateUpdate之前)。如果一个状态没打关键帧,就一直写入之前最后一个关键帧的值。如果用关键帧维护字段,要有开有关。关键帧不适合控制循环动画里的属性,因为不能在这个状态里设false,只能在它可能过渡到的状态设false。(不过循环动画一般都不是hasExitTime退出的,trigger触发的,通过animator.GetBool、GetInteger、GetFloat就能得到状态)
- 动画事件如果放在动画的开始或结尾,如果在动画过渡期,动画事件就不会执行。所以不适合标记hasExitTime动画的结束,更适合动画中途执行函数。
- 状态机行为脚本要先获得人物脚本,多了这一步。它的问题是下一个状态的OnStateEnter()在这一个状态的OnStateExit()之前执行,可能导致bool被错误设置。所以它适合只有一个状态写入bool,不适合连续多个状态都写入bool(这种适合关键帧)。
给人物声明一个变量public bool readyToFire,玩家按下射击时把这个变量加入判断条件,其他地方的代码设置这个变量,可以在1.人物脚本;2.关键帧;3.状态机脚本;4.动画事件。这样直接在瞄准状态的状态脚本的OnStateEnter()把这个置true,OnStateExit()里置false。趴着的状态还是需要根据VerticalSpeed、HorizontalSpeed判断,写在状态脚本的OnStateUpdate()里。
并非所有不能同时做的动作都要在相应的状态脚本里把另一个动作的readyToXXX置false,有一些因为状态之间没有转换,天然不会同时进行,比如在Rifle Run状态因为没有转换,不可能到Reload状态,但是这个图是Arm层,跑步在Body层也有状态,Reload没有,换弹过程中跑步会导致Body层进入跑步状态,这时候就要在Reload状态脚本里限制一下。具体哪些地方要加限制只能一个个试。我又试了一下,跑步中按换弹,虽然没有播放换弹动画,但是播放了换弹声音,子弹数更新了,因为状态之间没有转换只阻止了播放动画,我的Reload()方法是输入触发的。还是要加限制;要么就改成动画事件触发。
bool字段判断法的改进
后来又觉得使用readyToXXX,readyToYYY太不直观了,readyToRun是可以跑步,那么哪些动作会导致不能跑步?看不出来。不如使用reloading、jumping、running,设置跑步时直接:
void Update(){
player.Run(runPressed&&!player.jumping&&!player.reloading&&!player.healing);
哪些动作会打断跑步,一目了然。 而这些XXXing字段由相关动画的关键帧写入。
测试
先列出所有可能冲突的行为,得到nC2种关系,每中关系做先A后B和先B后A的测试,依次检查是否符合设计的行为。
1.跑步时按换弹,是否不换弹或结束跑步后换弹;
2.换弹时按跑步,是否不会跑步;
3.瞄准状态按换弹、跑步,是否会解除瞄准;
4.换弹、跑步状态按瞄准,是否不会瞄准;
以上测试可以总结为跑步、换弹、瞄准3个动作的互斥测试。
7.趴下、站起的过程中按换弹、跑步、机瞄,是否不会做相应动作或当前动作完成后再做;
8.瞄准状态按爬行,是否会解除瞄准
9.瞄准状态扔掉手里的枪、交换枪是否会解除瞄准
10.瞄准状态把枪收起来或者换成手枪是否会解除瞄准
写了一大堆,突然发现上面一大堆条件就是离开瞄准状态的各种途径,只需要在站、蹲瞄准状态脚本的OnStateExit()、趴瞄准状态脚本的OnStateUpdate()(水平、垂直速度比较大时)、OnStateExit()执行关闭瞄准方法即可,无需在各个输入的时候写。但是有一个例外,手枪射击的时候的上跳我是用人物的手腕上跳做的,这时候就不应该解除瞄准了。
为了解决这个问题我只好把人物手腕上跳状态删掉,单独给手枪做一个上跳,不属于人物动画状态机,射击的时候手腕不动,效果僵硬了点,但是先这样吧。
对叛乱:沙漠风暴做了上面的测试,结果是
1.没换弹,跑步结束也不换
2.换弹动作被打断,进入跑步,弹匣没插,此时开枪没子弹或者只有一发,此时按换弹只插弹匣。这个处理牛逼。
3.会
4.换弹时不会瞄准,会打断跑步进入瞄准
7.按换弹被直接忽略,瞄准会等趴下后进入,按跑步会再站起来开始跑步
约束权重的控制
经过上面的讨论我们知道让人物摆出射击姿势少不了约束,包括躯干保持稳定和控制仰角的约束,右手指向目标的约束,左手跟随枪的IK。而又只有部分状态需要打开这些约束,例如跑步要开左手IK,关闭躯干和右手。约束开关是和状态紧密联系的,那么用状态机的动画关键帧开关约束是最方便的方法,然而还要面对的问题是
约束权重的关键帧是直接放在动画里还是另外开一层?
直接放在动作动画里,首先是把fbx的动画复制一份,增加了项目里的动画文件数,然后在巨量的动画里加关键帧,且大多数动画加的关键帧是完全相同的,越加越想,为什么不把这些关键帧放在一个动画,另开一层。
另开一层又要面临的问题是人物有好几个约束,一个约束开一层还是所有约束共用一层?我试了一个约束一层,然后约束就有这么多层: