【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
– レーザーサイトの太さ。
– 例:3~8程度が扱いやすい。 - 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()
enabledOnStartがtrueの場合、自動でstartBlink()を呼び、点滅を開始します。falseの場合はstopBlink()を呼び、非表示のまま待機します。
- update(deltaTime)
_isBlinkingがtrueのとき、blinkIntervalごとに_toggleVisible()を呼んで表示/非表示を切り替えます。autoUpdateDirectionがtrueかつ現在表示中の場合、毎フレーム_drawLine()を呼び直し、ノードの回転変化に追従させます。
- _drawLine()
- ローカル座標で
startLocal=(lineOffset,0)、endLocal=(lineOffset+lineLength,0)を計算します。 useWorldSpaceがtrueの場合は、ノードのワールド変換行列からワールド座標を求めてその位置に線を描画します。falseの場合は、そのままローカル座標で線を描画します(通常はこちらで十分)。
- ローカル座標で
- startBlink() / stopBlink()
- ゲーム内で「発射準備中だけレーザーを点滅させたい」といった場合に備え、外部から呼び出せる API として用意しています。
- 本記事の範囲では、主に
enabledOnStartによる自動制御を想定しています。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を SniperLine.ts に変更します。
- 作成された
SniperLine.tsをダブルクリックしてエディタ(VSCode など)で開き、既存の中身をすべて削除して、前述のコードをそのまま貼り付けて保存します。
2. テスト用ノードの作成
ここでは 2D シーンで、銃口代わりのスプライトにアタッチしてテストしてみます。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、Gun など分かりやすい名前に変更します。
- Inspector の Sprite コンポーネントで、任意の画像を設定して銃っぽく見せます(画像がなければデフォルトのままでも構いません)。
- シーン上で Gun ノードの位置と回転を調整し、「右向き(X+ 方向)」に銃口が向くようにしておくと分かりやすいです。
3. SniperLine コンポーネントのアタッチ
- Hierarchy で先ほど作成した Gun ノードを選択します。
- Inspector の下部で Add Component をクリックします。
- Custom Component → SniperLine を選択して追加します。
- 追加直後、SniperLine の Inspector 上に以下のプロパティが表示されます。
- Enabled On Start: チェック(true)にしておくと、シーン開始と同時に点滅が始まります。
- Line Length: 例として 300 に設定。
- Line Width: 例として 4 に設定。
- Line Offset: 銃口の位置に合わせて調整。
– もし Gun ノードの中心が銃のグリップで、銃口が右端にある場合、20~40 くらいを試してみてください。 - Line Color: 既定で赤になっていますが、必要に応じて変更可能です。
- Blink Interval: 例として 0.2 ~ 0.3 に設定すると、レーザーサイトが素早く点滅しているように見えます。
- Initial Visible: 最初から光っていてほしい場合は ON(true)のまま。
- Use World Space: 通常は OFF(false)のままで問題ありません。
- Auto Update Direction: 銃が回転する予定があれば ON(true)にしておきます。
SniperLine は内部で Graphics コンポーネントを自動追加します。Inspector に Graphics が増えていることを確認できます。
4. シーンの実行と確認
- 上部ツールバーから Play(▶) ボタンをクリックしてゲームを実行します。
- シーン上で Gun ノードの位置から、右方向(X+ 方向)に向かって赤い線が点滅していることを確認します。
- もし線が銃口の位置と合っていない場合は、ゲームを停止し、Line Offset を微調整して再度再生してみてください。
- 銃のノードを回転させると、Auto Update Direction が有効な場合は、レーザーサイトも回転に追従して向きが変わります。
5. よくある調整ポイント
- レーザーが短すぎる/長すぎる → Line Length を調整。
- 線が太すぎる/細すぎる → Line Width を調整。
- 光り方が遅い/早すぎる → Blink Interval を調整(小さいほど高速点滅)。
- 別の色のレーザーにしたい → Line Color で色を変更。
- 銃の親ノードがスケーリングされていて線の見え方がおかしい → Use World Space を ON にして挙動を確認。
まとめ
この SniperLine コンポーネントは、
- 任意のノードにアタッチするだけで銃口から伸びるレーザーサイトを表現できる
- 長さ・太さ・色・点滅間隔・オフセットなどをインスペクタから柔軟に調整できる
- 外部の GameManager や他ノードに一切依存せず、このスクリプト単体で完結する
- Graphics コンポーネントの自動追加とエラーログにより、防御的に実装されている
といった特徴を持っています。
実際のゲームでは、
- 敵スナイパーがプレイヤーを狙う際の「警告用レーザー」
- ボスの砲台がチャージ中であることを示す「予兆演出」
- プレイヤーが狙いを定めるときの「照準補助」
など、多くの場面でそのまま再利用できます。
SniperLine をベースに、例えば「発射時に一時的にレーザーを伸ばす」「チャージ時間に応じて長さを変える」「フェードイン/アウトのアニメーションを付ける」などの拡張も容易です。
まずは本記事のコードをそのままプロジェクトに組み込み、シンプルなレーザーサイト演出から試してみてください。




