【Cocos Creator】アタッチするだけ!LedgeDetector (崖検知)の実装方法【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】LedgeDetector(崖検知)の実装:アタッチするだけで「足元の崖を検知して自動で反転する」敵AI補助スクリプト

横スクロール系のアクションや2Dアクションゲームで、敵キャラが「足場の端でちゃんと引き返す」挙動は定番です。本記事では、敵キャラ(に限らず移動オブジェクト)にアタッチするだけで、足元の前方にレイ(RayCast)を飛ばし、床がなくなったら自動で移動方向を反転させる汎用コンポーネント LedgeDetector を実装します。

移動処理そのものは各キャラ側で行い、LedgeDetector は「今は前に進んでよいか?」「崖なので引き返すべきか?」を判定して、方向フラグやイベントコールバックで通知するだけの設計にします。これにより、どんな移動ロジックでも簡単に組み合わせて使うことができます。


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

1. 要件整理

  • 親ノード(敵キャラなど)の前方足元に向けてレイキャストを行う。
  • レイが「床(コライダー)」に当たらなくなったら、崖とみなして方向を反転させる。
  • 敵AI本体など、他のカスタムスクリプトには一切依存しない
  • 移動は本コンポーネントが直接行わず、方向情報やイベントを提供するだけにして汎用性を確保する。
  • 2D物理(PhysicsSystem2D)の RayCast を利用する。
  • 物理ワールドに床となる Collider2D が存在しないと機能しないため、その点はログと解説で明示する。

2. 設計アプローチ

本コンポーネントは、以下のような仕組みで動作します。

  1. 親ノードのローカル座標系で「レイの発射開始オフセット(足元の少し前)」を指定。
  2. 親ノードの現在の向き(右向き or 左向き)に基づいて、レイの開始位置をワールド座標に変換。
  3. 指定された長さだけ真下(-Y方向)に RayCast を飛ばし、床コライダーに当たるか判定。
  4. 一定距離内に床がなければ崖と判定し、内部の _movingDirection(+1: 右, -1: 左)を反転。
  5. 方向が変わったタイミングで、インスペクタから設定した コールバックイベントを発火(任意)。

このコンポーネントを利用する側は、

  • currentDirection を参照して移動方向を決定する
  • または onDirectionChanged にイベントを登録し、反転時にアニメーションやスプライトの反転を行う

といった形で連携できます。

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

LedgeDetector に用意する @property 群とその役割は以下の通りです。

  • enabledDetection: boolean
    – 崖検知機能の有効/無効フラグ。
    true のときのみ RayCast を実行します。
    – 一時的に崖検知を止めたい場合に使用します。
  • initialDirection: number
    – 初期の移動方向。
    1 = 右向き、-1 = 左向き。
    – 敵キャラが最初にどちらへ歩き出すかを指定します。
  • raycastOffset: Vec2
    – レイの発射開始位置を、親ノードのローカル座標で指定します。
    – 例: (0.3, -0.5) なら、親の中心から右に0.3、下に0.5の位置からレイを飛ばします。
    – 敵の足元の形状に合わせて微調整します。
  • raycastLength: number
    – 真下に向けて飛ばすレイの長さ(ワールド単位)。
    – キャラの高さや足場の厚みに合わせて調整します。
    – 小さすぎると、足場の上でも床を検知できない可能性があります。
  • raycastInterval: number
    – レイキャストを行う間隔(秒)。
    0 または非常に小さい値にすると、毎フレームに近い頻度でチェックします。
    – パフォーマンスと精度のバランスを取るために調整します(例: 0.05〜0.2)。
  • groundLayerMask: number
    – どのレイヤーを「床」とみなすかを指定するビットマスク。
    – 2D物理の Layer 設定に合わせて指定します。
    – 例: 1 << 0(Defaultレイヤー)など。インスペクタで数値として設定します。
  • flipScaleXOnDirectionChange: boolean
    true の場合、方向が変わるたびに親ノードの scale.x を自動反転します。
    – 敵スプライトを左右反転する用途に便利です。
  • debugDraw: boolean
    true の場合、実際に飛ばしているレイを Graphics で簡易的に可視化します。
    – デバッグ時に足場判定の位置を確認するのに役立ちます。
  • onDirectionChanged: EventHandler[]
    – 方向が反転したときに呼び出すイベントリスト。
    – インスペクタから任意のノード/コンポーネント/メソッドを指定できます。
    – 例: EnemyControlleronDirectionChanged 関数を呼んで、速度ベクトルを更新するなど。

