【Cocos Creator】アタッチするだけ!ReflectorMirror (光の反射鏡)の実装方法【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】ReflectorMirror の実装:アタッチするだけで「入射RayCastを自動で反射し、反射レーザーの可視化まで行う」汎用スクリプト

レーザーギミックでよくある「鏡に当たったレーザーが反射して別のスイッチに当たる」といった演出を、ノードにコンポーネントをアタッチするだけで実現できるようにします。
この ReflectorMirror は、2D物理の RayCast 結果(入射方向)を受け取り、鏡の向きに応じた反射方向を計算し、さらに「反射レーザー」を RayCast+描画(Line)で可視化します。


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

想定する利用シーン

  • 2D レーザーギミック(上から落ちてくるレーザー、横から飛んでくるレーザーなど)
  • レーザーが鏡に当たると、物理的に正しい反射角で別の方向に飛んでいく演出
  • 反射レーザーを「線」として表示し、デバッグ・演出の両方で利用したい場合

今回のコンポーネントは「完全独立」が条件のため、以下の方針で設計します。

  • 外部の GameManager や LaserManager に依存しない。
  • 「入射レーザーの情報」は、他からイベント的に渡してもらう形にする(onLaserHit を公開)。
  • 反射レーザーの可視化には Graphics コンポーネントを利用し、アタッチされたノード自身に線を描画する。
  • 必要なコンポーネント(Graphics)はコード側で自動取得・自動追加し、防御的にログを出す。
  • RayCast は 2D 物理(PhysicsSystem2D)を使用し、ヒット先の情報を取得できるようにする。

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

ReflectorMirror でインスペクタから調整できるプロパティと役割は以下の通りです。

  • enabledReflection: boolean
    反射機能のオン/オフ。オフにすると onLaserHit を呼んでも反射レーザーを出さない。
  • maxReflectDistance: number
    反射レーザーの最大距離(ピクセル)。RayCast もこの距離を上限として飛ばす。
  • useNodeUpAsNormal: boolean
    • ON: このノードの「上方向(world space の up ベクトル)」を鏡面の法線として使用。
    • OFF: 入射レーザーから渡される「ヒット法線」を使う。

    鏡をノードの回転で向きを変えたい場合は ON にする。

  • lineColor: Color
    反射レーザー線の色(Graphics の strokeColor)。
  • lineWidth: number
    反射レーザー線の太さ(ピクセル)。
  • showLine: boolean
    反射レーザーを可視化するかどうか。OFF にすると内部的には反射演算だけ行い、線は描画しない。
  • autoClearLineTime: number
    0 より大きい場合、「最後にレーザーを受けてから指定秒数が経過すると線を自動で消す」。
    0 以下の場合は自動クリアしない(常に最後の線を表示したまま)。
  • debugLog: boolean
    true の場合、反射計算や RayCast 結果などを console.log に出力してデバッグしやすくする。

外部から呼び出す公開メソッド

ReflectorMirror は「レーザー発射側」から次のように呼ばれる想定です。

  • onLaserHit(origin: Vec2, direction: Vec2, hitPoint?: Vec2, hitNormal?: Vec2): void

パラメータの意味:

  • origin: 入射レーザーの発射位置(world 座標、2D)。
  • direction: 入射レーザーの方向ベクトル(正規化されていることを推奨)。
  • hitPoint: そのレーザーがこの鏡に当たった座標(world 座標)。省略した場合は、このノードの world 位置を起点とみなす。
  • hitNormal: 鏡の法線ベクトル(world)。useNodeUpAsNormal が OFF のときに使われる。省略可。

このメソッドを呼ぶだけで、ReflectorMirror が反射方向を計算し、必要に応じて RayCast と線描画まで行います。


TypeScriptコードの実装

以下が完成した ReflectorMirror.ts の全コードです。


import {
    _decorator,
    Component,
    Node,
    Vec2,
    Vec3,
    Color,
    Graphics,
    PhysicsSystem2D,
    ERaycast2DType,
    geometry,
    director,
} from 'cc';
const { ccclass, property } = _decorator;

/**
 * ReflectorMirror
 * 
 * 入射レーザー(Ray)の情報を受け取り、反射方向を計算して
 * RayCast および線描画で「反射レーザー」を表現する汎用コンポーネント。
 * 
 * 想定使用方法:
 *  - レーザー発射側から onLaserHit(...) を呼ぶだけで OK。
 *  - 他のカスタムスクリプトには依存しない。
 */
