【Cocos Creator】アタッチするだけ!LogConsole (ログ出力)の実装方法【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】LogConsoleの実装:アタッチするだけで print() の内容をゲーム画面上にリアルタイム表示 する汎用スクリプト

このコンポーネントは、ゲーム画面内に半透明の黒いコンソールウィンドウを自動生成し、console.log / console.warn / console.error / print() の内容をスクロール可能なテキストとして表示します。
任意のノードにアタッチするだけで動作し、他のカスタムスクリプトには一切依存しません。デバッグログをゲーム画面上で確認したいときに非常に便利です。


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

機能要件の整理

  • アタッチされたノード配下に「ログ表示用のUI」を自動生成する。
  • 半透明の黒い背景ウィンドウの上に、複数行テキストでログを表示する。
  • console.log / warn / errorprint をフックし、出力内容をゲーム内にも反映する。
  • 古いログは一定件数で自動的に削除し、最新ログが見えるようにスクロール位置を自動調整する。
  • インスペクタからサイズ・位置・フォントサイズ・最大行数・有効/無効などを調整できる。
  • 外部の GameManager やシングルトンに依存せず、このスクリプト単体で完結する。

UI構成(自動生成)

このコンポーネントは、アタッチされたノードの子として以下のノード構成を 自動生成 します(ユーザーが手で作る必要はありません)。

(LogConsole をアタッチしたノード)
└─ LogConsoleRoot (Node)
    ├─ Background (Sprite, UITransform, Widget)
    └─ ScrollView (ScrollView, Mask, UITransform, Widget)
         └─ Content (Label, UITransform)
  • Background: 半透明黒のパネル(Sprite)。
  • ScrollView: ログテキスト用のスクロール領域。
  • Content: 実際のログ文字列を表示する Label。

Cocos の標準 UI コンポーネント(Sprite, ScrollView, Label, Mask, Widget, UITransform)のみを使用し、他の自作スクリプトには依存しません。

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

以下のプロパティを @property で公開し、インスペクタから調整できるようにします。

  • enabledOnStart: boolean
    – デフォルト: true
    – 説明: ゲーム開始時にコンソールを有効化するかどうか。
    false にすると、ログフックや表示を無効にした状態で開始します。
  • capturePrint: boolean
    – デフォルト: true
    – 説明: Cocos の print() をフックしてゲーム内に表示するかどうか。
  • captureConsoleLog: boolean
    – デフォルト: true
    – 説明: console.log() をゲーム内に表示するかどうか。
  • captureConsoleWarn: boolean
    – デフォルト: true
    – 説明: console.warn() をゲーム内に表示するかどうか。
  • captureConsoleError: boolean
    – デフォルト: true
    – 説明: console.error() をゲーム内に表示するかどうか。
  • maxLines: number
    – デフォルト: 200
    – 説明: 保持するログの最大行数。これを超えた古いログは先頭から削除されます。
  • fontSize: number
    – デフォルト: 18
    – 説明: ログテキストのフォントサイズ。
  • lineSpacing: number
    – デフォルト: 4
    – 説明: 行間(ピクセル)。
  • backgroundOpacity: number
    – デフォルト: 180(0〜255)
    – 説明: 背景パネルの不透明度。0 で完全透明、255 で完全不透明。
  • backgroundColor: Color
    – デフォルト: RGBA(0, 0, 0, 255)(黒)
    – 説明: 背景パネルの色(アルファは backgroundOpacity で制御)。
  • width: number
    – デフォルト: 600
    – 説明: コンソールウィンドウの横幅(ピクセル)。
  • height: number
    – デフォルト: 260
    – 説明: コンソールウィンドウの高さ(ピクセル)。
  • anchorX: number
    – デフォルト: 0.0
    – 説明: 画面横方向の配置(0: 左端, 0.5: 中央, 1: 右端)。Widget で自動配置します。
  • anchorY: number
    – デフォルト: 0.0
    – 説明: 画面縦方向の配置(0: 下端, 0.5: 中央, 1: 上端)。
  • marginX: number
    – デフォルト: 20
    – 説明: 画面端からの横方向マージン(ピクセル)。
  • marginY: number
    – デフォルト: 20
    – 説明: 画面端からの縦方向マージン(ピクセル)。
  • autoScrollToBottom: boolean
    – デフォルト: true
    – 説明: 新しいログが追加されたとき、自動的に一番下までスクロールするかどうか。
  • showLogPrefix: boolean
    – デフォルト: true
    – 説明: 各ログ行の先頭に [LOG], [WARN], [ERROR] などの種別プレフィックスを付けるかどうか。
  • timeStamp: boolean
    – デフォルト: true
    – 説明: 各ログ行に HH:MM:SS 形式のタイムスタンプを付与するかどうか。

