NarraLeaf

变量

一个值存在哪里、能活多久,以及该选哪个作用域

项目里的每个值都得有个落脚处,而这个选择本质上是在选生命周期:一个随所在界面一起消亡的计数器、一个必须跟着玩家存档走的标记、一个要比每一次通关都活得更久的设置项。作用域一共四种,选错是「我的值不见了」最常见的来源

逐引脚的签名在变量节点。本页讲的是该选哪个作用域,以及值在那之后会经历什么

四种作用域

作用域声明方式值存放在何时重新开始
蓝图变量图中的一个Var节点内存,每个宿主实例一份 store该宿主实例消失时
场景变量场景中的一行/local存档文件,作为该场景的Scene.local进入该场景时
存档变量故事中的一行/save存档文件,作为一个Storable命名空间开始新游戏时
持久变量蓝图编辑器的成员面板,或一行/global项目之外的宿主存储,以storageKey为键永不——只有你自己清除

蓝图变量

在蓝图的任意位置放一个Var(声明变量)节点,它就为整份蓝图声明了一个变量。Var没有引脚,也从不执行:运行时会在第一个事件触发之前扫描该蓝图的每一张图——event、function、macro 一视同仁——找到的每个Var都成为这份蓝图 store 中的一个槽位。给它填上NameData typeDefault。除widgetValue外的每种宿主都能声明

值存在一个挂在宿主实例上的 store 里,而不是挂在图上,也不是挂在某条事件链上。在同一个按钮上点两次,读到的是同一个计数器。什么算一个实例,取决于宿主:

宿主每份 store 对应什么会让它重新开始
globalMain该全局蓝图应用运行期间不会
surfaceMainSurface 每一次被压入导航栈离开该 Surface 后再次打开
widgetMain · componentWidgetMain控件实例——列表中重复出现的每一项都算自己的实例控件卸载
sharedAsset该共享蓝图应用运行期间不会
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 的,因为宿主的读写是异步的——这同样是它们不能出现在function图中的原因。当某个键从未被存过任何值时,Get Persistent回退到编写的默认值,并且交给你的是它的深拷贝,所以改动读到的东西不会波及定义本身

storageKey只在创建时铸造一次,重命名变量不会改变它——正是这一点让玩家已存的值在重命名之后依然解析得到。反过来说:删除一个持久变量不会删除该键下已经存好的值,而用同一个名字重新建一个会得到一个键,读到的是默认值。要改名就改名,别删了重建

该选哪个作用域

从上往下读,停在第一条成立的:

  • 这个值只在当前界面、控件或 action 存活期间有意义 → 蓝图变量
  • 它描述玩家在某一个场景内走到了哪 → 场景变量
  • 读档时丢掉它玩家会不高兴 → 存档变量
  • 它必须横跨玩家手上的每一个存档文件都成立 → 持久变量

优先选能满足需求的最窄作用域。更宽的作用域不是免费的:每个存档变量都让每一份存档变大,而每个持久变量都是一笔在它含义改变那天需要你手工迁移的数据

什么能活下来

作用域存档与读档新游戏重新构建项目
蓝图变量没了——从不写入存档没了本来就没存过
场景变量随存档还原,再次进入该场景时又会重置回到声明的默认值已有存档保留各自的值
存档变量随存档还原回到声明的默认值已有存档保留各自的值
持久变量不受影响——它不在存档里不受影响只要storageKey没变就保留

类型

Var提供九种类型。你选的那一种决定卡片上Default的编辑器,也决定投影到Get VarSet Var上的类型

valueType默认值说明
string""
integer0可接入floatstring输入
float0可接入string输入
booleanfalse
json{}接受array来源
array[]可接入json输入;json不能反向接回
Timernull用来存放 Delay 交回的令牌
AnimationTokennull用来存放动画交回的令牌
anynullDefault字段被禁用——见下文

数值接到string的连线会在取值时做转换。其余允许的配对都是精确匹配或加宽,绝不会背着你收窄

场景变量、存档变量和持久变量使用一个更窄的封闭集合——booleannumberstringjson——因为这些值会被序列化进存档文件或宿主存储,而一个活的Timer没有什么可以序列化的

默认值

默认值以真正的 JSON 值存储,而不是字符串化的 JSON。json默认值{"hp": 3}在文档里就是一个对象,array默认值就是一个数组——正因如此,编辑器才能对你填的字面量做类型检查,运行时也能直接交出它而无需中间的一次解析

默认值在创建 store 时会被深拷贝。每个宿主实例都从自己的一份副本开始,所以运行时往json变量里写东西绝不会反过来改掉编写的默认值,共用同一个模板的两个列表项也不会共用同一个对象

声明时完全没有填默认值的变量,起始值为null

any是唯一一种起始值不由你决定的类型。它恒为null,卡片上以一个禁用的null字段把这件事说清楚。要给它值,用Set Var

Get VarSet Var上的类型推断

两个节点都把自己的value引脚声明为any,由编辑器把所选变量的类型投影上去。Get PersistentSet Persistent同理。这一次投影驱动了下游的一切:节点卡片上印出什么、拖动连线时哪些引脚会高亮、松手时创建浮窗提供哪些节点、一条连接会不会被接受,以及 graph validation 检查什么

改变某个VarData type,该图中所有指向它的Get VarSet Var上的投影都会被改写

如果这次改写让某条已有连线变得不兼容,这条连线会保留。validation 会在它上面报告类型不匹配,并把它留在图里——为了执行一秒钟前刚被改掉的类型而悄悄删掉作者的连线,毁掉的东西比省下的多。要么修这条线,要么把类型改回去

Get VarSet Var所选的变量不复存在时——被删除,或从节点所处的位置已不可达——处理方式相同:在节点上报一条诊断,而不是改写这张图

注册表与重名

持久变量有两处编写入口:项目注册表,以及故事中的声明行。每一个消费方——故事编译器、故事变量面板、蓝图成员面板——读的都是这两者以storageKey为键合并出的同一份视图,因此它们不可能对「存在哪些变量」产生分歧

一个显示名称同时出现在两处,就是一次真正的歧义,因为作者眼前出现了顶着同一个名字的两个变量。编译会就此报告一条警告,指名该变量并说明对它的引用是有歧义的。同一处入口内重名的两行走不到这一步:重复项在输入的地方就被拒绝了

Blueprint Value

Blueprint Value 是绑定到单个控件属性上的widgetValue蓝图,它的创建浮窗被刻意限制为安全的取值类节点。对变量而言有三条后果:

  • 它不能声明Var 该节点不在它的创建浮窗里,声明变量的扫描也会直接跳过widgetValue宿主
  • 它不能读写持久变量。 Get PersistentSet Persistent是 latent 的,而值绑定必须在被询问的那一拍里给出值——没有地方安放一个待定的 promise
  • 它照样能读写自己看得见的变量。 Get VarSet Var在白名单上,它们解析到页面蓝图和全局蓝图——那就是全部可访问集合,因为值蓝图自己一个变量也不声明

故事变量与表达式

故事变量和蓝图变量是两套独立的机制,只在一处交汇:持久作用域——两边都往里声明,两边也都读它

在场景编辑器里输入一行命令来声明故事变量:

/local hp 100 type=number desc='Player health'
/save chapter 1 type=number
/global seenIntro false type=boolean

/local声明场景变量,/save声明存档变量,/global声明持久变量。/var/persis仍然可用,作为/save/global的别名。没有显式的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 VarGet Saved Var / Set Saved Var,以及Get Persistent / Set Persistent。这四个故事节点只对storyAction宿主存在——在运行中的故事之外,既没有场景,也没有Storable可以对话

本页目录