【Cocos Creator】アタッチするだけ!PushableObject (押せる物体)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】PushableObject の実装:アタッチするだけで「プレイヤーがぶつかった方向と逆向きに押せる物体」を実現する汎用スクリプト

このガイドでは、PushableObject というコンポーネントを実装します。
任意のノードにこのスクリプトをアタッチし、RigidBody(2D/3D)や CharacterController を持つ「プレイヤー」がぶつかったときに、プレイヤーから見て反対側へ押される力(または速度) を与える仕組みを作ります。

外部の GameManager やプレイヤー管理スクリプトには一切依存せず、インスペクタから設定するだけで動く 汎用コンポーネントとして設計します。


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

1. 機能要件の整理

  • このコンポーネントをアタッチしたノードが「押せる物体」となる。
  • プレイヤーがこの物体に衝突した瞬間に、物体の Rigidbody に対して「プレイヤーから離れる方向」の力(または速度)を与える。
  • プレイヤー側には特別なスクリプトは不要。Rigidbody(2D/3D)または CharacterController を持つノードを「プレイヤー」とみなして処理する。
  • エディタのインスペクタで、押す強さ・押す方向の制限・対象の種類などを柔軟に設定できる。
  • 物理次元(2D/3D)に依存しないようにしつつ、どちらにも対応できるが、実行時に存在するコンポーネントだけ使う防御的実装とする。

2. 想定する物理コンポーネント

PushableObject 自身のノードが持つ可能性のあるコンポーネント:

  • RigidBody2D(2D 物理)
  • RigidBody(3D 物理)

「プレイヤー」とみなす相手側のコンポーネント:

  • RigidBody2D
  • RigidBody
  • CharacterController(3D キャラクター移動)

どの組み合わせでも、とにかく相手の「位置」から押し出し方向を計算し、自分の Rigidbody に力や速度を与えるようにします。

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

PushableObject に定義する @property 一覧と役割:

  • use2DPhysics: boolean
    – ツールチップ: 2D 物理を使用する場合はオン。RigidBody2D + Collider2D を利用します。オフの場合は 3D 物理(RigidBody + Collider)を前提とします。
    – 2D/3D どちらの物理をメインに扱うかをインスペクタで切り替える。
  • pushForce: number
    – ツールチップ: 押す力の大きさ。Impulse モードの場合はインパルス量、Velocity モードの場合は設定する速度の大きさになります。
    – 押し出しの強さ。例: 5 ~ 50 程度を目安。
  • pushMode: PushMode (enum)
    IMPULSE: Rigidbody にインパルス(瞬間的な力)を加える。
    SET_VELOCITY: Rigidbody の速度ベクトルを上書きする。
  • limitToHorizontal: boolean
    – ツールチップ: 押し出し方向を水平成分のみに制限します。オンの場合、Y(2D)/Y(3D)成分を 0 にしてから正規化します。
    – 横方向だけ押したい場合に便利(上方向に飛ばないようにする)。
  • minPushInterval: number
    – ツールチップ: 同じ相手から連続して押される間隔(秒)。この時間より短い間隔の衝突では再度押し出しを行いません。
    – 連続衝突による「ガクガク押され続ける」現象を抑えるためのクールタイム。
  • debugLog: boolean
    – ツールチップ: 押し出し処理のデバッグログを有効にします。
    – 衝突時のログ出力 ON/OFF。
  • playerTag: string
    – ツールチップ: プレイヤーとして扱うノード名の一部。空文字の場合は、Rigidbody/CharacterController を持つ全てのノードをプレイヤー候補とします。
    – 「Player」など、プレイヤーノード名の一部を指定してフィルタリングしたいときに使用。空なら無条件。

これらのプロパティにより、2D/3D、力の与え方、方向制限、プレイヤー判定の緩さをインスペクタから調整できます。


TypeScriptコードの実装

以下が完成した PushableObject コンポーネントの全コードです。


import {
    _decorator,
    Component,
    Node,
    Vec2,
    Vec3,
    RigidBody2D,
    RigidBody,
    Collider2D,
    IPhysics2DContact,
    PhysicsSystem2D,
    Contact2DType,
    Collider,
    ICollisionEvent,
    CharacterController,
    game,
    macro,
} from 'cc';
const { ccclass, property } = _decorator;

