零基础手把手 · 从"不会写"到"会写会查"

GM 指令框架 · 使用说明

这份文档只回答一个问题:我该怎么用它?

读完你能做到:3 分钟写出第一条指令 · 会用面板边打边联想地执行 · 知道参数怎么写 · 避开 8 个最容易白干的坑 · 知道怎么让它不上线

〇它是干什么的(一句话)

一行代码注册一条 GM 指令;在编辑器面板里边打边联想、回车执行,成功/失败与耗时直接回显。

// 一行一条:名字里的 / 会自动长成分组树
RevGM.Register("经济/加金币", "给当前玩家加金币", args => AddGold(args.Int(0, 1000)),
               RevGMArg.Int("数量", 1000));
面板在哪

菜单 Revolution.Tools/GM 指令面板,快捷键 Ctrl+Shift+G。 面板完全活在编辑器里(EditorWindow + IMGUI),不占运行时、不进包体。

面板长这样(示意)

┌────────────────────────────────────────────────────────────────────────────┐
│ ● Play 中(可执行)  命令 42 条                        [命令来源][刷新][帮助] │
│ 命令 [加金____________________________] [清空] [执行 (Enter)]               │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ 经济/加金币          <数量>  给当前玩家加金币   ← 命中字高亮、↑↓ 选择 │ │
│ │ 经济/清空金币                    把金币清零                            │ │
│ │ 战斗/清空全场敌人  ⚠             高危:执行前二次确认                    │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ ┌── 分组树 ──────────┐ ┌── 详情 ─────────────────────────────────────────┐ │
│ │ ▼ 经济 (2)         │ │ 经济/加金币                                     │ │
│ │   加金币           │ │ 给当前玩家加金币                                │ │
│ │   清空金币         │ │ 参数:· 数量|整数(默认 1000)                    │ │
│ │ ▼ 战斗 (1)         │ │ [填入输入框] [复制命令名] [执行]                 │ │
│ └────────────────────┘ └─────────────────────────────────────────────────┘ │
│ ✔ 执行成功(1.20 ms)  经济/加金币 500 → 金币 = 1500                         │
│ 历史(点一条重新填入) 12:31:07 ✔ 经济/加金币 500                            │
└────────────────────────────────────────────────────────────────────────────┘
什么时候用它

任何"想在跑着的游戏里立刻改点东西 / 看看状态"的场合 —— 加资源、跳关卡、改数值、开无敌、切频道、模拟协议回包、复现 bug 的前置状态。

一3 分钟跑起来(可粘贴)

三步:写注册入口 → 启动时调一次 → 面板里执行。

第 1 步:写一个注册入口(放一个静态类里)
using Revolution;

public static class MyGameCommands
{
    private static int _gold;

    [RevGMEntry("战斗 / 经济 相关")]     // ← 想让面板在"不进 Play"时也能列出命令,就加它
    public static void Register()
    {
        // ★ 一行注册:名字 / 说明 / 干什么 / 参数说明
        RevGM.Register("经济/加金币", "给当前玩家加金币",
                       args => { _gold += args.Int(0, 1000); return $"金币 = {_gold}"; },
                       RevGMArg.Int("数量", 1000));
    }
}
第 2 步:让游戏启动时调一次
// 你的启动流程里(游戏入口 / 场景 Boot / 或一个 [RuntimeInitializeOnLoadMethod]):
MyGameCommands.Register();
正式包怎么办?不调 Register 就等于不存在(没有静态构造、没有反射扫描、没有每帧开销)。见第九章。
第 3 步:打开面板执行
  1. 菜单 Revolution.Tools/GM 指令面板(或 Ctrl+Shift+G);
  2. 搜索框里打 加金 → 联想列表立刻出现 经济/加金币;
  3. Tab 补全 → 空格 → 输入 500 → Enter;
  4. 下方显示:✔ 执行成功(1.20 ms) 经济/加金币 500 → 金币 = 1500。

