刷怪不是越多越刺激
Phaser 生存射击、割草、塔防混合玩法里,刷怪系统经常从一个定时器开始:每隔 1 秒随机生成一个敌人。原型阶段没问题,但一旦进入内容制作,就会出现各种失控:玩家刚开局就被包围,远程怪在屏幕外无预警攻击,精英怪和小怪同一时间堆叠导致掉帧,地图某个角落刷怪太近,玩家死亡后团队说不清是数值太高还是出生点不合理。刷怪导演的职责不是把敌人丢进场景,而是控制压力、节奏和可读性。
刷怪系统应该像一个导演:知道当前时间、玩家强度、场上敌人、地图区域、性能压力和设计目标。它选择下一波敌人、出生位置和预警方式,并在必要时放缓。完全随机会制造偶然惊喜,也会制造不可控事故。一个可靠的导演系统既要让玩家感到被压迫,也要让失败看起来公平。
用压力预算管理场面
每种敌人都可以有压力成本。普通近战怪成本 1,快速怪 2,远程怪 3,精英怪 8,Boss 20。当前场上所有敌人的成本加起来就是当前压力。每个时间段有目标压力上限,导演只在预算允许时继续刷怪。这样比单纯限制敌人数更合理,因为 20 个小怪和 5 个远程精英对玩家的压力完全不同。
压力预算还可以和玩家表现联动。玩家血量低、刚受伤、帧率下降时,目标压力略微降低;玩家连续清场、装备成型时,目标压力提高。注意,这不是偷偷作弊,而是避免系统把玩家压到无意义死亡。动态调节要平滑,最好按几十秒尺度变化,不要玩家一掉血就立刻停止刷怪,那会被看出来。
flowchart TD
A["GameClock / WaveTimeline"] --> B["SpawnDirector 计算目标压力"]
C["FieldState:敌人数、敌人类型、帧率"] --> B
D["PlayerState:血量、等级、输出"] --> B
B --> E["WavePlanner 选择敌人组合"]
E --> F["SpawnPointSelector 选择安全出生点"]
F --> G["WarningLayer 出生预警"]
G --> H["EnemyFactory 从对象池激活敌人"]
H --> C
波次配置要有意图
波次不是一张敌人列表。每个波次应该包含时间范围、主题、目标压力、可用敌人、特殊规则和休息窗口。比如第 0 到 60 秒是教学波,只出现慢速近战;第 60 到 120 秒引入远程怪,但数量少;第 180 秒出现第一个精英,前后给 10 秒清场时间。波次主题让玩家学习规则,而不是被所有敌人同时教育。
策划配置敌人时,也要写清敌人职责:包围、驱赶、远程压制、区域封锁、奖励掉落。导演可以根据职责组合敌人,避免同一时间全是远程压制或全是高速追击。职责比具体敌人 id 更利于扩展,后期新增敌人时能加入已有波次池。
出生点要考虑玩家视野和逃生路线
敌人不能凭空刷在玩家脚下,除非有明确传送预警。出生点选择至少要检查:距离玩家不小于安全半径,不在玩家当前视野中心,不在墙内,不在封闭区域,不堵死唯一出口。对于屏幕外出生,可以在边缘或小地图给出提示;对于屏幕内出生,必须有地面警示、烟雾或传送门动画。
地图可以预先标记 spawn zones。每个 zone 有标签:普通、精英、飞行、远程、Boss、禁用。导演按当前波次和玩家位置选择 zone,再在 zone 内随机点。不要完全随机全地图坐标。Phaser 中可以用对象层或 JSON 配置导入这些区域,开发模式显示热区和冷却状态。
对象池是性能底线
生存游戏敌人多,频繁创建销毁 Sprite、Body、动画和粒子会带来 GC 抖动。EnemyFactory 应该从对象池激活敌人,死亡后回收。回收时要重置位置、速度、状态机、Buff、碰撞体、tint、透明度、事件监听和血条。刷怪导演还要知道池容量。如果池耗尽,不要继续创建无限对象,可以跳过低优先级小怪或延迟生成。
性能也应进入压力预算。帧率低于阈值时,导演降低目标压力或选择更少但更有意义的敌人。低端设备不应该因为刷怪系统坚持配置而彻底卡死。游戏体验是实时的,配置只是目标。
一个压力预算刷怪器
下面的代码展示核心决策:根据当前压力和目标压力选择可刷敌人。真实项目会加入权重、区域和冷却。
interface EnemySpawnConfig {
id: string;
cost: number;
minTimeMs: number;
maxTimeMs?: number;
weight: number;
}
interface DirectorState {
elapsedMs: number;
currentPressure: number;
targetPressure: number;
fps: number;
}
export function pickSpawn(configs: EnemySpawnConfig[], state: DirectorState) {
if (state.currentPressure >= state.targetPressure) return undefined;
if (state.fps < 45 && state.currentPressure > state.targetPressure * 0.7) return undefined;
const budget = state.targetPressure - state.currentPressure;
const candidates = configs.filter((cfg) => {
const inTime = state.elapsedMs >= cfg.minTimeMs && (!cfg.maxTimeMs || state.elapsedMs <= cfg.maxTimeMs);
return inTime && cfg.cost <= budget;
});
const total = candidates.reduce((sum, cfg) => sum + cfg.weight, 0);
let roll = Math.random() * total;
for (const cfg of candidates) {
roll -= cfg.weight;
if (roll <= 0) return cfg;
}
return candidates[0];
}
这段代码很简单,但已经比固定间隔随机可靠。它不会在预算不足时硬刷精英,也会在帧率低时保守。后续可以把 Math.random() 换成种子随机,方便每日挑战和回放。
预警让压力变得公平
强敌出生前需要预警。预警可以是地面标记、音效、屏幕边缘箭头、传送门、阴影或小地图闪烁。预警时间根据敌人威胁决定:普通小怪 0.3 秒足够,精英和自爆怪可能需要 1 秒。预警期间敌人不应立即造成伤害。玩家看到预警后有机会调整位置,死亡才显得合理。
预警也不能太多。全屏都是红圈时,玩家什么都看不懂。导演可以限制同时预警数量,把低优先级出生改到屏幕外或延迟。可读性永远优先于数量。
动态难度要记录原因
动态难度如果没有日志,很容易让团队误判。导演每次调整目标压力时,应该记录原因:玩家血量低、清场速度快、帧率低、波次进入高潮、Boss 存活。调试面板显示目标压力、当前压力、最近刷怪、拒绝刷怪原因和出生点选择。这样设计师能判断系统是否按预期工作。
玩家层面不需要知道具体数字,但可以感受到节奏:紧张、喘息、再紧张。休息窗口很重要。每几分钟给玩家 10 到 20 秒收集经验、选择升级、调整位置,会让后续高压更有意义。无休止刷怪只会让人疲劳。
上线前检查清单
确认每种敌人有压力成本和职责;确认波次有主题和休息窗口;确认出生点避开玩家脚下、墙内和死路;确认强敌出生有预警;确认对象池容量和回收状态正确;确认帧率压力会影响刷怪;确认动态难度有日志;确认固定种子能复现一局刷怪;确认调试面板显示当前压力、目标压力和最近刷怪原因;确认失败日志记录死亡前 10 秒的刷怪事件。
刷怪导演的目标不是让玩家被随机数淹没,而是制造有节奏的危险。Phaser 能轻松生成大量敌人,但真正决定生存玩法质量的是压力预算、出生规则和预警。把这些系统化,玩家才会觉得每次失败都像差一点,而不是被刷怪器背刺。
奖励掉落也要进入导演视野
生存游戏的刷怪和奖励不是两套独立系统。某一波敌人更难,通常也应该带来更多经验、金币或治疗机会。若刷怪导演只负责压力,掉落系统只按敌人 id 随机,可能出现高压波没有补给,玩家被长期耗死。可以让波次配置包含奖励节奏:某段时间提高经验掉落,精英波后保证一个回血,Boss 前给一次商店或升级间隙。
奖励也会影响压力。屏幕上大量经验球会诱导玩家冒险拾取,治疗道具会改变玩家路线。导演可以把关键奖励投放到相对安全但需要移动的位置,制造决策,而不是全部掉在敌人堆里。Phaser 表现层可以用吸附和边缘提示帮助玩家读到奖励,但经济节奏应由导演整体考虑。
出生点冷却和区域公平
出生点需要冷却。否则同一个角落连续刷远程怪,玩家会觉得地图针对他。每个 SpawnZone 可以记录最近使用时间、最近生成敌人类型和玩家最近距离。导演选择出生点时,对刚用过的区域降权,对玩家视线外但不太近的区域加权。若地图有多个房间,还要避免敌人生成在暂时不可达的房间,导致场上压力看似很高,实际玩家找不到敌人。
区域公平还包括方向。玩家长时间向右移动时,敌人不能永远从右侧迎面刷,也不能永远从背后偷袭。可以统计最近 10 秒出生方向,做轻微均衡。玩家感受到的是包围压力,而不是某个方向无休止被惩罚。
Boss 波和普通波的交接
Boss 出现时,普通刷怪要降级或转型。最常见的错误是 Boss 战开始后,普通导演仍然按原节奏刷小怪,结果 Boss 招式和小怪压力叠加成无解。Boss 波可以把普通敌人变成资源来源:少量小怪掉落弹药或回血,但不承担主要压力。导演需要知道当前是否处于 Boss 状态,并切换目标压力模型。
Boss 死亡后的清场也要设计。是立即清掉所有小怪,还是让残余敌人继续存在?如果清掉,要播放合理反馈;如果保留,要保证玩家不会在胜利动画中被打死。刷怪导演不仅负责开始危险,也负责结束危险。
地图机制和刷怪的耦合
地图上可能有陷阱、炮台、可破坏墙、治疗泉、传送门。刷怪导演需要知道这些机制,否则会把敌人刷在不合理位置。比如自爆怪生成在治疗泉旁,玩家会被迫离开补给;远程怪生成在不可到达高台,会变成单方面射击。SpawnZone 可以带上环境标签,敌人配置声明允许或禁止的标签。这样内容设计能表达“飞行怪可以从悬崖出现,近战怪不能”。
某些波次可以主动利用地图机制。毒雾波让玩家离开中心区域,冲锋怪波迫使玩家绕障碍走,奖励波把敌人引向炮台附近。导演不是只按时间刷敌人,也是在调度地图资源。把地图机制纳入配置,生存玩法会比单纯数值增长更丰富。
多人和排行榜模式
如果生存游戏有排行榜,刷怪必须可复现。每日挑战需要固定种子、固定波次和固定配置版本。动态难度可能影响公平性,因此排行榜模式可以关闭个人化调节,只保留性能保护;普通休闲模式再启用动态压力。玩家成绩上传时带上 seed、waveVersion、enemyConfigVersion。否则不同玩家遇到不同刷怪,却放在同一榜单比较,会失去可信度。
多人合作则需要权威导演。客户端本地导演容易不同步,最好由房主或服务器决定刷怪事件,其他客户端只表现。Phaser 客户端仍然可以预测预警动画,但真正敌人出生要以权威事件为准。刷怪导演在单机和多人中的边界要提前定,否则后期改联机会很痛。
记录权威事件时,也要记录被拒绝的刷怪请求原因。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。