路线阶段:Unity WebGL 小游戏实战第 4 章。
本章目标:让关卡节奏从“固定脚本”升级为“可控动态系统”,兼顾挑战性与公平性。
学习目标
完成本章后,你应该能做到:
- 设计可量化难度曲线(时间轴 + 强度目标)。
- 实现导演系统动态调节波次、掉落和精英出现率。
- 通过随机约束避免连续极端事件。
- 将难度调节结果记录进回放用于平衡分析。
为什么需要导演系统
固定波次脚本常见问题:
- 新手前期崩盘,高手后期无聊。
- 同一关卡重复游玩缺乏变化。
- 调整难度只能改大量配置,效率低。
导演系统思路:先定义目标难度,再用规则实时逼近目标。
难度曲线模型
[Serializable]
public sealed class DifficultyCurvePoint
{
public float TimeSeconds;
public float Intensity; // 0~1
}
[Serializable]
public sealed class DifficultyCurveConfig
{
public List<DifficultyCurvePoint> Points;
}
曲线采样
public static class DifficultyCurveSampler
{
public static float Evaluate(DifficultyCurveConfig cfg, float t)
{
if (cfg == null || cfg.Points == null || cfg.Points.Count == 0) return 0f;
if (cfg.Points.Count == 1) return cfg.Points[0].Intensity;
for (var i = 1; i < cfg.Points.Count; i++)
{
var a = cfg.Points[i - 1];
var b = cfg.Points[i];
if (t <= b.TimeSeconds)
{
var d = Mathf.Max(0.0001f, b.TimeSeconds - a.TimeSeconds);
var k = Mathf.Clamp01((t - a.TimeSeconds) / d);
return Mathf.Lerp(a.Intensity, b.Intensity, k);
}
}
return cfg.Points[cfg.Points.Count - 1].Intensity;
}
}
玩家表现指标
导演系统不只看时间,还看玩家状态:
- 最近 30 秒受击次数
- 最近 30 秒击杀效率
- 当前 HP 比例
- 当前局内升级层数
public sealed class PlayerPerformanceSnapshot
{
public float HpRate;
public float KillsPerMinute;
public float DamageTakenPerMinute;
public int RunUpgradeCount;
}
导演决策输出
public sealed class DirectorDecision
{
public float SpawnRateScale; // 刷怪频率缩放
public float EliteChance; // 精英概率
public float RewardScale; // 资源奖励缩放
public int MaxAliveEnemy; // 同屏上限
}
导演系统实现
public sealed class StageDirectorSystem : IUpdatable
{
public int Order { get { return 220; } }
private readonly DifficultyCurveConfig _curve;
private readonly StageRuntimeState _stage;
private readonly PerformanceTracker _perf;
private readonly EventBus _eventBus;
private float _timer;
public DirectorDecision CurrentDecision { get; private set; }
public StageDirectorSystem(
DifficultyCurveConfig curve,
StageRuntimeState stage,
PerformanceTracker perf,
EventBus eventBus)
{
_curve = curve;
_stage = stage;
_perf = perf;
_eventBus = eventBus;
CurrentDecision = new DirectorDecision
{
SpawnRateScale = 1f,
EliteChance = 0.1f,
RewardScale = 1f,
MaxAliveEnemy = 24
};
}
public void Tick(float dt, float unscaledDt)
{
_timer += dt;
if (_timer < 1.0f)
{
return;
}
_timer = 0f;
var elapsed = _stage.ElapsedSeconds;
var targetIntensity = DifficultyCurveSampler.Evaluate(_curve, elapsed);
var perf = _perf.BuildSnapshot();
CurrentDecision = Resolve(targetIntensity, perf);
_eventBus.Publish("DirectorDecisionUpdated", CurrentDecision);
}
private static DirectorDecision Resolve(float targetIntensity, PlayerPerformanceSnapshot perf)
{
var pressure = targetIntensity;
if (perf.HpRate < 0.35f)
{
pressure -= 0.18f;
}
if (perf.DamageTakenPerMinute > 260f)
{
pressure -= 0.12f;
}
if (perf.KillsPerMinute > 90f)
{
pressure += 0.10f;
}
pressure = Mathf.Clamp01(pressure);
var d = new DirectorDecision();
d.SpawnRateScale = Mathf.Lerp(0.75f, 1.45f, pressure);
d.EliteChance = Mathf.Lerp(0.05f, 0.35f, pressure);
d.RewardScale = Mathf.Lerp(1.1f, 0.85f, pressure);
d.MaxAliveEnemy = Mathf.RoundToInt(Mathf.Lerp(16f, 42f, pressure));
return d;
}
}
随机约束(防极端)
1. 精英生成冷却
public sealed class RandomGuard
{
private int _lastEliteTick;
public bool CanSpawnElite(int currentTick)
{
return currentTick - _lastEliteTick >= 240; // 4s @60fps
}
public void MarkElite(int currentTick)
{
_lastEliteTick = currentTick;
}
}
2. 失败保护
当玩家连续濒死:
- 降低 20% 刷怪速率
- 限制精英刷新
- 增加治疗掉落权重
与刷怪系统接入
public sealed class AdaptiveWaveSpawner
{
private readonly StageDirectorSystem _director;
public AdaptiveWaveSpawner(StageDirectorSystem director)
{
_director = director;
}
public SpawnPlan BuildPlan(BaseSpawnPlan basePlan)
{
var d = _director.CurrentDecision;
var p = new SpawnPlan();
p.SpawnInterval = basePlan.SpawnInterval / Mathf.Max(0.1f, d.SpawnRateScale);
p.EliteChance = d.EliteChance;
p.MaxAlive = d.MaxAliveEnemy;
return p;
}
}
与经济系统接入
导演输出 RewardScale 后,掉落奖励乘以系数:
var finalReward = Mathf.CeilToInt(baseReward * decision.RewardScale);
这样难度升高时可适当压缩收益,避免“越难越爆富”。
平衡分析日志
每 5 秒记录:
- 目标强度
- 实际压力
- 导演决策
- 玩家表现快照
FoundationLog.Info("Director",
"t=" + elapsed +
" target=" + targetIntensity +
" spawnScale=" + d.SpawnRateScale +
" elite=" + d.EliteChance +
" reward=" + d.RewardScale +
" hp=" + perf.HpRate);
回放一致性
为保证可重现:
- 记录导演决策与随机种子。
- 回放时使用同决策流而非重新计算。
- 对比实时与回放的波次时间点。
WebGL 注意点
- 决策计算频率控制在 1Hz~2Hz。
- 避免复杂统计遍历,每帧只增量更新指标。
- 导演调节不要过猛,避免“体感突变”。
验收清单
- 新手场景下难度可自适应下降,避免早期崩盘。
- 高水平玩家不会在后期无聊,精英与节奏会提升。
- 同一 seed 的回放可还原导演决策序列。
- 调整曲线配置即可显著改变关卡体感。
常见坑
坑 1:导演直接改过多系统参数
会造成联动难控。建议只输出少数关键决策变量。
坑 2:用短窗口指标强行调节
会来回抖动。需要平滑和节流。
坑 3:随机完全放开
会出现连续极端情况。必须有冷却与保护规则。
本月作业
做一版“可视化导演调试面板”:
- 实时显示目标强度、当前决策、玩家指标。
- 支持运行时修改曲线点位并立即生效。
- 导出一局完整决策日志用于离线分析。
下一章进入 Unity WebGL 小游戏实战 05:关卡内容管线(数据驱动关卡配置与热更新准备)。