也可以不用面板(游戏内控制台 / 自动化 / 单测)

RevGMResult r = RevGM.Execute("经济/加金币 500");
if (!r.Success) Debug.LogError(r.Message);      // Message 是人话,直接能看

二面板怎么用(4 步 + 键位表)

整个过程可以只用键盘完成。

边打边联想 纯 CSS 动画
命令 加金|
  • 加金经济/加金币<数量> 给当前玩家加金币
  • 经济/清空金币把金币清零
  • 战斗/清空全场敌人 ⚠高危:执行前二次确认

① 打「加金」:命中字高亮,前缀命中排在最前 —— 不用记完整名字。

② ↑ ↓ 选、Tab 补全(自动补一个空格,接着打参数)。

③ Enter 执行:结果、成功失败、耗时直接显示在下方;Ctrl+Enter 跳过二次确认。

支持三种命中:前缀(加金)、连续子串(金币)、字符级缩写(英文名命令如 agi → Battle/AddGoldInstant)。中文命令请打中文子串(拼音缩写不在范围内)。
键 / 操作作用
↑ ↓在联想列表里上下选择
Tab用选中的联想项补全(补完自动加一个空格)
Enter执行输入框里的命令
Ctrl+Enter强制执行(跳过"高危命令"的二次确认)
Esc清空输入框
左树单击 / 双击单击 = 看右侧详情;双击 = 把命令名填入输入框
枚举候选值按钮点一下就生成 命令名 候选值,省得手打
历史那一行点一下就重新填入(改个参数再执行很方便)

两种模式(很重要)

模式命令清单来自能不能执行
编辑模式(没点 Play)调一遍所有 [RevGMEntry] 注册入口拿到的快照❌ 不能(命令体常要碰运行时对象,编辑器里会崩)
Play 模式(游戏跑着)运行期真实注册表✅ 能 —— 被测环境就是游戏本身,结果真实
这就是 [RevGMEntry] 的唯一目的

没有它,编辑模式下面板是空的(命令清单只有在代码真的跑起来后才存在)。加了它,注册方法会被面板在编辑期调一遍 —— 所以那条方法里只做注册,不要初始化业务(这也是"组合根"该有的样子)。

三一行注册的六种形态(挑一种用)

都是"一行",区别只在写不写参数说明、要不要回显、要不要标记。

// ① 最简单:无参数、无返回值(框架自动替你回 done)
RevGM.Register("经济/清空金币", "把金币清零", args => _gold = 0);

// ② 要回显一句话:返回 string 就是面板上那行结果
RevGM.Register("工具/概览", "打印当前状态", args => $"金币={_gold}");

// ③ 带参数说明(推荐):面板显示帮助 + 执行前自动校验
RevGM.Register("战斗/缩放主角", "把主控英雄缩放成指定倍数",
               args => { _scale = args.Float(0); return $"缩放 = {_scale}"; },
               RevGMArg.Float("倍率", 1f));

// ④ 高危:面板执行前二次确认(Ctrl+Enter 跳过)
RevGM.Register("战斗/清空全场敌人", "把所有敌人血量清零", args => ClearEnemy(),
               RevGMFlags.HighRisk);

// ⑤ 隐藏:不进联想列表,但直接写完整名仍可执行(临时/废弃命令)
RevGM.Register("工具/内部复位", "把演示状态复位", args => Reset(), RevGMFlags.Hidden);

// ⑥ 逻辑复杂就别写大 lambda:把方法直接传进来(方法组)
RevGM.Register("聊天/发消息", "往指定频道发消息", SendChatMessage,
               RevGMArg.Enum("频道", "lobby", "guild", "customteam"), RevGMArg.Str("内容", ""));

private static string SendChatMessage(RevGMArgs args)
    => $"已发送到 {args.Str(0)}:{args.Str(1, "(空)")}";

lambda 支持情况(已逐条实测)

