命令注册表

开发者控制台的数据核心——命令表、分词器、执行、历史记录与自动补全。UI 由你来搭。

开发者控制台所需的一切,除了屏幕——命令、解析、历史记录和自动补全。

接口ICommandRegistry
关闭开关NullCommandRegistry
程序集CommonGameSystem.Core
启动启动时自动注册(23 个服务之一)——无需任何设置

它做什么

命令注册表是开发者控制台、作弊菜单或自动化脚本可以接入的引擎。它掌管数据:命令表(名称到处理函数)、原始输入行的分词器(把 give sword 3 拆成 givesword3 的那个部件)、有界的命令历史,以及前缀自动补全。它不在屏幕上绘制任何东西——控制台 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)对该行分词,把第一个 token 当作命令名、其余当作参数,把该行记入历史,并返回处理函数的结果。对运行时输入绝不抛异常。
CommandResult TryExecute(string name, IReadOnlyList<string> args)直接派发已解析好的名称与参数。跳过分词,且记入历史。
IReadOnlyList<string> Tokenize(string rawLine)单遍分词器:按空白切分、双引号成组、反斜杠转义。没有 shell 展开。绝不抛异常。

自动补全

成员说明
IReadOnlyList<string> Lookup(string prefix)prefix 开头的已注册名称,已排序。空或 null 前缀返回全部名称。绝不抛异常。

历史记录(纯数据——无 UI)

成员说明
string Previous()光标向更旧的条目移动一步并返回该行。到最旧条目后停住(不回绕);历史为空时返回 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)、CaseSensitiveNamestrue)、DuplicateRegistrationPolicyMaxArgumentCount(0 = 不限)、WarnOnUnknownCommandWarnOnDuplicateRegistrationWarnOnMalformedLineInitialCommandCapacity
enum CommandDuplicatePolicyReplaceWithWarning(默认)/ ReplaceSilently / Throw

自己搭一个控制台

注册表把控制台所需的一切都给了你,除了屏幕:TryExecute 产出输出文本,Previous/Next 驱动上/下键的历史回溯,Lookup 支撑自动补全。把这三样绑到任何你喜欢的 UI 上,一个能用的游戏内控制台就成了。

你不必从零开始。Command Console 示例——随包发布的六个示例之一,也是三个可直接运行的示例之一——就是建立在本服务之上的一个完整可用的控制台:一块深色 UGUI 面板,带输出视图、一行暗色的自动补全提示、一个输入框、五个演示命令(helpechoaddtimescaleclear)以及上/下键历史。通过 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);
}

实用提示——在 Update 中检测 Enter,而不是在 onEndEdit 中。 从输入框的 onEndEdit 事件里提交很有诱惑力,但等到 UI 抛出 onEndEdit 时,键盘的 enterKey.wasPressedThisFrame 已经变回 false(在 Unity 6.3 LTS 上实测)——在 onEndEdit 里用那个标志位做提交门禁的控制台会悄悄吞掉每一次提交。请像上面的代码片段那样,改在 Update 里轮询键盘。还有一个细节:同一次 Enter 按键可能在这一帧的更早时刻让输入框失焦,所以只要输入框现在处于焦点,或上一帧处于焦点就接受这次按键——_fieldFocusedLastFrame 就是干这个的。

示例里还有两个值得照抄的习惯:

  • 在共享注册表上注册,且只清理你自己的名称。OnDestroy 中,恰好对你添加的那些命令调用 Unregister。永远不要对注册表调用 Dispose()——它是由 Bootstrap 拥有的共享服务。
  • 如果打字或按 Enter 完全没有反应,你的项目很可能还停在 Unity 的旧输入后端上。打开 Tools → Common Game System → Welcome,点击 Enable the new Input System(需要重启一次编辑器),然后再次 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。按真实注册表接线的游戏照常运行,只是作弊与命令悄然失效。替换实现构造时记录一条警告,因此替换绝不会悄无声息。编程错误仍会抛异常——null/全空白名称、null 处理函数、释放后使用——这是框架中每个空实现共同遵循的规则。

常见陷阱

  • 控制台 UI 归你。 注册表只交付数据核心——没有打印、渲染或开关键 API。把你自己的 UI 绑到 TryExecute(输出)、Previous/Next(历史回溯)和 Lookup(自动补全)上。CommandResult.Message 是注册表产出的唯一文本。
  • 处理函数自己解析参数。 参数以 IReadOnlyList<string> 形式到达,且不含命令名。注册表不做类型化转换或反射绑定——int.Parse 之类请自己调用。这正是它保持 IL2CPP/AOT 安全的原因。
  • 两个 TryExecute 重载对历史的处理不同。 string rawLine 重载会分词记入历史;预解析的 (name, args) 重载两者都跳过。控制台输入用原始行形式,程序化派发用已解析形式。
  • default(CommandRegistryOptions) 不是 .Default 裸的 new CommandRegistryOptions()(或 default)是全零的选项包:历史容量 0、名称不区分大小写。注册表会从这些零值恢复出合理的数值,但请显式传入 CommandRegistryOptions.Default 来获得规范策略。
  • 仅限主线程。 与多数框架服务一样,注册表会断言主线程。从工作线程调用它属于编程错误。

相关页面

  • 日志服务 — 注册表用来写日志的诊断汇点
  • 服务定位器 — 解析服务,以及换入空实现
  • Bootstrap — 每个服务如何被自动注册与关停
  • 时间 — 示例中 timescale 命令所驱动的对象