路线阶段:Unity WebGL 小游戏实战第 2 章。
本章目标:搭建“可玩且可反复验证”的最小关卡循环,为后续内容扩展奠定模板。
学习目标
完成本章后,你应该能做到:
- 定义关卡目标、失败条件与胜利判定规则。
- 将倒计时、波次、玩家生存状态统一到关卡控制器。
- 打通战斗中 UI 提示与结算面板流程。
- 支持一键重开与下一关切换,形成持续游玩闭环。
最小可玩闭环定义
一个合格的首关应包含:
- 开始:展示关卡目标(例如“120 秒内存活并清理 3 波”)。
- 进行:实时显示时间、波次、剩余生命。
- 失败:玩家死亡或超时。
- 成功:达成目标后进入结算。
- 重开/继续:玩家可立刻再来一局。
关卡目标模型
public enum StageObjectiveType
{
SurviveTime = 0,
ClearWaves = 1,
KillBoss = 2
}
[Serializable]
public sealed class StageObjective
{
public StageObjectiveType Type;
public int TargetValue;
}
[Serializable]
public sealed class StageRuleConfig
{
public int StageId;
public float TimeLimitSeconds;
public int MaxWaves;
public List<StageObjective> Objectives;
}
运行时状态
public sealed class StageRuntimeState
{
public float RemainTime;
public int CurrentWave;
public int ClearedWave;
public bool PlayerDead;
public bool IsCompleted;
public bool IsFailed;
}
关卡控制器
public sealed class StageLoopController : IUpdatable
{
public int Order { get { return 40; } }
private readonly StageRuleConfig _config;
private readonly StageRuntimeState _state;
private readonly WaveDirector _wave;
private readonly EventBus _eventBus;
public StageLoopController(StageRuleConfig config, StageRuntimeState state, WaveDirector wave, EventBus eventBus)
{
_config = config;
_state = state;
_wave = wave;
_eventBus = eventBus;
}
public void Start()
{
_state.RemainTime = _config.TimeLimitSeconds;
_state.CurrentWave = 1;
_state.ClearedWave = 0;
_state.PlayerDead = false;
_state.IsCompleted = false;
_state.IsFailed = false;
_wave.StartBattle();
_eventBus.Publish("StageStarted", _config.StageId);
_eventBus.Publish("StageTimerChanged", _state.RemainTime);
}
public void Tick(float dt, float unscaledDt)
{
if (_state.IsCompleted || _state.IsFailed)
{
return;
}
_state.RemainTime -= dt;
if (_state.RemainTime < 0f)
{
_state.RemainTime = 0f;
}
_eventBus.Publish("StageTimerChanged", _state.RemainTime);
if (_state.PlayerDead)
{
Fail("player_dead");
return;
}
if (_state.RemainTime <= 0f)
{
Fail("timeout");
return;
}
if (CheckObjectives())
{
Complete();
}
}
public void OnWaveCleared(int waveId)
{
_state.ClearedWave = waveId;
_eventBus.Publish("StageWaveCleared", waveId);
}
public void OnPlayerDead()
{
_state.PlayerDead = true;
}
private bool CheckObjectives()
{
for (var i = 0; i < _config.Objectives.Count; i++)
{
var obj = _config.Objectives[i];
if (obj.Type == StageObjectiveType.ClearWaves)
{
if (_state.ClearedWave < obj.TargetValue)
{
return false;
}
}
else if (obj.Type == StageObjectiveType.SurviveTime)
{
var survived = _config.TimeLimitSeconds - _state.RemainTime;
if (survived < obj.TargetValue)
{
return false;
}
}
}
return true;
}
private void Complete()
{
_state.IsCompleted = true;
_eventBus.Publish("StageCompleted", _config.StageId);
FoundationLog.Info("Stage", "complete id=" + _config.StageId);
}
private void Fail(string reason)
{
_state.IsFailed = true;
_eventBus.Publish("StageFailed", reason);
FoundationLog.Warn("Stage", "failed reason=" + reason);
}
}
UI 同步
HUD
显示:
- 剩余时间(
mm:ss) - 当前波次 / 目标波次
- 目标文本(例如“清理 3 波敌人”)
Result
成功显示:
- 通关时间
- 剩余生命
- 击杀数
失败显示:
- 失败原因(超时/死亡)
- 重开按钮
事件接线
public sealed class StageEventBinder : IDisposable
{
private readonly EventBus _eventBus;
private readonly StageLoopController _loop;
public StageEventBinder(EventBus eventBus, StageLoopController loop)
{
_eventBus = eventBus;
_loop = loop;
_eventBus.Subscribe("WaveCleared", OnWaveCleared);
_eventBus.Subscribe("PlayerDead", OnPlayerDead);
}
public void Dispose()
{
_eventBus.Unsubscribe("WaveCleared", OnWaveCleared);
_eventBus.Unsubscribe("PlayerDead", OnPlayerDead);
}
private void OnWaveCleared(object payload)
{
_loop.OnWaveCleared((int)payload);
}
private void OnPlayerDead(object payload)
{
_loop.OnPlayerDead();
}
}
重开与继续
在 Result 状态面板触发:
Retry->FlowRequest(Loading)并复位关卡状态。Next-> 更新StageId后进入下一关加载。
要求:两条路径都通过流程状态机,不直接在 UI 脚本里切场景。
与前面系统联动
- 波次系统:驱动
ClearWaves目标。 - 存档系统:通关后写入关卡记录。
- 回放系统:记录失败原因与完成帧。
- 音频系统:成功/失败切换不同 BGM 层。
WebGL 注意点
- 计时与暂停要处理浏览器失焦。
- 结算界面资源提前预热,避免首次打开卡顿。
- 重开流程避免完整刷新页面,保持体验连贯。
验收清单
- 关卡可稳定完成“开始->进行->结算->重开”循环。
- 成功与失败判定无冲突。
- 计时显示与逻辑时间一致。
- 重开 20 次无状态残留。
常见坑
坑 1:目标判定直接依赖 UI 文本状态
逻辑应只依赖运行时状态,不依赖展示层。
坑 2:失败后战斗还在跑
进入 Result 前必须关停战斗输入与 AI Tick。
坑 3:重开只重置玩家不重置波次
会出现波次错位。重开必须完整重置关卡状态。
本月作业
做一个“2分钟生存关卡”:
- 目标:120 秒内清理 3 波敌人。
- 失败:玩家死亡或超时。
- 结算:显示评分(时间、伤害、受击)。
下一章进入 Unity WebGL 小游戏实战 03:经济与升级系统(局内成长、局外解锁)。