写法能否注册说明
表达式 lambda(无返回值)✅args => _counter++ → 匹配 Action<RevGMArgs>
块 lambda(多行 + return)✅返回值会回显
表达式 lambda(返回 string)✅匹配 Func<RevGMArgs,string>
闭包(捕获外部变量)✅能读写捕获值;但它会延长被捕获对象的生命周期(见第七章坑 7)
忽略参数 _ => ...✅
方法组 / 局部函数✅逻辑长就推荐这种,注册处保持一行
返回 null✅框架按 done 处理
lambda + 参数说明 + 高危标记✅参数校验对 lambda 同样生效
只有两种签名

Action<RevGMArgs>(不需要回显,框架自动返回 done)与 Func<RevGMArgs,string>(返回值就是面板上那行结果)。 所以 lambda 必须收一个 RevGMArgs 参数(不需要就写 _),参数一律从 args.Int(0, 默认值) 这类取值器里取。

四参数怎么写(新手最容易含糊的地方)

两步:声明参数说明(给人看 + 给框架校验)+ 用 args 取参数(不自己解析字符串)。

4.1 声明参数说明:RevGMArg

写法含义帮助里显示成
RevGMArg.Int("数量", 1000)整数,可省略数量|整数(默认 1000)
RevGMArg.IntRequired("英雄ID")整数,必填英雄ID|整数(必填)
RevGMArg.Float("倍率", 1f)小数,可省略倍率|小数(默认 1)
RevGMArg.Bool("开启", true)开关(true/false、1/0、on/off、是/否、开/关 都认)开启|开关(默认 true)
RevGMArg.Str("内容")文本,必填内容|文本(必填)
RevGMArg.Str("备注", "")文本,可空备注|文本(可空)
RevGMArg.Enum("频道", "lobby", "guild")枚举,候选值列在面板上(点一下就填)频道|枚举(lobby/guild)

4.2 取参数:args(永远不要自己解析字符串)

args.Count          // 参数个数
args.Has(1)         // 第 2 个参数有没有传
args.Raw(0)         // 原始字符串
args.Text           // 参数整体(原样拼回一行)
args.Str(0, "默认")   // 文本
args.Int(0, 1000)   // 整数(解析失败 → 抛一句人话)
args.Float(0, 1f)   // 小数
args.Bool(0, true)  // 开关
args.Enum<ChatChannel>(0, ChatChannel.Lobby)   // 枚举

4.3 框架会执行前先校验一遍(只要有参数说明)

所以下面这些错误在业务代码跑之前就被拦下并回显,不用等到业务里爆异常:

你输入面板显示
经济/加金币 abc经济/加金币 → 参数「数量」应该是整数,实际收到 "abc"
经济/加金币(必填时)经济/加金币 → 缺少必填参数「数量」(数量|整数(必填))
聊天/发消息 notachannel聊天/发消息 → 参数「频道」应该是 lobby / guild / customteam 之一,实际收到 "notachannel"
参数个数超过声明参数多了 2 个(本命令最多接受 1 个参数;多个词请用引号括起来,例如 "你好 世界")

4.4 带空格的参数要用引号

工具/回显 "你好 世界" 第二段    // 第 1 个参数是「你好 世界」,第 2 个是「第二段」
记住这条

命令名与参数用空格分开(中文全角空格、Tab 也算分隔符);引号内的空格不拆。 但 命令名里不能有空格(分组用 /)—— 框架在注册时就会报错提醒你。

五命令体怎么写得"有话说"(最容易踩的坑)

三条规则:成功回一句话;业务不行抛 RevGMUsageException;真 bug 交给框架。

5.1 成功:返回一句话(或什么都不返回)

RevGM.Register("经济/加金币", "给当前玩家加金币", args =>
{
    AddGold(args.Int(0, 1000));
    return "已发放";           // ← 面板上显示这句话;返回 null 也没问题(显示 done)
});

5.2 "业务上说不行" → 抛 RevGMUsageException

