【Cocos Creator 3.8】KeyboardMover の実装:アタッチするだけで矢印キー / WASD でノードを移動させる汎用スクリプト

このガイドでは、Cocos Creator 3.8.7 + TypeScript で「KeyboardMover」コンポーネントを実装します。ノードにアタッチするだけで、矢印キーや WASD 入力に応じてノードを移動させることができます。

Rigidbody2D を持つノードに付ければ「物理挙動を保ったまま velocity を制御する移動」、Transform だけのノードに付ければ「フレームごとに座標を更新するシンプルな移動」として動作します。外部の GameManager や入力マネージャには一切依存せず、このスクリプト単体で完結します。


コンポーネントの設計方針

要件整理

  • キーボード入力(矢印キー / WASD)を監視する。
  • 入力方向に応じて「親ノード(このコンポーネントが付いているノード)」を移動させる。
  • 可能であれば Rigidbody2D の linearVelocity を使って移動させる。
  • Rigidbody2D がない場合は、Transform の position を直接変更して移動させる。
  • 移動速度や入力タイプ(矢印キーのみ / WASD のみ / 両方)をインスペクタから調整できるようにする。
  • フレームレートに依存しない移動(dt を使った移動量計算)を行う。
  • 完全に独立したコンポーネントとし、他のカスタムスクリプトには依存しない。

外部依存をなくすためのアプローチ

  • 入力は Cocos の input モジュール (systemEvent ではなく) を直接使用する。
  • Rigidbody2D の有無は getComponent(RigidBody2D) で動的にチェックし、見つからなければ Transform 直接移動モードに自動フォールバックする。
  • Rigidbody2D が存在しない場合は、onLoad で警告ログを出しつつも動作は継続する。
  • インスペクタで調整可能なプロパティを十分に用意し、外部の設定スクリプトが不要なようにする。

インスペクタで設定可能なプロパティ設計

以下のようなプロパティを用意します。

  • moveSpeed (number)
    • 移動速度(単位: 単位距離 / 秒)。
    • Rigidbody2D モードでは linearVelocity の大きさ、Transform モードでは 1 秒あたりに移動する距離として扱う。
    • 例: 200〜600 程度で調整。
  • useArrowKeys (boolean)
    • 矢印キー(↑↓←→)を入力として使用するかどうか。
  • useWASD (boolean)
    • W / A / S / D キーを入力として使用するかどうか。
  • normalizeDiagonal (boolean)
    • 斜め入力時にベクトルを正規化するかどうか。
    • ON の場合: 斜め移動でも速度が一定になる(一般的な挙動)。
    • OFF の場合: 斜め移動は縦横より速くなる。
  • useRigidBodyIfAvailable (boolean)
    • ノードに Rigidbody2D が付いている場合、それを使って linearVelocity を設定するかどうか。
    • OFF の場合は Rigidbody2D が付いていても Transform 直接移動のみを使用する。
  • stopOnNoInput (boolean)
    • 入力がないときに Rigidbody2D の linearVelocity をゼロにするかどうか。
    • 物理挙動で慣性を残したい場合は OFF にする。
  • debugLog (boolean)
    • キーボード入力や Rigidbody2D 検出に関するログを出すかどうか。
    • 開発時のみ ON、本番ビルドでは OFF 推奨。

TypeScriptコードの実装


import { _decorator, Component, input, Input, EventKeyboard, KeyCode, Vec2, Vec3, RigidBody2D, math } from 'cc';
const { ccclass, property } = _decorator;

/**
 * KeyboardMover
 * キーボード入力(矢印キー / WASD)でノードを移動させる汎用コンポーネント。
 * - Rigidbody2D があれば linearVelocity を使用
 * - なければ position を直接変更
 */
@ccclass('KeyboardMover')
export class KeyboardMover extends Component {

    @property({
        tooltip: '移動速度(単位: 1秒あたりの移動量)。\nRigidbody2D がある場合は linearVelocity の大きさとして扱われます。'
    })
    public moveSpeed: number = 300;

    @property({
        tooltip: '矢印キー(↑↓←→)を入力として使用するかどうか。'
    })
    public useArrowKeys: boolean = true;

    @property({
        tooltip: 'W / A / S / D キーを入力として使用するかどうか。'
    })
    public useWASD: boolean = true;

    @property({
        tooltip: '斜め入力時に移動方向ベクトルを正規化して、速度を一定に保つかどうか。'
    })
    public normalizeDiagonal: boolean = true;

