【Cocos Creator】アタッチするだけ!ObjectiveMarker (目的マーカー)の実装方法【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】ObjectiveMarker の実装:アタッチするだけで「画面外の目的地の方向を画面端の矢印で示す」汎用スクリプト

このガイドでは、任意のノードにアタッチするだけで「画面外にある目的地の方角を、画面端の矢印アイコンで指し示す」機能を実現する ObjectiveMarker コンポーネントを実装します。

目的地(ターゲット)ノードがカメラの外に出たとき、画面の端に矢印アイコンが表示され、その矢印がターゲットの方向を向くようにします。ターゲットが画面内に入ると矢印は自動で非表示になります。

このコンポーネントは他のカスタムスクリプトに一切依存せず、必要な設定はすべてインスペクタから行えるように設計します。


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

1. 機能要件の整理

  • 対象ノード(ターゲット)への参照を設定できる。
  • 使用するカメラ(2D UI カメラなど)を設定できる。
  • 矢印アイコンとして表示するノード(スプライトなど)を設定できる。
  • ターゲットが画面内にあるときは矢印を非表示にし、画面外にあるときだけ表示する。
  • 矢印は常にターゲットの方向を向くように回転する。
  • 矢印は画面端から一定のマージンを空けて表示する。
  • ターゲットがカメラの真後ろにある場合も、最も近い画面端位置に矢印を表示する。
  • デバッグ用に、ターゲットが画面内でも常時表示させるオプションを用意する。

これらを満たしつつ、

  • 他のスクリプトへの依存なし(完全独立)
  • インスペクタからすべて設定可能
  • 標準コンポーネントが足りない場合はログで警告

という方針で実装します。

2. ノード構成の想定