RevGM.Register("工具/领取每日奖励", "领取今天的奖励", args =>
{
    if (AlreadyClaimedToday) throw new RevGMUsageException("今天已经领过了");   // 原样显示在面板
    Claim();
    return "领取成功";
});
这是那套参考实现最缺的一环

早期实现里"界面上连报错都没有",测试同学只能猜。本框架把用法错误和真 bug分开处理。

5.3 真 bug(自己写错了)→ 什么都别做,让框架兜

RevGM.Register("工具/会崩的命令", "演示异常", args => throw new InvalidOperationException("故意崩一下"));
// 面板显示:执行「工具/会崩的命令」时抛异常:InvalidOperationException: 故意崩一下
//            at 你的堆栈首行...
框架统一兜异常

RevGMUsageException → 原样回显人话;其它异常 → 显示类型 + 消息 + 堆栈首行(直接定位)。 绝不会出现"点了没反应"。

六"我要做 X" 对照表(背下就会用)

左边是你脑子里的需求,右边是可以直接抄的代码。

我想做的写法
注册一条无参数命令RevGM.Register("组/名", "说明", args => { ... })
注册一条要回显的命令RevGM.Register("组/名", "说明", args => "结果文本")
注册带参数说明的命令..., handler, RevGMArg.Int("数量", 1000)
加一个必填参数RevGMArg.IntRequired("英雄ID")
加一个枚举参数(面板列候选)RevGMArg.Enum("频道", "lobby", "guild")
标记高危(执行前二次确认)RevGM.Register(..., RevGMFlags.HighRisk)
不进联想(临时/废弃命令)RevGM.Register(..., RevGMFlags.Hidden)
动态注销一条命令RevGM.Unregister("组/名")
清空所有命令(收尾 / 重进 Play)RevGM.Clear()
临时关闭 GM(注册保留)RevGM.Enabled = false
代码里执行 / 自动化脚本RevGM.Execute("组/名 参数") → 判 result.Success
自己做一个输入联想RevGM.Suggest("加金", 8)
让面板在编辑期也能列命令给注册方法加 [RevGMEntry("说明")](方法必须 static、只做注册)
拿命令数量 / 清单RevGM.Count / RevGM.Commands

七八个最容易白干的坑

每一条都有对应报错,看到就知道怎么改。

#坑现象 / 报错正确做法
1命令名里带空格注册时直接抛:GM 命令名里不能有空格:「工具/打印 GM 概览」用 / 分组,不要空格(工具/打印GM概览)
2两条命令重名注册时抛:GM 命令「x」已经注册过了合并,或改分组(框架刻意不做静默覆盖)
3忘了调注册入口面板一条命令都没有启动流程里调一次;或给方法加 [RevGMEntry]
4编辑模式下点执行还没进入 Play:编辑模式只能查看与联想按 Play 再来(执行按钮也会置灰)
5只写末段名但重名「重置」匹配到 2 条命令(…)—— 请写完整名(含分组)写完整名,或从面板联想里选
6参数说明与实际用法不一致执行前被拦下(类型 / 必填 / 枚举不符)让 RevGMArg 列表与 args.Int(0) 一一对应;★ 默认值也是两处:RevGMArg.Int("数量", 1000) 只管面板提示与校验,"不传时实际用多少"由 args.Int(0, 1000) 的第二个参数决定 —— 两处必须写同一个数,否则会"面板说 1000、实际变 0"
7lambda 捕获了场景对象对象不释放、或用的时候已被销毁只捕获服务接口 / 静态数据,运行时再从定位器取真实对象(见第八章)
8联想打拼音缩写(如 jjb)没有匹配联想是字符级的:中文打中文子串(加金);英文名命令可用缩写 agi
坑 7 展开说一句

注册表是长期持有这些委托的:lambda 捕获了什么,什么就不会被回收。 最稳的写法是"命令体只做翻译" —— 捕获服务接口(或直接用全局组合根),运行时再取真实对象。

