【Cocos Creator】アタッチするだけ!LaserBeam (レーザー)の実装方法【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】LaserBeam の実装:アタッチするだけで「RayCast2D で障害物に当たるまでのレーザー線を Line2D で描画&判定」する汎用スクリプト

このガイドでは、任意のノードにアタッチするだけで「2D レーザー」のような振る舞いを実現する LaserBeam コンポーネントを実装します。
RayCast2D を使って障害物までの距離を取得し、その区間を Line2D で可視化するので、シューティングゲームのビームやセンサー、視線チェックなどにそのまま使えます。

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

目標は次の通りです:

  • このコンポーネント単体で完結し、他のカスタムスクリプトに依存しない。
  • RayCast2D を用いて「レーザーが当たる最初のコライダー」までの距離を取得。
  • Line2D を用いて「発射元ノードからヒット位置(または最大距離)」までを直線描画。
  • インスペクタからレーザーの長さ・方向・更新頻度・衝突レイヤーなどを柔軟に調整可能。
  • 必要な標準コンポーネント(Line2D, PhysicsSystem2D)不備に対して防御的にエラーログを出力。

LaserBeam は「アタッチしたノードのローカル空間の +X 方向」を基準にレーザーを飛ばします。ノードの回転を変えるだけで、レーザーの向きも変わります。

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

以下の @property を用意します。

  • enabledLaser: boolean
    レーザーの ON/OFF。false にすると Line2D のポイントをクリアし、RayCast も行いません。
  • maxDistance: number
    レーザーの最大射程距離(ワールド単位)。RayCast2D の長さでもあり、Line2D もこの距離を上限として描画します。
  • updateInterval: number
    RayCast と Line2D 更新の間隔(秒)。0 なら毎フレーム update() で更新、0.05 なら約 20fps で更新といった具合です。
  • useWorldSpace: boolean
    Line2D の useWorldSpace に対応。true の場合、Line2D のポイントはワールド座標、false の場合はノードのローカル座標で扱います。
  • collisionMask: number
    RayCast2D の mask。どの物理レイヤーの Collider2D に当たるかをビットマスクで制御します。
    例:0xffffffff で全レイヤー、特定レイヤーのみを狙う場合はインスペクタで値を変更。
  • drawWhenNoHit: boolean
    RayCast2D で何にも当たらなかった場合に、maxDistance までレーザーを描画するかどうか。
  • startOffset: number
    レーザーの始点をノードのローカル +X 方向にどれだけオフセットするか。銃口が少し前にある場合などに調整します。
  • lineWidth: number
    Line2D の線の太さ。Line2D コンポーネントに反映します。
  • autoAddLine2D: boolean
    アタッチしたノードに Line2D コンポーネントが無い場合、自動で追加するかどうか。false の場合はエラーログを出し、動作を停止します。
  • debugLogHit: boolean
    RayCast がヒットした情報(距離、ノード名など)を console.log に出すかどうか。

また、内部的に PhysicsSystem2D を使用して RayCast を行います。これはエンジンの標準機能なので追加スクリプトは不要ですが、「2D Physics」が有効でないと RayCast 自体が動作しないため、ガイド内で設定手順を説明します。

TypeScriptコードの実装

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


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

/**
 * LaserBeam
 * - ノードの +X 方向に RayCast2D を飛ばし、最初に当たった位置まで Line2D で描画するコンポーネント。
 * - 他のカスタムスクリプトに依存せず、このコンポーネント単体で完結します。
 */
@ccclass('LaserBeam')
export class LaserBeam extends Component {

    @property({
        tooltip: 'レーザーの有効/無効を切り替えます。false の場合、Line2D はクリアされ RayCast も行いません。'
    })
    public enabledLaser: boolean = true;

    @property({
        tooltip: 'レーザーの最大射程距離(ワールド単位)。RayCast2D の長さでもあり、Line2D の最大長さでもあります。'
    })
    public maxDistance: number = 1000;

    @property({
        tooltip: 'RayCast と Line2D の更新間隔(秒)。0 で毎フレーム更新、0.05 で約 20fps 相当の更新になります。'
    })
    public updateInterval: number = 0;

    @property({
        tooltip: 'Line2D の useWorldSpace 設定。true: ワールド座標で描画 / false: ノードのローカル座標で描画。'
    })
    public useWorldSpace: boolean = false;

    @property({
        tooltip: 'RayCast2D の衝突マスク(物理レイヤー)。0xffffffff で全レイヤーにヒットします。'
    })
    public collisionMask: number = 0xffffffff;

    @property({
        tooltip: 'RayCast が何にもヒットしなかった場合にも maxDistance までレーザーを描画するかどうか。'
    })
    public drawWhenNoHit: boolean = true;

