【Cocos Creator】アタッチするだけ!SniperLine (照準線)の実装方法【TypeScript】

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

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

脱・初心者!Godot 4 ゲーム開発の「2歩目」

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

Godot4ローグライク入門 ~ダンジョン自動生成~

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

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

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

【Cocos Creator 3.8】SniperLine(照準線)の実装:アタッチするだけで銃口から伸びる赤いレーザーサイトを点滅表示する汎用スクリプト

このガイドでは、任意のノードにアタッチするだけで「銃口から伸びる赤いレーザーサイト(照準線)」を点滅表示できる汎用コンポーネント SniperLine を実装します。
銃やタレットのノードに付けるだけで、発射前の「狙っている感」を簡単に演出でき、長さ・太さ・点滅速度・色などをインスペクタから調整可能な設計にします。


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

1. 機能要件の整理

  • このコンポーネントをアタッチしたノードの「ローカルX軸の正方向」に向かって、レーザーサイト(線)を表示する。
  • レーザーサイトは赤色を基本としつつ、インスペクタから色変更可能にする。
  • 線は一定の周期で「点滅」する(フェードイン・フェードアウトではなく、ON/OFFの切り替え)。
  • 線の長さ・太さ・オフセット(銃口から少し前方から出したい等)をインスペクタで調整可能にする。
  • 外部の GameManager や他ノードに依存せず、このスクリプト単体で完結する。
  • 標準コンポーネント(Graphics)に依存するが、コード内で自動追加を試み、防御的に動作する。

2. 表現方法の選択

レーザーサイトは以下の理由から Graphics コンポーネントで線を描画 する方式を採用します。

  • 1本のシンプルな線なので、スプライト画像を用意するよりも簡単で柔軟。
  • 線の長さ・太さ・色をコードから簡単に変更可能。
  • SniperLine コンポーネント内で完結しやすい。

SniperLine は自身のノードに Graphics を持っていない場合、自動で追加を試みます。
それでも取得できない場合はエラーログを出して動作を停止します。

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

SniperLine コンポーネントで用意する @property は以下の通りです。

  • enabledOnStart: boolean
    – 初期状態でレーザーを点滅させるかどうか。
    true: シーン開始と同時に点滅開始。
    false: 初期状態で非表示。必要なタイミングで startBlink() / stopBlink() を呼ぶ想定(将来的な拡張用)。
  • lineLength: number
    – レーザーサイトの長さ(ローカル座標系での X 方向の距離)。
    – 単位はピクセル相当(Canvas/2D空間の座標系)。
    – 例:300 なら、ノードの原点から X+ 方向へ 300 の線を描画。
  • lineWidth: number
    – レーザーサイトの太さ。
    – 例:38 程度が扱いやすい。
  • lineOffset: number
    – 線の開始位置をノード原点からどれだけ前方(X+ 方向)にずらすか。
    – 銃口がノードの中心から少し離れている場合などに使用。
    – 例:0 なら原点から描画、20 なら X=20 から線を描画。
  • lineColor: Color
    – レーザーサイトの色。
    – 既定値は赤 (Color.RED)。
    – 緑レーザーや他の色にしたい場合はここで変更。
  • blinkInterval: number
    – 点滅周期(秒)。ON/OFF を切り替える間隔。
    – 例:0.2 なら 0.2 秒ごとに表示・非表示を切り替える。
  • initialVisible: boolean
    – 点滅開始時に最初を「表示状態」にするかどうか。
    true: 最初は表示 → 1周期後に非表示 → …
    false: 最初は非表示 → 1周期後に表示 → …
  • useWorldSpace: boolean
    – 線の描画座標をローカル空間とワールド空間のどちらで扱うか。
    – 通常は false(ローカル空間)で問題ない。
    true にすると、親ノードのスケールや回転に影響されないワールド座標で線を描く。
  • autoUpdateDirection: boolean
    – 毎フレーム、ノードの回転に合わせて線の向きを更新するかどうか。
    – 銃やタレットが回転する場合は true にする。