@ccclass('ReflectorMirror')
export class ReflectorMirror extends Component {

    @property({
        tooltip: '反射機能のオン/オフ。false の場合、onLaserHit を呼んでも何もしません。',
    })
    public enabledReflection: boolean = true;

    @property({
        tooltip: '反射レーザーの最大距離(ピクセル)。RayCast もこの距離を上限とします。',
        min: 0,
    })
    public maxReflectDistance: number = 800;

    @property({
        tooltip: 'true: ノードの上方向(world up)を鏡面の法線として使用します。\nfalse: onLaserHit で渡された hitNormal を使用します。',
    })
    public useNodeUpAsNormal: boolean = true;

    @property({
        tooltip: '反射レーザー線の色(Graphics の strokeColor に設定されます)。',
    })
    public lineColor: Color = new Color(0, 255, 255, 255);

    @property({
        tooltip: '反射レーザー線の太さ(ピクセル)。',
        min: 1,
    })
    public lineWidth: number = 4;

    @property({
        tooltip: 'true の場合、反射レーザーを線で可視化します。false の場合は内部計算のみ行います。',
    })
    public showLine: boolean = true;

    @property({
        tooltip: '最後にレーザーを受けてからこの秒数が経過すると線を自動で消します。\n0 以下の場合、自動では消しません。',
    })
    public autoClearLineTime: number = 0.2;

    @property({
        tooltip: 'true にすると反射ベクトルや RayCast 結果をログ出力します。',
    })
    public debugLog: boolean = false;

    // --- 内部用フィールド ---

    private _graphics: Graphics | null = null;
    private _lastHitTime: number = -1;

    onLoad() {
        // Graphics コンポーネントを取得または自動追加
        this._graphics = this.getComponent(Graphics);
        if (!this._graphics) {
            this._graphics = this.node.addComponent(Graphics);
            if (this.debugLog) {
                console.log('[ReflectorMirror] Graphics コンポーネントが見つからなかったため、自動追加しました。');
            }
        }

        if (!this._graphics) {
            console.error('[ReflectorMirror] Graphics コンポーネントを取得・追加できませんでした。線描画は行えません。');
        } else {
            this._setupGraphicsStyle();
        }
    }

    start() {
        // 特に初期化は不要だが、物理システムが有効かどうかを一応確認
        if (!PhysicsSystem2D.instance.enable) {
            console.warn('[ReflectorMirror] PhysicsSystem2D が無効です。RayCast によるヒット判定は行われません。');
        }
    }

    update(deltaTime: number) {
        // 自動クリア処理
        if (this.autoClearLineTime > 0 && this._lastHitTime >= 0) {
            const now = director.getTotalTime() / 1000; // ms → s
            if (now - this._lastHitTime >= this.autoClearLineTime) {
                this.clearLine();
                this._lastHitTime = -1;
            }
        }
    }

