Flame Jenny 变量更新命令<<set>>实战指南:语法、类型约束与源码实现解析
【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame
在 flame_jenny(Flame 游戏引擎内置的 YarnSpinner 兼容对话脚本运行时)中,<<set>>是驱动对话状态流转的核心命令之一:它负责在对话运行期间更新全局变量或局部变量的值,配合 <<if>> 条件分支即可构建出分支剧情、好感度系统、金币结算等完整的交互逻辑。读完本文,你将掌握<<set>>的全部赋值语法(含复合赋值运算符)、类型约束规则,并能结合源码理解其在编译期与运行期的真实行为。
使用前提:变量必须先声明
<<set>>只能更新已存在的变量,因此在使用前,变量必须通过以下两种方式之一完成声明:
- <<declare>>:声明全局变量,该命令在脚本编译期执行,必须放置在节点(node)之外的脚本根级位置,变量一经声明即可在整个项目中任意节点使用;
- <<local>>:声明局部变量,作用域仅限于当前节点,且局部变量不能与任何全局变量重名。
从变量模型看,Yarn 脚本中的每个变量都包含名称、值、类型、作用域四个要素(参见 Variables 文档):
- 名称:所有变量以
$开头,后跟字母或下划线,再跟任意数量的字母、数字或下划线,例如$gold、$DoorPassword、$climbed_over_wall;$2000_years(数字开头)、victory(缺少$)等均不合法; - 类型:只有
Bool、Number、String三种,类型在声明时确定且此后不可改变; - 作用域:全局变量处处可用,局部变量仅限当前节点。
<<set>>与<<declare>>/<<local>>的分工非常清晰:声明命令负责"创建并赋初值",<<set>>负责"后续随时改写"。因此 commands.md 将<<set>>归入 "Variables" 类内置命令,描述为"更新变量(局部或全局均可)的值"。
<<set>>的完整语法
<<set>>支持普通赋值与**修改赋值(复合赋值)**两类形式,且变量与表达式之间既可以用=也可以用to关键字连接:
// 普通赋值(两种写法等价) <<set $VARIABLE = EXPRESSION>> <<set $VARIABLE to EXPRESSION>> // 修改赋值:先读取当前值参与运算,再写回 <<set $VARIABLE += EXPRESSION>> <<set $VARIABLE -= EXPRESSION>> <<set $VARIABLE *= EXPRESSION>> <<set $VARIABLE /= EXPRESSION>> <<set $VARIABLE %= EXPRESSION>>其中EXPRESSION可以是字面量、变量、函数调用(如randomRange(1, 6))或任意复杂的表达式(运算符详见 operators.md)。五个复合赋值运算符与下列展开写法完全等价:
<<set $VARIABLE = $VARIABLE + EXPRESSION>> <<set $VARIABLE = $VARIABLE - EXPRESSION>> <<set $VARIABLE = $VARIABLE * EXPRESSION>> <<set $VARIABLE = $VARIABLE / EXPRESSION>> <<set $VARIABLE = $VARIABLE % EXPRESSION>>也就是说,<<set $x += 12>>会先取$x的当前值,加上12后再写回$x,中途不会产生"读取与写入分离"的语义歧义。
注意:
+运算符对String类型是字符串拼接。例如<<set $name += " Jr.">>会把$name末尾追加" Jr.",这也是复合赋值可以作用于字符串类型的唯一例外;其余-、*、/、%只能用于数值类型。
类型一致性:编译期强制检查
在所有形式中,EXPRESSION的结果类型必须与$VARIABLE声明时的类型完全一致,否则会在解析(编译)阶段直接抛出编译期错误,脚本根本无法运行:
- 声明为
Number的变量不能被赋值为字符串或布尔值; - 声明为
String的变量不能接收数值(如<<set $name = 12>>会报错); - 类型不一致属于
TypeError,错误信息会精确到变量名、变量类型、赋值表达式的类型以及出错的行列位置。
这一约束与变量模型中的"类型在声明时确定且永不改变"原则一脉相承,从源头杜绝了运行时类型漂移带来的隐患。
源码实现解析:从解析到执行
要真正理解<<set>>的行为,可以顺藤摸瓜阅读 parse.dart 与 set_command.dart 两个核心文件。
解析期:parseCommandSet
在 parse.dart 的parseCommandSet()方法中,编译期做了四件事:
- 解析变量名并定位存储作用域:按"当前节点局部变量 → 项目全局变量"的顺序查找。若局部变量表中存在该变量则写入局部存储,否则若全局存储中存在则写入全局存储;两者都不存在时抛出
NameError: variable $xxx has not been declared; - 校验赋值运算符:仅接受
=(Token.operatorAssign)以及+=、-=、*=、/=、%=这五个复合赋值 Token,否则抛出SyntaxError; - 类型检查:将变量当前表达式类型与右侧表达式类型比对,不一致则抛出
TypeError,错误信息形如variable $x of type string cannot be assigned a value of type numeric; - 构造赋值表达式:普通
=直接使用右侧表达式;复合赋值则通过makeBinaryOpExpression把运算符映射回对应的二元运算符(见assignmentTokensToOperators映射表),与"变量当前值表达式 + 右侧表达式"组合成一个二元表达式,从而实现"先计算再写回"的语义。
运行期:SetCommand
解析产物是 set_command.dart 中的SetCommand类,它持有变量名、表达式与对应的VariableStorage。运行期执行逻辑极其精简:
@override void execute(DialogueRunner dialogue) { storage.setVariable(variable, expression.value); }即:求值右侧表达式,再将结果写入VariableStorage。由于复合赋值在编译期已被改写成二元表达式,运行期并不需要关心运算符差异。同时,<<set>>与<<declare>>/<<character>>不同——后两者只能在节点外出现,而<<set>>只能出现在节点内部(源码中parseMain会对节点外的非声明类命令抛出command <<set>> is only allowed inside nodes)。
实战示例:颜色问答与好感度
原文档提供了一个完整的 ColorQuiz 示例,综合展示了普通赋值、to语法、嵌套选项与复合赋值:
<<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 don't know how to spell that. I'll just put you down as "blue". <<set $favorite_color = "Blue">> -> Black <<set $favorite_color = "Black">> That's mine too! <<set $affinity += 3>> -> Prefer not to tell Aww... Maybe if I ask again really nicely? <<jump ColorQuiz>> ===注意其中$affinity并未在本示例中声明,实际使用时需在脚本根级补充<<declare $affinity = 0>>。该示例说明:<<set>>可以出现在选项(->)的缩进分支内,实现"选择即赋值";<<set $favorite_color to "White">>与<<set $favorite_color = "White">>完全等价,可混用。
再补充一个与<<if>>联动的经典好感度分支($reputation需预先声明):
<<declare $reputation = 0>> title: GuardGreeting --- <<if $reputation >= 100>> Guard: Hail to the savior of the people! <<elseif $reputation >= 30>> Guard: Nice to meet you, sir! <<else>> Guard: Hello <<endif>> <<set $reputation += 5>> ===对话结束时$reputation自动 +5,下次进入该节点即可看到不同问候——这就是"对话状态持久化"的最小闭环。
常见错误与定位技巧
以下错误均在编译期抛出,可借助 set_command_test.dart 中的用例对照排查:
| 错误示例 | 错误类型 | 说明 |
|---|---|---|
<<set>>(缺变量名) | SyntaxError: variable expected | 变量名缺失 |
<<set $foo = 123>>($foo未声明) | NameError: variable $foo has not been declared | 必须先<<declare>>或<<local>> |
<<set $x as String>> | SyntaxError: an assignment operator is expected | as TYPE仅用于声明,<<set>>不识别 |
<<set $x = 12>>($x声明为 String) | TypeError: variable $x of type string cannot be assigned a value of type numeric | 类型不一致 |
在节点外写<<set $x = 1>> | TypeError: command <<set>> is only allowed inside nodes | <<set>>必须在节点体内 |
测试文件中还有一条值得关注的"深度嵌套"用例(Basic.plan):<<set $foo += 47 + 6>>被放进<<if>>的多层嵌套块内依然正常生效,说明<<set>>在任意嵌套语句块中均可安全使用。
最佳实践小结
- 声明先行:把项目内所有
<<declare>>集中在脚本根级(建议单独成文件并最先解析),<<set>>只负责运行期改写,避免"边用边声明"导致作用域混乱; - 善用复合赋值:计数器、分数、好感度等场景优先使用
+=/-=,代码更短且语义清晰; - 类型对齐:牢记
Number/String/Bool三态,跨类型赋值一定在编译期报错,这是 Yarn 脚本的静态类型安全红利; - 存档注意:若游戏支持存档,恢复全局变量应在所有 yarn 脚本解析完成之后进行,否则引擎会认为变量被重复声明(此约定源自 <<declare>> 文档),而
<<set>>之后的取值则完全由运行时状态驱动。
掌握<<set>>,就等于掌握了在 flame_jenny 对话系统中自由读写状态的能力——它是一切分支剧情、数值养成与动态回应的基石。
【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考