TypeScriptコードの実装

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


import { _decorator, Component, Node, Graphics, Color, Vec3, math, warn, error } from 'cc';
const { ccclass, property } = _decorator;

/**
 * SniperLine
 * 任意のノードにアタッチするだけで、
 * ノードのローカルX+方向に「レーザーサイト(線)」を点滅表示するコンポーネント。
 */
@ccclass('SniperLine')
export class SniperLine extends Component {

    @property({
        tooltip: 'シーン開始時に自動で点滅を開始するかどうか。\ntrue: 自動で点滅開始 / false: 手動で startBlink() を呼ぶ想定'
    })
    public enabledOnStart: boolean = true;

    @property({
        tooltip: 'レーザーサイトの長さ(ローカルX+方向の距離)。単位はピクセル相当。',
        min: 0
    })
    public lineLength: number = 300;

    @property({
        tooltip: 'レーザーサイトの太さ(ピクセル相当)。',
        min: 0.1
    })
    public lineWidth: number = 3;

    @property({
        tooltip: 'レーザーの開始位置オフセット(ノード原点からローカルX+方向へどれだけずらすか)。'
    })
    public lineOffset: number = 0;

    @property({
        tooltip: 'レーザーサイトの色。既定値は赤。',
    })
    public lineColor: Color = Color.RED.clone();

    @property({
        tooltip: '点滅周期(秒)。この時間ごとに表示/非表示を切り替えます。',
        min: 0.01
    })
    public blinkInterval: number = 0.2;

    @property({
        tooltip: '点滅開始時に最初を「表示状態」にするかどうか。\ntrue: 最初は表示 / false: 最初は非表示'
    })
    public initialVisible: boolean = true;

    @property({
        tooltip: 'ワールド空間で線を描画するかどうか。\n通常は false(ローカル空間)で問題ありません。'
    })
    public useWorldSpace: boolean = false;

    @property({
        tooltip: '毎フレーム、ノードの回転に合わせて線の向きを更新するかどうか。\n銃やタレットが回転する場合は true を推奨。'
    })
    public autoUpdateDirection: boolean = true;

    // 内部用: 線描画に使用する Graphics コンポーネント
    private _graphics: Graphics | null = null;

    // 内部用: 現在の表示状態
    private _isVisible: boolean = false;

    // 内部用: 点滅制御
    private _blinkTimer: number = 0;
    private _isBlinking: boolean = false;

    onLoad() {
        // Graphics コンポーネントを取得(なければ追加を試みる)
        let graphics = this.getComponent(Graphics);
        if (!graphics) {
            // 自動追加を試みる
            graphics = this.node.addComponent(Graphics);
            if (!graphics) {
                error('[SniperLine] Graphics コンポーネントを追加できませんでした。このノードに Graphics を手動で追加してください。');
                return;
            } else {
                warn('[SniperLine] Graphics コンポーネントが見つからなかったため、自動で追加しました。');
            }
        }
        this._graphics = graphics;

        // Graphics の初期設定
        this._graphics.lineWidth = this.lineWidth;
        this._graphics.strokeColor = this.lineColor;

        // 初期状態では一旦クリア
        this._graphics.clear();

        // 初期表示状態を設定
        this._isVisible = this.initialVisible;
        if (this._isVisible) {
            this._drawLine();
        } else {
            this._graphics.clear();
        }
    }

    start() {
        if (!this._graphics) {
            // onLoad で取得に失敗している場合、ここでも警告を出して終了
            error('[SniperLine] Graphics コンポーネントが存在しないため、SniperLine は動作しません。');
            return;
        }

        // シーン開始時に自動で点滅開始するかどうか
        if (this.enabledOnStart) {
            this.startBlink();
        } else {
            this.stopBlink();
        }
    }

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

        // 点滅処理
        if (this._isBlinking) {
            this._blinkTimer += deltaTime;
            if (this._blinkTimer >= this.blinkInterval) {
                this._blinkTimer = 0;
                this._toggleVisible();
            }
        }

