コマンドレジストリ

開発者コンソールのデータコア — コマンドテーブル、トークナイザー、実行、履歴、オートコンプリート。UI は自分で作る。

開発者コンソールに必要な、画面以外のすべて — コマンド、パース、履歴、オートコンプリート。

インターフェースICommandRegistry
オフスイッチNullCommandRegistry
アセンブリCommonGameSystem.Core
起動起動時に自動登録(23 サービスの 1 つ)— セットアップ不要

概要

コマンドレジストリは、開発者コンソール、チートメニュー、自動化スクリプトが差し込まれるエンジンです。データを所有します:コマンドテーブル(名前からハンドラーへ)、生の入力行のトークナイザー(give sword 3givesword3 に分割する部分)、有界のコマンド履歴、そして前方一致のオートコンプリート。画面には何も描画しません — コンソール UI(レンダリング、入力キャプチャ、トグルキー)はあなたが作るものです。その方法は下の独自のコンソールを作るセクションが示しています。

ランタイムの問題が呼び出し元をクラッシュさせることはありません。未知のコマンド、不正な引数、例外を投げるハンドラーは隔離され、ログされ、失敗した CommandResult として返されます。例外を投げるのはプログラミングエラーだけです:null や空の名前・null ハンドラーの登録、破棄後のレジストリの使用、ワーカースレッドからの呼び出し。

クイックスタート

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)] // Run after the framework has started.
public class MyCheats : MonoBehaviour
{
    private ICommandRegistry _commands;

    private void Awake()
    {
        _commands = ServiceLocator.Resolve<ICommandRegistry>();   // registered automatically at startup

        _commands.Register("give", args =>
        {
            if (args.Count < 2) return CommandResult.Fail("usage: give <id> <count>");
            return CommandResult.Ok($"gave {args[1]}x {args[0]}");
        }, new CommandMetadata("Grant an item", "give <id> <count>"));

        CommandResult r = _commands.TryExecute("give sword 3");   // tokenizes, dispatches, records history
        Debug.Log(r.Message);
    }
}

上のように、サービスは Awake() で一度解決してキャッシュしてください。フレームワークの起動後に実行されるよう、クラスに [DefaultExecutionOrder(100)] を付けます。

API リファレンス

登録

メンバー説明
void Register(string name, CommandHandler handler, CommandMetadata metadata = default)名前をハンドラーにマップします。既存の名前を登録すると、構築時に選んだ重複ポリシーに従います:警告付きで置換、静かに置換、または例外。null/空白の名前や null ハンドラーは例外を投げます。
bool Unregister(string name)名前を削除します。存在していれば true、未知・null・空白の名前なら false を返します。例外は投げません。
bool IsRegistered(string name)名前が登録されているかどうか。null や空白は false を返します。
bool TryGetMetadata(string name, out CommandMetadata metadata)true とそのエントリのメタデータ、または falsedefault
IReadOnlyList<string> RegisteredNames { get; }登録済みの全名前の、読み取り専用でソート済みのスナップショット。スナップショットは変更できません。

実行

メンバー説明
CommandResult TryExecute(string rawLine)行をトークナイズし、最初のトークンをコマンド名、残りを引数として扱い、行を履歴に記録し、ハンドラーの結果を返します。ランタイム入力に対しては決して例外を投げません。
CommandResult TryExecute(string name, IReadOnlyList<string> args)事前にパース済みの名前と引数を直接ディスパッチします。トークナイズをスキップし、履歴には記録しません
IReadOnlyList<string> Tokenize(string rawLine)シングルパスのトークナイザー:空白での分割、二重引用符でのグループ化、バックスラッシュエスケープ。シェル展開はありません。決して例外を投げません。

オートコンプリート

メンバー説明
IReadOnlyList<string> Lookup(string prefix)prefix で始まる登録済みの名前、ソート済み。空または null のプレフィックスは全名前を返します。決して例外を投げません。

履歴(データのみ — UI なし)

メンバー説明
string Previous()カーソルを古いエントリの方向へ 1 つ動かし、その行を返します。最古のエントリで止まり(ラップアラウンドなし)、履歴が空のときは null を返します。
string Next()新しいエントリの方向へ動きます。最新を超えると null(空入力の状態)を返します。
void ResetHistoryCursor()カーソルを最新位置へ戻します。
IReadOnlyList<string> History { get; }時系列(古いものから新しいものへ)のスナップショット。呼び出しごとに新しいリストです。

サポート型