また、ランタイムで参照できる読み取り専用プロパティとして、

  • currentDirection: number
    – 現在の移動方向(1 or -1)。
    – 他のスクリプトから getComponent(LedgeDetector)?.currentDirection で参照できます。

TypeScriptコードの実装

以下が、Cocos Creator 3.8.7 向けの LedgeDetector.ts の完全な実装です。


import { _decorator, Component, Node, Vec2, Vec3, PhysicsSystem2D, ERaycast2DType, Contact2DType, Collider2D, RigidBody2D, Layers, EventHandler, Graphics, Color, UITransform, director } from 'cc';
const { ccclass, property } = _decorator;

/**
 * LedgeDetector
 * 親ノードの前方足元に向けてレイキャストを行い、
 * 足場がなくなったときに移動方向を反転させる汎用コンポーネント。
 *
 * - 他のカスタムスクリプトには依存しません。
 * - currentDirection を参照して移動処理に利用してください。
 * - onDirectionChanged イベントで反転タイミングを通知できます。
 */
@ccclass('LedgeDetector')
export class LedgeDetector extends Component {

    @property({
        tooltip: '崖検知を有効にするかどうか。\nfalse の場合、RayCast は行われません。'
    })
    public enabledDetection: boolean = true;

    @property({
        tooltip: '初期の移動方向。\n1 = 右向き, -1 = 左向き。',
    })
    public initialDirection: number = 1;

    @property({
        tooltip: 'レイの発射開始位置(ローカル座標)。\nキャラの足元より少し前方に設定してください。'
    })
    public raycastOffset: Vec2 = new Vec2(0.3, -0.5);

    @property({
        tooltip: 'レイの長さ(ワールド単位)。\nキャラの高さや足場の厚みに応じて調整します。'
    })
    public raycastLength: number = 1.0;

    @property({
        tooltip: 'RayCast を行う間隔(秒)。\n0 にすると毎フレーム近くチェックされます。'
    })
    public raycastInterval: number = 0.05;

    @property({
        tooltip: 'どの 2D 物理レイヤーを「床」とみなすかを表すビットマスク値。\n例: Default レイヤーのみ = 1 << 0 = 1'
    })
    public groundLayerMask: number = 1 << 0; // Default layer

    @property({
        tooltip: '方向が変わったときに、親ノードの scale.x を自動で反転させるかどうか。'
    })
    public flipScaleXOnDirectionChange: boolean = true;

    @property({
        tooltip: 'RayCast のラインを簡易的に描画してデバッグ表示します。\n実行時に Graphics コンポーネントを動的に利用します。'
    })
    public debugDraw: boolean = false;

    @property({
        type: [EventHandler],
        tooltip: '移動方向が変わったときに呼び出されるイベント群。\n引数として新しい方向 (1 or -1) が渡されます。'
    })
    public onDirectionChanged: EventHandler[] = [];

    // 現在の移動方向(1: 右, -1: 左)
    public get currentDirection(): number {
        return this._movingDirection;
    }

    private _movingDirection: number = 1;
    private _timeSinceLastRaycast: number = 0;

    // デバッグ描画用
    private _debugGraphics: Graphics | null = null;
    private _debugNode: Node | null = null;

    onLoad() {
        // 初期方向の正規化
        this._movingDirection = this.initialDirection >= 0 ? 1 : -1;

        // PhysicsSystem2D 有効チェック
        if (!PhysicsSystem2D.instance) {
            console.warn('[LedgeDetector] PhysicsSystem2D が有効ではありません。Project Settings で 2D Physics を有効にしてください。');
        }

        if (this.debugDraw) {
            this._createDebugGraphicsIfNeeded();
        }
    }

    start() {
        // ログで基本情報を出しておく
        if (!this.enabledDetection) {
            console.log('[LedgeDetector] 崖検知は無効化されています。enabledDetection を true にすると有効になります。');
        }
    }

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

        this._timeSinceLastRaycast += deltaTime;
        if (this._timeSinceLastRaycast < this.raycastInterval) {
            return;
        }
        this._timeSinceLastRaycast = 0;

