变量
一个值存在哪里、能活多久,以及该选哪个作用域
项目里的每个值都得有个落脚处,而这个选择本质上是在选生命周期:一个随所在界面一起消亡的计数器、一个必须跟着玩家存档走的标记、一个要比每一次通关都活得更久的设置项。作用域一共四种,选错是「我的值不见了」最常见的来源
逐引脚的签名在变量节点。本页讲的是该选哪个作用域,以及值在那之后会经历什么
四种作用域
| 作用域 | 声明方式 | 值存放在 | 何时重新开始 |
|---|---|---|---|
| 蓝图变量 | 图中的一个Var节点 | 内存,每个宿主实例一份 store | 该宿主实例消失时 |
| 场景变量 | 场景中的一行/local | 存档文件,作为该场景的Scene.local | 进入该场景时 |
| 存档变量 | 变量面板 | 存档文件,作为一个Storable命名空间 | 开始新游戏时 |
| 持久变量 | 变量面板,或蓝图编辑器的成员面板 | 项目之外的宿主存储,以storageKey为键 | 永不——只有你自己清除 |
蓝图变量
在蓝图的任意位置放一个Var(声明变量)节点,它就为整份蓝图声明了一个变量。Var没有引脚,也从不执行:运行时会在第一个事件触发之前扫描该蓝图的每一张图,找到的每个Var都成为这份蓝图 store 中的一个槽位。给它填上Name、Data type和Default。除widgetValue外的每种宿主都能声明
值存在一个挂在宿主实例上的 store 里,而不是挂在图上,也不是挂在某条事件链上。在同一个按钮上点两次,读到的是同一个计数器。什么算一个实例,取决于宿主:
| 宿主 | 每份 store 对应 | 什么会让它重新开始 |
|---|---|---|
globalMain | 应用逻辑 | 应用运行期间不会 |
surfaceMain | 该 Surface 每一次被压入导航栈 | 离开该 Surface 后再次打开 |
widgetMain · componentWidgetMain | 控件实例:组件的每一次摆放都是自己的实例,列表行模板里的控件在每一行中的那一份也是。列表自己的变量属于列表本身,由它的所有行共用 | 控件卸载 |
storyAction | 该图的一次调用 | 每一次调用 |
Get Var(读取变量)和Set Var(写入变量)提供三组变量,顺序恒定:页面(当前图所属 Surface 的surfaceMain蓝图)、蓝图(你正在编辑的这一份)、全局(globalMain蓝图,即应用逻辑)。若某份蓝图本身已经是其中之一,它只出现一次,不会重复。组内按名称排序;只有当两个可访问变量重名时,作用域标签才会印在名称旁边——于是常见情况下列表干干净净,而真正的重名一眼就能看到
编辑图不会抹掉运行中的值。store 被重新取用时,新声明的变量会被填入其默认值,不再声明的变量会被移除,其余槽位保持原有的值
蓝图变量从不写入存档。它只在一个宿主实例存活期间持有值,一刻也不多——存读档、开新游戏、关闭窗口,它都不会留下任何东西。如果一个值必须跟着玩家回来,它属于场景变量、存档变量或持久变量
场景变量
场景变量属于某一个故事场景,由 NarraLeaf 的Scene.local支撑。把它声明成场景内的一行。这一行就是变量本身:嵌在分支里的声明同样为整个场景声明,把该行禁用也不会取消声明,因为声明不是一条会执行的语句
Scene.local在每次进入场景时都会被清空,并重新应用已声明的默认值。所以场景变量既在存档文件里、能随一次场景中途的读档回来,但再次走进那个场景时,它会从默认值重新开始
存档变量
存档变量属于项目而不属于某一个场景,每个场景都能看见它。在变量面板的存档变量一栏中创建。它由一个带着全部已声明默认值创建出来的Storable命名空间支撑,因此新游戏从这些默认值开始,而读档会还原存档中保存的内容
存档变量与持久变量的值必须可序列化;写入函数、symbol 或 bigint 会在运行时被拒绝,而不是悄悄产出一份读不回来的存档
持久变量
持久变量是项目级的:一份定义,对每份蓝图和每个故事都可见,而它的值完全在存档文件之外。语言选择、「看过片头没有」、图鉴解锁,都放在这里
定义存放在项目的变量注册表(editor/variables.json)中,从变量面板的持久变量一栏,或蓝图编辑器成员面板的持久变量分组里编写。在成员面板里的编辑与你正在编辑的蓝图共用同一条撤销栈,所以一次 Ctrl+Z 会同时撤回变量改动和节点改动。注册表会被烘焙进开发模式 bundle 和游戏包一起分发;运行时只读它,从不写它
值存放在宿主管理的存储中,以条目的storageKey为键,且绝不在项目目录内。作者时它们位于 Studio 自己的用户数据下,按项目分键;在发布的游戏中它们位于游戏自己的用户数据下。把项目复制到另一台机器不会带走它们
Get Persistent(获取持久变量)和Set Persistent(设置持久变量)是 latent 的,因为宿主的读写是异步的,这同样是 Blueprint Value、内联故事值和分支条件都不提供它们的原因。当某个键从未被存过任何值时,Get Persistent回退到编写的默认值,并且交给你的是它的深拷贝,所以改动读到的东西不会波及定义本身
storageKey只在创建时铸造一次,重命名变量不会改变它——正是这一点让玩家已存的值在重命名之后依然解析得到。反过来说:删除一个持久变量不会删除该键下已经存好的值,而用同一个名字重新建一个会得到一个新键,读到的是默认值。要改名就改名,别删了重建
该选哪个作用域
从上往下读,停在第一条成立的:
- 这个值只在当前界面、控件或 action 存活期间有意义 → 蓝图变量
- 它描述玩家在某一个场景内走到了哪 → 场景变量
- 读档时丢掉它玩家会不高兴 → 存档变量
- 它必须横跨玩家手上的每一个存档文件都成立 → 持久变量
优先选能满足需求的最窄作用域。更宽的作用域不是免费的:每个存档变量都让每一份存档变大,而每个持久变量都是一笔在它含义改变那天需要你手工迁移的数据
什么能活下来
| 作用域 | 存档与读档 | 新游戏 | 重新构建项目 |
|---|---|---|---|
| 蓝图变量 | 没了——从不写入存档 | 没了 | 本来就没存过 |
| 场景变量 | 随存档还原,再次进入该场景时又会重置 | 回到声明的默认值 | 已有存档保留各自的值 |
| 存档变量 | 随存档还原 | 回到声明的默认值 | 已有存档保留各自的值 |
| 持久变量 | 不受影响——它不在存档里 | 不受影响 | 只要storageKey没变就保留 |
类型
Var提供九种类型。你选的那一种决定卡片上Default的编辑器,也决定投影到Get Var和Set Var上的类型。卡片、变量对话框与成员面板中显示界面名称,括号中是类型 id
| 类型 | 默认值 | 说明 |
|---|---|---|
字符串(string) | "" | |
整数(integer) | 0 | 可接入float或string输入 |
数字(float) | 0 | 可接入string输入 |
布尔值(boolean) | false | |
JSON(json) | {} | 接受array来源 |
列表(array) | [] | 可接入json输入;json不能反向接回 |
计时器(Timer) | null | 用来存放 Delay 交回的令牌 |
动画(AnimationToken) | null | 用来存放动画交回的令牌 |
任意值(any) | null | Default字段被禁用——见下文 |
数值接到string的连线会在取值时做转换。其余允许的配对都是精确匹配或加宽,绝不会背着你收窄
场景变量、存档变量和持久变量使用一个更窄的封闭集合——布尔值(boolean)、数字(number)、字符串(string)、JSON(json)——因为这些值会被序列化进存档文件或宿主存储,而一个活的Timer没有什么可以序列化的
默认值
默认值以真正的 JSON 值存储,而不是字符串化的 JSON。json默认值{"hp": 3}在文档里就是一个对象,array默认值就是一个数组——正因如此,编辑器才能对你填的字面量做类型检查,运行时也能直接交出它而无需中间的一次解析
默认值在创建 store 时会被深拷贝。每个宿主实例都从自己的一份副本开始,所以运行时往json变量里写东西绝不会反过来改掉编写的默认值,共用同一个模板的两个列表项也不会共用同一个对象
声明时完全没有填默认值的变量,起始值为null
any是唯一一种起始值不由你决定的类型。它恒为null,卡片上以一个禁用的null字段把这件事说清楚。要给它值,用Set Var
Get Var与Set Var上的类型推断
两个节点都把自己的value引脚声明为any,由编辑器把所选变量的类型投影上去。Get Persistent和Set Persistent同理。这一次投影驱动了下游的一切:节点卡片上印出什么、拖动连线时哪些引脚会高亮、松手时创建浮窗提供哪些节点、一条连接会不会被接受,以及 graph validation 检查什么
改变某个Var的Data type,该图中所有指向它的Get Var和Set Var上的投影都会被改写
如果这次改写让某条已有连线变得不兼容,这条连线会保留。validation 会在它上面报告类型不匹配,并把它留在图里——为了执行一秒钟前刚被改掉的类型而悄悄删掉作者的连线,毁掉的东西比省下的多。要么修这条线,要么把类型改回去
Get Var或Set Var所选的变量不复存在时——被删除,或从节点所处的位置已不可达——处理方式相同:在节点上报一条诊断,而不是改写这张图
Blueprint Value
Blueprint Value 是绑定到单个控件属性上的widgetValue蓝图,它的创建浮窗被刻意限制为安全的取值类节点。对变量而言有四条后果:
- 它不能声明
Var。 该节点不在它的创建浮窗里,声明变量的扫描也会直接跳过widgetValue宿主 - 它不能读写持久变量。
Get Persistent和Set Persistent是 latent 的,而值绑定必须在被询问的那一拍里给出值——没有地方安放一个待定的 promise - 它照样能读写自己看得见的变量。
Get Var和Set Var在白名单上,它们解析到页面蓝图和应用逻辑——那就是全部可访问集合,因为值蓝图自己一个变量也不声明 - 它能读取存档变量。
Get Saved Var在白名单上,Set Saved Var不在。变量每次被写入,绑定都重新求值:写入可以来自故事中的行、其它蓝图,或读取存档。示例:好感度与多结局用这种方式在对话框上显示存档变量
故事变量与表达式
故事变量和蓝图变量是两套独立的机制,在两个项目级作用域上交汇:存档变量和持久变量为整个项目定义一次,故事和蓝图都读写它们
故事只声明场景变量。在场景编辑器里输入一行命令来声明:
/local hp 100 type=number desc='Player health'/local(别名/scenevar)声明场景变量。没有显式的type=时,由默认值自身的类型决定;完全没有默认值的声明是boolean。存档变量和持久变量不在故事里声明,而是在变量面板中创建;该面板也会列出当前打开场景的场景变量
故事表达式——/set的右侧、/if的判定,以及行内文本插值——按名称寻址变量。裸写的名称沿场景 → 存档 → 持久依次查找,命中即止,因此最窄的作用域获胜。名称被遮蔽时,直接点名作用域:
/set gold gold + 10
/if scene.gold >= 100可用前缀是scene. / local.、saved. / var.,以及persis. / persistent. / global.。表达式在你输入的当下就被解析,文档存的是语法树而不是文本,所以运行时不会再解析一次;这门语言也无法命名任何宿主对象,因此没有沙箱可逃
故事 action 蓝图能触及全部三种故事作用域:Get Scene Var / Set Scene Var、Get Saved Var / Set Saved Var,以及Get Persistent / Set Persistent。Get Scene Var和Set Scene Var只对storyAction宿主存在,因为在运行中的故事之外没有场景。存档变量那一对也提供给其它蓝图,在那里访问运行中的游戏的存档变量;Blueprint Value 只提供Get Saved Var,见变量节点