How to properly use CharacterController in a 3d Physics world?

Hello, I’m learning how to properly set a CharacterController script for a TPS character together with native physics, but I’m not having much success.

If I just use the CharaterController + the Rigid body, collisions do not work.
ezgif-2-0e35975fe4

And if I add a cylinder collider, it does not stand up.
ezgif-2-c0debfa6ba

What would be the best way to use TPS CharacterController + real physics simulation world?

hi, please set angular forces to 0
and you can refer this demo(controller.zip - Google Drive)

1 Like

Thanks @iwae!

With your project example, I was able to understand better and pull it off!

Here is my code for reference if anyone is trying to do it.

import {
    _decorator,
    animation,
    CapsuleCharacterController,
    CharacterControllerContact,
    clamp,
    Component,
    lerp,
    PhysicsSystem,
    Vec3,
} from 'cc';

import OrbitCamera from '../camera/orbitCamera';
import inputMap from '../movement/input';
const { ccclass, property, type } = _decorator;

@ccclass('Character')
export class Character extends Component {
    @property
    walkSpeed!: number;

    @property
    runSpeed!: number;

    @property
    jumpForce!: number;

    @property
    gravityValue!: number;

    @property
    linearDamping!: number;

    @property
    pushPower!: number;

    @type(OrbitCamera)
    camera!: OrbitCamera;

    @type(animation.AnimationController)
    animationCtrl!: animation.AnimationController;

    @type(CapsuleCharacterController)
    characterCtrl!: CapsuleCharacterController;

    private controlZ = 0;
    private controlX = 0;

    private movement = new Vec3(0, 0, 0);
    private tempVec3 = new Vec3(0, 0, 0);
    private playerVelocity = new Vec3(0, 0, 0);

    private currentSpeed = 0;
    private tempRotation = 0;
    private targetRotation = 0;

    protected onLoad(): void {
        this.characterCtrl.on('onControllerColliderHit', this.onControllerColliderHit, this);
    }

    onControllerColliderHit(hit: CharacterControllerContact): void {
        const body = hit.collider.attachedRigidBody;
        if (body == null || body.isKinematic) {
            return;
        }

        // We dont want to push objects below us
        if (hit.motionDirection.y < -0.1) {
            return;
        }

        // Calculate push direction from move direction,
        // we only push objects to the sides never up and down
        const pushDir = new Vec3(hit.motionDirection.x, 0, hit.motionDirection.z);

        // If you know how fast your character is trying to move,
        // then you can also multiply the push velocity by that.
        // Apply the push
        Vec3.multiplyScalar(pushDir, pushDir, this.pushPower);
        body.setLinearVelocity(pushDir);
    }

    // eslint-disable-next-line complexity
    update(deltaTime: number): void {
        if (this.characterCtrl.isGrounded) {
            this.playerVelocity.y = 0;
            this.controlZ = 0;
            this.controlX = 0;
        }

        deltaTime = PhysicsSystem.instance.fixedTimeStep;

        if (this.characterCtrl.isGrounded) {
            this.currentSpeed = inputMap.key.shift ? this.runSpeed : this.walkSpeed;
        }

        this.playerVelocity.y += this.gravityValue * deltaTime;

        const forward = this.node.forward;
        // const right = this.node.right;

        this.tempRotation = lerp(this.tempRotation, this.targetRotation, deltaTime * 10);
        this.node.eulerAngles = this.tempVec3.set(0, this.tempRotation, 0);

        // Apply inputs based on player's orientation
        if (inputMap.key.up && this.characterCtrl.isGrounded) {
            this.controlZ -= forward.z * deltaTime;
            this.controlX -= forward.x * deltaTime;
        }

        if (inputMap.key.down && this.characterCtrl.isGrounded) {
            this.controlZ += forward.z * deltaTime;
            this.controlX += forward.x * deltaTime;
        }

        if (inputMap.key.left) {
            this.targetRotation += 180 * deltaTime;
            /*this.controlZ -= right.z * deltaTime;
            this.controlX -= right.x * deltaTime;*/
        }

        if (inputMap.key.right) {
            this.targetRotation -= 180 * deltaTime;
            /*this.controlZ += right.z * deltaTime;
            this.controlX += right.x * deltaTime;*/
        }

        this.controlZ = clamp(this.controlZ, -1, 1);
        this.controlX = clamp(this.controlX, -1, 1);

        if (inputMap.key.space && !this.animationCtrl.getValue('isJumping') && this.characterCtrl.isGrounded) {
            this.playerVelocity.y += this.jumpForce;
        }

        //control impulse
        this.playerVelocity.z += this.controlZ * this.currentSpeed;
        this.playerVelocity.x += this.controlX * this.currentSpeed;

        this.playerVelocity.x *= this.linearDamping;
        this.playerVelocity.z *= this.linearDamping;

        // Create a new movement vector based on the character's input
        this.movement = Vec3.multiplyScalar(this.movement, this.playerVelocity, deltaTime);
        this.characterCtrl.move(this.movement);

        // Apply animations states
        this.animationCtrl.setValue('hasKicked', inputMap.key.f);
        this.animationCtrl.setValue('hasPunched', inputMap.key.g);
        this.animationCtrl.setValue('isGrounded', this.characterCtrl.isGrounded);
        this.animationCtrl.setValue('isJumping', inputMap.key.space);
        this.animationCtrl.setValue('hasCrouched', inputMap.key.c);
        this.animationCtrl.setValue('isCrouched', inputMap.key.c);
        this.animationCtrl.setValue('isRunning', inputMap.key.shift);
        this.animationCtrl.setValue('isMoving', inputMap.key.up || inputMap.key.down);
    }
}