news 2026/9/16 23:30:52

Flame 游戏引擎 Jenny 对话系统随机函数指南:`dice`、`random` 与 `random_range` 的原理与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flame 游戏引擎 Jenny 对话系统随机函数指南:`dice`、`random` 与 `random_range` 的原理与实战

Flame 游戏引擎 Jenny 对话系统随机函数指南:dicerandomrandom_range的原理与实战

【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame

本篇技术指南聚焦 Flame 游戏引擎内置对话语言系统 Jenny(基于 YarnSpinner 语法)中三个随机函数:dice(n)random()random_range(a, b)。文中以 random.md 为骨架,结合 packages/flame_jenny 包内的源码实现与单元测试,系统讲解每个函数的调用语义、参数约束、截断规则、错误行为、可复现随机源的替换方式,以及在游戏叙事(掷骰、概率事件、随机掉落)中的落地写法。读完本文,你将能在自己的 Yarn 脚本中正确、稳妥地使用全部随机能力,并能用种子化随机源进行可复现调试。

随机函数在 Jenny 中的定位

在 Jenny 的表达式系统中,函数与任何编程语言或数学中的函数概念一致:接收若干参数,计算并返回结果,调用时函数名后必须紧跟括号(即使无参也要写())。Jenny 内置约 20 个函数,按用途分为随机、数值、类型转换与杂项四组,完整清单见 functions.md,其中随机组正是本文要讲的三个函数:

  • dice(n):模拟掷骰;
  • random():生成[0, 1)均匀随机浮点数;
  • random_range(a, b):在自定义闭区间内取随机整数。

三者每次求值都会产生不同结果。关键设计在于:它们并非各自使用独立的随机源,而是统一复用YarnProject.random这个随机数生成器。该字段在 yarn_project.dart 中定义为:

/// Random number generator used by the dialogue whenever randomization is /// needed. Random random;

默认情况下randomYarnProject构造时以Random()初始化(无固定种子),每次运行结果随机。但你可以替换它为自定义生成器——例如Random(seed)——以实现两件事:

  1. 调试可复现:同一脚本同一种子,每次跑出完全相同的随机序列,便于定位逻辑问题;
  2. 存档一致性:防止玩家重载游戏后获得与之前不同的随机结果(例如掷骰事件重载后结果变了)。

这一点在测试中体现得淋漓尽致:dice_test.dartrandom_range_test.dart均通过YarnProject()..random = Random(420)之类的方式注入固定种子,从而断言精确的期望输出。

dice(n):掷一颗 n 面骰

语义与边界规则

dice(n)返回1n之间(含两端)的随机整数。例如dice(6)等价于掷一颗常规六面骰,结果落在 1~6。规则要点:

  • 参数n必须是数值类型,且大于等于 1
  • n非整数,运行时会被截断为整数,因此dice(3.5)等价于dice(3)
  • n截断后小于等于 0,运行时抛出DialogueError(错误信息为Argument to dice() must be positive: $n);
  • 参数个数不为 1、或参数不是数值类型,则在编译阶段直接报TypeError

基本用法

<<set $roll = dice(6)>> <<set $coin_flip = if(dice(2) == 1, "H", "T")>>

第一行把一次 d6 的结果存入变量$roll;第二行用dice(2)模拟硬币(1 为正面 "H",2 为反面 "T"),再配合内置函数if()做条件映射。想实现"掷两枚 d6 求和"这种经典骰子判定,直接相加即可:

<<set $roll_2d6 = dice(6) + dice(6)>>

<<set>>指令与if()条件函数的完整说明分别见 set.md 与 misc.md。

源码级原理

dice的实现位于 dice.dart。其make静态构造器负责编译期校验:参数数量必须恰好为 1,且args[0].expression.isNumeric必须为真,否则调用errorFnTypeError。真正的随机逻辑在valuegetter 中:

@override num get value { final n = _n.value.toInt(); if (n <= 0) { throw DialogueError('Argument to dice() must be positive: $n'); } return _yarn.random.nextInt(n) + 1; }

可见其工作分三步:先把参数toInt()截断;再检查是否大于 0(不满足抛DialogueError);最后借助_yarn.random.nextInt(n) + 1得到 1~n 的均匀整数。nextInt(n)本身返回[0, n),加 1 后即得到闭区间[1, n],两端都可达。

dice通过 _common.dart 中的builtinFunctions注册表('dice': DiceFn.make)接入解析器。值得一提的是,该注册表注释提示:新增内置函数时需同步更新doc/_sphinx/extensions/yarn_lexer.py中的语法高亮词表,这保证了文档示例的代码着色与真实函数名保持一致。

测试验证