説明
delegate CommandResult CommandHandler(IReadOnlyList<string> args)パース済みの文字列引数に対するあなたのハンドラー。コマンド名は除外され、リストが null になることはありません。型付きの値はハンドラー自身がパースします — レジストリは型変換を一切行わず、それが IL2CPP/AOT 安全を保ちます。
readonly struct CommandResultbool Successstring Message(決して null になりません)。CommandResult.Ok(msg)CommandResult.Fail(msg) で作ります。default は失敗として扱われます。
readonly struct CommandMetadata(string description, string usage)DescriptionUsage のヘルプ文字列(どちらも決して null になりません)。
readonly struct CommandRegistryOptionsコンストラクタまたは .Default 経由の構築ポリシー:HistoryCapacity(64、8–1024 にクランプ)、CaseSensitiveNames(true)、DuplicateRegistrationPolicyMaxArgumentCount(0 = 無制限)、WarnOnUnknownCommandWarnOnDuplicateRegistrationWarnOnMalformedLineInitialCommandCapacity
enum CommandDuplicatePolicyReplaceWithWarning(デフォルト)/ ReplaceSilently / Throw

独自のコンソールを作る

レジストリは、コンソールに必要なもののうち画面以外のすべてを提供します:TryExecute が出力テキストを生み、Previous/Next が Up/Down の履歴呼び出しを駆動し、Lookup がオートコンプリートを支えます。この 3 つを好きな UI にバインドすれば、動くゲーム内コンソールの完成です。

ゼロから始める必要はありません。Command Console サンプル — パッケージに同梱される 6 つのサンプルの 1 つで、実行可能な 3 つのうちの 1 つ — は、このサービスの上に築かれた完全に動作するコンソールです:出力ビュー付きのダークな UGUI パネル、薄暗いオートコンプリートのヒント行、入力フィールド、5 つのデモコマンド(helpechoaddtimescaleclear)、そして Up/Down 履歴。Tools → Common Game System → Welcome → Import "Command Console Sample" からインポートし、インポートされた CommandConsole.unity を開いて Play を押してください。推奨のスタート地点です:プロジェクトにコピーして、コマンドリストを拡張してください。

配線パターンをコアだけに削ぎ落とすと:

using System.Collections.Generic;
using CommonGameSystem.Core;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;

[DefaultExecutionOrder(100)]
public class MyConsole : MonoBehaviour
{
    [SerializeField] private InputField _inputField;  // your input row
    [SerializeField] private Text _outputText;        // your output view

    private ICommandRegistry _commands;
    private bool _fieldFocusedLastFrame;

    private void Awake() => _commands = ServiceLocator.Resolve<ICommandRegistry>();

    private void Update()
    {
        Keyboard kb = Keyboard.current;
        if (kb == null) return;

        // History recall while the field is focused.
        if (_inputField.isFocused)
        {
            if (kb.upArrowKey.wasPressedThisFrame) Recall(_commands.Previous());
            else if (kb.downArrowKey.wasPressedThisFrame) Recall(_commands.Next() ?? string.Empty);
        }

        // Submit on Enter — detected here in Update (see the tip below).
        bool enter = kb.enterKey.wasPressedThisFrame || kb.numpadEnterKey.wasPressedThisFrame;
        if (enter && (_inputField.isFocused || _fieldFocusedLastFrame))
            Submit(_inputField.text);

        _fieldFocusedLastFrame = _inputField.isFocused;
    }

    private void Submit(string line)
    {
        if (string.IsNullOrWhiteSpace(line)) return;
        CommandResult result = _commands.TryExecute(line);   // tokenize + dispatch + history
        _outputText.text += $"\n> {line}\n{result.Message}";
        _commands.ResetHistoryCursor();                      // next Up starts from the newest entry
        _inputField.text = string.Empty;
        _inputField.ActivateInputField();                    // refocus for the next command
    }

    private void Recall(string line)
    {
        if (line == null) return;                            // null only when history is empty
        _inputField.text = line;
        _inputField.caretPosition = line.Length;
        _inputField.ActivateInputField();
    }

    // Autocomplete: call from the field's onValueChanged and render the matches yourself.
    public IReadOnlyList<string> Complete(string prefix) => _commands.Lookup(prefix);
}