enum PushMode {
    IMPULSE = 0,
    SET_VELOCITY = 1,
}

@ccclass('PushableObject')
export class PushableObject extends Component {

    @property({
        tooltip: '2D 物理を使用する場合はオン。RigidBody2D + Collider2D を利用します。オフの場合は 3D 物理(RigidBody + Collider)を前提とします。',
    })
    public use2DPhysics: boolean = true;

    @property({
        tooltip: '押す力の大きさ。IMPULSE モードではインパルス量、SET_VELOCITY モードでは設定する速度の大きさになります。',
        min: 0,
    })
    public pushForce: number = 10;

    @property({
        tooltip: '押し方のモードを選択します。IMPULSE: インパルスを加える / SET_VELOCITY: 速度ベクトルを上書きします。',
        type: macro.ENUM(PushMode),
    })
    public pushMode: PushMode = PushMode.IMPULSE;

    @property({
        tooltip: '押し出し方向を水平成分のみに制限します。オンの場合、Y(2D)/Y(3D)成分を 0 にしてから正規化します。',
    })
    public limitToHorizontal: boolean = true;

    @property({
        tooltip: '同じ相手から連続して押される間隔(秒)。この時間より短い間隔の衝突では再度押し出しを行いません。',
        min: 0,
    })
    public minPushInterval: number = 0.1;

    @property({
        tooltip: 'プレイヤーとして扱うノード名の一部。空文字の場合は、Rigidbody/CharacterController を持つ全てのノードをプレイヤー候補とします。',
    })
    public playerTag: string = '';

    @property({
        tooltip: '押し出し処理のデバッグログを有効にします。',
    })
    public debugLog: boolean = false;

    private _rb2d: RigidBody2D | null = null;
    private _rb3d: RigidBody | null = null;
    private _lastPushTimeMap2D: Map<Node, number> = new Map();
    private _lastPushTimeMap3D: Map<Node, number> = new Map();

    onLoad() {
        // 自身の Rigidbody を取得(2D/3D 両方試す)
        this._rb2d = this.node.getComponent(RigidBody2D);
        this._rb3d = this.node.getComponent(RigidBody);

        if (!this._rb2d && !this._rb3d) {
            console.error(
                '[PushableObject] このノードには RigidBody2D も RigidBody もアタッチされていません。' +
                '押し出しを物理的に反映するには、どちらか一方の Rigidbody コンポーネントを追加してください。',
                this.node.name
            );
        }

        // 2D 衝突コールバック登録
        const col2d = this.node.getComponent(Collider2D);
        if (col2d) {
            col2d.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact2D, this);
        } else if (this.use2DPhysics) {
            console.warn(
                '[PushableObject] use2DPhysics が true ですが Collider2D が見つかりません。' +
                '2D 衝突で押し出しを行うには Collider2D を追加してください。',
                this.node.name
            );
        }