    @property({
        tooltip: 'ノードに Rigidbody2D が付いている場合、linearVelocity を使って移動させるかどうか。'
    })
    public useRigidBodyIfAvailable: boolean = true;

    @property({
        tooltip: '入力がないときに Rigidbody2D の速度 (linearVelocity) をゼロにするかどうか。'
    })
    public stopOnNoInput: boolean = true;

    @property({
        tooltip: 'デバッグ用のログを出力するかどうか。開発時のみ ON 推奨。'
    })
    public debugLog: boolean = false;

    // 内部状態
    private _moveDir: Vec2 = new Vec2(0, 0);
    private _rigidBody2D: RigidBody2D | null = null;
    private _usingRigidBody: boolean = false;

    onLoad() {
        // Rigidbody2D の取得(存在しなくてもエラーにはしない)
        this._rigidBody2D = this.getComponent(RigidBody2D);
        if (this._rigidBody2D && this.useRigidBodyIfAvailable) {
            this._usingRigidBody = true;
            if (this.debugLog) {
                console.log('[KeyboardMover] Rigidbody2D を検出。velocity 制御モードで動作します。');
            }
        } else {
            this._usingRigidBody = false;
            if (!this._rigidBody2D && this.debugLog) {
                console.warn('[KeyboardMover] Rigidbody2D が見つかりません。Transform 直接移動モードで動作します。');
            } else if (this._rigidBody2D && !this.useRigidBodyIfAvailable && this.debugLog) {
                console.log('[KeyboardMover] Rigidbody2D はありますが、useRigidBodyIfAvailable=false のため使用しません。Transform 直接移動モードで動作します。');
            }
        }

        // キーボード入力の登録
        input.on(Input.EventType.KEY_DOWN, this._onKeyDown, this);
        input.on(Input.EventType.KEY_UP, this._onKeyUp, this);
    }

    onDestroy() {
        // イベント登録を解除してメモリリークや二重登録を防ぐ
        input.off(Input.EventType.KEY_DOWN, this._onKeyDown, this);
        input.off(Input.EventType.KEY_UP, this._onKeyUp, this);
    }

    /**
     * キーが押されたときの処理
     */
    private _onKeyDown(event: EventKeyboard) {
        const keyCode = event.keyCode;

        let handled = false;

        if (this.useArrowKeys) {
            switch (keyCode) {
                case KeyCode.ARROW_LEFT:
                    this._moveDir.x = -1;
                    handled = true;
                    break;
                case KeyCode.ARROW_RIGHT:
                    this._moveDir.x = 1;
                    handled = true;
                    break;
                case KeyCode.ARROW_UP:
                    this._moveDir.y = 1;
                    handled = true;
                    break;
                case KeyCode.ARROW_DOWN:
                    this._moveDir.y = -1;
                    handled = true;
                    break;
            }
        }

        if (this.useWASD) {
            switch (keyCode) {
                case KeyCode.KEY_A:
                    this._moveDir.x = -1;
                    handled = true;
                    break;
                case KeyCode.KEY_D:
                    this._moveDir.x = 1;
                    handled = true;
                    break;
                case KeyCode.KEY_W:
                    this._moveDir.y = 1;
                    handled = true;
                    break;
                case KeyCode.KEY_S:
                    this._moveDir.y = -1;
                    handled = true;
                    break;
            }
        }

        if (handled && this.debugLog) {
            console.log('[KeyboardMover] KeyDown:', KeyCode[keyCode], 'dir=', this._moveDir);
        }
    }