        // 向きの自動更新
        if (this.autoUpdateDirection && this._isVisible) {
            // 回転が変わっても線の方向を維持するため、毎フレーム描き直す
            this._drawLine();
        }
    }

    /**
     * 点滅を開始する(外部からも呼び出し可能)。
     */
    public startBlink(): void {
        if (!this._graphics) {
            error('[SniperLine] startBlink() が呼ばれましたが、Graphics コンポーネントが存在しません。');
            return;
        }
        this._isBlinking = true;
        this._blinkTimer = 0;
        // 初期状態を再設定
        this._isVisible = this.initialVisible;
        if (this._isVisible) {
            this._drawLine();
        } else {
            this._graphics.clear();
        }
    }

    /**
     * 点滅を停止し、線を非表示にする(外部からも呼び出し可能)。
     */
    public stopBlink(): void {
        this._isBlinking = false;
        this._blinkTimer = 0;
        this._isVisible = false;
        if (this._graphics) {
            this._graphics.clear();
        }
    }

    /**
     * 現在の表示状態を反転する(内部用)。
     */
    private _toggleVisible(): void {
        this._isVisible = !this._isVisible;
        if (!this._graphics) {
            return;
        }

        if (this._isVisible) {
            this._drawLine();
        } else {
            this._graphics.clear();
        }
    }

    /**
     * レーザーサイトの線を描画する。
     * ノードのローカルX+方向に対して lineOffset から lineLength 分だけ線を引く。
     * useWorldSpace が true の場合はワールド座標系で描画。
     */
    private _drawLine(): void {
        if (!this._graphics) {
            return;
        }

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

        // 線の開始点と終了点(ローカル空間)
        const startLocal = new Vec3(this.lineOffset, 0, 0);
        const endLocal = new Vec3(this.lineOffset + this.lineLength, 0, 0);

        if (this.useWorldSpace) {
            // ワールド空間で描画する場合、ローカル→ワールド変換
            const worldStart = new Vec3();
            const worldEnd = new Vec3();
            this.node.getWorldMatrix().transformPoint(startLocal, worldStart);
            this.node.getWorldMatrix().transformPoint(endLocal, worldEnd);

            g.moveTo(worldStart.x, worldStart.y);
            g.lineTo(worldEnd.x, worldEnd.y);
        } else {
            // ローカル空間で描画
            g.moveTo(startLocal.x, startLocal.y);
            g.lineTo(endLocal.x, endLocal.y);
        }

        g.lineWidth = this.lineWidth;
        g.strokeColor = this.lineColor;
        g.stroke();
    }
}

コードの要点解説

  • onLoad()
    • Graphics コンポーネントを getComponent で取得し、存在しなければ addComponent(Graphics) で自動追加を試みます。
    • 取得に失敗した場合は error ログを出して終了します。
    • 初期の線幅と色を設定し、初期表示状態(initialVisible)に応じて線を描画またはクリアします。
  • start()
    • enabledOnStarttrue の場合、自動で startBlink() を呼び、点滅を開始します。
    • false の場合は stopBlink() を呼び、非表示のまま待機します。
  • update(deltaTime)
    • _isBlinkingtrue のとき、blinkInterval ごとに _toggleVisible() を呼んで表示/非表示を切り替えます。
    • autoUpdateDirectiontrue かつ現在表示中の場合、毎フレーム _drawLine() を呼び直し、ノードの回転変化に追従させます。
  • _drawLine()
    • ローカル座標で startLocal=(lineOffset,0)endLocal=(lineOffset+lineLength,0) を計算します。
    • useWorldSpacetrue の場合は、ノードのワールド変換行列からワールド座標を求めてその位置に線を描画します。
    • false の場合は、そのままローカル座標で線を描画します(通常はこちらで十分)。
  • startBlink() / stopBlink()
    • ゲーム内で「発射準備中だけレーザーを点滅させたい」といった場合に備え、外部から呼び出せる API として用意しています。
    • 本記事の範囲では、主に enabledOnStart による自動制御を想定しています。