        this._performLedgeCheck();
    }

    /**
     * 崖チェックのメイン処理
     */
    private _performLedgeCheck() {
        const node = this.node;

        // 親ノードのローカルオフセットを、現在の向きに合わせて調整
        const localOffset = new Vec3(
            this.raycastOffset.x * this._movingDirection,
            this.raycastOffset.y,
            0
        );

        // ローカル座標をワールド座標に変換
        const worldStart = node.getWorldPosition().add(localOffset);

        // レイは真下(-Y)方向
        const worldEnd = new Vec3(
            worldStart.x,
            worldStart.y - this.raycastLength,
            worldStart.z
        );

        const physics2D = PhysicsSystem2D.instance;
        if (!physics2D) {
            console.warn('[LedgeDetector] PhysicsSystem2D.instance が取得できません。2D Physics が無効の可能性があります。');
            return;
        }

        // Raycast 実行
        const results = physics2D.raycast(
            new Vec2(worldStart.x, worldStart.y),
            new Vec2(worldEnd.x, worldEnd.y),
            ERaycast2DType.Closest,
            this.groundLayerMask
        );

        const hasGround = results.length > 0;

        // デバッグ描画
        if (this.debugDraw) {
            this._drawDebugRay(worldStart, worldEnd, hasGround);
        }

        if (!hasGround) {
            // 崖とみなして方向反転
            this._reverseDirection();
        }
    }

    /**
     * 方向を反転し、必要に応じて scale.x を反転&イベント発火
     */
    private _reverseDirection() {
        const oldDir = this._movingDirection;
        const newDir = -oldDir;
        this._movingDirection = newDir;

        // スケール反転
        if (this.flipScaleXOnDirectionChange) {
            const scale = this.node.scale;
            this.node.setScale(new Vec3(-scale.x, scale.y, scale.z));
        }

        // イベント発火
        if (this.onDirectionChanged.length > 0) {
            EventHandler.emitEvents(this.onDirectionChanged, newDir);
        }
    }

    /**
     * デバッグ描画ノードと Graphics コンポーネントを必要に応じて生成
     */
    private _createDebugGraphicsIfNeeded() {
        if (this._debugNode && this._debugGraphics) {
            return;
        }

        const parent = this.node.scene || director.getScene();
        if (!parent) {
            console.warn('[LedgeDetector] デバッグ描画用ノードを作成できませんでした(Scene が取得できません)。');
            return;
        }

        const debugNode = new Node('LedgeDetectorDebug');
        parent.addChild(debugNode);

        const graphics = debugNode.addComponent(Graphics);
        graphics.lineWidth = 2;
        graphics.strokeColor = Color.GREEN;

        this._debugNode = debugNode;
        this._debugGraphics = graphics;
    }

    /**
     * レイのデバッグ描画
     */
    private _drawDebugRay(start: Vec3, end: Vec3, hasGround: boolean) {
        this._createDebugGraphicsIfNeeded();
        if (!this._debugGraphics) {
            return;
        }

        const g = this._debugGraphics;
        g.clear();

        g.strokeColor = hasGround ? Color.GREEN : Color.RED;

        g.moveTo(start.x, start.y);
        g.lineTo(end.x, end.y);
        g.stroke();
    }

    onDestroy() {
        // デバッグ用ノードをクリーンアップ
        if (this._debugNode && this._debugNode.isValid) {
            this._debugNode.destroy();
        }
        this._debugNode = null;
        this._debugGraphics = null;
    }
}