        // 3D 衝突コールバック登録
        const col3d = this.node.getComponent(Collider);
        if (col3d) {
            col3d.on('onCollisionEnter', this._onCollisionEnter3D, this);
        } else if (!this.use2DPhysics) {
            console.warn(
                '[PushableObject] use2DPhysics が false ですが Collider(3D) が見つかりません。' +
                '3D 衝突で押し出しを行うには Collider を追加してください。',
                this.node.name
            );
        }
    }

    onDestroy() {
        // リスナー解除(安全のため)
        const col2d = this.node.getComponent(Collider2D);
        if (col2d) {
            col2d.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact2D, this);
        }
        const col3d = this.node.getComponent(Collider);
        if (col3d) {
            col3d.off('onCollisionEnter', this._onCollisionEnter3D, this);
        }
    }

    /**
     * 2D 衝突開始時のコールバック
     */
    private _onBeginContact2D(selfCol: Collider2D, otherCol: Collider2D, contact: IPhysics2DContact | null) {
        if (!this.use2DPhysics) {
            return;
        }
        const otherNode = otherCol.node;
        if (!this._isPlayerCandidate(otherNode)) {
            return;
        }

        const now = game.totalTime / 1000;
        const lastTime = this._lastPushTimeMap2D.get(otherNode) || 0;
        if (now - lastTime < this.minPushInterval) {
            return;
        }
        this._lastPushTimeMap2D.set(otherNode, now);

        const from = otherNode.worldPosition;
        const to = this.node.worldPosition;

        const dir3 = new Vec3();
        Vec3.subtract(dir3, to, from); // プレイヤー → 物体 方向
        this._applyPush2D(dir3, otherNode);
    }

    /**
     * 3D 衝突開始時のコールバック
     */
    private _onCollisionEnter3D(event: ICollisionEvent) {
        if (this.use2DPhysics) {
            return;
        }
        const otherNode = event.otherCollider.node;
        if (!this._isPlayerCandidate(otherNode)) {
            return;
        }

        const now = game.totalTime / 1000;
        const lastTime = this._lastPushTimeMap3D.get(otherNode) || 0;
        if (now - lastTime < this.minPushInterval) {
            return;
        }
        this._lastPushTimeMap3D.set(otherNode, now);

        const from = otherNode.worldPosition;
        const to = this.node.worldPosition;

        const dir = new Vec3();
        Vec3.subtract(dir, to, from); // プレイヤー → 物体 方向
        this._applyPush3D(dir, otherNode);
    }

    /**
     * プレイヤー候補かどうかの判定:
     * - Rigidbody2D / Rigidbody / CharacterController を持つ
     * - かつ playerTag が空でない場合は node.name に playerTag が含まれている
     */
    private _isPlayerCandidate(node: Node): boolean {
        const hasBody2D = !!node.getComponent(RigidBody2D);
        const hasBody3D = !!node.getComponent(RigidBody);
        const hasCharCtrl = !!node.getComponent(CharacterController);

        if (!hasBody2D && !hasBody3D && !hasCharCtrl) {
            return false;
        }

        if (!this.playerTag) {
            return true;
        }
        return node.name.includes(this.playerTag);
    }

    /**
     * 2D 物理で押し出しを適用
     */
    private _applyPush2D(directionWorld: Vec3, otherNode: Node) {
        if (!this._rb2d) {
            console.warn(
                '[PushableObject] 2D 押し出しを行おうとしましたが RigidBody2D が見つかりません。',
                this.node.name
            );
            return;
        }

        // 3D ベクトル → 2D ベクトル (x, y) を使用
        const dir2 = new Vec2(directionWorld.x, directionWorld.y);

        if (this.limitToHorizontal) {
            // Y 成分を 0 にして水平方向だけにする
            dir2.y = 0;
        }

        if (dir2.lengthSqr() === 0) {
            return;
        }
        dir2.normalize();

        switch (this.pushMode) {
            case PushMode.IMPULSE: {
                const impulse = new Vec2(dir2.x * this.pushForce, dir2.y * this.pushForce);
                this._rb2d.applyLinearImpulse(impulse, this._rb2d.getWorldCenter(), true);
                if (this.debugLog) {
                    console.log(
                        `[PushableObject][2D][IMPULSE] ${this.node.name} が ${otherNode.name} から押されました。impulse=(${impulse.x.toFixed(2)}, ${impulse.y.toFixed(2)})`
                    );
                }
                break;
            }
            case PushMode.SET_VELOCITY: {
                const vel = new Vec2(dir2.x * this.pushForce, dir2.y * this.pushForce);
                this._rb2d.linearVelocity = vel;
                if (this.debugLog) {
                    console.log(
                        `[PushableObject][2D][SET_VELOCITY] ${this.node.name} が ${otherNode.name} から押されました。velocity=(${vel.x.toFixed(2)}, ${vel.y.toFixed(2)})`
                    );
                }
                break;
            }
        }
    }

    /**
     * 3D 物理で押し出しを適用
     */
    private _applyPush3D(directionWorld: Vec3, otherNode: Node) {
        if (!this._rb3d) {
            console.warn(
                '[PushableObject] 3D 押し出しを行おうとしましたが RigidBody(3D) が見つかりません。',
                this.node.name
            );
            return;
        }

        const dir = directionWorld.clone();

        if (this.limitToHorizontal) {
            // Y 成分を 0 にして水平方向だけにする
            dir.y = 0;
        }

        if (dir.lengthSqr() === 0) {
            return;
        }
        dir.normalize();

        switch (this.pushMode) {
            case PushMode.IMPULSE: {
                const impulse = new Vec3(dir.x * this.pushForce, dir.y * this.pushForce, dir.z * this.pushForce);
                this._rb3d.applyImpulse(impulse);
                if (this.debugLog) {
                    console.log(
                        `[PushableObject][3D][IMPULSE] ${this.node.name} が ${otherNode.name} から押されました。impulse=(${impulse.x.toFixed(2)}, ${impulse.y.toFixed(2)}, ${impulse.z.toFixed(2)})`
                    );
                }
                break;
            }
            case PushMode.SET_VELOCITY: {
                const vel = new Vec3(dir.x * this.pushForce, dir.y * this.pushForce, dir.z * this.pushForce);
                this._rb3d.setLinearVelocity(vel);
                if (this.debugLog) {
                    console.log(
                        `[PushableObject][3D][SET_VELOCITY] ${this.node.name} が ${otherNode.name} から押されました。velocity=(${vel.x.toFixed(2)}, ${vel.y.toFixed(2)}, ${vel.z.toFixed(2)})`
                    );
                }
                break;
            }
        }
    }
}

