在之前的三篇文章当中,我们围绕 PhysX 介绍基础的物理组件设计
这篇文章中,我们将进入角色控制器组件 CharacterController,该组件实质上是碰撞器组件的一种高级封装,通过这一组件可以更容易实现角色控制相关的运动控制和事件触发。例如角色控制器可以控制角色的爬坡能力;翻越障碍物的能力;以及在物体运动后进行状态检测,用以触发不同的动画或者用户逻辑。
正如前面所说的,角色控制器实际上也是一种碰撞器组件,是碰撞器组件的一种高级封装。因此,在组件设计中,也将其作为碰撞器Collider
的子类:
/**
* The character controllers.
*/
export class CharacterController extends Collider {}
但是与其他碰撞器组件不同,角色控制器只支持包含一个 ColliderShape
,并且只支持:
BoxColliderShape
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 函数进行控制。 所以,物理组件和角色实体之间的同步方式,和动态碰撞器但同步方式是一致的:
因此,如果用户逻辑调用了 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 函数自动融合两端动画,切换到下一个状态。
角色控制器是碰撞器组件的一种高级封装,通过他可以串联 InputManager
,Animator
等等组件与系统,使得非常容易通过判断物理状态切换角色的动画逻辑。并且在上述介绍中大家也会注意到,角色控制器加上射线检测,可以很容易实现射击类游戏的逻辑。
除了角色控制器,在 0.8 里程碑当中还引入了基础的物理约束组件,包括 HingeJoint
,SpringJoint
和 FixedJoint
。在下一个里程碑当中,会继续专注提升基于物理的动画能力,包括布娃娃,弹性骨骼等等,这些组件内部实际上也都是使用物理约束来实现。通过结合角色控制器和各种物理动画效果,可以更好提升互动项目的可玩性和真实性。
如果你有想要了解的,或者急需的物理相关技术点,也欢迎给我们留言。