今日速览

  • 波次系统不是「每隔 N 秒刷 M 只怪」,而是 预算 → 挑选 → 分批投放 → 喘息 四段流水线;四段分开写,任何一段出问题都能单独调。
  • 用 战力点数预算(Budget) 代替硬编码数量:budget(w) = base × growth^(w-1),每只敌人有自己的 cost,同波能放几只由预算算出来——难度曲线天然平滑,不会第 7 波突然爆炸。
  • 波次表做成 自定义 Resource(.tres),在 Inspector 里拖拽调参,代码一行不改;这是 Godot 官方推荐的「数据与逻辑分离」做法(Resource 是纯数据容器,节点只是使用者)。
  • 两个必踩的坑:① Resource 默认 全局共享,改一只等于改全部,要逐实例用 duplicate(true);② 一波 instantiate() 几十只 = 掉帧尖刺,必须 对象池 + 每帧生成限额。

核心学习内容

一、四层结构:谁负责什么

层职责常见错误
WaveDirector计时、推进波次、发信号把刷怪逻辑写进 _process 每帧判断
WaveData(Resource)波次表:敌人池、预算、间隔、喘息时长写死在脚本里的 const WAVES = [...]
SpawnPoint提供合法生成位置(离玩家最小距离)刷在玩家脸上或墙里
EnemyPool复用敌人节点,避免反复 new/free每波 queue_free() 再重建

Godot 文档里一句话点透了关键:场景是 PackedScene,instantiate() 很快,且「图像、网格等在所有实例之间共享」。意思是——节点可以放心复用,但资源千万别当私有变量改。

二、难度预算:让曲线由公式长出来

不要问「第 10 波放几只」,要问「第 10 波玩家能承受多少战力」。做法:

budget(w) = base * pow(growth, w - 1)          # base=10, growth=1.18
每波把预算花完:按权重随机抽敌人,抽到就扣它的 cost,不够就停
  • 小怪 cost 1、精英 cost 4、Boss cost 15 —— 你调的是「强度」,不是「数量」。
  • 想加新敌人?只填 cost 和权重,全 20 波自动重新分配,不用回头改表。
  • 想突击加压?给某一波加 budget_multiplier = 1.5 即可,这是「波次修饰符」,比改全局曲线安全。

三、节奏:难度不是线性上升,是锯齿

玩家体感来自「压力—释放」的锯齿,而不是一条斜线。三个可调旋钮:

  1. 波内分批(trickle):一波 12 只不要一次性倒出,按 spawn_interval 每 0.4~0.8 秒放一只,屏幕压力更可控,也不会瞬间卡。
  2. 喘息(intermission):波间留 3~5 秒,配掉落/回血/升级三选一,玩家才有「我变强了」的实感。
  3. 前 20% 低压:前两波只放最基础的单位,让玩家在无风险环境里学会操作——这条和之前写的「新手引导」是同一件事。

四、性能:把尖刺磨平

# 反面教材:一波全倒出来
for i in wave.count:
    var e = enemy_scene.instantiate()
    add_child(e)

# 正解:池 + 每帧限额
func _process(delta: float) -> void:
    _timer -= delta
    if _timer <= 0.0 and _pending.size() > 0:
        _timer = current_wave.spawn_interval
        for i in mini(current_wave.per_tick, _pending.size()):   # 每 tick 最多 2 只
            _spawn_one(_pending.pop_front())

再配合「离玩家超过 2 屏的敌人自动回收进池」,100 只同屏也不会掉帧。

🎯 DeepSeek 提示词

提示词 1:生成整套波次系统(Resource + Director)

适用场景:从零搭一个 Roguelike / 塔防 / 波次生存的刷怪框架。

你是资深 Godot 4 游戏程序员,精通 GDScript 与数据驱动架构。

任务:为一个 2D 波次生存游戏实现刷怪系统,输出 3 个文件的完整代码。

【文件 1】wave_data.gd:class_name WaveData extends Resource
- @export 字段:enemies(Array[EnemyEntry])、budget(float)、budget_multiplier(float=1.0)、
  spawn_interval(float=0.6)、per_tick(int=2)、intermission(float=4.0)、is_boss_wave(bool=false)
- 另建 EnemyEntry(Resource):enemy_scene(PackedScene)、cost(int)、weight(float)、min_wave(int)
- 所有 @export 必须有默认值,支持在 Inspector 新建 .tres