コードのポイント解説

  • onLoad()
    – 自身のノードから RigidBody2D / RigidBody を取得し、存在しない場合は console.error で警告。
    Collider2D / Collider にそれぞれ衝突開始イベントを登録。
    use2DPhysics の設定と実際の Collider の有無が矛盾する場合は console.warn で通知。
  • _onBeginContact2D()
    – 2D 衝突開始時に呼ばれる。
    – 衝突相手が「プレイヤー候補」かどうかを _isPlayerCandidate() で判定。
    minPushInterval を使って、同じ相手からの連続押し出しを制限。
    – プレイヤー位置 → 自分の位置のベクトルを計算し、_applyPush2D() で力を加える。
  • _onCollisionEnter3D()
    – 3D 衝突開始時に呼ばれる。ロジックは 2D 版とほぼ同じだが、Vec3 を使い、_applyPush3D() に委譲する。
  • _isPlayerCandidate()
    – 相手ノードに RigidBody2D / RigidBody / CharacterController のいずれかが付いているか確認。
    playerTag が空でなければ、ノード名にその文字列が含まれているかもチェック。
  • _applyPush2D() / _applyPush3D()
    – 与えられたワールド方向ベクトルを、limitToHorizontal に応じて Y 成分を 0 にするかどうか決め、正規化。
    pushMode に応じて、applyLinearImpulse / applyImpulse もしくは linearVelocity / setLinearVelocity を使って押し出しを適用。
    debugLog が有効な場合は、実際に与えたインパルス/速度をログ出力。

使用手順と動作確認

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

  1. エディタの Assets パネルで任意のフォルダを右クリックします。
  2. Create > TypeScript を選択し、ファイル名を PushableObject.ts とします。
  3. 自動生成された中身をすべて削除し、このガイドで示した TypeScript コード全体 を貼り付けて保存します。

2. 押せる物体ノードの準備

ここでは 2D の例と 3D の例をそれぞれ示します。

2D でのセットアップ例

  1. Hierarchy パネルで右クリック → Create > 2D Object > Sprite を選択し、名前を Box2D などに変更します。
  2. Box2D ノードを選択し、Inspector で次を追加します:
    • Add Component > Physics 2D > RigidBody2D
    • Add Component > Physics 2D > BoxCollider2D(形状は任意の Collider2D)
  3. 同じく Inspector で:
    • Add Component > Custom > PushableObject を追加。
  4. PushableObject コンポーネントのプロパティを設定:
    • use2DPhysics: ON
    • pushForce: 例として 15
    • pushMode: IMPULSE(最初はインパルスがおすすめ)
    • limitToHorizontal: ON(横方向だけ押されるように)
    • minPushInterval: 0.10.2 くらい
    • playerTag: 空のまま、もしくは Player など
    • debugLog: 動作確認時は ON にしておくとログが見やすいです。