TypeScriptコードの実装

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


import {
    _decorator,
    Component,
    Node,
    Label,
    Color,
    UITransform,
    Sprite,
    SpriteFrame,
    ScrollView,
    Mask,
    Widget,
    Vec3,
    sys,
} from 'cc';
const { ccclass, property } = _decorator;

enum LogType {
    LOG = 'LOG',
    WARN = 'WARN',
    ERROR = 'ERROR',
    PRINT = 'PRINT',
}

@ccclass('LogConsole')
export class LogConsole extends Component {

    @property({
        tooltip: 'ゲーム開始時にコンソールを有効化するかどうか',
    })
    public enabledOnStart: boolean = true;

    @property({
        tooltip: 'Cocos の print() をゲーム内ログに取り込むかどうか',
    })
    public capturePrint: boolean = true;

    @property({
        tooltip: 'console.log() をゲーム内ログに取り込むかどうか',
    })
    public captureConsoleLog: boolean = true;

    @property({
        tooltip: 'console.warn() をゲーム内ログに取り込むかどうか',
    })
    public captureConsoleWarn: boolean = true;

    @property({
        tooltip: 'console.error() をゲーム内ログに取り込むかどうか',
    })
    public captureConsoleError: boolean = true;

    @property({
        tooltip: '保持する最大ログ行数。超えると古いログから削除されます',
        min: 10,
        max: 2000,
        step: 10,
    })
    public maxLines: number = 200;

    @property({
        tooltip: 'ログ文字のフォントサイズ',
        min: 8,
        max: 64,
    })
    public fontSize: number = 18;

    @property({
        tooltip: 'ログ行の行間(ピクセル)',
        min: 0,
        max: 30,
    })
    public lineSpacing: number = 4;

    @property({
        tooltip: '背景パネルの不透明度(0〜255)',
        min: 0,
        max: 255,
    })
    public backgroundOpacity: number = 180;

    @property({
        tooltip: '背景パネルの色(アルファは backgroundOpacity で制御)',
    })
    public backgroundColor: Color = new Color(0, 0, 0, 255);

    @property({
        tooltip: 'コンソールウィンドウの横幅(ピクセル)',
        min: 100,
        max: 2000,
    })
    public width: number = 600;

    @property({
        tooltip: 'コンソールウィンドウの高さ(ピクセル)',
        min: 80,
        max: 2000,
    })
    public height: number = 260;

    @property({
        tooltip: '画面横方向の配置(0: 左端, 0.5: 中央, 1: 右端)',
        min: 0,
        max: 1,
        step: 0.1,
    })
    public anchorX: number = 0.0;

    @property({
        tooltip: '画面縦方向の配置(0: 下端, 0.5: 中央, 1: 上端)',
        min: 0,
        max: 1,
        step: 0.1,
    })
    public anchorY: number = 0.0;

    @property({
        tooltip: '画面端からの横方向マージン(ピクセル)',
        min: 0,
        max: 500,
    })
    public marginX: number = 20;

