引言
在做《归义军传说》(火焰纹章式 SRPG)的时候,想加一个特色功能:让 NPC 根据玩家之前的战场行为和剧情选择做出不同反应。简单来说就是让 NPC "记住" 玩家是个什么样的指挥官——激进莽夫还是谨慎策略家,进而影响后续章节的对话、商店价格甚至敌人配置。
这个系统的核心可以拆成三层:行为追踪层(监听事件 → 翻译为标签)、记忆存储层(标签权重持久化)、以及决策消费层(NPC 读取标签做反应)。目前前两层已经实装,第三层留了接口待接入。
一、行为追踪层:BattleBehaviorTracker
这层的工作是订阅战斗事件总线,把玩家的每一次战场操作翻译成带权重的偏好标签。挂在场景里,Start 时订阅 GameEventBus,OnDestroy 时取消订阅。
代码很简单,就是一堆事件回调:
- 玩家造成伤害 →
aggressive_attack(侵略性),权重 0.3 - 玩家待机不攻击 →
cautious_wait(谨慎),权重 0.3 - 移动后进入敌人射程 →
risky_move(冒险),权重 0.3 - 移动后贴近残血友军 →
protect_ally(忠诚),权重 0.3 - 友军阵亡 →
sacrificial_rush(牺牲倾向),权重 0.4(立即落盘防止退出丢数据)
每个事件只记 0.3~0.4 的权重,意味着玩家打一场战斗(假设平均出手 20 次),某项标签的权重会累积到 6~8。这样设计的好处是单次行为不决定标签,整体倾向才决定——偶尔莽一次不会让 NPC 把你当莽夫,但整场都在莽就会。
追踪器本身不关心 NPC 怎么用这些标签,它只管"忠实记录"。这是有意拆开的:行为追踪和决策消费解耦,以后即使换了消费端的逻辑(比如从标签驱动改为机器学习特征输入),追踪器也不用改。
二、记忆存储层:NPCMemory
NPCMemory 是一个跨场景的单例(DontDestroyOnLoad),用 JSON 序列化到 Application.persistentDataPath/PlayerMemory/。核心数据结构是 PlayerPreference,内部维护一个 Dictionary<string, float> 存标签权重。
几个设计点:
分章节记录:
RecordEvent(tag, weight, chapterId)带上章节 ID,虽然目前权重是全局累加的,但数据结构预留了按章节聚合的空间。以后可以做"第一章 NPC 觉得你很谨慎,第二章开局时他仍然记得"。关键时刻立即落盘:正常情况下在
OnApplicationQuit时 Save。但友军阵亡这个事件会立即调SaveMemory()——因为玩家可能在角色死后直接关游戏,不等到正常退出。剧情选择也进记忆:
RecordStoryChoice(choiceId, alignment),alignment > 0.5 记benevolent(仁慈),< -0.5 记ruthless(冷酷)。这让 NPC 不仅根据战斗风格、也根据剧情中的道德选择来做反应。主导标签查询:
GetDominantTag()遍历所有权重,返回当前最高的一项。虽然目前消费端还没接入,但这个接口已经给 NPC 对话系统留好了——"你每次战斗都身先士卒"(aggressive)vs "你对部下很严厉"(牺牲倾向高)。
三、消费层的思路(待实现)
有了标签和权重,NPC 的决策树可以这样接入:
- 商店老板:
aggressive标签高 → 多进货攻击性武器、提高价格(他知道你需要);loyal标签高 → 打折出售伤药、支援道具 - 敌人 AI:
risk_taker标签高 → 多布置伏击单位(知道你容易突进);cautious标签高 → 加回合限制给你时间压力 - 队友对话文本:根据主导标签选择不同的对话分支,比如同样是夸你,"aggressive"对应"将军身先士卒","loyal"对应"你从不丢下战友"
这种设计在商业 SRPG 里其实不常见——大多数 SRPG 的分支只看选择不看行为。让 AI 观察玩家怎么玩而非怎么选,算是个有点意思的差异化设计。
四、踩过的坑
事件注册时机:最开始把
TrySubscribe()写在Awake里,发现GameEventBus.Instance为 null。原因是SceneSetup也是在Awake创建 GameEventBus,Unity 的 Awake 调用顺序不确定。改到Start里就解决了,因为 Start 在所有 Awake 之后执行。阵亡立即落盘:测试的时候发现玩家角色阵亡后直接 Alt+F4,记忆文件没有更新。加了一行
NPCMemory.Instance?.SaveMemory()在HandleUnitDeath里就解决了。类似的问题以后加存档系统的时候也要注意——关键时刻手动落盘比依赖OnApplicationQuit可靠。单例的 OnDestroy:
NPCMemory用DontDestroyOnLoad跨场景,但场景切换时如果新场景也有一个NPCMemoryGameObject,会触发旧的OnDestroy把Instance设为 null。标准做法是在Awake里做 Instance 检查时,如果发现已有单例直接Destroy(gameObject),在自己的OnDestroy里只在自己是 Instance 时才置 null。这关已经过了。
参考文献
- GitHub — 归义军传说 SRPG(个人项目,本文相关内容来自此仓库)
- 本项目设计文档 [Docs/fire-emblem-mechanics.md](NPC 记忆系统设计)