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);
    }
}

1 Like

Your 3rd person view is very smooth, how did you do it?

Please can you show the character controller code, without animation?

Is all the code minus this part. The animation logic here does not conflict with the movement part.