このコンポーネントは、基本的に「矢印アイコン用ノード」にアタッチして使うことを想定しています。

  • このコンポーネントをアタッチするノード:
    • Canvas > 任意の UI ノード(例: Arrow
    • このノードに Sprite コンポーネントで矢印画像を設定しておくと視覚的にわかりやすいです。
  • ターゲットノード:
    • マップ上の目的地、敵、アイテムなど、ワールド空間に配置された任意のノード。
  • カメラノード:
    • 2D ゲームであれば通常 Canvas に紐づく UI カメラを使用します。

矢印ノードそのものの位置は、このコンポーネントが毎フレーム制御します。UI レイアウトは基本的に不要です。

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

ObjectiveMarker に用意するプロパティと役割は以下の通りです。

  • targetNode
    • 矢印が指し示す目的地(ターゲット)のノード。
    • 例: マップ上の目的地、ボス、重要アイテムなど。
    • 必須。未設定の場合は警告ログを出し、処理をスキップします。
  • cameraCamera
    • 画面座標変換に使用するカメラ。
    • 通常は Canvas に紐づく 2D/UI カメラを設定します。
    • 必須。未設定の場合は警告ログを出し、処理をスキップします。
  • screenMarginnumber
    • 矢印を画面端からどれだけ内側に表示するか(ピクセル)。
    • 例: 32 なら、上下左右の端から 32px 内側に矢印が表示されます。
    • 最小 0。負の値が設定された場合は 0 として扱います。
  • hideWhenOnScreenboolean
    • true の場合、ターゲットが画面内にあるときは矢印を非表示にします。
    • false の場合、ターゲットが画面内でも矢印を表示し続けます(デバッグ用途など)。
  • arrowNodeNode
    • 実際に画面に表示する矢印アイコンのノード。
    • 未設定の場合は「このコンポーネントがアタッチされているノード自身」を矢印ノードとして使用します。
    • 任意。外部の子ノードを矢印として使いたい場合に設定します。
  • useTargetWorldPositionboolean
    • true: ターゲットの worldPosition をそのまま使用。
    • false: ターゲットのローカル位置を基準に、親の Transform を考慮した座標に変換(通常は true で問題ありません)。
    • カメラとターゲットの座標系が異常な場合の調整用オプションとして用意します。
  • debugLogboolean
    • true の場合、画面内/外の判定などをログ出力します。
    • 通常は false にしておき、挙動がおかしいときにオンにします。

また、標準コンポーネントとしては以下を防御的に扱います。

  • 矢印ノードに UITransform が存在することを前提としますが、存在しない場合はログを出して処理を続行(最悪でも位置決めは行う)。
  • Canvas(UITransform)は、カメラの camera.screen 情報から画面サイズを取得するため、直接参照は不要です。

TypeScriptコードの実装

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


import { _decorator, Component, Node, Camera, Vec3, Vec2, math, v3, v2, UITransform, screen, isValid } from 'cc';
const { ccclass, property } = _decorator;

/**
 * ObjectiveMarker
 * 画面外にあるターゲットの方向を、画面端の矢印アイコンで指し示す汎用コンポーネント。
 *
 * 想定使用方法:
 * - Canvas 配下の UI ノード(矢印アイコン)にアタッチ
 * - target に目的地ノード、camera に使用中のカメラを設定
 * - 矢印ノードはこのコンポーネントが毎フレーム位置と回転を制御します
 */
@ccclass('ObjectiveMarker')
export class ObjectiveMarker extends Component {

    @property({
        type: Node,
        tooltip: '矢印が指し示す目的地(ターゲット)ノード。\nワールド空間上の任意のノードを指定してください。'
    })
    public target: Node | null = null;

    @property({
        type: Camera,
        tooltip: 'ターゲットを表示しているカメラ。\n通常は Canvas に紐づく 2D/UI カメラを指定します。'
    })
    public camera: Camera | null = null;

    @property({
        tooltip: '矢印を画面端からどれだけ内側に表示するか(ピクセル)。\n0 以上の値を推奨します。'
    })
    public screenMargin: number = 32;

    @property({
        tooltip: 'ターゲットが画面内にあるとき、矢印を非表示にするかどうか。'
    })
    public hideWhenOnScreen: boolean = true;

    @property({
        type: Node,
        tooltip: '矢印アイコンとして表示するノード。\n未設定の場合、このコンポーネントがアタッチされているノード自身を使用します。'
    })
    public arrowNode: Node | null = null;

    @property({
        tooltip: 'true: ターゲットの worldPosition をそのまま使用します(通常はこちら)。\nfalse: 特殊な座標系調整用。'
    })
    public useTargetWorldPosition: boolean = true;

    @property({
        tooltip: 'デバッグ用に画面内/外の判定などをログ出力します。'
    })
    public debugLog: boolean = false;

    // 作業用一時ベクトル(GC削減)
    private _worldPosCache: Vec3 = v3();
    private _screenPosCache: Vec3 = v3();
    private _centerScreen: Vec2 = v2();

    onLoad() {
        // arrowNode が未設定なら自分自身を使用
        if (!this.arrowNode) {
            this.arrowNode = this.node;
        }

        // screenMargin の防御的補正
        if (this.screenMargin < 0) {
            console.warn('[ObjectiveMarker] screenMargin が負の値のため 0 に補正します。');
            this.screenMargin = 0;
        }

        // 必須参照のチェック(target, camera)
        if (!this.target) {
            console.warn('[ObjectiveMarker] target が設定されていません。インスペクタから目的地ノードを設定してください。');
        }
        if (!this.camera) {
            console.warn('[ObjectiveMarker] camera が設定されていません。インスペクタから使用するカメラを設定してください。');
        }

        // arrowNode の UITransform 存在チェック(必須ではないが推奨)
        if (this.arrowNode) {
            const uiTrans = this.arrowNode.getComponent(UITransform);
            if (!uiTrans) {
                console.warn('[ObjectiveMarker] arrowNode に UITransform がありません。' +
                    'UI ノードとして使用する場合は UITransform を追加してください。');
            }
        } else {
            console.error('[ObjectiveMarker] arrowNode が取得できません。コンポーネントのアタッチ先ノードが無効な可能性があります。');
        }
    }

    start() {
        // 初期状態では一度更新しておく
        this.update(0);
    }

    update(deltaTime: number) {
        // 必須参照が無い場合は何もしない
        if (!this.camera || !this.target || !this.arrowNode) {
            return;
        }
        if (!isValid(this.camera) || !isValid(this.target) || !isValid(this.arrowNode)) {
            return;
        }

        // ターゲットのワールド座標を取得
        if (this.useTargetWorldPosition) {
            this._worldPosCache.set(this.target.worldPosition);
        } else {
            // 何らかの理由でローカル座標系から変換したい場合のフック
            this._worldPosCache.set(this.target.worldPosition);
        }

        // カメラでスクリーン座標に変換
        // worldToScreen は Vec3 を返す: (x, y, depth)
        this.camera.worldToScreen(this._worldPosCache, this._screenPosCache);

        const screenX = this._screenPosCache.x;
        const screenY = this._screenPosCache.y;
        const depth = this._screenPosCache.z;

        const screenWidth = screen.width;
        const screenHeight = screen.height;

        // 画面中心
        this._centerScreen.set(screenWidth * 0.5, screenHeight * 0.5);

        // ターゲットが画面内かどうかを判定
        const isOnScreen =
            depth > 0 &&
            screenX >= 0 && screenX <= screenWidth &&
            screenY >= 0 && screenY <= screenHeight;

        if (this.debugLog) {
            console.log(`[ObjectiveMarker] target screen pos: (${screenX.toFixed(1)}, ${screenY.toFixed(1)}, depth=${depth.toFixed(2)}), onScreen=${isOnScreen}`);
        }

        // ターゲットが画面内で、かつ hideWhenOnScreen が true の場合は矢印を非表示
        if (isOnScreen && this.hideWhenOnScreen) {
            this.arrowNode.active = false;
            return;
        }

        // ここからは矢印を表示する処理
        this.arrowNode.active = true;

        // 画面中心からターゲットスクリーン座標への方向ベクトルを計算
        const dir = v2(screenX - this._centerScreen.x, screenY - this._centerScreen.y);

        // 深度が負(カメラの後ろ)にある場合、方向を反転して画面外扱いにする
        if (depth <= 0) {
            dir.x = -dir.x;
            dir.y = -dir.y;
        }

        // dir を使って画面端への交点を求める
        const clampedPos = this._computeEdgePosition(dir, screenWidth, screenHeight, this.screenMargin);

        // 矢印ノードのスクリーン座標をワールド座標に変換
        const arrowWorldPos = v3();
        this.camera.screenToWorld(v3(clampedPos.x, clampedPos.y, 0), arrowWorldPos);

        // 矢印ノードをその位置に移動
        this.arrowNode.worldPosition = arrowWorldPos;

        // 矢印の回転をターゲット方向に合わせる(UI は通常 Z 軸回転)
        const angleRad = Math.atan2(dir.y, dir.x); // X 軸からの角度
        const angleDeg = math.toDegree(angleRad);

        // 矢印画像が「右向き」を 0 度としている前提。
        // もし上向きが 0 度の画像なら +90/-90 など補正する。
        this.arrowNode.setRotationFromEuler(0, 0, angleDeg);
    }

    /**
     * 画面中心からの方向ベクトルに基づき、
     * 指定したマージン内で画面端上の位置を計算する。
     *
     * @param dir 画面中心からターゲットへの方向ベクトル
     * @param screenWidth 画面幅
     * @param screenHeight 画面高さ
     * @param margin 画面端からのマージン
     * @returns 画面座標系での位置(Vec2)
     */
    private _computeEdgePosition(dir: Vec2, screenWidth: number, screenHeight: number, margin: number): Vec2 {
        // dir がゼロベクトルの場合は中心に置く(防御的)
        if (math.approxEquals(dir.x, 0) && math.approxEquals(dir.y, 0)) {
            return v2(this._centerScreen.x, this._centerScreen.y);
        }

        // 正規化
        const nDir = dir.normalize();

        // 画面の半幅・半高(マージンを考慮)
        const halfW = screenWidth * 0.5 - margin;
        const halfH = screenHeight * 0.5 - margin;

        // 中心から見たときの、左右上下の最大距離
        // 中心を (0,0) としたときに、どの辺に当たるかを計算する。
        let tMax = Number.POSITIVE_INFINITY;

        // 右端 (x = +halfW)
        if (nDir.x > 0) {
            const t = halfW / nDir.x;
            if (t < tMax) tMax = t;
        }
        // 左端 (x = -halfW)
        if (nDir.x < 0) {
            const t = -halfW / nDir.x;
            if (t < tMax) tMax = t;
        }
        // 上端 (y = +halfH)
        if (nDir.y > 0) {
            const t = halfH / nDir.y;
            if (t < tMax) tMax = t;
        }
        // 下端 (y = -halfH)
        if (nDir.y < 0) {
            const t = -halfH / nDir.y;
            if (t < tMax) tMax = t;
        }

        // tMax を使って、中心からのオフセットを計算
        const edgeX = this._centerScreen.x + nDir.x * tMax;
        const edgeY = this._centerScreen.y + nDir.y * tMax;

        // 念のため最終的にクランプ
        const clampedX = math.clamp(edgeX, margin, screenWidth - margin);
        const clampedY = math.clamp(edgeY, margin, screenHeight - margin);

        return v2(clampedX, clampedY);
    }
}

コードのポイント解説

  • onLoad
    • arrowNode が未設定なら自ノードを採用。
    • screenMargin が負なら 0 に補正。
    • targetcamera の未設定を警告ログで通知。
    • arrowNodeUITransform がない場合も警告(必須ではないため強制はしない)。
  • update
    1. camera, target, arrowNode が有効かチェック。
    2. ターゲットのワールド座標を取得。
    3. camera.worldToScreen でスクリーン座標に変換。
    4. スクリーン座標と深度 z を使って「画面内かどうか」を判定。
    5. 画面内かつ hideWhenOnScreen == true の場合は矢印を非表示にして終了。
    6. 画面外の場合(または hideWhenOnScreen == false の場合)、画面中心からターゲット方向へのベクトルを計算。
    7. ターゲットがカメラの後ろ(depth <= 0)なら方向ベクトルを反転。
    8. _computeEdgePosition で、方向に沿って画面端の交点を計算し、マージン内にクランプ。
    9. そのスクリーン座標を screenToWorld でワールド座標に戻し、矢印ノードの worldPosition に設定。
    10. atan2 で角度を求め、矢印をターゲット方向に回転させる。
  • _computeEdgePosition
    • 画面中心を原点としたときの方向ベクトル dir を正規化し、
    • 左右上下それぞれの画面端に到達するスカラー量 t を計算。
    • 最も小さい t を採用することで、方向ベクトルが最初に交差する画面端を求める。
    • 最終的にマージン内にクランプして、矢印が画面外にはみ出さないようにする。

使用手順と動作確認

ここからは、Cocos Creator 3.8.7 エディタ上での具体的な設定手順を説明します。

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

  1. Assets パネルで任意のフォルダ(例: assets/scripts)を選択します。
  2. 右クリック → Create > TypeScript を選択します。
  3. ファイル名を ObjectiveMarker.ts に変更します。
  4. 作成された ObjectiveMarker.ts をダブルクリックして開き、中身をすべて削除してから、上記のコードをそのまま貼り付けて保存します。

2. テスト用シーンの準備

以下は 2D ゲームの一般的な構成を想定しています。

  1. Hierarchy パネルで、既に Canvas があることを確認します。
    • なければ Create > UI > Canvas で作成します。
  2. Canvas 配下に矢印用ノードを作成します。
    1. Canvas を右クリック → Create > UI > Sprite を選択し、名前を Arrow に変更します。
    2. Arrow ノードを選択し、Inspector の Sprite コンポーネントで矢印画像(右向きのアイコンなど)を設定します。
    3. 画像の向きは「右向き」を 0 度としておくと、コードの回転ロジックと一致します。
  3. ターゲット用ノードを作成します。
    1. Hierarchy で Create > 3D Object > Node などを選択し、名前を Target に変更します。
    2. このノードは 2D ゲームであれば Canvas の外(ワールド空間)に置いても構いません。例えば (x: 1000, y: 0, z: 0) など、カメラから離れた位置に動かしておくと画面外になります。
  4. カメラを確認します。
    • 通常は Canvas 作成時に自動生成されるカメラ(Main Camera など)を使用します。
    • Hierarchy からカメラノードを選択し、Inspector で Camera コンポーネントが付いていることを確認します。

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

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

4. Inspector でプロパティを設定

Arrow ノードに追加された ObjectiveMarker コンポーネントの各プロパティを設定します。

  1. Target:
    • 右側の「●」ボタンをクリックし、ポップアップから Target ノードを選択します。
    • もしくは Hierarchy から Target ノードをドラッグ&ドロップして設定しても構いません。
  2. Camera:
    • 同様に、「●」ボタンから Main Camera など使用中のカメラノードを選択します。
  3. Screen Margin:
    • 例として 32 を入力します。これにより、矢印は画面の上下左右から 32px 内側に表示されます。
    • もっと端に寄せたい場合は 816 などに調整してください。
  4. Hide When On Screen:
    • 通常は true のままで構いません。
    • ターゲットが画面内にあっても矢印を常に表示しておきたい場合は false に変更します。
  5. Arrow Node:
    • 空欄のままで OK です。この場合、Arrow ノード自身が矢印として使用されます。
    • もし Arrow の子ノードを矢印として使いたい場合は、その子ノードをここに設定します。
  6. Use Target World Position:
    • 通常は true のままで構いません。
  7. Debug Log:
    • 挙動を確認したい場合には true にしておくと、Console にスクリーン座標などが出力されます。
    • 問題なければ false に戻しておきます。

5. 再生して動作を確認

  1. エディタ上部の Play ボタンを押してゲームを実行します。
  2. ターゲットノード Target がカメラの外側にある場合:
    • 画面の端に矢印が表示され、常にターゲットの方向を指していることを確認します。
    • ターゲットを Scene ビューで動かすと、矢印がそれに追従して回転・位置調整されます。
  3. ターゲットをカメラの前方・画面内に移動した場合:
    • Hide When On Screen = true なら、矢印が自動的に非表示になることを確認します。
    • Hide When On Screen = false の場合は、画面内でも矢印が表示され、ターゲット方向を指し続けます。
  4. ターゲットをカメラの真後ろに移動した場合:
    • 矢印はカメラの背後にあるターゲットに対しても、最も近い画面端に表示され、方向を反転して指し示します。

もし矢印が表示されない・動かない場合は、以下を確認してください。

  • ObjectiveMarker コンポーネントの TargetCamera が正しく設定されているか。
  • Arrow ノードが Canvas 配下にあり、アクティブ状態(active = true)になっているか。
  • ターゲットが本当にカメラの画面外にあるか(近すぎて画面内に入っていると矢印は非表示になります)。
  • Debug Logtrue にして Console のログを確認し、スクリーン座標や onScreen 判定が想定通りか。

まとめ

ObjectiveMarker コンポーネントは、

  • ターゲットノード
  • 使用カメラ
  • 画面マージンや表示/非表示の挙動

をインスペクタから設定するだけで、

  • 画面外の目的地を画面端の矢印で指し示す
  • ターゲットが画面内に入れば自動で矢印を隠す
  • カメラの前後や画面外のどの方向でも正しく方向を示す

といった UI 演出を簡単に実現できます。

このスクリプトは他のカスタムスクリプトに一切依存しないため、

  • 任意のシーン・任意のプロジェクトにコピペして即利用可能
  • ターゲットを差し替えるだけで、複数の目的地マーカーを簡単に増やせる
  • 画面マージンや表示ポリシーをプロパティで調整するだけで、UI デザインの要件に柔軟に対応できる

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

応用例としては、

  • クエスト目的地マーカー
  • ボスや重要敵の位置インジケーター
  • マップ外にあるアイテムや仲間キャラクターの方向表示

などが挙げられます。同じシーン内で複数の ObjectiveMarker を使えば、複数の目的地を同時に画面端に表示することも簡単です。

このコンポーネントをベースに、矢印のフェードイン/アウトや距離に応じたスケール変更、色変化などを追加すれば、さらにリッチなナビゲーション UI を構築できます。

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