    @property({
        tooltip: '画面端からの縦方向マージン(ピクセル)',
        min: 0,
        max: 500,
    })
    public marginY: number = 20;

    @property({
        tooltip: '新しいログ追加時に自動的に一番下までスクロールするかどうか',
    })
    public autoScrollToBottom: boolean = true;

    @property({
        tooltip: '各ログ行の先頭に [LOG] / [WARN] / [ERROR] などの種別を付与するかどうか',
    })
    public showLogPrefix: boolean = true;

    @property({
        tooltip: '各ログ行に HH:MM:SS のタイムスタンプを付与するかどうか',
    })
    public timeStamp: boolean = true;

    // 内部用フィールド
    private _rootNode: Node | null = null;
    private _backgroundNode: Node | null = null;
    private _scrollViewNode: Node | null = null;
    private _contentNode: Node | null = null;
    private _scrollView: ScrollView | null = null;
    private _label: Label | null = null;

    private _logs: string[] = [];
    private _originalPrint: any = null;
    private _originalLog: any = null;
    private _originalWarn: any = null;
    private _originalError: any = null;
    private _isHooked: boolean = false;

    onLoad() {
        // UI 構築
        this._createUI();

        // enabledOnStart に応じてフック
        if (this.enabledOnStart) {
            this.enableConsole();
        } else {
            this.disableConsole();
        }
    }

    onDestroy() {
        // フックを元に戻す
        this._restoreConsole();
    }

    /**
     * コンソールを有効化(ログフックと表示をオン)
     */
    public enableConsole() {
        if (this._rootNode) {
            this._rootNode.active = true;
        }
        this._hookConsole();
    }

    /**
     * コンソールを無効化(ログフックと表示をオフ)
     */
    public disableConsole() {
        if (this._rootNode) {
            this._rootNode.active = false;
        }
        this._restoreConsole();
    }

    /**
     * ログを全消去
     */
    public clear() {
        this._logs.length = 0;
        this._refreshLabel();
    }

    /**
     * UI ノードを動的に生成
     */
    private _createUI() {
        const parent = this.node;

        // ルートノード
        this._rootNode = new Node('LogConsoleRoot');
        parent.addChild(this._rootNode);

        const rootTransform = this._rootNode.addComponent(UITransform);
        rootTransform.setContentSize(this.width, this.height);

        const rootWidget = this._rootNode.addComponent(Widget);
        rootWidget.isAlignTop = true;
        rootWidget.isAlignBottom = true;
        rootWidget.isAlignLeft = true;
        rootWidget.isAlignRight = true;

        // anchorX / anchorY と margin による配置
        rootWidget.top = this.anchorY >= 0.5 ? this.marginY : 0;
        rootWidget.bottom = this.anchorY <= 0.5 ? this.marginY : 0;
        rootWidget.left = this.anchorX <= 0.5 ? this.marginX : 0;
        rootWidget.right = this.anchorX >= 0.5 ? this.marginX : 0;

        // サイズを固定したいので左右上下全てを true にしつつ、実際には width/height を使う
        rootWidget.alignMode = Widget.AlignMode.ON_WINDOW_RESIZE;

        // 背景ノード
        this._backgroundNode = new Node('Background');
        this._rootNode.addChild(this._backgroundNode);

        const bgTransform = this._backgroundNode.addComponent(UITransform);
        bgTransform.setContentSize(this.width, this.height);

        const sprite = this._backgroundNode.addComponent(Sprite);
        sprite.color = new Color(
            this.backgroundColor.r,
            this.backgroundColor.g,
            this.backgroundColor.b,
            this.backgroundOpacity
        );
        // SpriteFrame は空で OK(単色矩形として描画される)
        sprite.spriteFrame = new SpriteFrame();

        // ScrollView ノード
        this._scrollViewNode = new Node('ScrollView');
        this._rootNode.addChild(this._scrollViewNode);

        const svTransform = this._scrollViewNode.addComponent(UITransform);
        svTransform.setContentSize(this.width, this.height);

        const svWidget = this._scrollViewNode.addComponent(Widget);
        svWidget.isAlignTop = true;
        svWidget.isAlignBottom = true;
        svWidget.isAlignLeft = true;
        svWidget.isAlignRight = true;
        svWidget.top = 0;
        svWidget.bottom = 0;
        svWidget.left = 0;
        svWidget.right = 0;
        svWidget.alignMode = Widget.AlignMode.ON_WINDOW_RESIZE;

        const mask = this._scrollViewNode.addComponent(Mask);
        mask.type = Mask.Type.RECT;

        this._scrollView = this._scrollViewNode.addComponent(ScrollView);

        // Content ノード(ログテキスト)
        this._contentNode = new Node('Content');
        this._scrollViewNode.addChild(this._contentNode);

        const contentTransform = this._contentNode.addComponent(UITransform);
        contentTransform.setContentSize(this.width, this.height);

        this._label = this._contentNode.addComponent(Label);
        this._label.string = '';
        this._label.fontSize = this.fontSize;
        this._label.lineHeight = this.fontSize + this.lineSpacing;
        this._label.overflow = Label.Overflow.RESIZE_HEIGHT;
        this._label.enableWrapText = true;

        // ScrollView 設定
        if (this._scrollView) {
            this._scrollView.content = this._contentNode;
            this._scrollView.horizontal = false;
            this._scrollView.vertical = true;
            this._scrollView.inertia = true;
        }
    }