dice_test.dart 覆盖了完整的边界情形:

  • 注入Random(420)后,连续四轮{dice(6)}的期望输出依次为36 32 1 21 6 4 3,证明序列可复现且分布在 1~6;
  • DiceFn(const NumLiteral(3.14), ...)连续取值得到1、1、2、3,实证小数参数被截断(等价于dice(3));
  • dice(0)触发hasDialogueError('Argument to dice() must be positive: 0')
  • dice()(无参)、dice(3, 6)(多参)、dice("three")(字符串)均在YarnProject.parse阶段报TypeError,并给出精确的行列号定位。

random():均匀随机浮点数与概率事件

语义与用法

random()返回一个01之间(含 0、不含 1)的随机浮点数,用于实现指定概率触发的事件。典型场景如下:

<<if random() < 0.001>> // This happens only with 0.1% probability You found it! The Holy Grail! <<endif>>

random() < p成立的概率恰为p,因此只需调整阈值即可精确控制事件出现频率:< 0.001对应 0.1%,< 0.25对应 25%,依此类推。<<if>>指令语法详见 if.md。它同样可以组合进表达式,例如<<set $random = random()>>将本次采样存入变量以便复用(同一次事件判定中多次调用random()会得到不同数值,务必注意)。

源码级原理

random.dart 的实现极其简洁:make只校验"不允许携带任何参数"(传参即编译报错function random() requires no arguments),取值逻辑仅一行:

@override num get value => _yarn.random.nextDouble();

nextDouble()dart:math.Random的标准 API,返回[0, 1)区间内均匀分布的浮点数,与文档语义完全一致。因为整段随机逻辑都汇聚在可替换的_yarn.random上,只要注入带种子的生成器,random()的取值序列同样完全可复现。

random_range(a, b):自定义闭区间内的随机整数

语义与边界规则

random_range(a, b)返回ab之间(含两端)的随机整数,可看作dice()的泛化版本——用于需要自定义范围的场合(不限于从 1 开始)。规则要点:

  • 参数ab必须为数值类型,求值时会各自toInt()截断为整数;
  • 要求a <= b,否则运行时抛出DialogueError(错误信息为In random_range(a=$lower, b=$upper) the upper bound cannot be less than the lower bound);
  • 参数个数不为 2、或任一参数非数值,均在编译期报TypeError(错误信息分别为function random_range() requires two argumentsthe first argument should be numericthe second argument should be numeric)。

基本用法

原文档给出的示例是用它模拟抛硬币(0 与 1 等概率):

<<set $coin_flip = bool(random_range(0, 1))>>

random_range(0, 1)只会产生 0 或 1,配合内置类型转换函数bool()(规则见 type.md)即得到布尔值,简洁地完成二分支随机。自定义范围的更多场景:random_range(3, 6)模拟四面的 3~6 骰、random_range(0, 100)生成百分比、random_range(-12, 12)生成含负数的偏移量。

源码级原理

random_range.dart 的valuegetter 清晰地呈现了算法:

@override num get value { final lower = _a.value.toInt(); final upper = _b.value.toInt(); if (upper < lower) { throw DialogueError( 'In random_range(a=$lower, b=$upper) the upper bound cannot be less ' 'than the lower bound', ); } return _yarn.random.nextInt(upper - lower + 1) + lower; }

其核心是nextInt(upper - lower + 1) + lower:区间宽度upper - lower + 1保证了闭区间内每个整数等概率出现(包括两端),加lower完成偏移。当a == b时,宽度为 1,nextInt(1)恒返回 0,函数退化为恒等返回a——这一退化行为在测试中有专门断言。

测试验证

random_range_test.dart 覆盖:

  • 注入Random(7)后,random_range(3, 6)random_range(-12, 12)random_range(0, 100)random_range(50, 70)的期望输出分别为305759,验证含负数的范围与正确性;
  • 100 次random_range(-20, 20)压力测试,统计出的min == -20max == 20,实证区间边界确实可达;
  • random_range(0, 0)恒为 0(min == max的退化情形);
  • random_range(10, 0)触发hasDialogueError,报错信息与源码完全一致;
  • random_range(1)(少参)、random_range(true, 12)(首参非数值)、random_range(3, "seventeen")(次参非数值)均抛TypeError并带行列号。

编译期检查与运行时错误的分工

通过源码与测试可以总结出 Jenny 对随机函数的两道防线,这是编写 Yarn 脚本时需要牢记的心智模型:

错误类别触发条件抛出时机示例
TypeError(编译期)参数个数不符、参数类型非数值、random()携带参数YarnProject.parse()解析阶段,附带行列号定位{dice()}{dice(3, 6)}{dice("three")}{random_range(1)}{random_range(true, 12)}
DialogueError(运行时)数值截断后超出语义边界:dicen <= 0random_rangeupper < lower表达式真正求值(对话执行)时dice(0)random_range(10, 0)

