【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()を呼び、初期状態を描画。
- ゲーム開始直後に 1 回だけ
- 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を空配列にして、レーザーを非表示にします。
- Line2D の
使用手順と動作確認
ここからは、Cocos Creator 3.8.7 エディタで実際に LaserBeam を使う手順を説明します。
1. スクリプトファイルを作成する
- Assets パネルで右クリックします。
Create → TypeScriptを選択します。- ファイル名を
LaserBeam.tsに変更します。 - 作成された
LaserBeam.tsをダブルクリックしてエディタ(VSCode など)で開き、前述のコードをそのまま貼り付けて保存します。
2. 2D Physics を有効にする(RayCast2D の前提設定)
- メインメニューから
Project → Project Settings...を開きます。 - 左側のメニューで
PhysicsまたはPhysics 2Dを選択します。 Enable(2D Physics 有効化)がオンになっていることを確認します。- 必要に応じて
Fixed TimeStepやVelocity Iterationなどを調整しますが、基本的にはデフォルトのままで問題ありません。 - 設定を閉じます。
3. テスト用ノード(レーザー発射元)を作成する
- Hierarchy パネルで右クリックします。
Create → Empty Nodeを選択し、ノード名をLaserOriginなどに変更します。- 選択したノードの Inspector で、Position(例:
(0, 0, 0))や Rotation を適宜設定します。- レーザーの向きはノードのローカル +X 方向なので、右向きに撃ちたい場合は Rotation = (0, 0, 0)。
- 上向きに撃ちたい場合は Rotation Z = 90° などにします。
4. LaserBeam コンポーネントをアタッチする
- Hierarchy で
LaserOriginノードを選択します。 - Inspector の下部にある
Add Componentボタンをクリックします。 CustomカテゴリからLaserBeamを選択して追加します。
追加すると、Inspector に LaserBeam の各種プロパティが表示されます。
5. Line2D の準備を確認する
LaserBeam は自動的に Line2D を追加する設計ですが、挙動を明示的に確認したい場合:
- LaserBeam コンポーネントの
autoAddLine2Dがtrueになっていることを確認します。 - ゲームを一度再生すると、Console に
[LaserBeam] Line2D コンポーネントが無かったため、自動で追加しました。
と出力され、LaserOriginノードに Line2D が追加されます。 - 手動で追加したい場合は、
autoAddLine2Dをfalseにし、Add Component → Rendering → Line2Dを選択して追加します。
6. 衝突判定用のオブジェクトを配置する
RayCast2D のヒットを確認するために、Collider2D を持つオブジェクトを用意します。
- Hierarchy で右クリック →
Create → 2D Objects → Spriteなどでテスト用スプライトを作成します。 - ノード名を
TargetBoxなどに変更します。 - Inspector で
Add Component → Physics 2D → BoxCollider2Dを追加します。- Collider のサイズ・オフセットはスプライトに合わせて調整します。
- 必要に応じて
GroupやMask(物理レイヤー)を設定します。
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. ゲームを再生して動作を確認する
- エディタ上部の
Playボタンをクリックしてゲームを再生します。 - Scene ビューまたは Game ビューで、
LaserOriginからTargetBoxに向けてレーザーの線が表示されることを確認します。 TargetBoxをドラッグしてレーザーの進路上に置いたり外したりすると、レーザーの終点が- 当たっているとき:
TargetBox上で止まる - 当たっていないとき:
maxDistanceまで伸びる(drawWhenNoHit = true の場合)
- 当たっているとき:
debugLogHitが true の場合、Console に
[LaserBeam] Hit: node=TargetBox, distance=...
のようなログが出力されることを確認します。LaserOriginノードの Rotation Z を変えて、レーザーの向きがノードの回転に応じて変わることも確認してみてください。
9. よくある調整パターン
- 銃口を少し前にずらしたい
モデルやスプライトの中心から少し先端でレーザーを出したい場合は、startOffsetを正の値(例: 20)に設定します。 - パフォーマンス重視で更新頻度を下げたい
レーザーが常に動いていない、もしくは敵の動きが遅い場合などは、updateIntervalを0.05や0.1にして、RayCast の呼び出し頻度を下げると良いです。 - 特定の物理レイヤーだけに反応させたい
Project Settings → Physics 2D でレイヤーを設定し、Collider2D のGroup/Maskを適切に設定した上で、
collisionMaskに対象レイヤーのビットマスク値を設定します。
まとめ
この LaserBeam コンポーネントは、
- RayCast2D で「最初に当たるオブジェクト」までの距離を取得し、
- Line2D でその区間を直線描画する
という一連の処理を、単一スクリプトだけで完結させた汎用コンポーネントです。外部の GameManager やシングルトンに依存しないため、
- 任意のノードにアタッチしてそのまま「レーザー」「センサー」「視線チェック」などとして再利用できる
- プロパティから射程・更新頻度・描画方式・衝突レイヤーを調整するだけで、さまざまなゲームに流用可能
- Line2D の見た目(色やマテリアル)は Line2D 側で自由にカスタマイズできる
といった利点があります。
このパターンを応用すれば、
- 複数本のレーザーを回転させるトラップ
- プレイヤーや敵の視界判定(視線が壁に遮られているかどうか)
- レーザーが当たったオブジェクトにダメージやステータス効果を与える処理
なども、同じく「単体コンポーネント」として設計していくことができます。
まずはこの LaserBeam.ts をプロジェクトに組み込み、さまざまなシーンでアタッチ&調整しながら、自分のゲームに合ったレーザー表現に発展させてみてください。