    /**
     * console / print をフック
     */
    private _hookConsole() {
        if (this._isHooked) {
            return;
        }
        this._isHooked = true;

        // 既存の関数を保存
        this._originalLog = console.log;
        this._originalWarn = console.warn;
        this._originalError = console.error;

        // print は Cocos のグローバル関数(環境によっては存在しない可能性もある)
        if ((globalThis as any).print) {
            this._originalPrint = (globalThis as any).print;
        }

        const self = this;

        if (this.captureConsoleLog) {
            console.log = function (...args: any[]) {
                if (self._originalLog) {
                    self._originalLog.apply(console, args);
                }
                self._addLogFromArgs(args, LogType.LOG);
            };
        }

        if (this.captureConsoleWarn) {
            console.warn = function (...args: any[]) {
                if (self._originalWarn) {
                    self._originalWarn.apply(console, args);
                }
                self._addLogFromArgs(args, LogType.WARN);
            };
        }

        if (this.captureConsoleError) {
            console.error = function (...args: any[]) {
                if (self._originalError) {
                    self._originalError.apply(console, args);
                }
                self._addLogFromArgs(args, LogType.ERROR);
            };
        }

        if (this.capturePrint && this._originalPrint) {
            (globalThis as any).print = function (...args: any[]) {
                self._originalPrint.apply(globalThis, args);
                self._addLogFromArgs(args, LogType.PRINT);
            };
        }
    }

    /**
     * console / print を元に戻す
     */
    private _restoreConsole() {
        if (!this._isHooked) {
            return;
        }
        this._isHooked = false;

        if (this._originalLog) {
            console.log = this._originalLog;
        }
        if (this._originalWarn) {
            console.warn = this._originalWarn;
        }
        if (this._originalError) {
            console.error = this._originalError;
        }
        if (this._originalPrint) {
            (globalThis as any).print = this._originalPrint;
        }
    }

    /**
     * 引数配列からログ文字列を生成して追加
     */
    private _addLogFromArgs(args: any[], type: LogType) {
        // エディタ上でも不要なときは無視
        if (!this.enabledInHierarchy) {
            return;
        }

        const text = args
            .map((v) => {
                if (typeof v === 'string') {
                    return v;
                }
                try {
                    return JSON.stringify(v);
                } catch {
                    return String(v);
                }
            })
            .join(' ');

        this._appendLog(text, type);
    }