コードのポイント解説

  • onLoad()
    initialDirection1 or -1 に正規化して _movingDirection にセットします。
    PhysicsSystem2D が存在しない(2D物理が無効)の場合は console.warn を出しておきます。
    debugDraw が有効なら、デバッグ描画用の Graphics を準備します。
  • start()
    enabledDetectionfalse のときにログを出して、誤設定に気づきやすくしています。
  • update(deltaTime)
    – 崖検知が無効なら何もしません。
    raycastInterval に基づいて RayCast の頻度を制御し、パフォーマンスを確保します。
    – 一定時間ごとに _performLedgeCheck() を呼び出します。
  • _performLedgeCheck()
    raycastOffset と現在の方向 _movingDirection を使って、レイの開始位置を計算します。
    – 親ノードのワールド座標にローカルオフセットを足して worldStart を求め、そこから raycastLength 分だけ下に worldEnd を設定します。
    PhysicsSystem2D.raycast() を呼び出し、groundLayerMask にヒットするか判定します。
    – ヒットがなければ崖とみなし、_reverseDirection() を呼び出します。
    debugDraw が有効なら、_drawDebugRay() でレイを可視化します。
  • _reverseDirection()
    – 内部の _movingDirection を反転します。
    flipScaleXOnDirectionChangetrue のときは、node.scale.x を反転してスプライトを左右反転します。
    onDirectionChanged に設定されたイベントをすべて発火し、新しい方向(1 or -1)を引数として渡します。
  • _createDebugGraphicsIfNeeded() / _drawDebugRay()
    – シーン直下に LedgeDetectorDebug ノードを作成し、Graphics でレイの線を描画します。
    – 床があるときは緑、崖のときは赤で描画し、どこを判定しているか一目で分かるようにしています。
  • onDestroy()
    – デバッグ用ノードをクリーンアップして、シーンにゴミが残らないようにします。

使用手順と動作確認

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

  1. エディタの Assets パネルで右クリックします。
  2. Create → TypeScript を選択します。
  3. 作成されたファイルの名前を LedgeDetector.ts に変更します。
  4. ダブルクリックしてエディタ(VS Code など)で開き、前述の TypeScript コードを丸ごと貼り付けて保存します。

2. テスト用のシーンと床の準備

  1. 2D シーンを開くか新規作成します。
  2. Hierarchy で右クリック → Create → 2D Object → Sprite などを選び、床となるノード(例: Floor)を作成します。
  3. Floor ノードを選択し、Inspector の Add Component ボタンから以下を追加します:
    • Physics 2D → BoxCollider2D(もしくは足場に合う Collider2D)
    • 必要に応じて RigidBody2D(Static Type)を追加すると、より安定します。
  4. Collider2D の Offset / Size を調整して、床の形状に合わせます。
  5. 床のレイヤーが Default であることを確認します(または後で groundLayerMask を合わせて変更します)。

3. 崖を作る

  1. 床ノード Floor を複製して、少し離れた位置に配置するか、
    もしくは Floor のスケールを短くして、キャラが落ちられる空白部分を作ります。
  2. 「床がない空間」ができていれば OK です。キャラがそこに差し掛かったときに崖判定が行われます。

4. 敵キャラ(テスト用オブジェクト)の作成

  1. Hierarchy で右クリック → Create → 2D Object → Sprite を選び、Enemy ノードを作成します。
  2. Enemy ノードに適当なスプライト画像を設定します(任意)。
  3. Enemy ノードを選択し、Inspector の Add Component ボタンを押します。
  4. Custom → LedgeDetector を選択してアタッチします。

5. LedgeDetector のプロパティ設定例

Enemy ノードにアタッチした LedgeDetector コンポーネントの Inspector で、以下のように設定してみてください。

  • Enabled Detection: true
  • Initial Direction: 1(右向きスタート)
  • Raycast Offset: (0.3, -0.5)(0.5, -0.5) あたり(スプライトのサイズに合わせて調整)
  • Raycast Length: 1.0(キャラの高さより少し大きめ目安)
  • Raycast Interval: 0.05(20fps程度でチェック)
  • Ground Layer Mask: 1(Default レイヤーのみを床とみなす場合)
  • Flip Scale X On Direction Change: true(左右反転したい場合)
  • Debug Draw: true(動作確認中は有効にすると分かりやすいです)

Ground Layer Mask は、床の Collider2D が属するレイヤーに合わせてください。
レイヤーをカスタマイズしている場合は、そのビット値を設定します(例: 1 << 3 = 8 など)。

6. 移動処理との連携(最小サンプル)

LedgeDetector 自体は移動を行いませんので、シンプルな移動スクリプトを用意して組み合わせてみます。
※このスクリプトは任意です。LedgeDetector は単体で完結しており、外部依存ではありません。

  1. Assets パネルで右クリック → Create → TypeScript を選択し、SimpleWalker.ts を作成します。
  2. 以下のような簡単なコードを貼り付けます。

