十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flame 引擎 Jenny 运行时中的 UserDefinedCommand:Yarn 脚本自定义命令的完整解析

Flame 引擎 Jenny 运行时中的 UserDefinedCommand:Yarn 脚本自定义命令的完整解析 Flame 引擎 Jenny 运行时中的 UserDefinedCommandYarn 脚本自定义命令的完整解析【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame本指南以 Flame 引擎内置对话系统 JennyYarnSpinner 方言实现的运行时类UserDefinedCommand为核心讲解自定义命令如smile、give、prompt如何从 Yarn 脚本被解析、求值、执行并最终派发给DialogueView的完整链路。读完本文你将掌握name、argumentString、arguments三个核心属性的语义与访问时机理解参数类型检查规则并能结合CommandStorage注册出可直接用于游戏对话的命令函数。UserDefinedCommand 是什么在 Jenny 运行时中UserDefinedCommand表示一个 Yarn 脚本中自定义非内置命令的单次调用。Yarn 脚本里的smile、give Gold 150这类命令在编译阶段并不会被展开为内置逻辑而是在运行时被表示为一个UserDefinedCommand对象并最终投递到对话界面组件 DialogueView 的onCommand()方法中由游戏逻辑接管处理。从源码结构看UserDefinedCommand直接继承自Command位于 user_defined_command.dart其执行入口非常简单override FutureOrvoid execute(DialogueRunner dialogue) { return dialogue.project.commands.runCommand(this); }也就是说命令执行的全部重担都交给了dialogue.project.commands——即 YarnProject 中的 CommandStorage。这一设计决定了只有预先注册过的命令才能被解析执行否则编译器会直接抛错。三个核心属性详解UserDefinedCommand对外暴露三个属性分别对应命令的“名字”“原始参数字符串”与“解析后的参数列表”。name不带尖括号的命令名name是String类型表示命令名本身不含尖括号。例如 Yarn 脚本中的smile其name就是smile。测试 user_defined_command_test.dart 中的parse simple dialogue command用例验证了这一点命令hello world {A B}解析后cmd.name为hello。argumentString求值后的原始参数字符串argumentString是String类型表示命令参数拼成的单个字符串。例如move Hippo {$delta}假设变量$delta的值是3.17那么执行时argumentString就是Hippo 3.17。这里有两个关键语义需要特别强调每次执行都会重新求值。argumentString并非解析时就固定下来而是在命令每次被运行时对参数内容重新求值得到。测试中的evaluate a command multiple times用例就是围绕这一点设计的同一个print {$i} {next()}命令在对话循环中执行 5 次每次next()函数都会自增因此 5 次输出的参数字符串分别是print 0 1、print 1 2……print 4 5。访问时机受限在命令被 dialogue runner 执行之前访问该属性属于错误。这是因为argumentString在源码中是late字段late String _argumentString真正赋值发生在CommandStorage.runCommand()内部见下文源码分析执行前访问会触发运行时异常。arguments按签名解析后的参数列表arguments是Listdynamic?类型表示按命令签名解析后的参数值列表。继续上面的例子若链接的 Dart 函数签名为move(String target, double distance)则arguments为[Hippo, 3.17]——注意3.17已经是double而非字符串。该属性为null的情况只有一种命令在注册时没有绑定任何 Dart 函数即所谓 “orphaned command”通过CommandStorage.addOrphanedCommand()注册。此时命令仍会通过onCommand()送达对话视图但其参数不会被解析arguments保持为null只有argumentString可用。测试access command arguments用例演示了带签名命令的完整行为命令xyz a b {fn()}绑定的是addCommand3(xyz, (String, String, String) ...)两次启动对话后视图收到的argumentString分别是a b 1和a b 2因为fn()返回自增值而arguments分别是[a, b, 1]与[a, b, 2]——证明每次执行都会重新求值并重新解析。参数从文本到类型的完整处理链路自定义命令的参数处理遵循一套明确规则这组规则定义在语言文档 User-defined Commands 中命令名之后、直到之前的所有内容按普通行的解析规则处理允许插值表达式不允许markup 与 hashtag运行时对整行内容求值替换所有表达式的值求值后的参数字符串按空白切分成一个个参数并与后端函数的签名做类型比对用解析后的参数调用后端函数最后dialogue runner 中的所有对话视图都会收到onCommand()事件。一个官方给出的典型例子give Gold {round(100 * $multiplier)}与内置命令不同自定义命令的参数一律按文本处理任何表达式都必须用花括号{}显式包裹。若$multiplier为1.5运行时求值后参数字符串变为Gold 150再按空白切分为Gold与150对照 Dart 函数签名void give(String item, int amount)完成类型转换后以give(Gold, 150)的形式调用。若参数数量或类型与签名不匹配则会抛出DialogueException。源码层面的执行细节CommandStorage.runCommand() 的源码完整还原了上述流程FutureOrvoid 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; return cmd.run(typedArgs); // 4. 调用后端函数 } }可以看到command.content.evaluate()会先计算出参数字符串并写回command.argumentString这正是该属性“执行前不可访问”的源码依据随后用ArgumentsLexer做词法切分这也是为何带空格的参数需要引号包裹再由unpackArguments按签名做类型转换最终调用 Dart 函数。需要说明的是runCommand末尾并没有显式触发onCommand()分发。UserDefinedCommand.execute()返回的 future 被 dialogue runner 等待完成后由 runner 统一向所有对话视图广播onCommand()事件具体流程可参见 DialogueRunner 的文档。如何注册命令CommandStorage 配合使用UserDefinedCommand只是运行时表示命令要真正可用必须先在 CommandStorage 中注册并且必须在解析 Yarn 脚本之前完成否则编译器会抛出“命令未识别”错误。测试用例undeclared user-defined command正是验证了这一点直接解析jenny而未注册会得到NameError: Unknown user-defined command jenny。注册函数需要满足以下硬性约束返回类型必须是void或Futurevoid。若返回 futuredialogue runner 会等待该 future 完成后再继续对话这使得walk、moveCamera、prompt这类耗时命令可以自然地阻塞对话流程参数类型必须是 Jenny 已知的类型String、num、int、double、bool所有参数必须是位置参数、非空、且不能有默认值按参数个数0 到 5 个选择addCommand0()…addCommand5()注册方法若签名末尾有 1 个或多个bool这些布尔参数会被视为可选参数默认值为false。示例一StartQuest同步命令给玩家任务命令绑定一个返回void的同步函数对话不会因此暂停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); } }对应的 Yarn 脚本调用注意名称用引号包裹否则会被切分为Get、rid、of、bandits四个参数StartQuest Q037 Get rid of bandits示例二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 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} 示例三give变量注入参数 引号语义命令参数可以在运行时由变量展开展开后的文本再按普通规则切分。假设变量$quest_reward为1 \Sword of Darkness\give {$quest_reward} 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) { // ... }引号在这里起着“把含空格的文本粘合成单个参数”的作用这正是ArgumentsLexer切分语义的直观体现。在 DialogueView 中接收命令当命令执行完毕后所有注册的对话视图都会通过onCommand(UserDefinedCommand command)收到回调游戏 UI如角色演出、镜头动画、物品栏更新在这里响应命令。测试中的辅助视图类给出了标准写法class _CommandDialogueView extends DialogueView { override void onCommand(UserDefinedCommand command) { // 同步命令时两个属性均可用 final name command.name; final argString command.argumentString; final args command.arguments; // 孤儿命令时为 null // 驱动游戏表现…… } }该视图需要作为 DialogueView 的实现被传入DialogueRunner详见测试 user_defined_command_test.dart 中的access command arguments用例——两个视图都各自收到一次回调且都拿到了求值后的参数。常见误区与边界行为执行前访问argumentString会报错它由CommandStorage.runCommand()在求值后写入属于late字段在未执行前读取属于对运行时状态的不当访问。孤儿命令orphaned command的arguments为null通过addOrphanedCommand(name)注册的命令不绑定 Dart 函数、不做参数解析但依然会送达onCommand()此时只能依赖argumentString。源码_commands[name] null与runCommand中if (cmd ! null)的判空逻辑印证了这一点。未注册命令无法通过编译在解析脚本前必须完成注册否则直接抛NameErrorCommandStorage.hasCommand(name)可用于注册前的存在性检查remove(name)与clear()则负责动态管理已注册命令详见 CommandStorage。命令内不允许 markup测试用例markup within user-defined command显示在hello Big [bad/] Wolf中混入标记会直接产生SyntaxError: invalid token。命令名不要求与 Dart 函数名一致addCommand2(StartQuest, startQuest)中命令名与函数名可以不同注册时映射即可。小结UserDefinedCommand是 Jenny 将 Yarn 脚本中的自定义命令“翻译”给游戏逻辑的关键载体name标识命令身份argumentString承载每次求值后的原始文本arguments提供按 Dart 签名解析后的强类型参数。配合 CommandStorage 的注册机制与 DialogueView 的回调分发开发者可以在 Flame 对话系统中自由扩展give、prompt、StartQuest等丰富的游戏交互命令其完整行为均可通过 user_defined_command_test.dart 中的测试用例对照验证。更底层的语言规则可继续阅读 User-defined Commands运行时全貌则参见 Jenny Runtime 文档索引。【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表