Oasis 物理第四弹:角色控制器驱动人物动画

avatar
花呗借呗前端团队 @蚂蚁集团

引言

在之前的三篇文章当中,我们围绕 PhysX 介绍基础的物理组件设计

  1. Oasis 物理第一弹:基于WebAssembly 的 PhysX 跨平台编译与 PVD 联调
  2. Oasis 物理第二弹:物理多后端与组件架构设计
  3. Oasis 物理第三弹:实现轻量级碰撞检测算法包

这篇文章中,我们将进入角色控制器组件 CharacterController,该组件实质上是碰撞器组件的一种高级封装,通过这一组件可以更容易实现角色控制相关的运动控制和事件触发。例如角色控制器可以控制角色的爬坡能力;翻越障碍物的能力;以及在物体运动后进行状态检测,用以触发不同的动画或者用户逻辑。
Sep-16-2022 10-29-09.gif

角色控制器的基础使用

正如前面所说的,角色控制器实际上也是一种碰撞器组件,是碰撞器组件的一种高级封装。因此,在组件设计中,也将其作为碰撞器Collider 的子类:

/**
 * The character controllers.
 */
export class CharacterController extends Collider {}

但是与其他碰撞器组件不同,角色控制器只支持包含一个 ColliderShape,并且只支持:

  1. BoxColliderShape
  2. CapsuleColliderShape(最为常用)

有了角色控制器组件后,可以通过一系列属性配置该控制器的能力,例如设置可以跑过的障碍物高度等等。在程序运行过程中,用户的操作会转换成调用 move 函数控制角色控制器的移动。在移动的过程中,角色控制器就可以根据配置参数,判断是否可以爬过障碍物,或者被过于陡峭的斜坡所阻挡。

/**
 * Moves the character using a "collide-and-slide" algorithm.
 * @param disp - Displacement vector
 * @param minDist - The minimum travelled distance to consider.
 * @param elapsedTime - Time elapsed since last call
 * @return flags - The ControllerCollisionFlag
 */
move(disp: Vector3, minDist: number, elapsedTime: number): number;

这个函数中最重要的参数是 disp,指定了控制器的位移,并且返回一个数值,该数值用于判断控制器移动后是否碰到了一些障碍物:

/**
 * The up axis of the collider shape.
 */
export enum ControllerCollisionFlag {
  /** Character is colliding to the sides. */
  Sides = 1,
  /** Character has collision above. */
  Up = 2,
  /** Character has collision below. */
  Down = 4
}

物理组件与角色实体的同步

角色控制器在某种程度上说和动态碰撞器是类似的,只是动态碰撞器,例如一个小球,在受到物理反馈的时候会按照物理规律发生运动。但是角色控制器则将控制运动的逻辑交给了用户,通过上述的 move 函数进行控制。
所以,物理组件和角色实体之间的同步方式,和动态碰撞器但同步方式是一致的:

  1. 物理更新前,会调用 _onUpdate 函数将用户其他改变控制器的操作同步到物理组件,这一步通过判断 Tansform 当中的脏标记后执行。
  2. 完成物理的更新后,就会调用 _onLateUpdate 函数将物理组件的位置同步回角色实体上面

因此,如果用户逻辑调用了 move 函数,物理系统会判断移动角色后是否会碰到其他的障碍物,然后觉得是否执行移动的指令。这样一来用户的所有操作都会经过物理系统的过滤,由此使得角色的行为显得更加符合“物理”。

一个简单的状态机

move 函数返回角色控制器位移后的状态,判断是否为 Down 可以检查角色是否落到地面,判断是否为 Sides 可以检查角色是否碰到侧边的物体等等。角色控制器拥有多种复合状态,而这些状态往往都会和不同的动画片段有关。例如角色进行一个跳跃,就会涉及到跳跃动画,下落动画,落地动画,最终回到角色的呼吸态。动画状态机可以通过 AnimatorStateScript 进行编写,由此构建复杂的动画逻辑。
为了简单起见,并且为了突出角色控制器的使用,我们尝试编写一个简单的状态机。


type State = "Run" | "Idle" | "Jump_In" | "Fall" | "Landing";

class AnimationState {
  private _state: State = "Idle";
  private _lastKey: Keys = null;
  
  get state(): State {
    return this._state;
  }
  
  setFallKey() {
    this._state = "Fall";
  }
  
  setIdleKey() {
    if (this._state == "Jump_In") {
      return;
    }
  
    if (this._state === "Fall") {
      this._state = "Landing";
    }
  
    if (this._state === "Landing") {
      this._state = "Idle";
    }
  }
}

假设我们拥有五种动画状态,AnimationState 维护当前的状态,并且可以通过调用其内部的函数切换不同的状态。接着我们需要在 onPhysicsUpdate 脚本函数中移动角色控制器。需要特别指出的是,该函数和物理系统的更新保持同频调用,因此每一个渲染帧有可能会调用一次,也可能会调用多次。

onPhysicsUpdate() {
  const physicsManager = this.engine.physicsManager;
  const gravity = physicsManager.gravity;
  const fixedTimeStep = physicsManager.fixedTimeStep;
  this._fallAccumulateTime += fixedTimeStep;
  const character = this._controller;
  character.move(this._displacement, 0.0001, fixedTimeStep);

  const flag = character.move(new Vector3(0, gravity.y * fixedTimeStep * this._fallAccumulateTime, 0), 0.0001, fixedTimeStep)
  if (flag & ControllerCollisionFlag.Down) {
    this._fallAccumulateTime = 0;
    this._animationState.setIdleKey();
  } else {
    this._animationState.setFallKey();
  }
  this._playAnimation();
}

_playAnimation() {
  if (this._animationName !== this._animationState.state) {
    this._animator.crossFade(this._animationState.state, 0.1);
    this._animationName = this._animationState.state;
  }
}

在这一函数中,我们先根据用户的输入,在 _displacement 方向移动角色控制器。由于用户的操作只是在地面上移动,所以此时函数的返回值并不重要。为了让角色控制器自动落回到地面,因此尝试对角色在 y 方向移动特定的距离,并且通过返回值判断是否落到地面上,同时设置特定的动画状态。_playAnimation 函数会判断动画状态是否发生改变,如果改变则使用 crossFade 函数自动融合两端动画,切换到下一个状态。

总结

角色控制器是碰撞器组件的一种高级封装,通过他可以串联 InputManagerAnimator 等等组件与系统,使得非常容易通过判断物理状态切换角色的动画逻辑。并且在上述介绍中大家也会注意到,角色控制器加上射线检测,可以很容易实现射击类游戏的逻辑。
除了角色控制器,在 0.8 里程碑当中还引入了基础的物理约束组件,包括 HingeJointSpringJointFixedJoint。在下一个里程碑当中,会继续专注提升基于物理的动画能力,包括布娃娃,弹性骨骼等等,这些组件内部实际上也都是使用物理约束来实现。通过结合角色控制器和各种物理动画效果,可以更好提升互动项目的可玩性和真实性。
如果你有想要了解的,或者急需的物理相关技术点,也欢迎给我们留言。

如何进一步了解我们

Oasis 开源社区群 (钉钉):

JPG.png

Oasis 开源社区群管理员 (微信):

image.png

网站

官网地址
oasisengine.cn
Engine 源码地址
github.com/oasis-engin…
Engine Toolkit 源码地址
github.com/oasis-engin…