    /**
     * キーが離されたときの処理
     */
    private _onKeyUp(event: EventKeyboard) {
        const keyCode = event.keyCode;

        let handled = false;

        if (this.useArrowKeys) {
            switch (keyCode) {
                case KeyCode.ARROW_LEFT:
                    if (this._moveDir.x < 0) this._moveDir.x = 0;
                    handled = true;
                    break;
                case KeyCode.ARROW_RIGHT:
                    if (this._moveDir.x > 0) this._moveDir.x = 0;
                    handled = true;
                    break;
                case KeyCode.ARROW_UP:
                    if (this._moveDir.y > 0) this._moveDir.y = 0;
                    handled = true;
                    break;
                case KeyCode.ARROW_DOWN:
                    if (this._moveDir.y < 0) this._moveDir.y = 0;
                    handled = true;
                    break;
            }
        }

        if (this.useWASD) {
            switch (keyCode) {
                case KeyCode.KEY_A:
                    if (this._moveDir.x < 0) this._moveDir.x = 0;
                    handled = true;
                    break;
                case KeyCode.KEY_D:
                    if (this._moveDir.x > 0) this._moveDir.x = 0;
                    handled = true;
                    break;
                case KeyCode.KEY_W:
                    if (this._moveDir.y > 0) this._moveDir.y = 0;
                    handled = true;
                    break;
                case KeyCode.KEY_S:
                    if (this._moveDir.y < 0) this._moveDir.y = 0;
                    handled = true;
                    break;
            }
        }

        if (handled && this.debugLog) {
            console.log('[KeyboardMover] KeyUp:', KeyCode[keyCode], 'dir=', this._moveDir);
        }
    }

    /**
     * 毎フレームの更新処理
     * dt: 前フレームからの経過時間(秒)
     */
    update(dt: number) {
        // 現在の入力方向ベクトルを元に移動処理
        const dir = this._moveDir;

        if (dir.x === 0 && dir.y === 0) {
            // 入力がないとき
            if (this._usingRigidBody && this.stopOnNoInput && this._rigidBody2D) {
                // 慣性を残したくない場合、速度をゼロにする
                this._rigidBody2D.linearVelocity = new Vec2(0, 0);
            }
            return;
        }

        // ベクトル正規化(斜め移動時も速度一定にする)
        const moveDir = new Vec2(dir.x, dir.y);
        if (this.normalizeDiagonal) {
            moveDir.normalize();
        }

        if (this._usingRigidBody && this._rigidBody2D) {
            // Rigidbody2D の velocity を使って移動
            const velocity = new Vec2(
                moveDir.x * this.moveSpeed,
                moveDir.y * this.moveSpeed
            );
            this._rigidBody2D.linearVelocity = velocity;
        } else {
            // Transform の position を直接変更して移動
            const node = this.node;
            const pos = node.position;

            const deltaX = moveDir.x * this.moveSpeed * dt;
            const deltaY = moveDir.y * this.moveSpeed * dt;

            // 2D ゲームを想定して X-Y 平面で移動
            const newPos = new Vec3(
                pos.x + deltaX,
                pos.y + deltaY,
                pos.z
            );

            node.setPosition(newPos);
        }
    }
}

コードのポイント解説

  • onLoad
    • getComponent(RigidBody2D) で Rigidbody2D を探し、存在すれば _usingRigidBody を true にします。
    • 見つからない場合や useRigidBodyIfAvailable が false の場合は Transform 直接移動モードに切り替えます。
    • input.on を使って KEY_DOWN / KEY_UP イベントを登録します。
  • _onKeyDown / _onKeyUp
    • 矢印キーと WASD を、それぞれ useArrowKeys / useWASD の設定に応じて処理します。
    • 押下時には _moveDir の x / y を -1, 0, 1 に設定。
    • 離したときは、対応する方向だけ 0 に戻します(同時押しにも対応)。
  • update(dt)
    • _moveDir が (0, 0) なら移動しません。
    • normalizeDiagonal が true のとき、斜め入力時にベクトルを正規化して速度を一定に保ちます。
    • Rigidbody2D モード:
      • linearVelocity に「方向 × moveSpeed」をそのまま設定します。
      • stopOnNoInput が true の場合、入力がないフレームで velocity を (0, 0) にリセットします。
    • Transform モード:
      • dt を使って「方向 × moveSpeed × dt」を現在位置に加算し、フレームレートに依存しない移動を実現します。

使用手順と動作確認

1. スクリプトファイルの作成

  1. エディタの Assets パネルで、任意のフォルダ(例: assets/scripts)を右クリックします。
  2. Create > TypeScript を選択します。
  3. ファイル名を KeyboardMover.ts に変更します。
  4. 生成された KeyboardMover.ts をダブルクリックして開き、内容をすべて削除して、上記の TypeScript コードを貼り付けて保存します。

2. テスト用ノードの作成(Transform 直接移動モード)

