NPC 会按时出现,世界才像在运转
RPG、农场、生活模拟和沙盒游戏里,NPC 如果永远站在同一个位置,会显得像功能按钮。给 NPC 加日程后,世界立刻活起来:早上在家,上午去店里,下午去广场,雨天留在室内,节日去会场,剧情后改变路线。但日程系统也容易变成一堆特例。某个 NPC 被剧情带走,另一个 NPC 卡在门口,玩家约好的任务目标不在,商店营业时间和 NPC 位置不一致。日程不是装饰,它会影响任务、商店、对话和存档。
Phaser 层面负责让 NPC 移动、播放动画和显示交互提示。真正的日程应该由 ScheduleService 决定:当前游戏时间、日期、天气、剧情变量和 NPC 状态共同决定 NPC 应该在哪里、做什么、是否可交互。这样 NPC Scene 重建、地图切换、离线推进时都能恢复一致状态。
日程表要表达条件和优先级
一个 NPC 的日程不是简单的时间段列表。它需要条件:晴天、雨天、节日、任务阶段、好感度、是否生病。也需要优先级:剧情事件高于普通工作,节日高于日常,玩家约会高于逛街。没有优先级,多个条件同时满足时会随机冲突。日程项应包含开始时间、结束时间、目标地点、行为、条件和优先级。
地点不要只写坐标,最好引用 location id 和 anchor id。比如 blacksmith.shop.counter、town.square.fountain。这样地图调整坐标时,日程不用改。进入目标地点后,NPC 可以播放 idle 动画、工作动画或对话状态。
flowchart TD
A["GameTime:日期、小时、天气"] --> B["ScheduleResolver"]
C["NarrativeState:任务、好感、剧情"] --> B
D["NpcState:是否受伤、是否被事件占用"] --> B
B --> E["ScheduleEntry:目标地点和行为"]
E --> F["PathPlanner:跨地图或本地图路径"]
F --> G["NpcController:移动、动画、交互"]
G --> H["InteractionService:对话、商店、任务"]
H --> I["SaveStore:当前位置和当前日程"]
离屏 NPC 不需要逐帧走路
玩家不在同一地图时,NPC 不需要真的一步步走。ScheduleResolver 可以直接根据当前时间推导 NPC 应在的位置。玩家进入地图时,NPC 出现在合理位置;若玩家在同一地图观察 NPC,才需要实际沿路径移动。这样节省性能,也避免跨地图寻路复杂化。世界看起来连续,不代表所有 NPC 都在后台实时模拟。
如果玩家跟踪 NPC,从一张地图走到另一张地图,系统需要过渡。NPC 到达出口时可以切换地图状态,玩家进入目标地图后继续看到 NPC。跨地图路径可以用抽象节点,而不是详细 tile 路径。只在当前地图内使用详细寻路。
交互窗口要和日程一致
NPC 在柜台时可以开商店,在路上时只打招呼,剧情中可能拒绝普通对话。InteractionService 应读取当前日程行为。不要让商店按钮永远可用,也不要让 NPC 离开店后柜台仍然营业,除非设计上有代理店员。营业时间、对话内容和任务交付都要和 NPC 状态对齐。
玩家最讨厌“任务目标不见了”。如果任务要求找某 NPC,任务追踪应告诉玩家当前或下一次出现地点:“米娜 18:00 后会回到旅店”。这需要日程系统能查询未来日程。不是所有游戏都要精确显示,但至少不要让玩家盲找。
一个日程解析器
下面的代码展示按时间、条件和优先级选择日程项。
interface ScheduleEntry {
id: string;
startMinute: number;
endMinute: number;
locationId: string;
behavior: "work" | "walk" | "rest" | "shop" | "event";
priority: number;
conditions: string[];
}
export function resolveSchedule(
entries: ScheduleEntry[],
minuteOfDay: number,
matchCondition: (key: string) => boolean,
) {
return entries
.filter((entry) =>
minuteOfDay >= entry.startMinute &&
minuteOfDay < entry.endMinute &&
entry.conditions.every(matchCondition),
)
.sort((a, b) => b.priority - a.priority)[0];
}
这个函数没有处理跨天时间段,生产版本要支持 22:00 到 02:00 这类日程。核心思路是先过滤,再按优先级选择。若没有匹配项,可以回到默认位置或隐藏 NPC。
打断和恢复
剧情、节日、战斗、玩家邀请都可能打断日程。打断应有 token 和恢复策略。比如 NPC 正在去商店路上,被剧情叫到广场;剧情结束后,是回到原路线、直接去当前时间对应地点,还是留在广场?规则要按事件类型定义。普通对话不应打断日程太久,剧情事件可以覆盖全天。
打断状态要存档。玩家在剧情中退出游戏,再回来 NPC 仍应处于剧情位置。不要只在内存里暂停日程。事件结束后清理打断 token,日程解析恢复正常。
路径和拥堵
多个 NPC 使用同一路径时,可能堵门。简单游戏可以允许穿过;重视沉浸的游戏可以做轻量避让。不要为了 NPC 互相避让引入过重物理。门口、柜台、窄桥这些点最好设计足够宽,或设置 NPC 专用路径。路径失败时,NPC 可以瞬移到目标附近并记录日志,不能永远卡在路上。
开发模式显示 NPC 当前日程、目标地点、路径、打断原因和下一个日程。日程 bug 没有可视化会非常难查。特别是条件和优先级冲突,调试面板应显示候选日程和为什么被选中。
上线前检查清单
确认 NPC 日程由全局服务解析;确认日程项有条件和优先级;确认地点使用 anchor id 而不是硬编码坐标;确认离屏 NPC 可按时间推导位置;确认交互内容和当前行为一致;确认任务追踪能查询 NPC 出现地点;确认剧情打断有恢复策略和存档;确认路径失败不会永久卡住;确认调试面板显示候选日程、当前目标和打断原因。
NPC 日程的意义不是让每个人都忙得像真实城市,而是让玩家相信角色有自己的生活。Phaser 可以把 NPC 移动和动画做出来,但日程规则必须稳定。让时间、条件、路径和交互统一,沙盒世界才会真正活起来。
玩家等待和跳时机制
有日程就会有等待。玩家想找 NPC,但 NPC 晚上才回家。等待可以是玩法,也可能是阻碍。游戏应提供合理工具:地图上显示 NPC 下一次出现时间,旅店或长椅允许跳到早晨,任务追踪给出时间提示。不要让玩家真实等待十分钟,只为交一个任务。生活模拟可以强调时间节奏,但也要避免无意义空等。
跳时会影响日程、作物、商店刷新和事件。TimeService 跳过时间时,ScheduleService 不需要逐分钟模拟每个 NPC,而是直接解析目标时间的位置。但重要事件要处理,比如玩家约会错过、限时任务失败、商店关门。跳时前可以提示会错过的关键事件。这样时间系统更可控。
群体事件和节日
节日会覆盖大量 NPC 日程。不要为每个 NPC 单独写节日特例。可以定义 EventSchedule:在某日期某时间,符合条件的 NPC 前往会场,使用节日行为和对话。普通日程被低优先级覆盖。事件结束后,NPC 回到当时应该在的位置,而不是回到早晨起点。这样节日不会把整个世界状态弄乱。
群体事件还要考虑路径拥堵。所有 NPC 同时走向广场,会在门口挤成一团。可以在事件开始前直接把远处 NPC 放到会场附近,只有玩家视野内 NPC 真实走路。沉浸感和性能之间要取舍。玩家看不到的地方,不必完整表演。
商店和服务的代理规则
如果店主不在,商店是否关闭?有些游戏为了便利,会让柜台自动营业;有些游戏强调生活感,店主不在就不能买。两种都可以,但规则要一致。若有代理店员,日程系统应知道当前服务提供者。UI 上显示“由学徒代班”,玩家就不会困惑。
任务 NPC 同理。主线关键 NPC 不应因为普通日程导致玩家长时间找不到。可以在关键任务期间提高任务日程优先级,让 NPC 待在可达位置。生活感要服务玩法,不应压过主线可用性。
存档和版本迁移
NPC 日程配置更新后,玩家可能正处于旧日程中。存档里如果保存了当前 scheduleEntryId,而新版本删除了它,加载时要回退到当前时间可解析的默认日程。不要因为一个日程 id 改名导致 NPC 消失。存档保存 NPC 的特殊状态和打断 token,普通位置可以从日程推导,减少迁移压力。
如果 NPC 被玩家阻挡或推离路径,是否保存偏移?多数游戏不需要。离开地图后回到日程位置即可。只有特殊沙盒玩法,比如玩家能雇佣、搬家、囚禁 NPC,才需要保存更详细状态。不要为不需要的自由度付出复杂成本。
NPC 记忆和一次性对话
日程系统经常和对话记忆结合。NPC 早上第一次见到玩家说早安,晚上说要关店;任务完成后换一套对白;玩家连续几天不来,NPC 可以提一句。对话条件应读取同一套 GameTime 和 NarrativeState。不要在对话脚本里重新判断一套时间规则,否则日程和对白会冲突。
一次性对话要有 id,触发后写入存档。若玩家在 NPC 去商店路上触发了一次性对话,之后不应因为 NPC 到店又重复。对话记忆和日程状态分开保存:日程决定 NPC 在哪,记忆决定他说什么。
大量 NPC 的性能策略
生活模拟可能有几十上百个 NPC。不要为每个 NPC 每帧解析日程。可以按游戏分钟更新日程,或只在时间段边界、天气变化、剧情变量变化时重新解析。玩家附近 NPC 才需要路径和动画,远处 NPC 只保存抽象状态。ScheduleService 可以维护“下一次变更时间”,到点再更新。
调试时显示日程解析耗时和活跃 NPC 数量。若某个剧情变量变化导致所有 NPC 重算,也要能看到。日程系统看起来是内容功能,但 NPC 多了以后也会成为性能点。
玩家干预 NPC 日程
有些沙盒游戏允许玩家邀请、雇佣、阻拦或赠送道具改变 NPC 日程。玩家干预应变成高优先级临时日程,而不是直接改写基础表。比如邀请 NPC 明天参加庆典,系统创建一个 event override;活动结束后回到普通日程。这样既尊重玩家行为,也不会永久污染日程配置。
干预还需要失败反馈。NPC 忙于主线、天气太差、好感不足,都可能拒绝邀请。拒绝原因要明确,玩家才知道如何达成条件。日程系统和好感、任务、天气共同决定结果,UI 只展示解释后的原因。
日程内容的制作规范
内容团队需要一套简单规范:每个 NPC 必须有默认日程、雨天回退、剧情占用规则和夜间位置;每个日程地点必须有 anchor;每个可交互服务必须说明 NPC 不在时是否可用。没有规范,日程表越写越像临时脚本,后期很难查。
发布前可以跑日程巡检:模拟一周游戏时间,检查 NPC 是否有无地点时段、是否长时间卡在不可达区域、关键任务 NPC 是否在任务期间可找到。巡检报告比人工逐小时进游戏找人可靠得多。日程系统一旦内容量上来,工具就是生产力。
巡检还应覆盖雨天、节日和关键剧情变量,避免只验证普通晴天日程。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。