使用手順と動作確認

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

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

2. テスト用ノードの作成

ここでは 2D シーンで、銃口代わりのスプライトにアタッチしてテストしてみます。

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、Gun など分かりやすい名前に変更します。
  2. Inspector の Sprite コンポーネントで、任意の画像を設定して銃っぽく見せます(画像がなければデフォルトのままでも構いません)。
  3. シーン上で Gun ノードの位置と回転を調整し、「右向き(X+ 方向)」に銃口が向くようにしておくと分かりやすいです。

3. SniperLine コンポーネントのアタッチ

  1. Hierarchy で先ほど作成した Gun ノードを選択します。
  2. Inspector の下部で Add Component をクリックします。
  3. Custom ComponentSniperLine を選択して追加します。
  4. 追加直後、SniperLine の Inspector 上に以下のプロパティが表示されます。
  • Enabled On Start: チェック(true)にしておくと、シーン開始と同時に点滅が始まります。
  • Line Length: 例として 300 に設定。
  • Line Width: 例として 4 に設定。
  • Line Offset: 銃口の位置に合わせて調整。
    – もし Gun ノードの中心が銃のグリップで、銃口が右端にある場合、20~40 くらいを試してみてください。
  • Line Color: 既定で赤になっていますが、必要に応じて変更可能です。
  • Blink Interval: 例として 0.20.3 に設定すると、レーザーサイトが素早く点滅しているように見えます。
  • Initial Visible: 最初から光っていてほしい場合は ON(true)のまま。
  • Use World Space: 通常は OFF(false)のままで問題ありません。
  • Auto Update Direction: 銃が回転する予定があれば ON(true)にしておきます。

SniperLine は内部で Graphics コンポーネントを自動追加します。Inspector に Graphics が増えていることを確認できます。

4. シーンの実行と確認

  1. 上部ツールバーから Play(▶) ボタンをクリックしてゲームを実行します。
  2. シーン上で Gun ノードの位置から、右方向(X+ 方向)に向かって赤い線が点滅していることを確認します。
  3. もし線が銃口の位置と合っていない場合は、ゲームを停止し、Line Offset を微調整して再度再生してみてください。
  4. 銃のノードを回転させると、Auto Update Direction が有効な場合は、レーザーサイトも回転に追従して向きが変わります。

5. よくある調整ポイント

  • レーザーが短すぎる/長すぎる → Line Length を調整。
  • 線が太すぎる/細すぎる → Line Width を調整。
  • 光り方が遅い/早すぎる → Blink Interval を調整(小さいほど高速点滅)。
  • 別の色のレーザーにしたい → Line Color で色を変更。
  • 銃の親ノードがスケーリングされていて線の見え方がおかしい → Use World SpaceON にして挙動を確認。

まとめ

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

  • 任意のノードにアタッチするだけで銃口から伸びるレーザーサイトを表現できる
  • 長さ・太さ・色・点滅間隔・オフセットなどをインスペクタから柔軟に調整できる
  • 外部の GameManager や他ノードに一切依存せず、このスクリプト単体で完結する
  • Graphics コンポーネントの自動追加とエラーログにより、防御的に実装されている

といった特徴を持っています。

実際のゲームでは、

  • 敵スナイパーがプレイヤーを狙う際の「警告用レーザー」
  • ボスの砲台がチャージ中であることを示す「予兆演出」
  • プレイヤーが狙いを定めるときの「照準補助」

など、多くの場面でそのまま再利用できます。

SniperLine をベースに、例えば「発射時に一時的にレーザーを伸ばす」「チャージ時間に応じて長さを変える」「フェードイン/アウトのアニメーションを付ける」などの拡張も容易です。
まずは本記事のコードをそのままプロジェクトに組み込み、シンプルなレーザーサイト演出から試してみてください。

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

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

脱・初心者!Godot 4 ゲーム開発の「2歩目」

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

Godot4ローグライク入門 ~ダンジョン自動生成~

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

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

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

URLをコピーしました!