    /**
     * 外部から呼び出す公開メソッド。
     * 入射レーザーの情報を渡すことで、反射レーザーの計算・描画を行います。
     * 
     * @param origin     入射レーザーの発射位置(world 座標, Vec2)
     * @param direction  入射レーザーの方向ベクトル(world, Vec2。正規化推奨)
     * @param hitPoint   鏡に当たった位置(world 座標, Vec2)。省略時はこのノードの world 位置を使用。
     * @param hitNormal  鏡の法線ベクトル(world, Vec2)。useNodeUpAsNormal=false のときに使用。省略可。
     */
    public onLaserHit(
        origin: Vec2,
        direction: Vec2,
        hitPoint?: Vec2,
        hitNormal?: Vec2,
    ): void {
        if (!this.enabledReflection) {
            if (this.debugLog) {
                console.log('[ReflectorMirror] enabledReflection=false のため処理をスキップしました。');
            }
            return;
        }

        // 反射起点(ヒット位置)
        const worldHitPoint = hitPoint ? hitPoint.clone() : this._getNodeWorldPosition2D();

        // 使用する法線ベクトルを決定
        let normal = new Vec2();
        if (this.useNodeUpAsNormal) {
            normal = this._getNodeWorldUp2D();
        } else if (hitNormal) {
            normal.set(hitNormal.x, hitNormal.y);
        } else {
            // 法線情報がない場合は警告して終了
            console.warn('[ReflectorMirror] 法線ベクトルが指定されていないため、反射計算を行えませんでした。');
            return;
        }

        // 入射方向を正規化
        const inDir = direction.clone();
        if (inDir.lengthSqr() === 0) {
            console.warn('[ReflectorMirror] 入射方向ベクトルの長さが 0 です。処理を中断します。');
            return;
        }
        inDir.normalize();

        // 法線も正規化
        if (normal.lengthSqr() === 0) {
            console.warn('[ReflectorMirror] 法線ベクトルの長さが 0 です。処理を中断します。');
            return;
        }
        normal.normalize();

        // 反射ベクトル R = I - 2 * dot(I, N) * N
        const dotIN = inDir.dot(normal);
        const reflectDir = new Vec2(
            inDir.x - 2 * dotIN * normal.x,
            inDir.y - 2 * dotIN * normal.y
        );
        reflectDir.normalize();

        if (this.debugLog) {
            console.log('[ReflectorMirror] 入射方向:', inDir.toString(),
                ' 法線:', normal.toString(),
                ' 反射方向:', reflectDir.toString());
        }

        // 反射レーザーの終点を RayCast で決定
        const endPoint = this._computeReflectEndPoint(worldHitPoint, reflectDir);

        // 線描画
        if (this.showLine && this._graphics) {
            this._drawLine(worldHitPoint, endPoint);
        }

        // 最終ヒット時間を更新(自動クリア用)
        this._lastHitTime = director.getTotalTime() / 1000;
    }

    /**
     * 反射レーザー線をクリアします(外部からも呼べるように公開)。
     */
    public clearLine(): void {
        if (this._graphics) {
            this._graphics.clear();
        }
    }

    // =========================
    // 内部ユーティリティ
    // =========================

    private _setupGraphicsStyle(): void {
        if (!this._graphics) return;
        this._graphics.lineWidth = this.lineWidth;
        this._graphics.strokeColor = this.lineColor;
    }

    private _getNodeWorldPosition2D(): Vec2 {
        const worldPos3 = this.node.worldPosition;
        return new Vec2(worldPos3.x, worldPos3.y);
    }

    /**
     * ノードの「上方向」を world 空間の 2D ベクトルとして取得。
     */
    private _getNodeWorldUp2D(): Vec2 {
        // ノードのローカル up ベクトル (0,1,0) を world に変換
        const upLocal = new Vec3(0, 1, 0);
        const upWorld3 = this.node.getWorldMatrix().transformDirection(upLocal);
        const upWorld2 = new Vec2(upWorld3.x, upWorld3.y);
        if (upWorld2.lengthSqr() === 0) {
            // 何らかの理由で 0 ベクトルになった場合はデフォルトで (0,1)
            upWorld2.set(0, 1);
        }
        upWorld2.normalize();
        return upWorld2;
    }

    /**
     * 反射レーザーの終点を RayCast で計算します。
     * 物理ヒットがあればその位置、なければ maxReflectDistance 分だけ進んだ位置を返します。
     */
    private _computeReflectEndPoint(start: Vec2, dir: Vec2): Vec2 {
        const maxDist = Math.max(this.maxReflectDistance, 0);
        const rayEnd = new Vec2(
            start.x + dir.x * maxDist,
            start.y + dir.y * maxDist
        );

        if (!PhysicsSystem2D.instance.enable) {
            // 物理が無効なら RayCast せず、そのまま最大距離先を返す
            return rayEnd;
        }

        const results: geometry.RayCast2DResult[] = [];
        const hitCount = PhysicsSystem2D.instance.raycast(
            start,
            rayEnd,
            ERaycast2DType.Closest,
            0xffffffff, // 全レイヤー対象
            results
        );

        if (hitCount > 0 && results.length > 0) {
            const hit = results[0];
            const hp = hit.point;
            const end = new Vec2(hp.x, hp.y);

            if (this.debugLog) {
                console.log('[ReflectorMirror] 反射レーザーがヒットしました。hitPoint:', end.toString());
            }

            return end;
        }

        // ヒットしなかった場合は最大距離先
        if (this.debugLog) {
            console.log('[ReflectorMirror] 反射レーザーは何にもヒットしませんでした。最大距離まで進みます。');
        }
        return rayEnd;
    }