実践的なヒント — Enter の検出は onEndEdit ではなく Update で。 入力フィールドの onEndEdit イベントからサブミットしたくなりますが、UI が onEndEdit を上げる頃には、キーボードの enterKey.wasPressedThisFrame はすでに false に戻っています(Unity 6.3 LTS で計測)— onEndEdit の内側でそのフラグにサブミットをゲートするコンソールは、すべてのサブミットを静かに飲み込みます。上のスニペットのように、代わりに Update でキーボードをポーリングしてください。もう 1 つの機微:同じ Enter 押下がフレームの早い段階で入力フィールドを非アクティブ化することがあるため、フィールドが今フォーカスされているか、前のフレームでフォーカスされていた場合に押下を受け入れてください — _fieldFocusedLastFrame はそのためにあります。

サンプルから真似する価値のある習慣を、あと 2 つ:

  • 共有レジストリに登録し、クリーンアップは自分の名前だけ。 OnDestroy で、自分が追加したコマンドに限って Unregister を呼んでください。レジストリを Dispose() しては決していけません — Bootstrap が所有する共有サービスです。
  • 入力や Enter にまったく反応がない場合、プロジェクトがまだ Unity の古い入力バックエンドのままの可能性が高いです。Tools → Common Game System → Welcome を開いて Enable the new Input System をクリックし(エディタ再起動 1 回)、もう一度 Play してください。

使用例

using System.Collections.Generic;
using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class DebugConsoleInput : MonoBehaviour
{
    private ICommandRegistry _commands;

    private void Awake()
    {
        _commands = ServiceLocator.Resolve<ICommandRegistry>();
        _commands.Register("god", _ => CommandResult.Ok("god mode toggled"),
            new CommandMetadata("Toggle invulnerability", "god"));
    }

    // Called by your own UI when the player submits a console line.
    public string Submit(string line)
    {
        CommandResult r = _commands.TryExecute(line);   // recorded in history automatically
        return r.Message;                               // your UI prints it
    }

    // Up/Down arrow recall — pure data; you bind the keys.
    public string RecallPrevious() => _commands.Previous();
    public string RecallNext() => _commands.Next();

    // Tab autocomplete — you render the candidate list.
    public IReadOnlyList<string> Complete(string prefix) => _commands.Lookup(prefix);
}

無効化する

ServiceLocator.Replace<ICommandRegistry>(new NullCommandRegistry());

これですべてのコマンドがミュートされます。Register/Unregister は何もせず、TryExecute は失敗した CommandResult を返し、すべてのクエリは空のコレクションまたは null を返します。実際のレジストリに対して配線されたゲームは、チートやコマンドが静かに不活性のまま動き続けます。置き換えの構築時に 1 回警告がログされるので、差し替えが無音で起きることはありません。プログラミングエラーは引き続き例外を投げます — null/空白の名前、null ハンドラー、破棄後の使用 — フレームワークのすべての null 実装が従うのと同じルールです。

よくある落とし穴

  • コンソール UI はあなたのものです。 レジストリが出荷するのはデータコアだけです — print、レンダリング、キートグルの API はありません。自分の UI を TryExecute(出力)、Previous/Next(履歴呼び出し)、Lookup(オートコンプリート)にバインドしてください。レジストリが生成する唯一のテキストは CommandResult.Message です。
  • 引数のパースはハンドラー自身が行います。 引数は、コマンド名を除いた IReadOnlyList<string> として届きます。レジストリは型付き変換もリフレクションバインディングも行いません — int.Parse などは自分で呼んでください。これこそが IL2CPP/AOT 安全を保っているものです。
  • 2 つの TryExecute オーバーロードは履歴の扱いが異なります。 string rawLine オーバーロードはトークナイズ履歴記録を行い、パース済みの (name, args) オーバーロードは両方をスキップします。コンソール入力には raw-line 形式を、プログラムからのディスパッチにはパース済み形式を使ってください。
  • default(CommandRegistryOptions).Default ではありません。 素の new CommandRegistryOptions()(または default)は、すべてゼロの袋です:履歴容量 0、大文字小文字を区別しない名前。レジストリはゼロから妥当な値を復元しますが、正準のポリシーを得るには CommandRegistryOptions.Default を明示的に渡してください。
  • メインスレッド専用です。 ほとんどのフレームワークサービスと同じく、レジストリはメインスレッドをアサートします。ワーカースレッドからの呼び出しはプログラミングエラーです。

関連ページ

  • ロガー — レジストリがログを流す診断シンク
  • サービスロケーター — サービスの解決と null 実装への差し替え
  • ブートストラップ — すべてのサービスが自動で登録・シャットダウンされる仕組み
  • Time — サンプルの timescale コマンドが駆動するもの