Implementing Horizontal Character Movement with Keyboard Controls in Cocos Creator

Horizontal character movement represents one of the most fundamental mechanics in 2D game development. This implementation demonstrates how to capture keyboard input, apply acceleration-based movement, enforce velocity limits, and constrain characters within screen boundaries using Cocos Creator's component system.

Component Implementation

The following TypeScript component handles all aspects of horizontal character control:

const { ccclass, property } = ccdecorator;

@ccclass
class CharacterController extends cc.Component {
    @property({ type: cc.Integer })
    maxVelocity: number = 300;

    @property({ type: cc.Integer })
    moveAcceleration: number = 600;

    private horizontalVelocity: number = 0;
    private isMovingLeft: boolean = false;
    private isMovingRight: boolean = false;
    private screenHalfWidth: number = 0;

    onLoad(): void {
        this.initializeMovement();
        this.setupKeyboardListeners();
    }

    onDestroy(): void {
        this.removeKeyboardListeners();
    }

    private initializeMovement(): void {
        this.screenHalfWidth = this.node.parent.width / 2;
        this.isMovingLeft = false;
        this.isMovingRight = false;
        this.horizontalVelocity = 0;
    }

    private setupKeyboardListeners(): void {
        cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.handleKeyPress, this);
        cc.systemEvent.on(cc.SystemEvent.EventType.EventType.KEY_UP, this.handleKeyRelease, this);
    }

    private removeKeyboardListeners(): void {
        cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN, this.handleKeyPress, this);
        cc.systemEvent.off(cc.SystemEvent.EventType.KEY_UP, this.handleKeyRelease, this);
    }

    private handleKeyPress(event: cc.Event.EventKeyboard): void {
        switch (event.keyCode) {
            case cc.macro.KEY.leftArrow:
            case cc.macro.KEY.a:
                this.isMovingLeft = true;
                break;
            case cc.macro.KEY.rightArrow:
            case cc.macro.KEY.d:
                this.isMovingRight = true;
                break;
        }
    }

    private handleKeyRelease(event: cc.Event.EventKeyboard): void {
        switch (event.keyCode) {
            case cc.macro.KEY.leftArrow:
            case cc.macro.KEY.a:
                this.isMovingLeft = false;
                this.horizontalVelocity = 0;
                break;
            case cc.macro.KEY.rightArrow:
            case cc.macro.KEY.d:
                this.isMovingRight = false;
                this.horizontalVelocity = 0;
                break;
        }
    }

    update(deltaTime: number): void {
        this.applyMovement(deltaTime);
        this.clampVelocity();
        this.applyPositionChange(deltaTime);
        this.enforceScreenBounds();
    }

    private applyMovement(deltaTime: number): void {
        if (this.isMovingLeft) {
            this.horizontalVelocity -= this.moveAcceleration * deltaTime;
        } else if (this.isMovingRight) {
            this.horizontalVelocity += this.moveAcceleration * deltaTime;
        }
    }

    private clampVelocity(): void {
        if (Math.abs(this.horizontalVelocity) > this.maxVelocity) {
            this.horizontalVelocity = this.maxVelocity * Math.sign(this.horizontalVelocity);
        }
    }

    private applyPositionChange(deltaTime: number): void {
        this.node.x += this.horizontalVelocity * deltaTime;
    }

    private enforceScreenBounds(): void {
        const characterHalfWidth = this.node.width / 2;
        const leftBoundary = -this.screenHalfWidth + characterHalfWidth;
        const rightBoundary = this.screenHalfWidth - characterHalfWidth;

        if (this.node.x < leftBoundary) {
            this.node.x = leftBoundary;
            this.horizontalVelocity = 0;
        } else if (this.node.x > rightBoundary) {
            this.node.x = rightBoundary;
            this.horizontalVelocity = 0;
        }
    }
}

Core Architecture

Property Configuration

The component exposes two configurable properties through the Cocos Creator editor. The maxVelocity property establishes the upper bound for horizontal movement speed, preventing characters from accelerating beyond a playable limit. The moveAcceleration property controls how quickly the character reaches maximum velocity, providing a sense of weight and momentum to the movement.

The horizontalVelocity property serves as the internal state tracking the character's current speed and direction. Positive values indicate rightward movement while negative values indicate leftward movement. Boolean flags isMovingLeft and isMovingRight track the current input state without directly modifying velocity, allowing for smooth acceleration curves.

Input Handling System

The keyboard event system in Cocos Creater operates through global system events. The component registers listeners during the onLoad lifecycle callback and removes them during onDestroy to prevent memory leaks and unexpected behavior when the component is destroyed.

The handleKeyPress method responds to key press events by setting the appropriate directional flag. Supporting both arrow keys and WASD inputs provides better accessibility for different user preferences. The handleKeyRelease method clears directional flags and immediately zeroes the velocity, creating an instant stop response when keys are released.

Movement Physics

The update method executes four distinct operations every frame. First, applyMovement modifies velocity based on active directional flags and the configured acceleration rate. The acceleration calculation uses deltaTime to ensure frame-rate independent movement, meaning the character accelerates consistently regardless of the current frame rate.

The clampVelocity method uses Math.sign to preserve direction while limiting magnitude. This approach is more concise than manually computing absolute values and maintaining sign information, reducing the chance of velocity sign errors during rapid direction changes.

Boundary Constraints

Screen boundary enforcement calculates the available movement space by subtracting half the character's width from half the screen width. This adjustment ensures the character stops when its edge reaches the screen edge rather than stopping when its center reaches the boundary.

The boundary check also resets velocity to zero when collision occurs, preventing the character from building up velocity while constrained against a wall. This behavior prevents the "sticky wall" effect where characters would immediately jump to maximum speed upon being released from boundary constraints.

Acceleration Fundamentals

Movement with acceleration creates a more natural feel compared to instant velocity changes. When a player presses a movement key, the character gradually increases speed rather than teleporting to maximum velocity. This acceleration phase provides visual feedback about the character's mass and the force being applied.

The mathematical relationship between acceleration, velocity, and time follows the formula where velocity change equals acceleration multiplied by time interval. By multiplying the acceleration value by deltaTime (the seconds elapsed since the last frame), the component ensures consistant acceleration regardless of processing load or frame rate variations.

Velocity capping prevents the character from acccelerating indefinitely. Without this constraint, holding a movement key would eventually cause the character to move so fast that it becomes uncontrollable or passes through collision boundaries between frames. The maximum velocity should be tuned based on the game's scale and desired pacing.

Tags: Cocos Creator TypeScript game development Input Handling physics

Posted on Tue, 07 Jul 2026 16:47:40 +0000 by kmussel