【Cocos Creator】アタッチするだけ!PushableBlock (押せる岩)の実装方法【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】PushableBlock の実装:アタッチするだけで「プレイヤーが押した時だけ動く岩」を実現する汎用スクリプト

このガイドでは、CharacterBody2D を使った 2D アクションなどで使える「押せる岩」コンポーネント PushableBlock を実装します。
任意のノードにこのスクリプトをアタッチし、最低限の物理設定を行うだけで、プレイヤーが横から押したときだけ動く岩 を簡単に作れるようになります。

  • プレイヤー以外のオブジェクト(敵や弾など)が当たっても動かしたくない
  • プレイヤーが上に乗ったり、ぶつかったりしても「押す」方向でのみ動かしたい
  • ゲームごとに押せる重さ・摩擦感を Inspector から調整したい

といった場面で、そのノード単体だけで完結する汎用コンポーネントとして利用できます。


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

要件整理

  • CharacterBody2D を持つ 2D オブジェクト にアタッチする。
  • プレイヤーが横方向から接触して「押している」間だけ、岩が押された方向に動く。
  • プレイヤーの判定は「特定のグループ名」または「特定のタグ名」を Inspector から指定して行う。
  • プレイヤーが押していないときは、岩は慣性で少しだけ動いて徐々に止まる(摩擦)ようにできる。
  • 外部の GameManager やプレイヤースクリプトに依存せず、このコンポーネントだけで完結させる。

物理挙動は CharacterBody2Dvelocity を直接制御する形で実装し、「押されているかどうか」 の検出には、onBeginContact / onEndContact または onCollisionEnter / onCollisionExit を利用します。
Cocos Creator 3.8 では 2D 物理に Box2DCannon などのバックエンドがありますが、ここでは標準的な Physics2D(Box2D)を前提に、Collider2D のコンタクトコールバックでプレイヤー接触を検出します。

Inspector で設定可能なプロパティ設計

PushableBlock コンポーネントに用意するプロパティと役割は次の通りです。

  • playerGroupName: string
    • プレイヤーが属する 2D 物理グループ名(例: "player")。
    • ここで指定されたグループのノードとの接触のみ「押し」とみなします。
    • 空文字の場合はグループによるフィルタを行わず、playerTag だけで判定します。
  • playerTag: string
    • プレイヤーノードに設定しておく Node.name もしくは Node.layer とは別の、単純な名前判定用の文字列。
    • ここでは簡易的に「相手ノードの名前がこの文字列を含む場合」をプレイヤーとみなす例を示します(例: "Player")。
    • 空文字の場合はタグによるフィルタを行わず、playerGroupName だけで判定します。
  • moveSpeed: number
    • 押されたときに岩が動く 最大速度(単位: 単位/秒)
    • 大きいほど軽く押せる岩、小さいほど重い岩になります。
    • 例: 1.5 ~ 5.0 程度で調整。
  • acceleration: number
    • 押されたときに速度が増加する 加速度(単位: 単位/秒^2)
    • 大きいほど押し始めからスッと動き、小さいほどじわじわ動き出します。
  • friction: number
    • プレイヤーが押していないときに速度が減衰する係数。
    • 0 ~ 1 の範囲で、1 に近いほどよく滑り、0 に近いほどすぐ止まります。
    • 実装では velocity.x *= friction のように毎フレーム乗算します。
  • minVelocityThreshold: number
    • この値よりも速度が小さくなったら 0 に丸めて完全に停止させるためのしきい値。
    • 小さすぎるといつまでも微小な速度が残るため、0.01 などに設定します。
  • debugLog: boolean
    • オンにすると、プレイヤー接触の開始・終了や、必須コンポーネントが見つからない場合などに詳細なログを出力します。
    • デバッグ時のみ有効にし、リリース時はオフにする想定です。

必要な標準コンポーネント

このスクリプトが正しく動作するためには、アタッチ先のノードに次のコンポーネントが必要です。

  • CharacterBody2D
    • 岩の移動を制御するために必須です。
    • 見つからない場合、スクリプト内でエラーログを出し、処理を中断します。
  • Collider2D(BoxCollider2D など)
    • プレイヤーとの接触判定に必須です。
    • 見つからない場合もエラーログを出し、処理を中断します。