八完整实战:把 GM 指令接上服务定位器

真实项目里,GM 命令不该自己 new 业务系统,而是从服务定位器里取 —— 两者是天然搭配。

启动期:装配服务 + 注册 GM→ 面板输入:经济/加金币 500→ 命令体:取服务 → 调业务→ 回显:金币 = 1500
① 启动期:装配服务 + 注册 GM(组合根,只写一处)
public static class GameBoot
{
    // 服务定位器那份文档里说过:真要"全局入口",就自己在启动代码里放一个 static readonly 字段
    public static readonly RevServiceLocator Services = RevServiceLocator.Create()
        .AddSingleton<IPlayerService, PlayerService>()
        .AddSingleton<IBattleService, BattleService>()
        .Build();

    [RuntimeInitializeOnLoadMethod]          // 或者你的游戏入口
    public static void Boot()
    {
        GameCommands.Register();             // 注册 GM 指令(正式包不调这一行 = 不存在)
    }
}
② GM 指令只做"翻译":把面板输入翻译成业务调用
public static class GameCommands
{
    [RevGMEntry("局内:经济 / 战斗")]
    public static void Register()
    {
        RevGM.Register("经济/加金币", "给当前玩家加金币(不填默认 1000)",
            args =>
            {
                var player = GameBoot.Services.GetRequired<IPlayerService>();   // ← 运行时取服务
                int amount = args.Int(0, 1000);
                player.AddGold(amount);
                return $"金币 = {player.Gold}";
            },
            RevGMArg.Int("数量", 1000));

        RevGM.Register("战斗/秒杀当前目标", "把当前选中目标血量清零",
            args =>
            {
                var battle = GameBoot.Services.Get<IBattleService>();     // 可选能力:没进战斗就是 null
                if (battle == null) throw new RevGMUsageException("当前不在战斗中");
                if (!battle.HasTarget) throw new RevGMUsageException("没有选中目标");
                battle.KillTarget();
                return "已秒杀";
            },
            RevGMFlags.HighRisk);        // ← 高危:面板会二次确认
    }
}
为什么这么写

① 命令体只做翻译(参数 → 业务调用),业务逻辑仍在 IPlayerService / IBattleService 里 —— GM 指令不会长成第二个业务系统;
② 环境不满足时抛 RevGMUsageException,测试同学看到的是"当前不在战斗中",而不是空引用;
③ 高危命令加 RevGMFlags.HighRisk,避免手滑。

九正式包策略(怎么保证它不上线)

本框架没有用编译宏裁剪,而是用更硬的一条:没人在启动期注册 = 不存在。

手段说明
不注册 = 不存在(推荐)正式包里没人调 RevGM.Register(...) → 注册表是空的;程序集里只有几十行"空转"代码,没有静态构造、没有反射扫描、没有每帧开销
一键关闭RevGM.Enabled = false(注册保留,所有执行一律被拒绝)
整块删除删掉注册入口 + 命令实现文件即可(框架代码可留可删,没有耦合)
真正的权限客户端不做权限伪造:HighRisk 只是"提醒 + 二次确认",真权限必须由服务端裁决(与参考实现的结论一致)
✗ 那套参考实现:靠多层编译宏
// 真码里是这种矩阵,还要靠人工纪律
//#define ENABLE_CHEAT_COMMANDS
#if ENABLE_CHEAT_COMMANDS && ENABLE_AITEST ...
// 结果:89% 的命令处于注释死代码状态
✓ 本框架:靠"注册即存在"
// 正式包不调这一行,就什么都不存在
MyGameCommands.Register();
// 需要临时关:RevGM.Enabled = false

十一页速查卡(可打印)

贴在显示器旁边,写指令时不用回来翻文档。

