路线阶段:Unity WebGL 小游戏实战第 3 章。
本章目标:把“打完一局就结束”升级为“有成长目标的循环玩法”。
学习目标
完成本章后,你应该能做到:
- 设计局内临时成长与局外永久成长的双循环。
- 搭建货币流入/流出模型并控制经济稳定性。
- 实现升级选项池与权重刷新机制。
- 将经济与存档、结算、UI联动形成闭环。
经济循环结构
建议采用双层模型:
- 局内资源(Run Currency):本局掉落,本局内用于临时升级,局结束清空。
- 局外资源(Meta Currency):通关结算发放,用于永久解锁与养成。
这样既有短期刺激,也有长期目标。
核心数据模型
public enum CurrencyType
{
RunGold = 0,
MetaCrystal = 1
}
public sealed class EconomyState
{
public int RunGold;
public int MetaCrystal;
}
[Serializable]
public sealed class UpgradeConfig
{
public int UpgradeId;
public string Name;
public string Desc;
public bool IsRunUpgrade;
public int BaseCost;
public float CostScale;
public int MaxLevel;
public string EffectKey;
public float EffectValue;
public int Weight;
}
public sealed class UpgradeRuntime
{
public readonly UpgradeConfig Config;
public int CurrentLevel;
public UpgradeRuntime(UpgradeConfig config)
{
Config = config;
CurrentLevel = 0;
}
public int NextCost()
{
var c = Config.BaseCost * Mathf.Pow(Config.CostScale, CurrentLevel);
return Mathf.CeilToInt(c);
}
public bool IsMaxed()
{
return CurrentLevel >= Config.MaxLevel;
}
}
经济服务
public sealed class EconomyService
{
private readonly EconomyState _state;
private readonly EventBus _eventBus;
public EconomyService(EconomyState state, EventBus eventBus)
{
_state = state;
_eventBus = eventBus;
}
public int Get(CurrencyType type)
{
if (type == CurrencyType.RunGold) return _state.RunGold;
return _state.MetaCrystal;
}
public void Add(CurrencyType type, int value, string reason)
{
if (value <= 0) return;
if (type == CurrencyType.RunGold)
{
_state.RunGold += value;
}
else
{
_state.MetaCrystal += value;
}
_eventBus.Publish("CurrencyChanged", new CurrencyPayload(type, Get(type), reason));
}
public bool Spend(CurrencyType type, int value, string reason)
{
if (value <= 0) return true;
var current = Get(type);
if (current < value)
{
return false;
}
if (type == CurrencyType.RunGold)
{
_state.RunGold -= value;
}
else
{
_state.MetaCrystal -= value;
}
_eventBus.Publish("CurrencyChanged", new CurrencyPayload(type, Get(type), reason));
return true;
}
public void ResetRunCurrency()
{
_state.RunGold = 0;
_eventBus.Publish("CurrencyChanged", new CurrencyPayload(CurrencyType.RunGold, 0, "run_reset"));
}
}
public struct CurrencyPayload
{
public CurrencyType Type;
public int Value;
public string Reason;
public CurrencyPayload(CurrencyType type, int value, string reason)
{
Type = type;
Value = value;
Reason = reason;
}
}
掉落与结算
敌人掉落
public sealed class LootRewardSystem
{
private readonly EconomyService _economy;
public LootRewardSystem(EconomyService economy)
{
_economy = economy;
}
public void OnEnemyDead(EnemyRuntime enemy)
{
var reward = enemy.IsElite ? 25 : 8;
_economy.Add(CurrencyType.RunGold, reward, "enemy_kill");
}
}
关卡结算转换
public sealed class StageRewardCalculator
{
public int CalcMetaCrystal(StageResult result)
{
var baseReward = result.Win ? 20 : 6;
var killBonus = result.KillCount / 5;
var timeBonus = result.Win ? Mathf.Max(0, 10 - result.MinuteUsed) : 0;
return baseReward + killBonus + timeBonus;
}
}
局内升级选择
升级池选择器
public sealed class UpgradeDraftService
{
private readonly List<UpgradeRuntime> _runUpgrades;
private readonly System.Random _rng;
public UpgradeDraftService(List<UpgradeRuntime> runUpgrades, int seed)
{
_runUpgrades = runUpgrades;
_rng = new System.Random(seed);
}
public List<UpgradeRuntime> DrawOptions(int count)
{
var candidates = ListPool<UpgradeRuntime>.Get();
for (var i = 0; i < _runUpgrades.Count; i++)
{
if (!_runUpgrades[i].IsMaxed())
{
candidates.Add(_runUpgrades[i]);
}
}
var result = new List<UpgradeRuntime>(count);
while (result.Count < count && candidates.Count > 0)
{
var idx = WeightedPick(candidates);
result.Add(candidates[idx]);
candidates.RemoveAt(idx);
}
ListPool<UpgradeRuntime>.Release(candidates);
return result;
}
private int WeightedPick(List<UpgradeRuntime> list)
{
var sum = 0;
for (var i = 0; i < list.Count; i++) sum += Mathf.Max(1, list[i].Config.Weight);
var v = _rng.Next(0, sum);
var acc = 0;
for (var i = 0; i < list.Count; i++)
{
acc += Mathf.Max(1, list[i].Config.Weight);
if (v < acc) return i;
}
return list.Count - 1;
}
}
升级购买
public sealed class UpgradePurchaseService
{
private readonly EconomyService _economy;
private readonly EventBus _eventBus;
public UpgradePurchaseService(EconomyService economy, EventBus eventBus)
{
_economy = economy;
_eventBus = eventBus;
}
public bool TryBuyRunUpgrade(UpgradeRuntime u)
{
if (u == null || u.IsMaxed()) return false;
var cost = u.NextCost();
if (!_economy.Spend(CurrencyType.RunGold, cost, "run_upgrade_buy"))
{
return false;
}
u.CurrentLevel++;
_eventBus.Publish("RunUpgradeBought", u.Config.UpgradeId);
return true;
}
}
升级效果应用
建议把效果映射统一放在一个地方:
public sealed class UpgradeEffectApplier
{
public void Apply(Actor player, UpgradeRuntime u)
{
var v = u.Config.EffectValue;
if (u.Config.EffectKey == "atk_pct") player.AttackBonus += v;
else if (u.Config.EffectKey == "crit_pct") player.CritRate += v;
else if (u.Config.EffectKey == "move_pct") player.MoveSpeedMultiplier += v;
else if (u.Config.EffectKey == "cdr_pct") player.CooldownReduce += v;
}
}
局外永久升级
使用 MetaCrystal 购买:
- 永久攻击成长
- 初始技能槽位
- 初始金币加成
购买成功后写入存档,下一局生效。
防通胀策略
- 升级价格按指数增长(
CostScale)。 - 同一局内高等级收益递减。
- 每局 Meta 奖励设上限,防止速刷爆表。
- 定期观察“每局平均收入/支出比”。
UI 流程
- HUD 显示
RunGold。 - 升级弹窗显示 3 选 1 与价格。
- 结算页显示
MetaCrystal收益明细。 - 主菜单养成页展示永久升级树。
与前面系统联动
- 波次系统:每清一波触发一次升级抽选。
- 存档系统:保存 Meta 货币和永久升级等级。
- 回放系统:记录每次升级选择,支撑平衡分析。
- 流程状态机:升级弹窗期间切到可控暂停态。
WebGL 注意点
- 升级弹窗资源应预热,避免打开卡顿。
- 价格与收益计算尽量纯 CPU 轻量。
- 货币变更事件节流,避免 UI 高频刷新。
验收清单
- 击杀敌人可稳定获得局内金币。
- 可购买局内升级且效果即时生效。
- 结算可转换局外货币并正确入档。
- 重开后局内货币清零,局外成长保留。
常见坑
坑 1:局内与局外资源混用
会导致数值体系失控。两类资源必须隔离。
坑 2:升级效果直接修改基础配置资产
会污染全局。应改运行时实例数据。
坑 3:结算奖励直接写死常量
后续调平衡困难。应集中到计算器与配置。
本月作业
实现“每波 3 选 1 升级”:
- 每清一波自动弹出升级选择。
- 选中后立刻生效并记录到回放。
- 关卡结束展示本局升级路径摘要。
下一章进入 Unity WebGL 小游戏实战 04:关卡难度曲线与导演系统(节奏控制与随机性约束)。