【文件 2】wave_director.gd:extends Node
- 信号:wave_started(index)、wave_cleared(index)、all_waves_cleared、enemy_spawned(node)
- 用状态机管理:IDLE → SPAWNING → FIGHTING → INTERMISSION
- 按预算抽取敌人:先过滤 min_wave <= 当前波,再按 weight 加权随机,扣 cost 直到花完预算
- 生成必须分批:每 spawn_interval 秒最多生成 per_tick 只(不要一帧全生成)
- 用对象池复用敌人节点:敌人死亡时 hide + set_process(false) 回池,不 queue_free
- 生成点必须距离玩家 >= min_spawn_distance,且用 RayCast2D 或物理查询确认不在墙内

【文件 3】enemy_pool.gd:extends Node
- 提供 acquire(scene) -> Node 与 release(node) 两个方法
- 池上限 200,超出时真正 queue_free

【约束】
- Godot 4.3+ 语法;全部带中文注释;变量与函数用 snake_case
- 不要用 await get_tree().create_timer() 做波次计时(暂停时会出错),用 Timer 节点或累加 delta
- 每个类给出 .tscn 挂载说明与 Inspector 需要填什么

【输出格式】按文件分块给代码,最后附一段「5 步接线说明」。

提示词 2:难度预算调参器(让 AI 帮你算 20 波)

适用场景:已有敌人表,但不知道每波放几只、曲线是否合理。

你是游戏数值设计师。下面是敌人表与目标体验,请设计 20 波难度预算表。

【输入】
敌人:Slime(cost 1, weight 5),Bat(cost 1, weight 3),Archer(cost 2, weight 2),
Brute(cost 4, weight 1, min_wave 5),Necromancer(cost 6, weight 0.5, min_wave 10)
目标:单局 15~20 分钟;第 5 波出现第一只精英;第 10 波一次小高潮;第 20 波 Boss 战
玩家成长:每波结束可选 1 个升级,DPS 约每波 +9%

【任务】
1. 用 budget(w) = base × growth^(w-1) 定出 base 与 growth,并解释为什么这两个值能对上「DPS 每波 +9%」
2. 输出 Markdown 表格:波次 | 预算 | 实际花费 | 各敌人数 | 预计清怪秒数 | 压力等级(1-5)
3. 标出 3 个「节奏问题」并给出修正(例如某波实际花费远低于预算 = 玩家会觉得空)
4. 第 20 波给一个 Boss 波配置,说明为什么预算要单独设

【约束】预计清怪秒数要和玩家 DPS 对得上;不要出现某波敌人数量 > 40;用中文表格,数字保留 1 位小数。

提示词 3:代码审查(专门查帧率与状态 bug)

适用场景:刷怪系统能跑,但偶发卡顿、敌人不消失、暂停后乱刷。

你是 Godot 4 性能与架构审查员。请审查下面这段刷怪代码,只看事实,不要重写整个系统。

【审查维度,按此顺序】
1. 帧率尖刺:是否有单帧大量 instantiate / add_child / 物理查询
2. 内存与生命周期:是否有 queue_free 漏掉、信号未断开、Resource 被跨实例修改(共享 bug)
3. 状态机漏洞:暂停 / 玩家死亡 / 波次切换时会不会重复计时、重复刷怪
4. 逻辑边界:预算花不完、敌人全被抽完、生成点无合法位置时会怎样

【输出格式】
| 严重度(高/中/低) | 位置(函数名+行号) | 问题 | 为什么会发生 | 最小修复方案 |
最后给一个 unified diff 形式的补丁,只改必要行,不改架构。

【约束】每条问题必须能在代码里指到具体行;不确定的写「需实测确认」,不要猜。

学习心得

今天这层想清楚了一件事:刷怪系统是「难度曲线」的唯一执行者。数值表算得再漂亮,如果刷怪按「每波 +2 只」硬编码,曲线就是一条歪的直线。而把预算、挑选、投放、喘息拆成四段之后,我调「手感」的方式从改代码变成了改 .tres——这正是用 DeepSeek 做游戏最舒服的协作方式:让它写骨架和约束,让参数留在编辑器里给自己调。

对「一个人 + AI 做 Steam 游戏」来说,这个系统的价值还在于复用到任何品类:塔防、幸存者 Like、Roguelike 的关卡推进,底下的波次引擎是同一套。写一次,之后每个原型都能少两天。

明日关注

  • 把 WaveData 做成 .tres 实测一遍:拖 3 个波次进去,确认 Inspector 能新建、能覆盖,且不会出现「改一只全变」的共享 bug
  • 给对象池加一个调试面板(当前池内数量 / 本波剩余预算 / 压力等级),上架前你就是靠它调平衡
  • 查一下 Steam 上「波次生存」品类头部作品的单局时长与波数(多半是 15~20 分钟 / 20 波),反推自己的 base 和 growth

本文由自动化学习流水线每日生成,坚持即学即记。