    @property({
        tooltip: 'レーザーの始点をノードの +X 方向にどれだけオフセットするか(ローカル座標系)。'
    })
    public startOffset: number = 0;

    @property({
        tooltip: 'Line2D の線の太さ。Line2D コンポーネントの width に反映されます。'
    })
    public lineWidth: number = 5;

    @property({
        tooltip: 'ノードに Line2D コンポーネントが無い場合、自動で追加するかどうか。false の場合はエラーログを出して動作を停止します。'
    })
    public autoAddLine2D: boolean = true;

    @property({
        tooltip: 'RayCast がヒットした情報(距離、ノード名など)をログ出力するかどうか。'
    })
    public debugLogHit: boolean = false;

    private _line2D: Line2D | null = null;
    private _timeAccumulator: number = 0;
    private _worldStart: Vec2 = new Vec2();
    private _worldEnd: Vec2 = new Vec2();

    onLoad() {
        // Line2D の取得 or 自動追加
        this._line2D = this.getComponent(Line2D);
        if (!this._line2D) {
            if (this.autoAddLine2D) {
                this._line2D = this.addComponent(Line2D);
                console.warn('[LaserBeam] Line2D コンポーネントが無かったため、自動で追加しました。ノード名:', this.node.name);
            } else {
                console.error('[LaserBeam] Line2D コンポーネントが見つかりません。autoAddLine2D=false のため、自動追加も行いません。ノードに Line2D を追加してください。ノード名:', this.node.name);
            }
        }

        if (this._line2D) {
            this._line2D.width = this.lineWidth;
            this._line2D.useWorldSpace = this.useWorldSpace;
        }

        // 2D 物理システムが有効かどうかの簡易チェック(無効でもエラーにはしないが、警告を出す)
        const physics2D = PhysicsSystem2D.instance;
        if (!physics2D) {
            console.error('[LaserBeam] PhysicsSystem2D が利用できません。Project Settings で 2D Physics が有効になっているか確認してください。');
        }
    }

    start() {
        // 初期状態の描画を行う
        this._updateLaser(0);
    }

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

        if (this.updateInterval <= 0) {
            // 毎フレーム更新
            this._updateLaser(deltaTime);
        } else {
            // 指定間隔で更新
            this._timeAccumulator += deltaTime;
            if (this._timeAccumulator >= this.updateInterval) {
                this._timeAccumulator = 0;
                this._updateLaser(deltaTime);
            }
        }
    }

    /**
     * レーザーの更新処理:RayCast2D を実行し、結果に応じて Line2D を描画。
     */
    private _updateLaser(deltaTime: number) {
        if (!this._line2D) {
            // Line2D が無い場合は何もしない
            return;
        }

        // ノードのローカル +X 方向をワールド空間に変換して Ray の方向ベクトルを求める
        const node = this.node;

        // ノードのワールド位置
        const worldPos = node.worldPosition;

        // 始点(ローカル空間で +X 方向に startOffset 分ずらした点)をワールド座標に変換
        const localStart = new Vec3(this.startOffset, 0, 0);
        const worldStart3D = node.getWorldMatrix().transformPoint(localStart, new Vec3());
        this._worldStart.set(worldStart3D.x, worldStart3D.y);

        // レーザーの方向:ノードのローカル +X (1,0) をワールド空間に変換して方向ベクトルを作る
        const localDirEnd = new Vec3(1, 0, 0);
        const worldDirEnd3D = node.getWorldMatrix().transformPoint(localDirEnd, new Vec3());
        const dir = new Vec2(
            worldDirEnd3D.x - worldPos.x,
            worldDirEnd3D.y - worldPos.y
        );
        if (dir.lengthSqr() === 0) {
            // 方向がゼロベクトルになることは通常無いが、念のため防御
            console.warn('[LaserBeam] レーザー方向ベクトルがゼロです。ノードのスケールが 0 になっていないか確認してください。');
            this._clearLine();
            return;
        }
        dir.normalize();

        // RayCast2D の終点
        this._worldEnd.x = this._worldStart.x + dir.x * this.maxDistance;
        this._worldEnd.y = this._worldStart.y + dir.y * this.maxDistance;

        const physics2D = PhysicsSystem2D.instance;
        if (!physics2D) {
            // 物理システムが無効な場合でもエラーにはせず、単に maxDistance まで描画する
            if (this.drawWhenNoHit) {
                this._drawLine(this._worldStart, this._worldEnd);
            } else {
                this._clearLine();
            }
            return;
        }

        // RayCast2D 実行
        const results = physics2D.raycast(this._worldStart, this._worldEnd, ERaycast2DType.Closest, this.collisionMask);

        if (results.length > 0) {
            // 最も近いヒット結果(ERaycast2DType.Closest を使っているため 0 番目が最も近い)
            const hit = results[0];
            const hitPoint = hit.point;

            if (this.debugLogHit) {
                const hitNodeName = hit.collider ? hit.collider.node.name : '(unknown)';
                console.log(`[LaserBeam] Hit: node=${hitNodeName}, distance=${hit.fraction * this.maxDistance}`);
            }

            this._drawLine(this._worldStart, hitPoint);
        } else {
            // 何にも当たらなかった
            if (this.drawWhenNoHit) {
                this._drawLine(this._worldStart, this._worldEnd);
            } else {
                this._clearLine();
            }
        }
    }

    /**
     * Line2D に 2 点の線分を描画する。
     * useWorldSpace 設定に応じてワールド/ローカル座標を変換。
     */
    private _drawLine(startWorld: Vec2, endWorld: Vec2) {
        if (!this._line2D) {
            return;
        }

        this._line2D.width = this.lineWidth;
        this._line2D.useWorldSpace = this.useWorldSpace;

        if (this.useWorldSpace) {
            // そのままワールド座標として設定
            this._line2D.points = [startWorld.clone(), endWorld.clone()];
        } else {
            // ワールド座標をローカル座標に変換して設定
            const startLocal3D = new Vec3();
            const endLocal3D = new Vec3();
            this.node.inverseTransformPoint(startLocal3D, new Vec3(startWorld.x, startWorld.y, 0));
            this.node.inverseTransformPoint(endLocal3D, new Vec3(endWorld.x, endWorld.y, 0));

            const startLocal2D = new Vec2(startLocal3D.x, startLocal3D.y);
            const endLocal2D = new Vec2(endLocal3D.x, endLocal3D.y);

            this._line2D.points = [startLocal2D, endLocal2D];
        }
    }

    /**
     * Line2D の描画をクリアする。
     */
    private _clearLine() {
        if (this._line2D) {
            this._line2D.points = [];
        }
    }
}