これらはコード内で getComponent して存在確認を行い、足りない場合はログに明示的なメッセージを出すようにします。


TypeScriptコードの実装


import { _decorator, Component, Node, CharacterBody2D, Collider2D, IPhysics2DContact, Contact2DType, Vec2, Vec3, log, error } from 'cc';
const { ccclass, property } = _decorator;

/**
 * PushableBlock
 * プレイヤーが横から押したときだけ動く 2D ブロック。
 * - CharacterBody2D の velocity.x を制御して横方向の移動のみ行う。
 * - プレイヤーとの接触を Collider2D のコールバックで検出する。
 */
@ccclass('PushableBlock')
export class PushableBlock extends Component {

    @property({
        tooltip: 'プレイヤーが属する 2D 物理グループ名。\n' +
                 '空文字の場合はグループ名ではフィルタせず、playerTag だけで判定します。\n' +
                 '例: "player"'
    })
    public playerGroupName: string = 'player';

    @property({
        tooltip: 'プレイヤーノード名に含まれる文字列。\n' +
                 '空文字の場合は名前ではフィルタせず、playerGroupName だけで判定します。\n' +
                 '例: "Player"'
    })
    public playerTag: string = 'Player';

    @property({
        tooltip: '押されたときにブロックが動く最大速度(単位/秒)。\n' +
                 '大きいほど軽く押せるブロックになります。'
    })
    public moveSpeed: number = 3.0;

    @property({
        tooltip: '押されたときに速度が増加する加速度(単位/秒^2)。\n' +
                 '大きいほど押し始めからスッと動きます。'
    })
    public acceleration: number = 10.0;

    @property({
        tooltip: 'プレイヤーが押していないときの減速係数。\n' +
                 '0 ~ 1 の範囲で、1 に近いほどよく滑り、0 に近いほどすぐ止まります。'
    })
    public friction: number = 0.85;

    @property({
        tooltip: 'この値より小さい速度になったとき、完全に停止させるためのしきい値。'
    })
    public minVelocityThreshold: number = 0.01;

    @property({
        tooltip: 'デバッグログを出力するかどうか。'
    })
    public debugLog: boolean = false;

    private _body: CharacterBody2D | null = null;
    private _collider: Collider2D | null = null;

    // 現在プレイヤーに押されているかどうか
    private _isPushed: boolean = false;

    // プレイヤーが押している方向(-1: 左向きに押されている, 1: 右向きに押されている)
    private _pushDirection: number = 0;

    onLoad() {
        // 必須コンポーネントを取得
        this._body = this.getComponent(CharacterBody2D);
        if (!this._body) {
            error('[PushableBlock] CharacterBody2D が見つかりません。このコンポーネントを使うノードには CharacterBody2D を追加してください。');
        }

        this._collider = this.getComponent(Collider2D);
        if (!this._collider) {
            error('[PushableBlock] Collider2D が見つかりません。このコンポーネントを使うノードには BoxCollider2D などの Collider2D を追加してください。');
        }

        if (this._collider) {
            // コンタクトイベントの登録
            this._collider.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
            this._collider.on(Contact2DType.END_CONTACT, this._onEndContact, this);
        }
    }

    start() {
        if (this.debugLog) {
            log('[PushableBlock] start: initialized on node', this.node.name);
        }
    }

    onDestroy() {
        if (this._collider) {
            this._collider.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
            this._collider.off(Contact2DType.END_CONTACT, this._onEndContact, this);
        }
    }

    /**
     * プレイヤーとの接触開始時に呼ばれる。
     */
    private _onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        const otherNode = otherCollider.node;

        if (!this._isPlayer(otherNode)) {
            return;
        }

        // プレイヤーがどちら側から押しているかを判定する
        const blockPos = this.node.worldPosition;
        const playerPos = otherNode.worldPosition;

        // プレイヤーがブロックの左側にいれば、右向きに押されている (= ブロックは正方向に動く)
        const dir = playerPos.x < blockPos.x ? 1 : -1;
        this._pushDirection = dir;
        this._isPushed = true;