    /**
     * 内部ログ配列に追加し、最大行数を超えたら古いものから削除
     */
    private _appendLog(message: string, type: LogType) {
        const prefixParts: string[] = [];

        if (this.timeStamp) {
            const d = new Date();
            const hh = d.getHours().toString().padStart(2, '0');
            const mm = d.getMinutes().toString().padStart(2, '0');
            const ss = d.getSeconds().toString().padStart(2, '0');
            prefixParts.push(`${hh}:${mm}:${ss}`);
        }

        if (this.showLogPrefix) {
            prefixParts.push(type === LogType.PRINT ? 'PRINT' : type);
        }

        const prefix = prefixParts.length > 0 ? `[${prefixParts.join(' ')}] ` : '';
        const finalLine = prefix + message;

        this._logs.push(finalLine);
        if (this._logs.length > this.maxLines) {
            const overflow = this._logs.length - this.maxLines;
            this._logs.splice(0, overflow);
        }

        this._refreshLabel();
    }

    /**
     * Label の文字列をログ配列から再構築
     */
    private _refreshLabel() {
        if (!this._label) {
            return;
        }
        this._label.string = this._logs.join('\n');

        // コンテンツ高さを更新
        const lineHeight = this._label.lineHeight || (this.fontSize + this.lineSpacing);
        const contentHeight = Math.max(this.height, this._logs.length * lineHeight);

        const contentTransform = this._contentNode?.getComponent(UITransform);
        if (contentTransform) {
            contentTransform.setContentSize(this.width, contentHeight);
        }

        if (this.autoScrollToBottom && this._scrollView) {
            // 次のフレームでスクロールを更新(直接すぐにやると反映されない場合があるため)
            this.scheduleOnce(() => {
                if (this._scrollView) {
                    this._scrollView.scrollToBottom(0.05, true);
                }
            }, 0);
        }
    }
}

コードの主要部分の解説

  • onLoad()
    _createUI() を呼び出し、背景・スクロールビュー・ラベルを自動生成します。
    enabledOnStart に応じて enableConsole()disableConsole() を呼びます。
  • _createUI()
    – アタッチされたノードの子に LogConsoleRoot を作成し、その中に Background(Sprite)と ScrollView(Mask + ScrollView + Content + Label)を構築します。
    Widget を使って、画面端基準(anchorX/anchorY + marginX/marginY)で配置します。
  • _hookConsole() / _restoreConsole()
    – 既存の console.log/warn/errorprint を一度保存してから上書きし、呼び出し時に元の関数を呼びつつ内部ログ配列にも追加します。
    onDestroy() では必ず _restoreConsole() を呼び、コンポーネントが破棄された後に他のスクリプトへ影響が残らないようにします。
  • _appendLog()
    – タイムスタンプとログ種別プレフィックスを組み立て、1 行の文字列を作成します。
    maxLines を超えた分は先頭から削除し、_refreshLabel() で Label の文字列を更新します。
  • _refreshLabel()
    _logs'\n' で結合して Label にセットし、行数に応じて Content の高さを調整します。
    autoScrollToBottom が有効な場合は、次フレームで scrollToBottom() を呼んで常に最新ログを表示します。

使用手順と動作確認

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

  1. エディタ左下の Assets パネルで、ログ関連のフォルダ(例: scripts/debug)を作成します。
  2. そのフォルダを右クリックして、
    Create → TypeScript を選択し、ファイル名を LogConsole.ts にします。
  3. 作成された LogConsole.ts をダブルクリックして開き、本文をすべて削除して、上記の TypeScript コードを丸ごと貼り付けて保存します。

2. テスト用シーンとノードの用意

  1. 任意のシーン(例: TestScene.scene)を開きます。
  2. Hierarchy パネルで右クリック → Create → Empty Node を選択し、
    ノード名を LogConsoleRoot など分かりやすい名前に変更します。