コードのポイント解説

  • onLoad()
    • アタッチされたノードから Line2D を取得し、無ければ autoAddLine2D が true のとき自動追加。
    • Line2D が最終的に無い場合はエラーログを出し、その後の描画はスキップします。
    • 2D 物理システム(PhysicsSystem2D)の存在をチェックし、無効なら警告ログを出します。
  • start()
    • ゲーム開始直後に 1 回だけ _updateLaser() を呼び、初期状態を描画。
  • update(deltaTime)
    • enabledLaser が false の場合は Line2D をクリアして処理終了。
    • updateInterval が 0 以下なら毎フレーム更新、それ以外なら積算時間が閾値を超えたときだけ更新。
  • _updateLaser()
    • ノードのローカル +X 方向をワールド空間に変換し、Ray の始点と方向を算出。
    • 始点は startOffset によってローカル +X 方向にずらしています。
    • PhysicsSystem2D が無効な場合は RayCast をスキップし、drawWhenNoHit に応じて最大距離まで描画 or クリア。
    • RayCast2D の結果があれば、最も近いヒット位置まで線を描画し、debugLogHit が true ならログ出力。
    • 何も当たらなければ drawWhenNoHit に従って描画 or クリア。
  • _drawLine()
    • useWorldSpace に応じて、ワールド座標のまま設定するか、ローカル座標に変換して設定するかを切り替え。
    • Line2D の points に始点と終点の 2 点だけを渡し、シンプルな直線レーザーを描画。
  • _clearLine()
    • Line2D の points を空配列にして、レーザーを非表示にします。

使用手順と動作確認

ここからは、Cocos Creator 3.8.7 エディタで実際に LaserBeam を使う手順を説明します。

1. スクリプトファイルを作成する

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

2. 2D Physics を有効にする(RayCast2D の前提設定)

  1. メインメニューから Project → Project Settings... を開きます。
  2. 左側のメニューで Physics または Physics 2D を選択します。
  3. Enable(2D Physics 有効化)がオンになっていることを確認します。
  4. 必要に応じて Fixed TimeStepVelocity Iteration などを調整しますが、基本的にはデフォルトのままで問題ありません。
  5. 設定を閉じます。

3. テスト用ノード(レーザー発射元)を作成する

  1. Hierarchy パネルで右クリックします。
  2. Create → Empty Node を選択し、ノード名を LaserOrigin などに変更します。
  3. 選択したノードの Inspector で、Position(例: (0, 0, 0))や Rotation を適宜設定します。
    • レーザーの向きはノードのローカル +X 方向なので、右向きに撃ちたい場合は Rotation = (0, 0, 0)。
    • 上向きに撃ちたい場合は Rotation Z = 90° などにします。

