为什么这个系统值得单独设计
一款俯视角恐怖解谜游戏里,玩家拿着手电穿过废弃医院。停留在黑暗里越久,画面边缘开始收缩,远处传来低频噪声,墙上的影子似乎移动了。系统要制造压迫感,但不能让玩家误以为游戏坏了。
理智值不是一条扣血条。它连接光照、音频、可见度、输入反馈、幻觉事件、恢复道具和无障碍设置。若所有效果都直接读 sanity 数字,项目后期会很难调。需要把压力来源、状态计算和表现层分开。 本文按实际项目会遇到的问题来拆,不停留在“能跑”的 Demo 层。重点会放在数据边界、状态流、玩家反馈、调试方式和后续维护成本上。Phaser 很适合快速做出手感,但越是能快速表现,越需要把规则层写清楚。
核心架构
flowchart TD
N1["StressSource"] --> N2["SanityModel"]
N2["SanityModel"] --> N3["VisibilityProfile"]
N2["SanityModel"] --> N4["HallucinationQueue"]
N2["SanityModel"] --> N5["AudioPressure"]
N6["RecoveryRule"] --> N2["SanityModel"]
N7["AccessibilityGate"] --> N3["VisibilityProfile"]
N7["AccessibilityGate"] --> N5["AudioPressure"]
这套结构的原则是单向流动:输入或场景事件进入 StressSource,核心模型完成计算,再由 Phaser 表现层消费结果。SanityModel、VisibilityProfile、HallucinationQueue、AudioPressure、RecoveryRule、AccessibilityGate 都应尽量保持可序列化、可测试、可回放。不要让某个 Tween 完成回调、某个 Sprite 是否可见、某个按钮是否高亮成为玩法事实。
压力来源要可解释
黑暗、低血量、追逐、剧情物件、诅咒区域都可能降低理智值。每个 StressSource 应有强度、范围、衰减和优先级。SanityModel 汇总这些来源,输出当前值和主要原因。调试面板显示来源贡献,策划才能知道玩家为什么在某个走廊掉得太快。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
表现层不要直接扣状态
画面扭曲、视野收缩、字幕抖动和音频压迫都只是 sanity 的表现。它们不能反过来修改 sanity。否则某个特效关闭后,规则也变了。SanityModel 只根据压力来源和恢复规则变化,VisibilityProfile 和 AudioPressure 订阅它。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
幻觉事件要排队
恐怖游戏常用一闪而过的影子、门把手声、墙面文字变化。不要每次 sanity 低就随机触发。HallucinationQueue 根据冷却、区域、剧情进度和强度排队,保证事件之间有间隔,也避免关键解谜时连续干扰。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
可见度变化要有底线
视野收缩能制造紧张,但不能让玩家完全看不清交互物。VisibilityProfile 应设置最低可见半径、关键物高亮保护和 UI 不受影响区域。低理智状态可以降低边缘清晰度,但主交互区仍应可读。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
恢复规则要防刷
站在灯下、使用药品、听到安全音乐都可以恢复理智。恢复不能瞬间清空压力,否则玩家会在门口反复进出刷状态。RecoveryRule 可以有延迟、上限和来源互斥:安全屋恢复快,普通灯光只减缓下降,道具提供一次性恢复。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
无障碍选项不是删体验
强烈闪烁、画面扭曲和低频声音可能让部分玩家不适。提供 reducedHorrorFX、disableFlash、audioComfort 等选项。开启后仍保留玩法信息,例如用颜色、图标或轻微暗角表达压力,而不是完全隐藏理智系统。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
存档恢复要避免突袭
读档后如果立刻恢复到低理智和高压音频,玩家可能还没准备好。可以在读档后的几秒内平滑恢复表现,但规则值保持不变。这样既不破坏难度,也避免加载完成的一瞬间产生不必要惊吓。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
TypeScript 实现骨架
interface StressSource { id: string; strength: number; remainingMs: number; reason: string }
interface SanitySnapshot { value: number; dominantReason?: string; pressure: number }
class SanityModel {
private value = 1;
private sources = new Map<string, StressSource>();
addSource(source: StressSource) { this.sources.set(source.id, source); }
update(dt: number, recovery: number): SanitySnapshot {
let pressure = 0;
let dominant: StressSource | undefined;
for (const source of this.sources.values()) {
source.remainingMs -= dt;
if (source.remainingMs <= 0) { this.sources.delete(source.id); continue; }
pressure += source.strength;
if (!dominant || source.strength > dominant.strength) dominant = source;
}
this.value = Phaser.Math.Clamp(this.value + recovery * dt / 1000 - pressure * dt / 1000, 0, 1);
return { value: this.value, dominantReason: dominant?.reason, pressure };
}
}
function visibilityRadius(sanity: number, reducedFx: boolean) {
const min = reducedFx ? 210 : 150;
return Phaser.Math.Linear(min, 420, sanity);
}
这段代码不是完整框架,而是把关键边界先立出来。实际项目里应继续补上配置加载、错误码、事件派发、性能统计和单元测试。只要骨架保持清楚,后续接入 Phaser 的 Graphics、Sprite、Matter、Tilemap 或 Sound 都不会污染规则层。
具体落地步骤
- 第一步,把 StressSource 和 SanityModel 从 Scene 中拆出来,写成可以直接用 TypeScript 调用的模型。这个模型只接收普通对象,不接收 Sprite、Camera 或 Tween。只要这一步做到,后面的测试、调试、存档和工具预览都会简单很多。
- 第二步,在 Phaser Scene 里建立很薄的适配层。输入事件、物理回调、计时器和资源加载都可以在适配层发生,但它们只提交意图,不直接改核心状态。核心系统产出快照后,适配层再更新显示对象、音效、粒子和 HUD。
- 第三步,给每个关键状态准备调试可视化。不要等 QA 报问题才补日志。开发模式下至少能看到当前状态、最近输入、失败原因、候选列表、耗时和重要阈值。对复杂玩法来说,能看见中间状态比多写一层封装更重要。
- 第四步,用三类样例保护系统:正常流程、边界流程、错误配置。正常流程证明体验能跑通,边界流程证明快速输入、暂停、切场景和重复触发不会破坏状态,错误配置证明系统会给出明确报告,而不是静默失败。
项目检查清单
- 确认 StressSource 的输入输出能被 JSON 记录,便于复现玩家操作。
- 确认 SanityModel 的配置有默认值、版本号和校验错误信息。
- 确认快速点击、暂停、切后台、重开场景和读档不会重复提交关键状态。
- 确认失败反馈比成功反馈更具体,玩家能理解自己为什么没有成功。
- 确认低端机或高负载场景有降级策略,而不是等帧率下降后再猜瓶颈。
- 确认调试面板能在不改代码的情况下打开,并能导出最近关键事件。
常见误区
第一类误区,是把 Phaser 的显示对象当成状态来源。显示对象适合表达结果,却不适合保存规则事实。它可能被对象池回收、被摄像机隐藏、被动画临时修改,也可能因为画质档变化而不存在。核心状态必须独立存在。
第二类误区,是只为当前关卡写逻辑。当前关卡对象少、节奏慢、输入简单,临时判断看起来没有问题。等到内容增加、节奏加快、平台变多,临时逻辑会互相覆盖。每个系统至少要提前考虑配置错误、重复触发和性能上限。
第三类误区,是没有把失败当成流程设计。复杂系统一定会失败:条件不满足、资源缺失、网络超时、玩家中断、配置非法。失败不应该只是 console 里的一行错误,而应该是玩家、QA 和内容团队都能理解的状态。
音画压迫要给玩家出口
恐怖体验不能一直把压力推到最高。SanityModel 可以把压力分成 build、peak、release 三段,表现层按阶段调暗角、噪声和音量。release 阶段不一定安全,但要让玩家感觉自己重新掌握了局面,例如手电恢复稳定、脚步声重新清楚、UI 抖动停止。没有出口的压迫只会让人疲劳,尤其在解谜关卡里,玩家需要安静地观察线索。
关键交互物要被保护
低理智状态下可以隐藏远处细节,却不能隐藏完成关卡所必需的信息。每个关键交互物可以带 criticalVisibility 标记,VisibilityProfile 在极低理智时仍给它保留最小对比度或轮廓提示。这不是降低恐怖感,而是避免玩家把视觉惩罚误解为设计不公平。恐怖游戏越强调氛围,越要保护规则信息的可读性。
结语
恐怖游戏理智值系统:视野扰动、音画反馈和恢复边界 的难点不在某个 API,而在边界。把数据、规则、表现和调试分开后,Phaser 的优势会更明显:你可以很快做出反馈,也可以放心迭代规则。反过来,如果所有逻辑都散落在 Scene 的回调里,第一版越快,后续越难维护。
额外实践建议
- 理智系统要先做调试数值,再做画面压迫,否则很难判断恐怖感来自规则还是特效。
- 所有强刺激表现都应该有配置开关,并在同一条 AccessibilityGate 下控制。
- 低理智不应阻止玩家理解目标,压迫和可读性必须一起调。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。