このノードがコンソール UI の親になります(UI はスクリプトが自動で子ノードを生成します)。

3. LogConsole コンポーネントをアタッチ

  1. Hierarchy で先ほど作成した LogConsoleRoot ノードを選択します。
  2. 右側の Inspector パネルで Add Component ボタンをクリックします。
  3. Custom ComponentLogConsole を選択してアタッチします。

4. プロパティの調整

Inspector 上の LogConsole コンポーネントに、以下のように設定してみます(例):

  • Enabled On Start: チェック(true)
  • Capture Print: チェック
  • Capture Console Log: チェック
  • Capture Console Warn: チェック
  • Capture Console Error: チェック
  • Max Lines: 200
  • Font Size: 18
  • Line Spacing: 4
  • Background Opacity: 180(半透明)
  • Background Color: 黒(デフォルトのままでOK)
  • Width: 600
  • Height: 260
  • Anchor X: 0(画面左)
  • Anchor Y: 0(画面下)
  • Margin X: 20
  • Margin Y: 20
  • Auto Scroll To Bottom: チェック
  • Show Log Prefix: チェック
  • Time Stamp: チェック

これで、画面左下に半透明の黒いログウィンドウが表示される設定になります。

5. 動作確認用のログ出力

方法 A: 任意のスクリプトからログを出す

  1. 適当なゲーム用ノード(例: GameRoot)に新規 TypeScript スクリプト(例: LogTest.ts)を作成してアタッチします。
  2. LogTest.ts に以下のようなコードを書きます。

import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;

@ccclass('LogTest')
export class LogTest extends Component {
    start() {
        console.log('これは console.log のテストです');
        console.warn('これは console.warn のテストです');
        console.error('これは console.error のテストです');
        if ((globalThis as any).print) {
            (globalThis as any).print('これは print() のテストです');
        }
    }

    update(deltaTime: number) {
        // 毎フレーム何かしらログを出したい場合の例
        // console.log('deltaTime =', deltaTime);
    }
}

方法 B: 即席で print() を叩いてみる

エディタの Preview でゲームを再生し、ブラウザの開発者ツールコンソール(F12)から以下を入力しても構いません。


print('ブラウザコンソールからの print テスト');
console.log('ブラウザコンソールからの console.log テスト');

6. 実行して確認

  1. Cocos Creator 上部の Play ボタン(▶)を押してゲームを再生します。
  2. 画面左下に半透明の黒ウィンドウが表示され、LogTest.tsstart() で出力したログが、
    タイムスタンプと種別付きで表示されていることを確認します。
  3. ログが増えていくと、ウィンドウ内で縦スクロールできるようになります。
    Auto Scroll To Bottom が有効なら、常に最新ログが一番下に表示されます。

まとめ

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

  • アタッチするだけで UI を自動生成し、
  • console.log / warn / errorprint をキャプチャしてゲーム内に表示し、
  • 最大行数・フォントサイズ・背景の透明度・配置などをインスペクタから簡単に調整でき、
  • 他の自作スクリプトやシングルトンに一切依存しない

という、現場で即使える汎用デバッグコンソールです。

例えば、

  • モバイル端末での挙動確認時に、PC のブラウザコンソールが見られない状況でも内部状態を確認したい
  • QA チームにビルドを渡す際、特定の操作手順で何が起こっているかをゲーム画面上で見せたい
  • プレイヤーからの不具合報告時に、スクリーンショットだけである程度のログ状況を把握したい

といったシーンで、そのまま役立ちます。

このコンポーネントをプロジェクトのテンプレートに入れておけば、どのゲームでもすぐに画面内ログコンソールを用意でき、デバッグ効率が大幅に向上します。必要に応じて、特定キーボード入力で表示/非表示を切り替える機能や、ログフィルタ(WARN/ERROR のみ表示など)を追加して拡張することも容易です。

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