    /**
     * world 座標の 2 点を、このノードのローカル座標に変換して Graphics で線を描画します。
     */
    private _drawLine(worldStart: Vec2, worldEnd: Vec2): void {
        if (!this._graphics) return;

        this._graphics.clear();
        this._setupGraphicsStyle();

        const localStart3 = new Vec3();
        const localEnd3 = new Vec3();
        const worldStart3 = new Vec3(worldStart.x, worldStart.y, 0);
        const worldEnd3 = new Vec3(worldEnd.x, worldEnd.y, 0);

        this.node.inverseTransformPoint(localStart3, worldStart3);
        this.node.inverseTransformPoint(localEnd3, worldEnd3);

        this._graphics.moveTo(localStart3.x, localStart3.y);
        this._graphics.lineTo(localEnd3.x, localEnd3.y);
        this._graphics.stroke();
    }
}

コードの要点解説

  • onLoad
    • Graphics コンポーネントを getComponent で取得し、なければ addComponent で自動追加。
    • 取得できなかった場合は console.error を出し、線描画を諦める(防御的実装)。
  • start
    • 2D 物理システムが無効な場合に警告を出す。RayCast 自体は enable == false なら行わず、最大距離まで伸ばすだけになる。
  • update
    • autoClearLineTime が 0 より大きいとき、最後にレーザーを受けてから一定時間経過すると clearLine() を呼んで線を消す。
    • 時間は director.getTotalTime()(ミリ秒)から算出。
  • onLaserHit
    • 外部から呼ぶメイン API。入射方向・法線から反射ベクトルを計算。
    • 反射方向に maxReflectDistance だけ RayCast し、ヒットすればそこで止める。
    • showLine が true で Graphics が存在する場合にのみ線を描画。
  • その他の内部メソッド
    • _getNodeWorldUp2D: ノードの up ベクトルを world 空間に変換して 2D ベクトルとして返す。
    • _computeReflectEndPoint: RayCast を行って反射レーザーの終点を決定。
    • _drawLine: world 座標の 2 点をローカルに変換し、Graphics で線を描画。

使用手順と動作確認

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

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. ファイル名を ReflectorMirror.ts に変更します。
  3. 作成された ReflectorMirror.ts をダブルクリックして開き、既存のコードをすべて削除して、上記の TypeScript コードを貼り付けます。
  4. 保存します(Ctrl+S / Cmd+S)。

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

  1. Hierarchy パネルで 2D シーン を開きます(なければ新規シーンを作成)。
  2. メニューから GameObject → Create Empty などで空ノードを 1 つ作成し、名前を Mirror にします。
    • または、Sprite ノードなど既存のノードを鏡として使っても構いません。
  3. Mirror ノードを選択し、Inspector で Add Component → Custom → ReflectorMirror を選択してアタッチします。

3. 反射線を表示するための設定

ReflectorMirror コンポーネントを選択した状態で、Inspector から以下を確認・調整します。

  • Enabled Reflection: チェックを入れる(true)。
  • Max Reflect Distance: 例として 800 に設定。
  • Use Node Up As Normal:
    • まずは ON(true)にしておくと、ノードの回転で反射方向を直感的に変えられます。
  • Line Color: 見やすい色(例: シアン (0,255,255,255))。
  • Line Width: 例として 4
  • Show Line: チェックを入れる。
  • Auto Clear Line Time: 例として 0.2(0.2 秒後に線を自動で消す)。
  • Debug Log: 動作確認中は ON にしておくとログが見やすいです。

4. PhysicsSystem2D の有効化(RayCast を使う場合)

  1. メニューから Project → Project Settings を開きます。
  2. 左側メニューで Physics 2D を選択します。
  3. Enable にチェックが入っていることを確認します(入っていない場合は ON にして保存)。

これで PhysicsSystem2D による RayCast が使えるようになります。

5. レーザー発射側の簡易テストスクリプト

ReflectorMirror 自体は完全独立ですが、動作確認のために「レーザーを発射して onLaserHit を呼ぶだけ」の簡単なスクリプトを用意します。(これはあくまでテスト用であり、本番では自分のレーザーシステムから onLaserHit を呼べばOKです)

  1. Assets パネルで右クリック → Create → TypeScript を選択し、TestLaserShooter.ts というファイルを作成します。
  2. 以下のコードを貼り付けます。

import { _decorator, Component, Node, Vec2, Vec3, input, Input, EventMouse } from 'cc';
import { ReflectorMirror } from './ReflectorMirror';
const { ccclass, property } = _decorator;