注册(无参数)RevGM.Register("组/名", "说明", args => { ... })
注册(要回显)RevGM.Register("组/名", "说明", args => "文本")
带参数说明..., RevGMArg.Int("数量", 1000)
必填 / 枚举RevGMArg.IntRequired("ID") · RevGMArg.Enum("频道","a","b")
高危 / 隐藏RevGMFlags.HighRisk · RevGMFlags.Hidden
取参数args.Int(0, 默认) · Float · Bool · Str · Enum<T>
成功return "一句话"(或不返回 → 自动 done)
业务说不行throw new RevGMUsageException("为什么")
面板Revolution.Tools/GM 指令面板(Ctrl+Shift+G)
面板键位↑↓ 选 · Tab 补全 · Enter 执行 · Ctrl+Enter 跳过确认 · Esc 清空
代码里执行RevGM.Execute("组/名 参数")
自己联想的接口RevGM.Suggest("加金", 8)
编辑期也能列命令[RevGMEntry("说明")](方法必须 static、只做注册)
关闭 / 清空RevGM.Enabled = false · RevGM.Clear()
三条铁律

① 注册只写在一处(组合根),命令体里不要再注册别的命令;
② 参数不要自己解析字符串 —— 用 args.Int(0, 默认值),解析失败会给你一句人话;
③ 业务拒绝要"有话说" —— 抛 RevGMUsageException,别用静默 return。

十一与参考实现的对照 + 已知边界

完整 12 条精华 + 16 条糟粕(每条带出处)在代码目录的 README 第四节。

✓ 保留的精华✗ 改掉的糟粕
命令名即层级(组/子组/名 → 自动分组树)反射扫全程序集注册(IL2CPP/AOT 风险,真码要靠 [Preserve] 打补丁)
参数元数据自动拼帮助同名命令静默覆盖
模板方法固定流程 + done 约定必须手输完整名、没有补全
只写末段名也能执行参数靠业务手解字符串(失败抛英文异常)
"永不静默失败"(异常转可读返回)枚举候选值不校验,只能执行完回一段长文案
面板要能模糊搜(且不用"去掉 /"再搜)异常处理推给业务(实测"界面上连报错都没有")
风险 / 可见性标记没有风险等级,只能把注意事项写进命令名
一行注册(TS 侧 RegisteCommand 形态)靠多层编译宏裁剪(宏矩阵复杂、难体检)
—面板占运行时(PC / 手机 / 性能三套视图)
—多套入口并存(类式 / 静态方法式 / 网络式 / 帧式 / TS / 控制台变量)
—单文件 7000 行塞 384 条命令;89% 的命令处于注释死代码状态

已知边界(诚实清单)

十二文件都在哪(你只需要读两个)

运行时:Assets\Revolution\Runtime\RevGMCommand\ 编辑器:Assets\Revolution\Editor\RevGMCommand\

文件要不要读
Facade\RevGM.cs★ 必读:一行注册(6 个重载)/ Execute / Suggest
Core\RevGMArgs.cs★ 必读:取参数(Int / Float / Bool / Str / Enum)
Core\RevGMArg.cs · RevGMFlags.cs · RevGMResult.cs · RevGMUsageException.cs用到再读(参数说明 / 标记 / 结果 / 用法错误)
Implementation\RevGMRegistry.cs · RevGMCommand.cs · RevGMParser.cs · RevGMMatcher.cs不用读(注册表 / 记录 / 分词 / 匹配打分)
Core\RevGMEntryAttribute.cs想让面板编辑期列命令时读
Support\RevGMUnityHooks.cs唯一的 Unity 依赖(33 行):进 Play 清一次命令表,关 Domain Reload 时防跨局残留
Editor\RevGMCommand\RevGMWindow.cs · RevGMEditorCatalog.cs想改面板时读(共 820 行)
Assets\Revolution.Demo\RevGMCommand.Demo\RevGMCommandDemo.cs★ 建议先读这个(9 条示例,直接抄)
下一步读什么

想搞清楚"为什么这么设计"(以及那套参考实现每一处取舍的出处):读 Assets\Revolution\Runtime\RevGMCommand\README.md。