import { _decorator, Component, Vec3 } from 'cc';
import { LedgeDetector } from './LedgeDetector';
const { ccclass, property } = _decorator;

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

    @property({ tooltip: '左右移動の速度(単位/秒)。' })
    public moveSpeed: number = 2;

    private _ledgeDetector: LedgeDetector | null = null;

    start() {
        this._ledgeDetector = this.getComponent(LedgeDetector);
        if (!this._ledgeDetector) {
            console.warn('[SimpleWalker] LedgeDetector が同じノードに見つかりません。崖検知は行われません。');
        }
    }

    update(deltaTime: number) {
        const dir = this._ledgeDetector ? this._ledgeDetector.currentDirection : 1;
        const pos = this.node.position;
        const dx = dir * this.moveSpeed * deltaTime;
        this.node.setPosition(new Vec3(pos.x + dx, pos.y, pos.z));
    }
}

この SimpleWalkerEnemy ノードに追加することで、

  • LedgeDetector が方向を判定
  • SimpleWalker がその方向に沿って移動

という構成になります。

  1. Enemy ノードを選択し、Inspector → Add Component → Custom → SimpleWalker を追加します。
  2. SimpleWalkerMove Speed25 くらいに設定します。

7. プレイして動作確認

  1. 上部ツールバーの Play(再生)ボタンを押してゲームを実行します。
  2. Enemy が右方向へ歩き出し、床の端に近づくと、崖の手前で方向を反転することを確認します。
  3. Debug Drawtrue にしている場合、
    – 緑色の線: 床を検知している状態
    – 赤色の線: 床がない(崖)と判定された状態
    となっているかを確認します。
  4. もし崖から落ちてしまう場合は、以下を確認してください:
    • Raycast Offset が前方すぎて、レイが床の外に出ていないか。
    • Raycast Length が短すぎて、床コライダーに届いていないか。
    • 床コライダーのレイヤーと、Ground Layer Mask が一致しているか。
    • 2D Physics が Project Settings で有効になっているか。

8. onDirectionChanged イベントの活用例

アニメーション制御や速度ベクトルの更新をイベントで行いたい場合は、以下のように設定できます。

  1. 方向変更時に呼び出したいメソッドを持つコンポーネント(例: EnemyController)を Enemy ノードに追加します。
  2. EnemyController に、以下のようなメソッドを実装します。

public onDirectionChanged(newDir: number) {
    // ここでアニメーションや速度などを更新
    console.log('Direction changed to:', newDir);
}
  1. Inspector で Enemy ノードの LedgeDetector コンポーネントを開き、On Direction Changed の右にある + ボタンを押します。
  2. Event 要素が追加されるので、
    • Node: Enemy ノードをドラッグ&ドロップ
    • Component: EnemyController を選択
    • Handler: onDirectionChanged を選択
  3. これで、方向が反転するたびに EnemyController.onDirectionChanged(newDir) が呼び出されます。

まとめ

本記事では、Cocos Creator 3.8.7 と TypeScript を用いて、

  • 親ノードの前方足元に RayCast を飛ばし、
  • 床がなくなったら自動で移動方向を反転する

汎用コンポーネント LedgeDetector を実装しました。

ポイントは以下の通りです。

  • 他のカスタムスクリプトに依存せず、このコンポーネント単体で崖検知ロジックが完結している。
  • raycastOffset, raycastLength, groundLayerMask などをインスペクタから調整でき、どんなキャラ・足場形状にも対応しやすい
  • currentDirection を参照するだけで、任意の移動ロジックと簡単に連携可能。
  • onDirectionChanged イベントを使えば、アニメーションや速度ベクトル、エフェクトなども疎結合に連携できる。
  • debugDraw によるレイの可視化で、判定位置を直感的に確認できる。

このような「アタッチするだけでよくある挙動をカプセル化したコンポーネント」をライブラリ化しておくと、新しい敵キャラやギミックを追加するたびにロジックをコピペする必要がなくなり、ゲーム開発の効率が大きく向上します。

本稿の LedgeDetector をベースに、

  • 前方の「壁」も検知して反転する
  • 崖に近づいたときに一瞬止まってから反転する
  • 崖をあえて飛び降りる敵用に「崖無視モード」を切り替える

といったバリエーションも簡単に作れるはずです。プロジェクトに合わせて、ぜひ拡張してみてください。

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