
Flame 游戏引擎中的 Yarn 命令系统Jenny 方言内置命令与用户自定义命令完全指南【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flameYarnSpinner 是书写.yarn对话脚本的语言而 Flame 生态中的 Jenny 是其 Dart 实现命令commands是这门语言中最重要的执行单元——它们以双尖括号...包裹用于变量操作、流程控制和与游戏逻辑交互。本文将完整讲解 Jenny 命令系统的全貌内置命令的语法、语义与源码级实现以及如何声明带类型参数的用户自定义命令帮助你在基于 Flame 的游戏中编写可维护、可扩展的对话系统。命令概览命令的两种类型命令是 Yarn 脚本中一类特殊指令统一由双尖括号包裹例如stop。命令分为两类内置命令built-in commands由 YarnSpinner/Jenny 运行时自身支持通常用于改变对话的执行流程或执行与对话相关的功能完整清单见下文。用户自定义命令user-defined commands由你自己创建并在 yarn 脚本中使用详细说明见用户自定义命令。一个.yarn文件可以包含注释、标签tag、命令和节点nodes。关于 Yarn 文件的基础结构与语言整体介绍可参考 YarnSpinner 语言总览。命令在文件中的位置编译期与运行期需要特别注意的是并非所有命令都可以出现在任何位置。在 language.md 中明确规定文件根层级即节点之外只允许出现两类命令declarecharacter位于节点之外文件根层级的命令属于编译期指令它们在YarnProject编译解析期间就被执行而不是在对话运行时执行。这一区分是理解 Jenny 命令系统行为的关键前提。变量相关命令declare声明全局变量declare用于创建新的全局变量并赋予初始值。命令被执行后该变量即可在任意需要变量的地方使用——包括内联表达式、其他命令甚至是其他declare语句。与大多数命令不同declare在编译期即 yarn 脚本被解析时执行。当对话运行时它已经不起作用因为此时变量早已初始化完毕。正因如此declare必须放置在节点之外、脚本的根层级以此明确这些命令不会在节点运行时执行。基础示例摘自 declare.mddeclare $monicker boy --------------- title: Greeting --------------- Teacher: Welcome to the class, {$monicker}! 这里declare引入了一个名为$monicker、类型为String、初始值为boy的变量。之后该变量在 Greeting 节点中被使用——到那时变量的值可能是任何内容可能在其他节点或游戏本身中被修改但declare语句是必需的它告诉 Jenny 这是一个合法变量名以及它的类型是什么。declare的三种语法// 形式一由表达式推断类型最常见 declare $VARIABLE EXPRESSION // 形式二显式指定类型初始值取该类型的默认值 declare $VARIABLE as TYPE // 形式三组合形式表达式与类型显式绑定 declare $VARIABLE EXPRESSION as TYPE形式一中$VARIABLE是变量名Yarn 中所有变量都以$开头EXPRESSION可以是字面量或更复杂的表达式该表达式会在编译期求值以提供初始值变量的类型由表达式的类型推导得出。形式二中TYPE只能是Bool、Number或String三者之一创建出的变量分别初始化为false、0或。形式三适用于表达式类型不够直观、希望显式标注的场景编译器会校验EXPRESSION的类型与TYPE一致否则抛出编译期错误。更多示例declare $prefix Mr. declare $gold 100 declare $been_to_hell false declare $name as String declare $distanceTraveled as Number declare $birthDay randomRange(1, 365) as Number declare $vulgarity GetObscenitySetting() as Bool工程组织建议原文要点必须遵循推荐将所有declare语句集中放入单独的一个 yarn 文件并确保该文件最先被解析从而保证所有全局变量在任何节点使用之前就已声明完毕。如果你的游戏支持存档通常还需要保存 yarn 全局变量的值。此时恢复存档值必须在所有 yarn 脚本解析完成之后进行否则引擎会认为变量被声明了两次。建议为每个declare附带一条文档注释doc-comment说明变量的用途就像为类的公共成员写文档那样。local声明节点级局部变量local与declare类似区别在于它创建的变量仅在当前节点内可见适合只在一段对话中临时使用的数据。其语法为local $VARIABLE EXPRESSION local $VARIABLE EXPRESSION as TYPE第二种形式会对表达式类型与TYPE做一致性校验不匹配则编译报错相当于为局部变量做显式类型标注。限制条件摘自 local.md同一节点内每个局部变量只能声明一次局部变量的名字不能与任何全局变量重名。示例骰子投掷的结果$roll只在当前节点内短暂使用没必要声明为全局变量。title: a_dice_roll --- local $roll dice(6) if $roll 1 Youve rolled 1, rotten luck... elseif $roll 2 Youve rolled 2, which is still below the average. Try harder! elseif $roll 3 Youve rolled 3.14159265 (well, almost). elseif $roll 4 Your roll is an unlucky number. Please roll again else Youve rolled 10 (when rounded to the nearest ten). Good job! endif set更新变量值set用于更新已存在变量的值。变量必须先通过declare或local声明才能出现在set中。它支持常规赋值和修改赋值两种形式摘自 set.md// 常规赋值 set $VARIABLE EXPRESSION set $VARIABLE to EXPRESSION // 修改赋值 set $VARIABLE EXPRESSION set $VARIABLE - EXPRESSION set $VARIABLE * EXPRESSION set $VARIABLE / EXPRESSION set $VARIABLE % EXPRESSION // 上述修改赋值等价于 set $VARIABLE $VARIABLE EXPRESSION set $VARIABLE $VARIABLE - EXPRESSION set $VARIABLE $VARIABLE * EXPRESSION set $VARIABLE $VARIABLE / EXPRESSION set $VARIABLE $VARIABLE % EXPRESSION在所有情况下EXPRESSION的类型必须与$VARIABLE相同否则会抛出编译期错误。综合示例颜色问答 好感度累加declare $favorite_color as String title: ColorQuiz --- What is your favorite color? - White set $favorite_color to White - Red set $favorite_color to Red - Yellow set $favorite_color Yellow - Blue Oh, Nice! Which shade of blue? - Azure - Cerulean - Lapis Lazuli Umm, I dont know how to spell that. Ill just put you down as blue. set $favorite_color Blue - Black set $favorite_color Black Thats mine too! set $affinity 3 - Prefer not to tell Aww... Maybe if I ask again really nicely? jump ColorQuiz 注意这里同时展示了set ... to ...与set ... ...两种写法以及修改赋值、jump循环提问的用法。character声明角色与别名character用于声明一个角色以及一个或多个可在脚本中使用的别名它有以下用途摘自 character.md防止在脚本中意外拼错角色名允许角色拥有不必是 ID 的全名full name允许为同一角色声明多个别名可在不同节点中使用别名甚至可以与全名使用不同语言可以为每个角色关联附加数据这些数据在运行时可用。语法character FULL NAME alias1 alias2...全名是可选的若给出则视为该角色的正式名字若省略则第一个别名被视为角色的正式名字。每个别名必须是合法的 ID且至少提供一个别名。// 一个很有礼貌的七岁小女孩却总是卷入各种奇妙的冒险。 character Alice // 一只以灿烂微笑和部分隐身能力闻名的魔法猫。他自己承认他疯了。 character Cheshire Cat Cat Cheshire // 一位脾气暴躁的王后同时也是一张扑克牌。 character Queen of Hearts Queen QoH QH角色声明之后其任意别名都可在脚本中使用它们都指向同一个Character对象。与此同时不声明就使用角色是不被允许的——除非在YarnProject中设置了允许如此的特殊标志。在对话中角色别名会出现在台词前缀位置title: Alice_and_the_Cat --- Alice: But I dont want to go among mad people. Cat: Oh, you cant help that, were all mad here. Im mad. Youre mad. Alice: How do you know Im mad? Cat: You must be, or you wouldnt have come here. Alice: And how do you know that youre mad? Cat: To begin with, a dogs not mad. You grant that? Alice: I suppose so. Cat: Well then, you see a dog growls when its angry, and wags its tail \ when its pleased. Cat: Now, [i]I[/i] growl when Im pleased, and wag my tail when Im angry. \ Therefore, Im mad. Alice: [i]I[/i] call it purring, not growling. Cat: Call it what you like. 从源码结构看Jenny 在 character.dart 与 character_storage.dart 中实现Character对象及其存储/查找逻辑character命令在编译期完成角色注册。控制流命令if条件分支if求值其条件并据此决定接下来执行哪些语句等价于大多数编程语言中的if关键字。它可以有多个部分摘自 if.mdif condition1 statements1... elseif condition2 statements2... else statementsN... endif规则要点每个条件必须为布尔类型elseif块的数量不限elseif块和else块均可选结尾的endif必须存在每个块内的语句必须缩进。运行时首先求值if块的条件若为true执行statements1不再求值其他条件若为false则依次求值condition2……全部为false时落入else块执行statementsN。最终对话会继续执行最终endif之后的语句。示例守卫的不同态度取决于你的声望title: GuardGreeting --- if $reputation 100 Guard: Hail to the savior of the people! elseif $reputation 30 Guard: Nice to meet you, sir! elseif $reputation 0 Guard: Hello elseif $reputation -30 Guard: Im keeping an eye on you... elseif $reputation -100 Guard: You filthy scum! else Guard: Youll pay for your crimes! #auto attack endif 这个例子还展示了条件判断的分层递减写法从 100到 -100再到 else以及#auto自动续行标记和自定义命令attack的组合使用——当声望低于 −100 时守卫会当场攻击你。jump切换到另一个节点jump停止执行当前节点并立即开始运行目标节点类似许多语言中的goto摘自 jump.mdjump FarewellScene参数是目标节点的 id既可以直接给出纯节点 ID也可以用花括号包一个表达式jump {Ending_ $ending}如果表达式在运行时求值得到未知的节点名将抛出NameError异常。注意jump是一去不回的跳转如果你需要跳过去再回来请使用visit。visit临时跳转并返回visit将当前节点暂时挂起执行目标节点待其结束后恢复上一个节点的执行——类似编程语言中的函数调用摘自 visit.md。它非常适合把大型对话拆分成多个较小的节点或在多个节点间复用公共对话片段title: RoamingTrader1 --- if $roaming_trader_introduced Hello again, {$player}! else visit RoamingTraderIntro endif - What do you think about the Calamity? if $calamity_started visit RoamingTrader_Calamity - Have you seen a weird-looking girl running by? if $quest_little_girl visit RoamingTrader_LittleGirl - What do you have for trade? OpenTrade Pleasure doing business with you! #auto 参数同样是目标节点 id支持纯 ID 或花括号表达式两种形式visit {RewardChoice_ string($choice)}与jump一样若运行时表达式求值得到未知节点名将抛出NameError。上例还展示了选项-后跟if条件判断的选项条件门控写法。stop停止当前节点stop立即停止当前节点的求值就像跳到了它的结尾一样。该命令不接受任何参数摘自 stop.mdstop通常情况下它的效果是停止整个对话但如果你是从另一个节点visit进来的那么stop只会退出当前节点执行流返回到父节点。因此stop类似许多编程语言中的return;。wait暂停对话wait强制对话引擎在恢复对话前等待指定的时长单位秒。秒数可以为 0但不能为负数。该命令接受单个参数必须是一个数值表达式摘自 wait.md// 等待四分之一秒 wait 0.25 // 等待 $delay 变量给出的时长 wait $delay这在表现对话节奏、配合角色动画或音效时非常实用。用户自定义命令把对话接进游戏逻辑除了内置命令你还可以在 yarn 脚本中声明和使用自己的用户自定义命令。典型用途是执行可作为对话自然组成部分的游戏内动作例如wave、smile、frown、moveCamera、zoom、shakeCamera、fadeOut、walk、give、take、achievement、GainExperience、startQuest、finishQuest、openTrade、drawWeapon等摘自 user_defined_commands.md。参数处理规则五步流程多数情况下自定义命令需要携带参数。其参数按照以下规则处理解析命令名之后直到的所有内容按照常规行解析规则解析——允许插值表达式但不允许标记markup和标签hashtag。求值运行时对该行内容求值即代入所有表达式的值。拆分求值后的参数字符串按空白拆分成独立参数并与后端函数的签名做类型比对。调用调用后端函数传入解析好的参数。分发事件对话运行器中的所有对话视图dialogue views都会收到onCommand()事件。一个具体例子考虑下面的命令give Gold {round(100 * $multiplier)}首先注意与内置命令不同自定义命令的参数被当作文本处理任何表达式都必须放在花括号里。然后运行时求值表达式——假设$multiplier为 1.5命令的参数字符串就变成Gold 150。接着按空白拆分并依据后端 Dart 函数的参数类型逐个解析。例如若函数签名为void give(String item, int amount)则会被调用为give(Gold, 150)。反之如果参数个数或类型与签名不匹配则会抛出DialogueException。源码层面的实现印证从 user_defined_command.dart 的源码可以看出UserDefinedCommand类在运行时持有命令名与LineContent内容其execute()方法最终委托给dialogue.project.commands.runCommand(this)对应 command_storage.dart由命令存储负责参数求值、类型校验与后端函数调用。这也是文档所描述的五步参数流程在引擎内部的落点而onCommand()事件则定义在 dialogue_view.dart 中由 dialogue_runner.dart 在命令执行后统一分发让所有 UI 层都能感知到命令的发生。设计建议与最佳实践综合原文档与源码在设计对话脚本时建议遵循以下实践全局变量集中声明将全部declare放入一个独立的 yarn 文件并确保最先解析避免变量未声明即使用。存档恢复时机恢复 yarn 全局变量的存档值必须在所有脚本解析完成之后进行否则会触发重复声明错误。文档注释为每个declare编写 doc-comment说明变量用途就像为公共类成员写文档。局部变量够用即用只在单节点内需要的数据用local避免污染全局命名空间注意局部变量不得与全局变量重名、且每节点只能声明一次。善用visit拆分对话将大段对话拆成小节点并通过visit组合可显著提升脚本的可读性与复用性需要一去不回时用jump需要提前终止用stop。自定义命令负责游戏动作把表现型动作动画、镜头、音效、任务进度、交易等封装成自定义命令让对话编写者专注于叙事本身。结语Jenny 的命令系统为 Yarn 对话脚本提供了完整的变量管理与控制流能力declare/local/set管理数据if/jump/visit/stop/wait控制流程character管理角色而用户自定义命令则把对话安全地接进游戏逻辑。理解了编译期命令根层级与运行期命令节点内的区别以及自定义命令的五步参数处理流程你就能在 Flame 项目中搭建出结构清晰、扩展性强的对话系统。需要进一步深入时可继续阅读表达式与操作符、节点结构以及行与选项等配套章节。【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考