まずは Rigidbody2D を使わないシンプルな移動から確認します。

  1. Hierarchy パネルで右クリック → Create > 2D Object > Sprite を選択し、テスト用のスプライトを作成します。
  2. 作成された Sprite ノードを選択し、Inspector を確認します。
  3. Add Component ボタンをクリックし、Custom Script(または Custom)の中から KeyboardMover を選択してアタッチします。
  4. Inspector 上で KeyboardMover コンポーネントのプロパティを設定します:
    • Move Speed: 300〜500 あたりから試してみる(例: 400)。
    • Use Arrow Keys: ON(チェック)。
    • Use WASD: ON(チェック)。
    • Normalize Diagonal: ON(チェック)。
    • Use Rigid Body If Available: OFF(チェックを外す)。
    • Stop On No Input: (Transform モードでは意味がないのでどちらでも良い)。
    • Debug Log: OFF(必要なら ON)。

動作確認(Transform モード)

  1. エディタ右上の Play ボタン(プレビュー再生)をクリックします。
  2. ゲームビューが立ち上がったら、キーボードの
    • 矢印キー(↑↓←→)
    • または W / A / S / D

    を押して、スプライトが対応する方向に移動することを確認します。

  3. 斜め方向(例: ↑ と → を同時押し)でも速度が一定であることを確認します(Normalize Diagonal が ON の場合)。

3. テスト用ノードの作成(Rigidbody2D velocity モード)

次に、物理ボディ付きノードで linearVelocity を使った移動を確認します。

  1. 先ほど作成した Sprite ノードをそのまま使うか、新しく Sprite ノードを作成します。
  2. 対象ノードを選択し、Add ComponentPhysics 2DRigidBody2D を追加します。
  3. RigidBody2D の設定例:
    • Body Type: Dynamic(動的)
    • Gravity Scale: 0(重力の影響を受けたくない場合)
  4. 同じノードに KeyboardMover をアタッチします(すでに付いている場合はそのまま使用)。
  5. Inspector 上で KeyboardMover コンポーネントのプロパティを設定します:
    • Move Speed: 5〜10 あたりから試してみる(Rigidbody2D の単位感に合わせて調整。例: 8)。
    • Use Arrow Keys: ON。
    • Use WASD: 好みで ON/OFF。
    • Normalize Diagonal: ON。
    • Use Rigid Body If Available: ON(重要)。
    • Stop On No Input: ON(入力がないときにピタッと止めたい場合)。
    • Debug Log: 必要に応じて ON。

動作確認(Rigidbody2D モード)

  1. 再度 Play ボタンでプレビューを開始します。
  2. 矢印キー / WASD を押して、ノードが物理ボディとして移動することを確認します。
    • Gravity Scale = 0 の場合は、純粋な水平・垂直移動になります。
    • Gravity Scale > 0 の場合は、重力と合成された動きになります。
  3. Stop On No Input を OFF にすると、キーを離したときに慣性で動き続ける挙動になることも確認してみてください。

4. よくある調整ポイント

  • 移動が速すぎる / 遅すぎる:
    • moveSpeed を調整します。
    • Transform モードでは 100〜800 程度、Rigidbody2D モードでは 2〜20 程度から調整すると扱いやすいです。
  • 矢印キーだけにしたい / WASD だけにしたい:
    • useArrowKeys / useWASD のチェックを切り替えます。
  • 斜め移動が速く感じる:
    • normalizeDiagonal を ON にします。
  • Rigidbody2D を付けているのに速度が変わらない:
    • KeyboardMover の useRigidBodyIfAvailable が ON になっているか確認します。
    • Rigidbody2D の Body Type が Dynamic になっているか確認します。

まとめ

この「KeyboardMover」コンポーネントは、

  • Transform だけのノードでも
  • Rigidbody2D を持つ物理ノードでも

アタッチしてプロパティを少し調整するだけで、矢印キー / WASD による移動を簡単に追加できます。

外部の GameManager や入力マネージャに依存せず、すべてをインスペクタの設定で完結させているため、

  • プレイヤーキャラの移動
  • デバッグ用カメラの移動
  • UI デバッグ用のカーソル移動
  • ミニゲーム内の簡易操作キャラ

など、さまざまなシーンで「その場でポン付けして使える」再利用性の高いコンポーネントとして活用できます。

本記事のコードをベースに、例えば「Shift でダッシュ」「スペースでジャンプ」「境界外に出ないように制限」などの機能を追加していけば、より本格的なキャラクターコントローラへと発展させることも容易です。

まずはこの KeyboardMover をプロジェクトの標準入力コンポーネントとして組み込み、移動系処理の再実装を減らして開発効率を高めてみてください。