        if (this.debugLog) {
            log(`[PushableBlock] BEGIN_CONTACT with player: ${otherNode.name}, pushDirection=${this._pushDirection}`);
        }
    }

    /**
     * プレイヤーとの接触終了時に呼ばれる。
     */
    private _onEndContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        const otherNode = otherCollider.node;

        if (!this._isPlayer(otherNode)) {
            return;
        }

        this._isPushed = false;
        this._pushDirection = 0;

        if (this.debugLog) {
            log(`[PushableBlock] END_CONTACT with player: ${otherNode.name}`);
        }
    }

    /**
     * 対象ノードがプレイヤーかどうかを判定する。
     * - playerGroupName が設定されていればグループ名でチェック
     * - playerTag が設定されていれば名前に含まれるかをチェック
     * どちらも設定されていれば AND 条件ではなく OR 条件で判定する。
     */
    private _isPlayer(node: Node): boolean {
        let isByGroup = false;
        let isByTag = false;

        if (this.playerGroupName.trim().length > 0) {
            // PhysicsSystem2D では group は数値ですが、ここでは Node のグループ名を使う簡易実装にします。
            // 実際のプロジェクトでは 2D 物理グループのビットマスク設定に合わせて判定してください。
            if (node.group === this.playerGroupName) {
                isByGroup = true;
            }
        }

        if (this.playerTag.trim().length > 0) {
            if (node.name.indexOf(this.playerTag) !== -1) {
                isByTag = true;
            }
        }

        // どちらも未設定の場合は常に false
        if (this.playerGroupName.trim().length === 0 && this.playerTag.trim().length === 0) {
            return false;
        }

        return isByGroup || isByTag;
    }

    update(deltaTime: number) {
        if (!this._body) {
            return;
        }

        const v = this._body.velocity;

        if (this._isPushed && this._pushDirection !== 0) {
            // 押されている間は加速度を加えていく
            let targetVx = v.x + this.acceleration * this._pushDirection * deltaTime;

            // 最大速度を制限
            if (targetVx > this.moveSpeed) {
                targetVx = this.moveSpeed;
            } else if (targetVx < -this.moveSpeed) {
                targetVx = -this.moveSpeed;
            }

            v.x = targetVx;
        } else {
            // 押されていないときは摩擦で減速
            v.x *= this.friction;

            // しきい値以下なら完全停止
            if (Math.abs(v.x) < this.minVelocityThreshold) {
                v.x = 0;
            }
        }

        // 縦方向の速度はそのまま維持(重力などが働いている場合を考慮)
        this._body.velocity = v;
    }
}

コードのポイント解説

  • onLoad
    • CharacterBody2DCollider2DgetComponent で取得し、存在しない場合は error ログを出します。
    • Collider2D が存在する場合、Contact2DType.BEGIN_CONTACTEND_CONTACT のリスナーを登録します。
  • _onBeginContact / _onEndContact
    • 接触した相手がプレイヤーかどうかを _isPlayer で判定します。
    • プレイヤーとの接触開始時に _isPushed = true とし、プレイヤーの位置から押す方向(左 or 右)を決定して _pushDirection に格納します。
    • 接触終了時には _isPushed = false, _pushDirection = 0 に戻します。
  • _isPlayer
    • playerGroupNameplayerTag の両方(どちらか一方でも可)で、相手ノードがプレイヤーかどうかを判定します。
    • どちらも空文字の場合は常に false を返し、誤判定を防ぎます。
  • update
    • 毎フレーム CharacterBody2D.velocity を更新します。
    • 押されている間:acceleration に基づいて velocity.x を増減させ、moveSpeed を超えないようクランプします。
    • 押されていない間:friction を乗算して減速させ、minVelocityThreshold 未満になったら 0 に丸めます。
    • 縦方向の速度 velocity.y はそのまま維持することで、落下などの他の物理挙動を邪魔しません。

使用手順と動作確認

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

  1. エディタの Assets パネルで任意のフォルダ(例: assets/scripts)を右クリックします。
  2. Create > TypeScript を選択し、ファイル名を PushableBlock.ts として作成します。
  3. 自動生成されたテンプレートコードをすべて削除し、前節の PushableBlock のコードを丸ごと貼り付けて保存します。

