【Cocos Creator 3.8】VisionCone(視界判定)の実装:アタッチするだけで「RayCast2Dによる視界チェック」を実現する汎用スクリプト
このガイドでは、任意のノードにアタッチするだけで、2D物理の RayCast を使って「ターゲットが見えているか(壁などの遮蔽物がないか)」を判定できる VisionCone コンポーネントを実装します。
敵 AI の「プレイヤーを見つけたかどうか」や、見通し線のチェックなどにそのまま使えるよう、外部スクリプトに一切依存しない独立コンポーネントとして設計します。
コンポーネントの設計方針
機能要件の整理
- このコンポーネントをアタッチしたノード(以下「オーナー」)から、指定したターゲットノードに向けて 2D RayCast を飛ばす。
- RayCast の結果から ターゲットまでの間に遮蔽物(Collider2D)があるかどうか を判定する。
- ターゲットが視界内かどうかを、角度・距離・レイヤーマスクで制限できる。
- 結果は
isVisibleという 読み取り専用の公開プロパティとして他コンポーネントから参照できる。 - デバッグ用に Scene ビュー上に Ray を可視化できる(任意)。
- 他のカスタムスクリプトに依存せず、@property で設定された情報だけで完結する。
外部依存をなくすための設計アプローチ
- ターゲットは @property(Node) で Inspector から直接指定する。
- RayCast には PhysicsSystem2D と ERaycast2DType.CLOSEST を使用し、物理レイヤー(group)と mask を @property で指定できるようにする。
- 特定の「壁タグ」などには依存せず、物理レイヤー(グループ)とマスクだけで遮蔽物を判定する。
- 必須コンポーネントはないが、PhysicsSystem2D が有効になっていない場合は警告ログを出す。
- デバッグ描画はあくまで補助機能とし、有効・無効を Inspector から切り替え可能にする。
インスペクタで設定可能なプロパティ設計
- target :
Node | null
視界判定の対象となるターゲットノード。通常はプレイヤーや敵など。 - maxDistance :
number(初期値: 10)
RayCast の最大距離(ワールド単位)。ターゲットがこれ以上離れている場合はisVisible = false。 - fieldOfView :
number(度数、初期値: 120)
視野角(FOV)。オーナーノードの「前方」から左右にこの角度の半分だけ見える範囲。
例: 120 を指定すると前方 ±60° が視界となる。 - forwardAxis :
number(0: +X, 1: +Y, 初期値: 0)
ノードの「前方」をどのローカル軸とみなすか。
– 0: ローカル +X 方向を前方とする(横スクロール系に便利)
– 1: ローカル +Y 方向を前方とする(縦方向が前のキャラなど) - useWorldSpaceTarget :
boolean(初期値: true)
ターゲット位置の取得方法。通常は true のままでよい。
– true: ターゲットのワールド座標を使用
– false: ターゲットのローカル座標を使用(特殊なケース用) - raycastMask :
number(初期値: 0xFFFFFFFF)
RayCast が衝突判定を行う物理レイヤー(ビットマスク)。
「壁」や「障害物」グループを含めるように設定する。 - checkInterval :
number(初期値: 0)
視界判定を行う間隔(秒)。
– 0 以下: 毎フレームupdate()でチェック
– 0.1 など: 0.1 秒ごとにチェック(パフォーマンス節約) - debugDraw :
boolean(初期値: false)
Scene ビュー上に視界 Ray を描画するかどうか。 - debugColorVisible :
Color(初期値: 緑)
ターゲットが「見えている」場合の Ray 線の色。 - debugColorBlocked :
Color(初期値: 赤)
ターゲットが「遮蔽物に隠れている」場合の Ray 線の色。 - debugColorOutOfFOV :
Color(初期値: 黄)
ターゲットが距離や視野角の条件を満たしていない場合の Ray 線の色。
また、判定結果を他のコンポーネントから参照できるように、以下の 読み取り専用プロパティも用意します。
- isVisible :
boolean
現在のフレームまたは最後のチェック時点での「ターゲットが見えているか」の結果。
TypeScriptコードの実装
import {
_decorator,
Component,
Node,
Vec2,
Vec3,
PhysicsSystem2D,
ERaycast2DType,
IPhysics2DRaycastResult,
Color,
Graphics,
Camera,
geometry,
} from 'cc';
const { ccclass, property } = _decorator;
/**
* VisionCone
* 2D RayCast を用いた視界判定コンポーネント。
* - 親ノードからターゲットノードに向けて RayCast を飛ばし、
* 距離・視野角・遮蔽物の有無から「見えているか」を判定する。
* - 他のカスタムスクリプトには依存しない。
*/
@ccclass('VisionCone')
export class VisionCone extends Component {
@property({
type: Node,
tooltip: '視界判定の対象となるターゲットノード。通常はプレイヤーなどを指定します。',
})
public target: Node | null = null;
@property({
tooltip: 'RayCast の最大距離(ワールド単位)。この距離を超えるターゲットは見えない扱いになります。',
min: 0.01,
})
public maxDistance: number = 10;
@property({
tooltip: '視野角(度数)。オーナーノードの前方から左右にこの角度の半分だけが視界となります。',
min: 0,
max: 360,
})
public fieldOfView: number = 120;
@property({
tooltip: 'ノードの「前方」とみなすローカル軸。\n0: +X 方向を前方\n1: +Y 方向を前方',
min: 0,
max: 1,
})
public forwardAxis: number = 0; // 0: +X, 1: +Y
@property({
tooltip: 'ターゲットのワールド座標を使用するかどうか。\n通常は true のままで問題ありません。',
})
public useWorldSpaceTarget: boolean = true;
@property({
tooltip: 'RayCast が衝突判定を行う 2D 物理レイヤー(ビットマスク)。\n壁や障害物グループを含めるように設定してください。',
})
public raycastMask: number = 0xFFFFFFFF;
@property({
tooltip: '視界判定を行う間隔(秒)。\n0 以下の場合は毎フレームチェックします。',
min: 0,
})
public checkInterval: number = 0;
@property({
tooltip: 'Scene ビュー上に Ray を描画してデバッグするかどうか。',
})
public debugDraw: boolean = false;
@property({
tooltip: 'ターゲットが見えている場合の Ray 線の色。',
})
public debugColorVisible: Color = new Color(0, 255, 0, 255);
@property({
tooltip: 'ターゲットが遮蔽物に隠れている場合の Ray 線の色。',
})
public debugColorBlocked: Color = new Color(255, 0, 0, 255);
@property({
tooltip: 'ターゲットが距離や視野角の条件を満たしていない場合の Ray 線の色。',
})
public debugColorOutOfFOV: Color = new Color(255, 255, 0, 255);
/**
* 現在の視界判定結果。
* true: ターゲットが視界内かつ遮蔽物がない
* false: それ以外
*/
public get isVisible(): boolean {
return this._isVisible;
}
// 内部状態
private _isVisible: boolean = false;
private _timeSinceLastCheck: number = 0;
// デバッグ描画用
private _debugGraphics: Graphics | null = null;
private _debugLineStart: Vec3 = new Vec3();
private _debugLineEnd: Vec3 = new Vec3();
private _debugLineColor: Color = new Color(255, 255, 0, 255);
onLoad() {
// 2D 物理システムが有効かどうかを確認
if (!PhysicsSystem2D.instance) {
console.warn('[VisionCone] PhysicsSystem2D が有効になっていません。Project Settings > Physics 2D を確認してください。');
}
// デバッグ描画ノードの準備(必要な場合のみ)
this._setupDebugGraphics();
}
start() {
// 初回チェックを即時に行う
this._performCheck();
}
update(deltaTime: number) {
// チェック間隔が 0 以下なら毎フレームチェック
if (this.checkInterval <= 0) {
this._performCheck();
} else {
this._timeSinceLastCheck += deltaTime;
if (this._timeSinceLastCheck >= this.checkInterval) {
this._timeSinceLastCheck = 0;
this._performCheck();
}
}
// デバッグ描画更新
this._updateDebugDraw();
}
/**
* 視界判定のメイン処理。
*/
private _performCheck() {
this._isVisible = false;
if (!this.target) {
// ターゲット未設定
return;
}
const owner = this.node;
// オーナーのワールド座標
const ownerWorldPos = owner.worldPosition.clone();
// ターゲットの座標(ワールド or ローカル)
const targetPos = this.useWorldSpaceTarget
? this.target.worldPosition.clone()
: this.target.position.clone();
// owner → target のベクトル
const dir3D = new Vec3(
targetPos.x - ownerWorldPos.x,
targetPos.y - ownerWorldPos.y,
0
);
const distance = dir3D.length();
// デフォルトのデバッグ線情報
this._debugLineStart.set(ownerWorldPos);
this._debugLineEnd.set(
ownerWorldPos.x + dir3D.x,
ownerWorldPos.y + dir3D.y,
0
);
this._debugLineColor.set(this.debugColorOutOfFOV);
// 距離チェック
if (distance > this.maxDistance) {
// 視界外(遠すぎる)
return;
}
// 視野角チェック
const inFOV = this._isWithinFieldOfView(ownerWorldPos, dir3D, distance);
if (!inFOV) {
// 視野角の外
return;
}
// RayCast に使う 2D ベクトル
const from = new Vec2(ownerWorldPos.x, ownerWorldPos.y);
const to = new Vec2(targetPos.x, targetPos.y);
// RayCast 実行
const physics2D = PhysicsSystem2D.instance;
if (!physics2D) {
console.warn('[VisionCone] PhysicsSystem2D.instance が取得できません。2D 物理が有効か確認してください。');
return;
}
const results: IPhysics2DRaycastResult[] = [];
physics2D.raycast(
from,
to,
ERaycast2DType.CLOSEST,
this.raycastMask,
results
);
if (results.length === 0) {
// 衝突なし:ターゲットまで遮蔽物がない
this._isVisible = true;
this._debugLineColor.set(this.debugColorVisible);
return;
}
// 最も近いヒットを取得
const hit = results[0];
const hitPoint = hit.point;
const hitDistance = Vec2.distance(from, hitPoint);
// デバッグ線は実際のヒット位置まで
this._debugLineEnd.set(hitPoint.x, hitPoint.y, 0);
// ヒット距離がターゲットまでの距離より短い = 手前に遮蔽物がある
if (hitDistance + 0.001 < distance) {
// 遮蔽物あり
this._isVisible = false;
this._debugLineColor.set(this.debugColorBlocked);
} else {
// 遮蔽物なし(Ray はターゲットまで届いているとみなす)
this._isVisible = true;
this._debugLineColor.set(this.debugColorVisible);
}
}
/**
* 視野角内かどうかを判定する。
*/
private _isWithinFieldOfView(ownerWorldPos: Vec3, dir3D: Vec3, distance: number): boolean {
if (this.fieldOfView >= 360 || this.fieldOfView <= 0) {
// 360°以上なら常に視野角内とみなす
return true;
}
// オーナーの前方ベクトル(ローカル → ワールド)
const forward = this._getForwardVectorWorld();
// 正規化
const toTarget = dir3D.clone();
if (distance > 0) {
toTarget.multiplyScalar(1 / distance);
}
// cosθ = a・b
const dot = forward.x * toTarget.x + forward.y * toTarget.y;
const angleRad = Math.acos(Math.max(-1, Math.min(1, dot)));
const angleDeg = angleRad * 180 / Math.PI;
return angleDeg <= this.fieldOfView * 0.5;
}
/**
* ローカル前方ベクトルをワールド空間に変換して取得する。
* forwardAxis:
* 0 => +X
* 1 => +Y
*/
private _getForwardVectorWorld(): Vec3 {
const localForward = new Vec3(
this.forwardAxis === 0 ? 1 : 0,
this.forwardAxis === 1 ? 1 : 0,
0
);
const worldForward = new Vec3();
this.node.getWorldMatrix().transformVector(localForward, worldForward);
worldForward.z = 0;
const len = worldForward.length();
if (len > 0) {
worldForward.multiplyScalar(1 / len);
}
return worldForward;
}
/**
* デバッグ描画用 Graphics コンポーネントを準備する。
* - 自身の子ノードに "VisionConeDebug" という名前で作成する。
* - すでに存在する場合はそれを再利用する。
*/
private _setupDebugGraphics() {
// すでにあれば何もしない
if (this._debugGraphics) {
return;
}
// 子ノードから検索
let debugNode = this.node.getChildByName('VisionConeDebug');
if (!debugNode) {
debugNode = new Node('VisionConeDebug');
this.node.addChild(debugNode);
}
let g = debugNode.getComponent(Graphics);
if (!g) {
g = debugNode.addComponent(Graphics);
}
this._debugGraphics = g;
}
/**
* デバッグ描画の更新。
*/
private _updateDebugDraw() {
if (!this.debugDraw || !this._debugGraphics) {
return;
}
const g = this._debugGraphics;
g.clear();
// 線の太さと色を設定
g.lineWidth = 2;
g.strokeColor = this._debugLineColor;
// ワールド座標をローカル座標に変換して描画
const startLocal = new Vec3();
const endLocal = new Vec3();
this.node.inverseTransformPoint(startLocal, this._debugLineStart);
this.node.inverseTransformPoint(endLocal, this._debugLineEnd);
g.moveTo(startLocal.x, startLocal.y);
g.lineTo(endLocal.x, endLocal.y);
g.stroke();
}
}
コードの主要部分の解説
- onLoad()
–PhysicsSystem2D.instanceが存在するか確認し、無効な場合はconsole.warnで警告します。
– デバッグ描画用にGraphicsコンポーネントを子ノードに準備します(必要な場合のみ)。 - start()
– 初回の視界チェックを即時に実行し、ゲーム開始時点でのisVisibleを確定させます。 - update(deltaTime)
–checkIntervalの設定に応じて、毎フレームまたは一定間隔で_performCheck()を呼び出します。
– デバッグ描画の更新 (_updateDebugDraw()) を行い、Scene ビューに Ray 線を表示します。 - _performCheck()
– ターゲットが未設定なら何もせず終了。
– オーナーとターゲットの位置から方向ベクトルと距離を計算。
– 距離チェック:maxDistanceを超えていれば即座にisVisible = false。
– 視野角チェック:_isWithinFieldOfView()で FOV 内か判定。
– 条件を満たす場合のみ PhysicsSystem2D.raycast() を実行し、ERaycast2DType.CLOSESTで最も近いヒットを取得。
– ヒット距離がターゲットまでの距離より短ければ「手前に遮蔽物あり」としてisVisible = false、そうでなければisVisible = true。 - _isWithinFieldOfView()
– オーナーの「前方ベクトル」(forwardAxisに基づく +X または +Y)をワールド空間に変換。
– その前方ベクトルとターゲットへの方向ベクトルの 内積から角度を算出し、fieldOfView / 2以下なら視野内と判定。 - _getForwardVectorWorld()
– ローカル前方(+X または +Y)をnode.getWorldMatrix().transformVector()でワールド空間に変換し、正規化して返します。 - デバッグ描画関連
–_setupDebugGraphics()で子ノードにGraphicsを準備し、「VisionConeDebug」という名前を付けます。
–_updateDebugDraw()で、オーナーのローカル座標系に変換した線分を描画します。
– 線の色はdebugColorVisible / debugColorBlocked / debugColorOutOfFOVに応じて変化します。
使用手順と動作確認
1. スクリプトファイルの作成
- Editor の Assets パネルで任意のフォルダ(例:
assets/scripts)を選択します。 - 右クリック → Create → TypeScript を選択します。
- ファイル名を
VisionCone.tsに変更します。 - 作成した
VisionCone.tsをダブルクリックしてエディタ(VSCode など)で開き、
先ほどの TypeScript コード全文 を貼り付けて保存します。
2. テスト用シーンの準備
- Hierarchy で右クリック → Create → 2D Object → Sprite などを選択し、
敵キャラや視界の発生源となるノード(例:Enemy)を作成します。 - 同様に、ターゲットとなるノード(例:
Player)を作成します。 - 遮蔽物となる壁オブジェクトも作成したい場合は、
– 2D オブジェクト(Sprite など)を作成
– Add Component → Physics 2D → BoxCollider2D などを追加
– 必要に応じて RigidBody2D も追加(静的な壁なら Type を Static に)
を行ってください。
3. VisionCone コンポーネントのアタッチ
- Hierarchy で
Enemyノードを選択します。 - Inspector の下部で Add Component → Custom → VisionCone を選択します。
- Inspector 上に VisionCone のプロパティが表示されます。
4. プロパティの設定
Inspector で VisionCone の各プロパティを以下のように設定してみます。
- target :
Playerノードをドラッグ&ドロップで割り当てる。 - maxDistance : 10 ~ 20 程度(シーンのスケールに合わせて調整)。
- fieldOfView : 120(前方 ±60°)。
前後どこでも見えるようにしたい場合は 360 にしてもよい。 - forwardAxis :
– 横向きのキャラで右方向を「前」としたい → 0(+X)
– 上向きのキャラで上方向を「前」としたい → 1(+Y) - useWorldSpaceTarget : 通常は
trueのままで OK。 - raycastMask :
– 壁や障害物に設定している Physics 2D グループを含むビットマスクを指定。
– 特定グループだけを対象にしたい場合は、グループのビット値に合わせて設定してください。 - checkInterval :
– テスト時は 0(毎フレームチェック)でよい。
– パフォーマンスを気にする場合は 0.1 ~ 0.2 などに設定するとよい。 - debugDraw :
trueにすると Scene ビューに Ray が描画されます。 - debugColorVisible : 緑(デフォルト)。
- debugColorBlocked : 赤(デフォルト)。
- debugColorOutOfFOV : 黄(デフォルト)。
5. 物理 2D の有効化確認
RayCast2D が動作するには、プロジェクトで Physics 2D が有効化されている必要があります。
- メニューから Project → Project Settings を開きます。
- 左側メニューで Physics 2D を選択します。
- Enable Physics 2D(または同等のチェックボックス)が有効になっていることを確認します。
- 必要に応じて、ここで 2D 物理グループの設定や、各グループのビットマスクを確認・調整します。
6. 動作確認
- Scene ビューで
EnemyとPlayerの位置を調整し、
– Player が Enemy の前方かつmaxDistance以内にいる場合、
– かつその間に Collider2D を持つ壁がない場合、
緑色の Ray が Player 方向に描画されることを確認します(debugDraw = trueの場合)。 - Player を Enemy の背後に回り込ませる、または
fieldOfViewの外側に移動させると、
– Ray は黄色になり、isVisibleが false になります。 - Enemy と Player の間に Collider2D を持つ壁オブジェクトを配置すると、
– 壁までの距離で Ray が赤色で描画され、
–isVisibleは false になります。
7. 他コンポーネントからの利用例
VisionCone は独立コンポーネントですが、他のスクリプトから isVisible を読むだけで簡単に AI 行動に利用できます。
// EnemyController.ts の一部例(あくまで参考。VisionCone とは独立している)
import { _decorator, Component } from 'cc';
import { VisionCone } from './VisionCone';
const { ccclass, property } = _decorator;
@ccclass('EnemyController')
export class EnemyController extends Component {
@property(VisionCone)
public visionCone: VisionCone | null = null;
update() {
if (this.visionCone && this.visionCone.isVisible) {
// プレイヤーを追いかける処理など
} else {
// 徘徊モードなど
}
}
}
このように、VisionCone 自体は他のスクリプトに依存せず、他スクリプトからは「視界センサー」として読み取るだけの構造になっています。
まとめ
今回実装した VisionCone コンポーネントは、以下の特徴を持つ汎用スクリプトです。
- アタッチしてターゲットを指定するだけで、RayCast2D による視界判定がすぐに使える。
- 距離・視野角・前方方向・レイヤーマスク・チェック間隔などを すべて Inspector から調整可能。
- 他のカスタムスクリプトやシングルトンに一切依存しない完全な独立コンポーネント。
- デバッグ描画機能により、Scene ビューで視界の状態を視覚的に確認できる。
このコンポーネントを基盤として、例えば以下のような応用が可能です。
- 敵 AI がプレイヤーを発見した瞬間にアラート状態へ移行する。
- ステルスゲームで、プレイヤーが敵の視界に入らないように動くゲームプレイを実現する。
- 見通し線が通っている場合のみ射撃可能にするタレットやタワーの実装。
VisionCone.ts 単体で完結しているため、プロジェクト間のコピー&ペーストで簡単に再利用できます。
まずはシンプルなシーンで動作を確認し、自分のゲームのルールに合わせてプロパティを調整しながら活用してみてください。