4. LaserBeam コンポーネントをアタッチする

  1. Hierarchy で LaserOrigin ノードを選択します。
  2. Inspector の下部にある Add Component ボタンをクリックします。
  3. Custom カテゴリから LaserBeam を選択して追加します。

追加すると、Inspector に LaserBeam の各種プロパティが表示されます。

5. Line2D の準備を確認する

LaserBeam は自動的に Line2D を追加する設計ですが、挙動を明示的に確認したい場合:

  1. LaserBeam コンポーネントの autoAddLine2Dtrue になっていることを確認します。
  2. ゲームを一度再生すると、Console に
    [LaserBeam] Line2D コンポーネントが無かったため、自動で追加しました。
    と出力され、LaserOrigin ノードに Line2D が追加されます。
  3. 手動で追加したい場合は、autoAddLine2Dfalse にし、Add Component → Rendering → Line2D を選択して追加します。

6. 衝突判定用のオブジェクトを配置する

RayCast2D のヒットを確認するために、Collider2D を持つオブジェクトを用意します。

  1. Hierarchy で右クリック → Create → 2D Objects → Sprite などでテスト用スプライトを作成します。
  2. ノード名を TargetBox などに変更します。
  3. Inspector で Add Component → Physics 2D → BoxCollider2D を追加します。
    • Collider のサイズ・オフセットはスプライトに合わせて調整します。
    • 必要に応じて GroupMask(物理レイヤー)を設定します。
  4. LaserOrigin ノードの +X 方向(右側)に TargetBox を移動し、レーザーが通る位置に配置します。

7. LaserBeam プロパティの調整例

Inspector で LaserBeam の各プロパティを以下のように設定してみてください。

  • enabledLaser: true
  • maxDistance: 1000
  • updateInterval: 0(毎フレーム更新)
  • useWorldSpace: false(ノードローカル座標で描画)
  • collisionMask: 0xffffffff(全レイヤーにヒット)
  • drawWhenNoHit: true(何も当たらなくても maxDistance まで描画)
  • startOffset: 0(ノード中心から発射)
  • lineWidth: 5
  • autoAddLine2D: true
  • debugLogHit: true

8. ゲームを再生して動作を確認する

  1. エディタ上部の Play ボタンをクリックしてゲームを再生します。
  2. Scene ビューまたは Game ビューで、LaserOrigin から TargetBox に向けてレーザーの線が表示されることを確認します。
  3. TargetBox をドラッグしてレーザーの進路上に置いたり外したりすると、レーザーの終点が
    • 当たっているとき:TargetBox 上で止まる
    • 当たっていないとき:maxDistance まで伸びる(drawWhenNoHit = true の場合)
  4. debugLogHit が true の場合、Console に
    [LaserBeam] Hit: node=TargetBox, distance=...
    のようなログが出力されることを確認します。
  5. LaserOrigin ノードの Rotation Z を変えて、レーザーの向きがノードの回転に応じて変わることも確認してみてください。

9. よくある調整パターン

  • 銃口を少し前にずらしたい
    モデルやスプライトの中心から少し先端でレーザーを出したい場合は、startOffset を正の値(例: 20)に設定します。
  • パフォーマンス重視で更新頻度を下げたい
    レーザーが常に動いていない、もしくは敵の動きが遅い場合などは、updateInterval0.050.1 にして、RayCast の呼び出し頻度を下げると良いです。
  • 特定の物理レイヤーだけに反応させたい
    Project Settings → Physics 2D でレイヤーを設定し、Collider2D の Group/Mask を適切に設定した上で、
    collisionMask に対象レイヤーのビットマスク値を設定します。

まとめ

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

  • RayCast2D で「最初に当たるオブジェクト」までの距離を取得し、
  • Line2D でその区間を直線描画する

という一連の処理を、単一スクリプトだけで完結させた汎用コンポーネントです。外部の GameManager やシングルトンに依存しないため、

  • 任意のノードにアタッチしてそのまま「レーザー」「センサー」「視線チェック」などとして再利用できる
  • プロパティから射程・更新頻度・描画方式・衝突レイヤーを調整するだけで、さまざまなゲームに流用可能
  • Line2D の見た目(色やマテリアル)は Line2D 側で自由にカスタマイズできる

といった利点があります。

このパターンを応用すれば、

  • 複数本のレーザーを回転させるトラップ
  • プレイヤーや敵の視界判定(視線が壁に遮られているかどうか)
  • レーザーが当たったオブジェクトにダメージやステータス効果を与える処理

なども、同じく「単体コンポーネント」として設計していくことができます。
まずはこの LaserBeam.ts をプロジェクトに組み込み、さまざまなシーンでアタッチ&調整しながら、自分のゲームに合ったレーザー表現に発展させてみてください。

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