宴会没有因为你离席而暂停

设想一个虚构的世界:北方庄园“枫尾”正在举行冬季宴会,玩家是受邀而来的远房亲戚,身上带着一封没有拆开的家书。宴会进行到一半,玩家输入:

“不参加了,我累了,回房睡觉。”

这是一句再普通不过的话,多数互动小说的处理方式有两种。一种是拒绝:“现在还不是睡觉的时候”,把玩家按回原来的位置;另一种是照办:“你回到房间,很快睡着了。第二天清晨,阳光洒进窗子。”第二种看起来更自由,但它有一个隐蔽的问题:睡着的只是玩家,故事却像是跟着一起睡着了。宴会上正在发生的一切被悄悄抹掉,第二天的内容和昨晚毫无关系。

如果把玩家的输入理解为对世界的一次写入,而不是聊天,情形就完全不同。同一句话可能带来这些改变:

  • 老公爵原本打算在宴会后半程把一件事告诉玩家,现在他找不到人;
  • 女主人注意到玩家提前离席,对这位客人的印象发生变化;
  • 一位年轻的伯爵本想与玩家结盟,现在转向了另一位客人;
  • 第二天早晨,玩家的房门下多了一张字条。

这里的关键不是“后果一定很大”,而是后果有来源。它们不是模型为了显得精彩而临时编造的,而是从当前世界状态里推出来的。要做到这一点,就需要先弄清世界状态里到底存什么。

世界状态的五张“表”

我们习惯把世界状态拆成五类记录。它们不必是五张真正的数据库表,但把它们分开思考,很多错误就会提前暴露。

类别回答的问题 枫尾宴会中的示意内容
位置谁在哪里 玩家:客房;老公爵:书房;女主人:大厅
知识谁知道什么 老公爵知道粮仓账目有问题;玩家不知道
关系 谁对谁怎么看玩家与女主人:好感 55;玩家与伯爵:信任 40
时间现在是什么时候,哪些事有期限第一晚,二十二点;账目将在第三天被查
任务 有哪些未完成的事,谁在推动 主线:查清账目;支线:拆开家书

这几类记录有一个共同特征:它们都能被检查。生成一段文字,读起来对不对很难判断;但“玩家在客房、老公爵在书房,两人不可能面对面谈话”,程序一眼就能发现。世界状态的价值,一部分正在于它把故事里那些最容易前后不一的事实,变成了可查验的数据。

另一个要点是状态的粒度要适中。如果每一个物品的摆放、每个人的心情都要记录,成本会迅速失控,也没有意义;如果只记录“玩家在宴会”,又无法支撑后果。经验上,只记录那些“会影响之后剧情走向的事实”,其余交给文字描写。

你不在的那几个小时,别人在做什么

玩家回房之后,系统不应该只等待下一句输入。更合理的做法是,让世界向前推进一个“时间片”,在这段时间里,非玩家角色按照各自的目标行动。

假设三位关键角色有如下目标(示意):

  • 老公爵:想在账目被查之前,找到一个可靠的人把证据转交出去;
  • 年轻伯爵:想争取玩家的支持,以便在继承争执中占据上风;
  • 女主人:想保证宴会平稳结束,不希望出现任何丑闻。

玩家离席后,这三个人各自“做一步”。每一步可以理解为一个很小的决策循环:看看自己的目标,看看现在的世界状态,选一个行动,把结果写回去。比如:

  1. 老公爵在书房里等了一阵,没有等到玩家,于是决定试探另一位客人,把半句话说给了女主人的妹妹;
  2. 伯爵发现玩家已离席,转而向妹妹示好,两人的关系从“陌生”变为“熟络”;
  3. 女主人注意到妹妹与两位男士交谈甚欢,心里生出疑虑,准备第二天单独找她谈话。

这三步没有一步是围绕玩家写的,但每一步都因为玩家缺席而与本来的走向不同。这就是“涌现”的朴素含义:后果不是被编剧逐条写好的,而是由多位角色的目标与状态相互作用产生。同样的思路放大到整座城市,就是虚拟社会里多个角色互相影响的模拟,只是那里的规模和成本问题要复杂得多。

知识不是全局变量

在上面的过程里,最容易被写错的是“谁知道什么”。很多AI叙事的失误恰好出在这里:模型自己“知道”账目有问题,于是让所有角色都表现得心知肚明,或者让玩家在没有任何线索的情况下,忽然说出真相。

为了防止这种情况,我们把知识当作按人存放的记录,而不是一个全局的“剧情事实”:

  • 每条事实带有一个知晓者列表:老公爵知道;女主人的妹妹听到了半句;玩家不知道;
  • 每次信息传递都要有渠道:谁对谁说了、写了、暗示了,而不是凭空出现;
  • 角色在生成对白时,只能读取自己知晓的事实。