也就是说:能在编译期发现的写错(数量/类型)绝不拖到运行期,而涉及动态取值边界的问题(例如参数来自变量)则留待运行时校验并抛出带上下文的DialogueError。开发时应尽可能保证编译零错误,同时在运行时对可能越界的动态参数做好兜底判断。

实战组合:在游戏对话中安全使用随机

掷骰判定与概率分支

title: Treasure --- <<set $roll = dice(20)>> <<if $roll >= 15>> Your keen eyes spot a hidden lever. (Rolled $roll) <<else>> You find nothing but dust. (Rolled $roll) <<endif>> ===

dice()结果先存入变量再参与多次判定,可避免多次采样导致逻辑不一致。

可复现调试

在初始化YarnProject时注入种子生成器,即可让随机序列确定化:

final project = YarnProject()..random = Random(2024);

随后无论运行多少次,dicerandom()random_range的取值序列都完全一致,非常适合对齐期望输出、回归测试与排查概率逻辑;正式发布时再改回默认的无种子Random()。运行时完整接入方式可参考 jenny_runtime.md。

与其他函数的协作

随机函数可与数值函数(floorroundint等,见 numeric.md)、类型转换函数(boolnumberstring,见 type.md)自由组合。例如:

// 在 0~99 中随机取数并四舍五入到十位 <<set $luck = round(random_range(0, 99) / 10) * 10>> // 10% 概率的暴击,结果存为布尔 <<set $crit = bool(random_range(0, 9) == 0)>>

小结

  • dice(n):1~n 闭区间随机整数,参数截断且必须 ≥ 1;
  • random()[0, 1)均匀浮点数,适合random() < p的概率事件;
  • random_range(a, b):a~b 闭区间随机整数,要求a <= b
  • 三者共用YarnProject.random随机源,可替换为种子化生成器实现调试可复现与存档一致性;
  • 参数数量/类型错误在编译期暴露,边界越界在运行期抛DialogueError

随机函数是 Jenny 叙事系统中最常用的表达式之一,正确理解其截断规则、区间闭包与随机源替换机制,能显著提升游戏内随机事件的可控性与可测试性。若需自定义更复杂的行为(如加权随机、洗牌),可参考 functions.md 中关于用户自定义函数的说明,结合 function_storage.md 将游戏引擎能力接入 Yarn 脚本。

【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 23:29:44

PLC与组态软件实现水箱水位控制系统实战

1. 项目概述与系统架构这个水箱水位控制系统是我去年在化工厂改造项目中实际应用的方案&#xff0c;核心目标是通过PLC实现水位精准控制&#xff0c;同时用组态软件搭建可视化监控界面。系统采用西门子S7-200 SMART PLC作为控制核心&#xff0c;搭配MCGS组态王进行人机交互&…

作者头像 李华
网站建设 2026/9/16 23:29:41

GVIM高效文本处理实战:三模式操作与配置优化

1. 为什么今天还要学 GVIM&#xff1f;——一个被低估的文本处理核心工具GVIM 不是古董&#xff0c;而是我每天打开频率最高的三个软件之一。很多人看到“VIM”两个字就下意识划走&#xff0c;觉得这是程序员才碰的硬核工具&#xff0c;或者以为它和 VS Code、Sublime Text 一样…

作者头像 李华
网站建设 2026/9/16 23:25:41

工控安全成熟度模型怎么用?从GB/T 41400-2026看企业安全能力落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 23:23:51

Codeforces 2167D题解:前缀和约束与盈余池构造最少步数

Codeforces 2167D 这道题的题名又长又劝退——"Yet Another Array Problem"&#xff0c;看到"Yet Another"就知道出题人没打算在标题上花心思。但这道题我在赛时卡了很久&#xff0c;倒不是因为操作复杂&#xff0c;而是我一直往 DP、数据结构那边想&#…

作者头像 李华
网站建设 2026/9/16 23:23:43

Windows下如何正确检测TCP端口连通性

1. 项目概述&#xff1a;为什么“ping端口”这个需求如此高频却总被误解&#xff1f;在Windows运维、开发联调、网络排障的日常中&#xff0c;我几乎每天都会遇到这样的场景&#xff1a;同事急匆匆跑过来问&#xff0c;“服务器8080端口通不通&#xff1f;你快ping一下&#xf…

作者头像 李华
网站建设 2026/9/16 23:20:49

微信小程序回收业务源码解析:定位、表单与订单闭环实现

简介&#xff1a;本资源是一套完整的物品回收类微信小程序源码&#xff0c;面向前端开发者、小程序初学者及环保类创业项目技术选型人员&#xff0c;旨在提供可快速部署的轻量级回收业务落地解决方案。压缩包共100个文件&#xff0c;包含21个JS逻辑文件&#xff08;如login.js、…

作者头像 李华