路线阶段:Unity WebGL 小游戏实战第 5 章。
本章目标:把关卡内容从“写死在场景和脚本里”升级为“可配置、可校验、可演进”的管线。
学习目标
完成本章后,你应该能做到:
- 把关卡参数、刷怪脚本、奖励规则拆成配置资产。
- 构建配置校验器,提前发现脏数据与逻辑冲突。
- 设计版本字段和兼容策略,支撑长期内容更新。
- 让策划可在不改代码情况下产出新关卡。
背景问题
当关卡数量上升后,常见痛点:
- 新关卡需要程序改代码并重打包。
- 配置分散在多个 Prefab,难追踪。
- 小改动容易破坏旧关卡。
- 线上问题无法快速定位到具体配置版本。
关卡配置拆分
建议拆成 4 类配置:
StageMeta:关卡基础信息(ID、名称、目标、时长)。SpawnTimeline:刷怪波次与导演参数。RewardProfile:结算奖励与掉落规则。PresentationProfile:BGM、背景、UI文案、演出钩子。
数据结构示例
[Serializable]
public sealed class StageMetaConfig
{
public int StageId;
public int Version;
public string Name;
public float TimeLimitSeconds;
public int TargetWave;
public string SpawnTimelineKey;
public string RewardProfileKey;
public string PresentationProfileKey;
}
[Serializable]
public sealed class SpawnTimelineConfig
{
public string Key;
public int Version;
public List<SpawnSegment> Segments;
}
[Serializable]
public sealed class SpawnSegment
{
public float StartTime;
public float Duration;
public string EnemyPool;
public float SpawnInterval;
public float EliteChance;
public int MaxAlive;
}
[Serializable]
public sealed class RewardProfileConfig
{
public string Key;
public int Version;
public int BaseGold;
public int BaseCrystal;
public float TimeBonusScale;
public float NoDeathBonusScale;
}
配置仓库
public sealed class StageConfigRepository
{
private readonly Dictionary<int, StageMetaConfig> _stageMeta = new Dictionary<int, StageMetaConfig>(128);
private readonly Dictionary<string, SpawnTimelineConfig> _spawn = new Dictionary<string, SpawnTimelineConfig>(128);
private readonly Dictionary<string, RewardProfileConfig> _reward = new Dictionary<string, RewardProfileConfig>(64);
public void AddStageMeta(StageMetaConfig cfg)
{
_stageMeta[cfg.StageId] = cfg;
}
public void AddSpawn(SpawnTimelineConfig cfg)
{
_spawn[cfg.Key] = cfg;
}
public void AddReward(RewardProfileConfig cfg)
{
_reward[cfg.Key] = cfg;
}
public StageMetaConfig GetStageMeta(int stageId)
{
StageMetaConfig cfg;
return _stageMeta.TryGetValue(stageId, out cfg) ? cfg : null;
}
public SpawnTimelineConfig GetSpawn(string key)
{
SpawnTimelineConfig cfg;
return _spawn.TryGetValue(key, out cfg) ? cfg : null;
}
public RewardProfileConfig GetReward(string key)
{
RewardProfileConfig cfg;
return _reward.TryGetValue(key, out cfg) ? cfg : null;
}
}
关卡装配器
public sealed class StageAssembler
{
private readonly StageConfigRepository _repo;
public StageAssembler(StageConfigRepository repo)
{
_repo = repo;
}
public StageAssemblyResult Build(int stageId)
{
var meta = _repo.GetStageMeta(stageId);
if (meta == null)
{
throw new InvalidOperationException("Stage meta missing id=" + stageId);
}
var spawn = _repo.GetSpawn(meta.SpawnTimelineKey);
if (spawn == null)
{
throw new InvalidOperationException("Spawn timeline missing key=" + meta.SpawnTimelineKey);
}
var reward = _repo.GetReward(meta.RewardProfileKey);
if (reward == null)
{
throw new InvalidOperationException("Reward profile missing key=" + meta.RewardProfileKey);
}
return new StageAssemblyResult(meta, spawn, reward);
}
}
public sealed class StageAssemblyResult
{
public readonly StageMetaConfig Meta;
public readonly SpawnTimelineConfig Spawn;
public readonly RewardProfileConfig Reward;
public StageAssemblyResult(StageMetaConfig meta, SpawnTimelineConfig spawn, RewardProfileConfig reward)
{
Meta = meta;
Spawn = spawn;
Reward = reward;
}
}
配置校验器
校验目标
- ID/Key 是否重复。
- 引用链是否完整(Meta -> Spawn/Reward)。
- 数值是否越界(负时间、间隔为0、概率 >1)。
- 时间段是否重叠冲突。
校验实现
public sealed class StageConfigValidator
{
public List<string> Validate(StageConfigRepository repo, IEnumerable<int> stageIds)
{
var errors = new List<string>(64);
foreach (var id in stageIds)
{
var meta = repo.GetStageMeta(id);
if (meta == null)
{
errors.Add("StageMeta missing id=" + id);
continue;
}
if (meta.TimeLimitSeconds <= 0f)
{
errors.Add("StageMeta invalid timeLimit id=" + id);
}
var spawn = repo.GetSpawn(meta.SpawnTimelineKey);
if (spawn == null)
{
errors.Add("SpawnTimeline missing key=" + meta.SpawnTimelineKey + " stage=" + id);
}
else
{
ValidateSpawn(spawn, errors, id);
}
var reward = repo.GetReward(meta.RewardProfileKey);
if (reward == null)
{
errors.Add("RewardProfile missing key=" + meta.RewardProfileKey + " stage=" + id);
}
}
return errors;
}
private static void ValidateSpawn(SpawnTimelineConfig spawn, List<string> errors, int stageId)
{
for (var i = 0; i < spawn.Segments.Count; i++)
{
var s = spawn.Segments[i];
if (s.Duration <= 0f)
errors.Add("SpawnSegment duration invalid stage=" + stageId + " index=" + i);
if (s.SpawnInterval <= 0f)
errors.Add("SpawnSegment interval invalid stage=" + stageId + " index=" + i);
if (s.EliteChance < 0f || s.EliteChance > 1f)
errors.Add("SpawnSegment elite chance invalid stage=" + stageId + " index=" + i);
if (s.MaxAlive <= 0)
errors.Add("SpawnSegment maxAlive invalid stage=" + stageId + " index=" + i);
}
}
}
版本与兼容策略
每份配置都携带 Version,加载时:
- 低版本配置先迁移。
- 迁移失败时降级到默认关卡模板。
- 记录配置版本到运行日志与回放头。
FoundationLog.Info("StageConfig",
"stage=" + meta.StageId +
" metaVer=" + meta.Version +
" spawnVer=" + spawn.Version +
" rewardVer=" + reward.Version);
热更新准备(不改核心逻辑)
即便暂不接远程拉取,也应预留:
IStageConfigProvider接口。- 本地 provider 与远程 provider 可替换。
- 配置加载失败可回退上一版缓存。
public interface IStageConfigProvider
{
IEnumerator LoadAllAsync(Action<StageConfigRepository> onDone, Action<string> onError);
}
与前面系统联动
- 导演系统:读取
SpawnTimeline动态调节。 - 经济系统:读取
RewardProfile结算奖励。 - 流程状态机:Loading 阶段先校验配置再进战斗。
- 存档系统:记录当前关卡配置版本用于追踪。
WebGL 注意点
- 配置文件体积要小,建议按章节拆分。
- 解析阶段避免大量字符串分配。
- 加载失败必须给出可恢复 UI(重试/回退)。
验收清单
- 新增一个关卡只改配置,不改代码。
- 配置错误可在加载阶段被明确拦截。
- 关卡运行日志可定位配置版本与 key。
- 回放可还原对应配置版本下的行为。
常见坑
坑 1:把配置对象当运行时对象直接修改
会污染后续关卡。运行时应复制或映射到独立状态。
坑 2:校验器只做非空检查
数值边界和时间重叠才是线上高频问题。
坑 3:热更新路径与本地路径两套代码
维护成本翻倍。应统一 provider 接口。
本月作业
落地“配置驱动新关卡”流程:
- 新增 Stage 02 配置(不同刷怪与奖励)。
- 通过校验器检查并修复问题。
- 打包后验证 Stage 01/02 都可正常运行。
下一章进入 Unity WebGL 小游戏实战 06:广告与商业化接入(激励视频、插屏与收益归因)。