这样一来,同一个夜晚可以有几种不同的第二天。玩家如果留在宴会,当晚就会听到老公爵的话;如果回房,玩家只会在第二天从妹妹的嘴里,得到一个不完整、带着她自己理解的版本。信息的失真和延迟,本身就成了故事的一部分。

这也是为什么“互动小说要有世界状态”不是一个技术洁癖,而是叙事本身的需要:没有知识的隔离,就没有误会、秘密和悬念。

第二天早晨:从状态到叙述

到了第二天,玩家醒来。这时系统面对的不是“接下来写点什么”,而是一份已经被更新的状态清单。叙述应当从这份清单里长出来,而不是先随手写一段再倒回去补状态。大致的顺序是:

  1. 汇总变化:把玩家离席期间发生的、且对玩家有意义的事筛出来(老公爵的试探、伯爵的转向);
  2. 决定呈现方式:哪些变化通过字条呈现,哪些通过他人的态度暗示,哪些暂时不让玩家知道;
  3. 生成任务书:把“这些事实可以被玩家察觉,这些不能”连同角色语气交给生成层;
  4. 更新任务列表:主线“查清账目”依旧存在,但入口变了:从“找老公爵谈”变成“先弄清妹妹听到了什么”;
  5. 校验:确认叙述里没有提到玩家不可能知道的事,也没有出现违反位置、时间的情节。

有了这个顺序,同一句“去睡觉”在不同状态下会有不同的结果。可能玩家什么也没有错过,因为老公爵的行动没有成功;也可能错过一场关键谈话。差别来自状态,而不是随机数。如果玩家在事后复盘,也能顺着状态找到原因,这一点对“多结局的因果可追溯”同样重要,可以参考多结局越多是否越自由一文里对因果链的讨论。

同一句话,三个不同的世界

为了检验世界状态是否真的起作用,可以把同一句“回房睡觉”放进三个不同的初始状态里,看第二天会有什么差异(示意):

初始状态 玩家离席后的变化 第二天的入口
玩家与女主人好感高,老公爵尚未行动 女主人替玩家解释离席,宴会平稳结束 早餐时老公爵主动邀约
玩家与伯爵已结盟,账目截止日期临近 伯爵独自行动,试图抢先取得证据玩家被告知伯爵一夜未归
玩家此前得罪过女主人的妹妹妹妹把半句话添油加醋地传开 玩家发现自己成了流言的主角

三行的输入完全相同,差异全部来自事前的状态。这也说明了一个容易被忽略的事实:世界状态越清晰,玩家的每一句话就越有分量,而不是越受限制。因为系统知道在什么样的世界里,这句话意味着什么。

状态写乱了怎么办

世界状态越丰富,越容易出错。我们认为下面几种问题最常见,也最值得提前设防:

  • 叙述与状态脱节:文字写了“伯爵与妹妹一同离开”,状态里却没有更新位置。对策是规定“先写状态、再写叙述”,叙述里出现的每个事实变化,都要能在状态变更记录里找到对应项;
  • NPC行动没有约束:角色为了戏剧效果,做出与其目标和性格不符的事,比如谨慎的老公爵突然公开揭发。对策是行动前检查“这个行动是否符合角色当前目标与关系状态”;
  • 状态膨胀:每一夜都往里追加内容,几十个回合后清单过长,模型反而抓不住重点。对策是定期把已结束的事件压缩成摘要,只保留仍会影响后续的条目;
  • 冲突的事实:前面说女主人在大厅,后面又写她在书房。对策是每次写入前做一致性检查,冲突时以状态表为准,重新生成叙述。

一个示意的数字

为了让“状态膨胀”更直观,假设每个回合平均新增五条状态记录,玩家玩了一百个回合,就是五百条。如果全部原样交给模型,不仅费用上升,关键条目还会被淹没。更合理的做法是分层保存:当前场景的状态原样保留;三个回合以前的事件压成一句摘要;已经无关的事项归档,只在需要时检索。这些数字只是示意,用来说明为什么需要压缩,而不是给出具体的性能指标。

涌现不等于随机

最后还要划一条边界。“后果由状态产生”并不意味着“什么都可能发生”。如果老公爵今晚忽然被外星人接走,这当然是涌现,但没有人会认为这是好故事。我们更倾向于:

  • 变化有限度:NPC 的行动受目标、性格、世界规则约束;
  • 重要转折有兜底:关键节点由导演层维护,不能因为某个角色的一次“自主行动”而被永久破坏,这一点可以看为什么互动电影需要导演
  • 玩家永远有可追溯的理由:出了意外,能回头看到是哪条状态导致的。
本文的枫尾庄园、人物与数值均为虚构示意,仅用来说明世界状态的工作方式,并不代表已上线的效果,也不代表任何具体产品的实现细节。

回到开头。玩家输入“去睡觉”,最有价值的回应不是一段更华丽的睡眠描写,而是让这一句话像石子一样落进世界:它可能激起涟漪,也可能沉下去不留痕迹,但无论哪一种,都是有原因的。这才是我们理解的互动小说的“真正自由”。