2D プレイヤーノードの簡易作成

  1. Hierarchy パネルで右クリック → Create > 2D Object > Sprite を選択し、名前を Player2D にします。
  2. Inspector で:
    • Add Component > Physics 2D > RigidBody2D
    • Add Component > Physics 2D > BoxCollider2D
  3. Player2D をシーン上で Box2D の近くに配置します。

この状態で、プレイ中に Player2D をキーボード移動させるスクリプトなどがあれば、Box2D にぶつかった瞬間に Box2D がプレイヤーから離れる方向に押されるのが確認できます。
(移動スクリプトは任意ですが、もし無い場合は、Scene ビューでドラッグして動かしながらテストしても衝突は確認できます。)

3D でのセットアップ例

  1. Hierarchy パネルで右クリック → Create > 3D Object > Cube を選択し、名前を Box3D にします。
  2. Inspector で:
    • Add Component > Physics > RigidBody
    • Add Component > Physics > BoxCollider
  3. 同じく Inspector で:
    • Add Component > Custom > PushableObject を追加。
  4. PushableObject のプロパティ:
    • use2DPhysics: OFF
    • pushForce: 例として 520
    • pushMode: IMPULSESET_VELOCITY を好みで選択
    • limitToHorizontal: ON(Y 方向に飛びすぎないように)
    • minPushInterval: 0.1
    • playerTag: Player など(プレイヤーノード名に合わせる)
    • debugLog: ON

3D プレイヤーノードの簡易作成

  1. Hierarchy パネルで右クリック → Create > 3D Object > Capsule などを選び、名前を Player3D にします。
  2. Inspector で:
    • Add Component > Physics > RigidBody(もしくは CharacterController を使っている場合はそれでも可)
    • Add Component > Physics > CapsuleCollider などの 3D Collider
  3. Player3DBox3D の近くに配置します。

プレイヤー移動スクリプトで Player3D を動かし、Box3D にぶつかると、ぶつかった方向の反対側へ Box3D が押される動作が確認できます。

3. デバッグと調整のコツ

  • 押され方が弱い/強すぎる
    pushForce を上下させて調整します。2D と 3D で適切な値は異なるので、シーンごとに調整してください。
  • 連続でガクガク押されてしまう
    minPushInterval0.2 ~ 0.3 程度に上げると安定しやすくなります。
  • 上方向に飛んでしまう
    limitToHorizontalON にして、水平成分だけに制限します。
  • 特定のプレイヤーだけに反応させたい
    playerTagPlayer など、対象プレイヤーノード名に含まれる文字列を設定します。
  • そもそも押されていないように見える
    debugLogON にしてコンソールを確認し、衝突イベントやプレイヤー判定が正しく行われているかをチェックします。

まとめ

この PushableObject コンポーネントを使うことで、

  • 任意のノードにアタッチするだけで「プレイヤーに押されるオブジェクト」を簡単に実装できる。
  • 2D/3D のどちらにも対応し、押す強さ・方向・頻度・対象プレイヤーをすべてインスペクタから調整できる。
  • 外部の GameManager やプレイヤースクリプトには依存せず、このスクリプト単体で完結するため、プロジェクトをまたいだ再利用性が高い

応用例としては、

  • パズルゲームの「押せる箱」や「スイッチ付きブロック」
  • アクションゲームの「押して動かす足場」
  • NPC がぶつかると動くオブジェクト(プレイヤー以外でも Rigidbody/CharacterController を持てば押せる)

など、多くのシーンでそのまま利用できます。
プロジェクトの共通ユーティリティとして PushableObject.ts を 1 つ置いておけば、「押せるギミック」を作るたびにコードを書く必要がなくなり、ゲーム開発のスピードと保守性が大きく向上します。

必要に応じて、今後は「押された量に応じてスイッチを ON にする」「一定距離だけ押されたらロックする」など、追加機能を別コンポーネントとして重ねていくと、さらに柔軟なギミック構成が作れるようになります。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!