2. 押せる岩用ノードの作成

  1. Hierarchy パネルで右クリックし、Create > 2D Object > Sprite などを選択して、岩用のノードを作成します。
    • 名前の例: PushableRock
    • 任意の岩の画像(スプライト)を設定します。
  2. 作成した岩ノードを選択し、Inspector で次のコンポーネントを追加します。
    1. Add Component > Physics 2D > CharacterBody2D
    2. Add Component > Physics 2D > BoxCollider2D(または適切な形状の Collider2D
      • 岩の見た目に合わせてサイズを調整します。
  3. 同じく Inspector で Add Component > Custom > PushableBlock を選択して、このスクリプトをアタッチします。

3. プレイヤーノードの準備

プレイヤー側も 2D 物理で動いている想定です。最小限の設定例を示します。

  1. Hierarchy で Create > 2D Object > Sprite を選択して、プレイヤーノード(例: Player)を作成します。
  2. Inspector で次のコンポーネントを追加します。
    • Physics 2D > CharacterBody2D
    • Physics 2D > BoxCollider2D(または他の Collider2D)
  3. プレイヤーノードの Groupplayer に設定します。
    • Group 設定はメインメニューの Project > Project Settings > Groups から追加できます。
    • ここで "player" というグループを作成し、プレイヤーノードに割り当ててください。
  4. プレイヤーノードの NamePlayer にしておくと、playerTag = "Player" で判定しやすくなります。

4. PushableBlock のプロパティ設定

岩ノードを選択し、Inspector の PushableBlock コンポーネントのプロパティを次のように設定します(例)。

  • Player Group Name: player
  • Player Tag: Player
  • Move Speed: 3.0
  • Acceleration: 10.0
  • Friction: 0.85
  • Min Velocity Threshold: 0.01
  • Debug Log: 開発中は ON にしてログを確認、安定したら OFF にする

この設定では、プレイヤーが岩に横から接触している間だけ、徐々に加速しながら最大速度 3.0 まで動き、離れると摩擦で少し滑って止まるような挙動になります。

5. 物理システムの有効化と動作確認

  1. メインメニューから Project > Project Settings を開きます。
  2. Physics 2D タブを開き、物理システムが有効になっていることを確認します。
    • Enable がチェックされているか。
    • 必要に応じて重力などを設定します(例: Gravity: (0, -320))。
  3. Scene を保存し、Preview(再生ボタン) を押して実行します。
  4. プレイヤーを操作して、岩に横からぶつかるように動かします。
    • プレイヤーが岩に接触している間、岩がゆっくりと押されて動くこと。
    • プレイヤーが離れると、岩が徐々に減速して止まること。
    • プレイヤー以外のオブジェクト(敵や弾など)をぶつけても岩が動かないこと。
  5. Debug Log を ON にしている場合、コンソールに次のようなログが出力されることを確認します。
    • [PushableBlock] BEGIN_CONTACT with player: Player, pushDirection=1
    • [PushableBlock] END_CONTACT with player: Player

もし岩が全く動かない場合は、次を確認してください。

  • 岩ノードに CharacterBody2DCollider2D が正しく追加されているか。
  • プレイヤーノードの Groupplayer になっているか。
  • プレイヤーノードの Name"Player" が含まれているか。
  • Physics 2D が有効になっているか。

まとめ

この PushableBlock コンポーネントは、

  • CharacterBody2D + Collider2D を持つ任意のノードにアタッチするだけで「押せる岩」ギミックを実現できる、独立した汎用スクリプトです。
  • プレイヤー判定は グループ名とタグ文字列で柔軟に行えるため、プロジェクト構成に合わせて簡単に調整できます。
  • 重さ・滑りやすさ・加速感 をすべて Inspector から調整できるため、ゲームごとの手触りに合わせたチューニングが容易です。

応用例としては、

  • スイッチの上に乗せると仕掛けが動く「仕掛け用ブロック」
  • 落とし穴に落とすことで足場を作る「落とせる岩」
  • 複数人協力で押すと動く「協力ギミック」

など、さまざまなパズル・アクション要素に発展させられます。
このコンポーネントは外部のマネージャやシングルトンに依存していないため、別プロジェクトへの持ち込みや、チーム内での共有もしやすい構成になっています。

まずはそのまま使い、慣れてきたら「縦方向にも押せるようにする」「押している間だけ効果音を鳴らす」など、プロジェクトに合わせた拡張に挑戦してみてください。

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をコピーしました!