/**
 * シンプルなテスト用レーザー発射スクリプト。
 * クリックした方向にレーザーを飛ばし、Mirror ノードに onLaserHit を通知します。
 */
@ccclass('TestLaserShooter')
export class TestLaserShooter extends Component {

    @property({
        tooltip: 'レーザーを反射させたい ReflectorMirror コンポーネントをアタッチしたノードを指定します。',
    })
    public mirrorNode: Node | null = null;

    private _mirrorComp: ReflectorMirror | null = null;

    onLoad() {
        if (this.mirrorNode) {
            this._mirrorComp = this.mirrorNode.getComponent(ReflectorMirror);
            if (!this._mirrorComp) {
                console.error('[TestLaserShooter] 指定された mirrorNode に ReflectorMirror コンポーネントが見つかりません。');
            }
        } else {
            console.warn('[TestLaserShooter] mirrorNode が設定されていません。');
        }
    }

    start() {
        input.on(Input.EventType.MOUSE_DOWN, this._onMouseDown, this);
    }

    onDestroy() {
        input.off(Input.EventType.MOUSE_DOWN, this._onMouseDown, this);
    }

    private _onMouseDown(event: EventMouse) {
        if (!this._mirrorComp) return;

        // このノードの位置をレーザー発射位置とする
        const worldPos3 = this.node.worldPosition;
        const origin = new Vec2(worldPos3.x, worldPos3.y);

        // クリック位置をワールド座標に変換
        const location = event.getLocation();
        const target = new Vec2(location.x, location.y);

        // 発射方向ベクトル
        const dir = new Vec2(target.x - origin.x, target.y - origin.y);
        dir.normalize();

        // 今回は「レーザーが鏡に当たる位置」を簡易的に「鏡ノードの位置」とする
        const mirrorPos3 = this.mirrorNode!.worldPosition;
        const hitPoint = new Vec2(mirrorPos3.x, mirrorPos3.y);

        // 法線は ReflectorMirror の設定に任せるので、ここでは渡さない
        this._mirrorComp.onLaserHit(origin, dir, hitPoint, undefined);
    }
}

テストスクリプトのセットアップ

  1. Hierarchy で新しく空ノードを作成し、名前を LaserShooter にします。
  2. LaserShooter ノードを選択し、Inspector で Add Component → Custom → TestLaserShooter を追加します。
  3. TestLaserShooter コンポーネントの Mirror Node プロパティに、先ほど作成した Mirror ノードをドラッグ&ドロップします。
  4. シーンを保存します。

6. 実行して動作を確認する

  1. エディタ右上の Play ボタンを押してゲームを実行します。
  2. ゲーム画面内の適当な位置をクリックすると、その方向にレーザーが飛び、Mirror ノード位置で反射が起こります。
  3. ReflectorMirror の Use Node Up As Normal を ON にしている場合:
    • シーンビューで Mirror ノードを回転させてみてください。
    • ノードの「上方向」に応じて、反射レーザーの方向が物理的に変わるのが確認できます。
  4. Auto Clear Line Time を 0.2 にしている場合、0.2 秒後に線が自動的に消えます。
  5. Debug Log を ON にしていると、Console に入射ベクトル・法線・反射ベクトル・RayCast ヒット情報が表示されます。

まとめ

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

  • 入射レーザーの情報を onLaserHit で受け取るだけで、
  • 反射ベクトルの計算、
  • RayCast によるヒット検出、
  • 反射レーザーの線描画、
  • 一定時間後の自動クリア

までをすべて 1 つのスクリプトで完結させています。外部の GameManager や専用のレーザー管理クラスに依存しないため、

  • どのシーンでも、どのノードでも、ReflectorMirror をアタッチして onLaserHit を呼ぶだけ
  • インスペクタから距離・色・太さ・法線の扱いなどを調整するだけで、さまざまな鏡ギミックに即対応

といった柔軟な再利用が可能です。

応用例としては、

  • 反射レーザーが別の ReflectorMirror に当たったときに、さらに onLaserHit をチェーンして「多段反射」を実装する
  • RayCast のヒット先にスイッチ用コンポーネントがいたら、そこでイベントを発火する
  • 反射線の色や太さをゲーム内の状態(パワーアップ等)に応じて変更する

などが考えられます。
まずは今回のコンポーネントをベースに、あなたのゲームのレーザーギミックに合わせてカスタマイズしてみてください。

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