Flame 游戏引擎对话系统进阶:Jenny 运行时 CommandStorage 自定义指令注册与执行全解析 Flame 游戏引擎对话系统进阶Jenny 运行时 CommandStorage 自定义指令注册与执行全解析【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flameFlame 的官方对话引擎 Jenny位于 packages/flame_jenny在运行时通过CommandStorage统一管理所有用户自定义指令user-defined commands让游戏开发者可以把give、walk、prompt这类指令接入真实的游戏逻辑。本文以 doc/other_modules/jenny/runtime/command_storage.md 为核心结合 command_storage.dart 源码与 command_storage_test.dart 测试用例系统讲解自定义指令的注册约束、全部 API、底层参数解析与执行流程读完即可在 Flame 项目中稳定接入任意自定义对话指令。CommandStorage 是什么CommandStorage是 YarnProject 中负责存放所有用户自定义指令的容器通过YarnProject.commands属性访问。它允许你注册任意数量的自定义命令使它们可以在 yarn 脚本中直接使用。一个关键的时序约束是自定义指令必须在解析parseyarn 脚本之前完成注册否则编译器在遇到未知指令时会直接报错。这与函数注册FunctionStorage见 function_storage.md的约束完全一致。标准初始化顺序如下摘自 yarn_project.md链接用户自定义函数functions链接用户自定义指令commands设置 locale如果默认en不满足需求解析包含全局变量与角色声明的.yarn脚本解析其余所有.yarn脚本从存档恢复变量。final yarn YarnProject() ..functions.addFunction0(money, player.getMoney) ..commands.addCommand1(achievement, player.earnAchievement) ..parse(readFile(project.yarn)) ..parse(readFile(chapter1.yarn)) ..parse(readFile(chapter2.yarn));从源码看CommandStorage的内部实现非常轻量——它只是用一个MapString, _Cmd?来存储命令名到封装函数的映射command_storage.dartclass CommandStorage { CommandStorage() : _commands {}; final MapString, _Cmd? _commands;其中_Cmd是包裹 Dart 函数的内部类负责记录函数签名参数类型列表并在运行时做类型转换与调用。注册自定义指令的硬性约束要把一个 Dart 函数注册为 yarn 指令该函数必须满足以下要求与原文档一致并有源码_Cmd类作证约束说明返回值必须是void或Futurevoid。返回 Future 的指令会被await对话会等待其完成后再进入下一步——这正是walk、moveCamera、prompt这类需要时间展开的指令的实现基础参数类型必须是 Jenny 已知的类型String、num、int、double、bool。源码中的类型映射表位于 command_storage.dartbool → boolean、int → integer、double → double、num → numeric、String → string参数形式必须全部是位置参数positional非空non-nullable且不能有默认值注册方法按参数个数选择addCommand0()~addCommand5()中对应的方法尾部布尔参数若函数签名末尾有 1 个及以上bool参数这些参数将被视为可选缺省时自动取false尾部布尔参数的可选特性在源码中有明确实现_Cmd构造时通过_signature.reversed.takeWhile((type) type _Type.boolean).length计算出numTrailingBooleanscommand_storage.dart在unpackArguments中先把这些位置预填为false第 194-196 行再按实际提供的参数字符串逐位覆盖。测试用例 command_storage_test.dart 验证了true true true、true true、true、空串四种情况分别得到(true,true,true)、(true,true,false)、(true,false,false)、(false,false,false)。另外指令名本身受_checkName校验command_storage.dart必须满足三条规则否则触发 assert不能与已注册指令重名Command $name has already been defined不能与内置指令同名Command $name is built-in必须是一个合法标识符正则^[a-zA-Z_]\w*$Command name $name is not an identifier。内置指令白名单位于 command_storage.dartdeclare、else、elseif、endif、for、if、jump、local、set、stop、visit、wait、while其中部分为预留。测试 command_storage_test.dart 验证了注册if/set/for/while/local都会抛断言错误。全部 API 一览CommandStorage对外提供的接口可分为注册类和查询/管理类两类。注册类方法方法签名功能addCommand0(String name, FutureOrvoid Function() fn)注册一个无参函数为指令nameaddCommand1(String name, FutureOrvoid Function(T1) fn)注册单参数函数addCommand2(String name, FutureOrvoid Function(T1, T2) fn)注册两参数函数addCommand3(String name, FutureOrvoid Function(T1, T2, T3) fn)注册三参数函数addCommand4(String name, FutureOrvoid Function(T1, T2, T3, T4) fn)注册四参数函数addCommand5(String name, FutureOrvoid Function(T1, T2, T3, T4, T5) fn)注册五参数函数addOrphanedCommand(String name)注册一个没有 Dart 函数支撑的孤儿指令泛型参数T1~T5的取值被限制在上述五个已知类型内。如果在注册时使用了不支持的参数类型例如List_unpackTypes中的 assert 会直接失败Unsupported type Listdynamic of argument 1测试见 command_storage_test.dart。addOrphanedCommand 的特殊语义该指令不绑定任何 Dart 函数注册时在映射中存入nullcommand_storage.dart。它依然会被投递到所有 DialogueView 的onCommand()回调中但它的参数不会被解析——对应的 UserDefinedCommand 对象的arguments属性保持为null。这适合那些纯 UI 通知类指令例如让视图播放一段动画而无需参数类型检查。查询与管理类方法方法/属性功能bool hasCommand(String name)查询指令name是否已注册源码实现即_commands.containsKey(name)void clear()清空所有用户自定义指令void remove(String name)按名字移除某个用户自定义指令int length已注册的用户自定义指令数量bool isEmpty是否一个指令都未注册bool isNotEmpty是否已有任意指令注册测试 command_storage_test.dart 验证了clear()后isEmpty为true以及remove(foo)后hasCommand(foo)变为false、其余指令不受影响。指令执行的底层流程当对话运行到一条用户自定义指令时CommandStorage.runCommand()标注为internal由对话运行时调用会执行如下流水线command_storage.dartFutureOrvoid runCommand(UserDefinedCommand command) { command.argumentString command.content.evaluate(); // 1. 求值参数文本 final cmd _commands[command.name]; if (cmd ! null) { final stringArgs ArgumentsLexer(command.argumentString).tokenize(); // 2. 分词 final typedArgs cmd.unpackArguments(stringArgs); // 3. 类型检查与转换 command.arguments typedArgs; // 4. 回填解析结果 return cmd.run(typedArgs); // 5. 调用 Dart 函数 } }各步骤的细节如下参数求值指令内容按普通行解析规则求值其中允许插值表达式放在花括号{}中但不允许 markup 和 hashtag。例如give Gold {round(100 * $multiplier)}在$multiplier 1.5时求值后的参数字符串为Gold 150。UserDefinedCommand的argumentString属性会保存这份求值结果user_defined_command.dart。分词ArgumentsLexer参数字符串按空白空格、Tab切分为独立参数同时支持双引号包裹的带空格参数以及转义序列\\、\、\ncommand_storage.dart。该词法分析器位于同一文件内测试集中在 command_storage_test.dart例如Hello World被解析为单个参数Hello World而hello会抛出Unterminated quoted string的DialogueError。类型检查与转换unpackArguments首先校验参数个数——既不能超出签名长度也不能少于必需参数数stringArguments.length numTrailingBooleans _arguments.length时报错然后逐个按签名类型转换command_storage.dartboolean命中YarnProject.trueValues记true命中falseValues记false否则抛TypeError。默认集合定义在 yarn_project.darttrueValues {true,yes,on,,T,1}falseValues {false,no,off,-,F,0}integerint.tryParse失败抛TypeErrordoubledouble.tryParse失败抛TypeErrornumeric对应numnum.tryParse整数、浮点、Infinity、-0.0均可接受string原样保留字符串。回填与调用解析后的类型化参数写入UserDefinedCommand.arguments随后调用被包裹的 Dart 函数。返回的FutureOrvoid会被对话运行器等await——这就是prompt这类指令能让对话卡住等待用户输入的原因。一个常见的报错示例测试 command_storage_test.dartaddCommand2(xyz, (int z, bool f) null)后运行xyz 1 true 3会得到TypeError: Command xyz expects 2 arguments but received 3 arguments。实战示例一StartQuest发起任务假设我们想要一个发起任务的指令StartQuest它携带任务 ID 与任务名两个参数。如果只传 IDyarn 脚本的可读性会很差无法一眼看出是哪个任务因此同时传 ID 与名称并在运行时校验二者匹配。典型调用如下注意任务名带引号否则Get rid of bandits会被解析成Get、rid、of、bandits四个独立参数StartQuest Q037 Get rid of bandits对应的 Dart 实现函数返回void任务提示动画不需要对话等待用addCommand2注册class MyGame { late YarnProject yarnProject; void startQuest(String questId, String questName) { assert(quests.containsKey(questId)); assert(quests[questId]!.name questName); // ... 实际发起任务的逻辑 } override void onLoad() { yarnProject YarnProject() ..commands.addCommand2(StartQuest, startQuest); } }注意 Dart 函数名startQuest与指令名StartQuest可以不同注册时的name才是 yarn 脚本中实际使用的名字你可以按自己的编程风格自由命名。实战示例二prompt弹出输入框并回写变量prompt会打开一个模态对话框等待用户输入。由于必须等待用户响应该函数返回Futurevoid。指令本身不是表达式、无法返回值因此把结果写入全局变量$prompt对话脚本后续再读取该变量class MyGame { final YarnProject yarnProject YarnProject(); Futurevoid prompt(String message) async { // 一直等到模态对话框从路由栈中被弹出 final name await router.pushAndWait(KeyboardDialog(message)); yarnProject.variables.setVariable(r$prompt, name); } override void onLoad() { yarnProject ..variables.setVariable(r$prompt, ) ..commands.addCommand1(prompt, prompt); } }在 yarn 脚本中的用法如下——注意先declare $name as String声明再把$prompt的值转移给玩家名变量declare $name as String title: Greeting --- Guide: Hello, my name is Jenny, and you? prompt Enter your name: set $player $prompt // Store the name for later Guide: Nice to meet you, {$player} $prompt的读写依赖 VariableStorage这是 Jenny 中全局变量的统一容器。实战示例三give发放物品再实现一个给玩家发放物品的指令它接收三个参数物品来源谁给的、物品名、数量。yarn 中的写法及变量替换效果如下give {$quest_reward} TraderJoe假设任务奖励变量$quest_reward的内容是100 gold、5 potion_of_healing或1 Sword of Darkness运行时会替换成对应的三参数指令再解析give 100 gold TraderJoe give 5 potion_of_healing TraderJoe give 1 Sword of Darkness TraderJoe对应的 Dart 函数签名/// Takes [amount] of [item]s from [source] and gives them to the player. void give(int amount, String item, String source) { // ... 发放逻辑 }这个例子同时展示了三个关键点变量插值在运行时才求值、带空格参数必须用引号包裹、int参数会做严格整数解析100→100。小结CommandStorage是 Flame Jenny 对话体系中把脚本指令接到游戏逻辑的唯一入口。使用时牢记三条原则先注册后解析、参数类型限于五个已知类型、尾部布尔参数自动可选。配合addOrphanedCommand处理纯 UI 通知型指令配合返回Future的函数实现等待型指令即可覆盖绝大多数游戏内对话驱动的交互场景。延伸阅读YarnProject 总览指令、函数、变量、角色的统一入口用户自定义指令语言层语法指令参数的解析规则与求值细节UserDefinedCommand 运行时对象name、argumentString、arguments三属性的完整语义DialogueViewonCommand()回调如何接收指令事件CommandStorage 源码 与 测试用例完整的注册校验、类型转换与错误分支实现【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考