【OpenHarmony/HarmonyOS】游戏成长系统实战:晶石收集、商城升级、数值结算与持久化
【OpenHarmony/HarmonyOS】游戏成长系统实战:晶石收集、商城升级、数值结算与持久化
一个完整游戏不仅有“当局战斗”,还需要把当局奖励转化为长期目标。本文讲解迷宫坦克项目如何用晶石、升级商城和 Preferences 建立最小可用的局外成长循环。💎
一、先画出成长闭环
项目中的循环可以概括为:
flowchart LR
A[进入关卡] --> B[收集晶石]
B --> C[胜利或失败结算]
C --> D[写入 UpgradeManager]
D --> E[商城购买升级]
E --> F[下一局应用倍率]
F --> A
当局中的 coinsCollected 是临时数据,只有在结算节点才转入持久货币。这样重开、失败惩罚和模式规则都可以明确控制。
二、晶石实体的数据和动画
晶石只保存位置、半径、价值和收集状态:
export class Crystal {
position: Vector2;
radius: number;
value: number;
isCollected: boolean = false;
public floatOffset: number = 0;
update(deltaTime: number) {
this.floatTime += deltaTime;
this.floatOffset = Math.sin(this.floatTime * 5) * 3;
}
}
正弦函数让晶石上下浮动。绘制时用四条线构成菱形,并配合描边和发光。大量晶石由引擎批量加入同一 Canvas Path,而不是每个对象独立 save/restore。
三、刷新密度随模式变化
基础数量与波次相关,再按难度和模式放大:
let count = 5 + Math.floor(this.currentWave * 2);
if (this.difficulty === 'nightmare') {
count = Math.floor(count * 2.0);
} else if (this.difficulty === 'normal') {
count = Math.floor(count * 1.5);
}
if (this.gameMode === 'puzzle') {
count = Math.floor(count * 6.0);
} else if (this.gameMode === 'time_attack') {
count = Math.floor(count * 10.0);
}
解谜和限时模式更强调路线收集,因此密度更高;极限难度风险更高,也给更多奖励机会。奖励与风险共同变化,比只增加敌人更有驱动力。
四、收集检测与当局统计
玩家和晶石使用圆形距离判断:
const dx = player.position.x - crystal.position.x;
const dy = player.position.y - crystal.position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < player.radius + crystal.radius) {
crystal.collect();
this.gameStats.coinsCollected += crystal.value;
AudioManager.getInstance().playSound('coin');
this.swapRemoveCrystal(index);
}
更高频的实现可比较平方距离,省去 sqrt。收集后立即从活动数组交换删除,避免后续继续更新和绘制。
音效名称必须与预加载表一致。当前工程播放 coin,而音频管理器加载列表中没有对应资源,这是联调时应修正的契约问题:要么增加 coin.wav,要么映射到已有 powerup 音效。
五、不同模式的结算规则
PvE 或限时模式中玩家失败,也会把已收集晶石加入长期账户:
UpgradeManager.getInstance()
.addCoins(this.gameStats.coinsCollected);
解谜模式进入错误出口时只保留约四分之一:
this.gameStats.coinsCollected = Math.round(
this.gameStats.coinsCollected * 0.25
);
正确出口则保留全部,并按晶石数量增加得分。结算规则集中在模式结束位置,避免收集一枚就直接写长期货币,导致退出关卡无法实施惩罚。
六、UpgradeManager 的持久状态
export interface UpgradeState {
coins: number;
speedLevel: number;
fireRateLevel: number;
shieldLevel: number;
}
状态作为一个 JSON 整体保存:
await this.pref.put(
'upgrade_state',
JSON.stringify(this.state)
);
await this.pref.flush();
整体保存简单且保持字段一致,但多个异步修改仍应串行化,避免读改写竞争。正式游戏还应加入 schemaVersion 和非法数值校验,防止负金币或等级超过上限。
七、购买升级要由数据层校验
商城页面可以显示价格,但最终扣费不能只相信 UI 传入值。Manager 重新计算实际价格:
public async purchaseUpgrade(
type: 'speed' | 'fireRate' | 'shield',
ignoredCost: number
): Promise<boolean> {
if (!this.canAffordUpgrade(type)) return false;
const level = this.getUpgradeLevel(type);
const realCost = this.getUpgradeCost(level);
this.state.coins -= realCost;
if (type === 'speed') this.state.speedLevel++;
else if (type === 'fireRate') this.state.fireRateLevel++;
else this.state.shieldLevel++;
await this.saveState();
return true;
}
即使这是本地游戏,也应该让业务规则只有一个可信来源。以后迁移到云端时,同样原则会变成服务端权威校验。
八、价格契约必须统一 ⚠️
项目页面定义:速度基础价 50、射速基础价 50、护盾基础价 100;但 Manager 的 getUpgradeCost() 统一按 50 * (level + 1) 计算。由于 Manager 忽略页面传入价格,护盾实际扣费可能与 UI 展示不一致。
正确做法是把配置集中在数据层:
const UPGRADE_CONFIG = {
speed: { baseCost: 50, maxLevel: 5, step: 0.05 },
fireRate: { baseCost: 50, maxLevel: 5, step: 0.05 },
shield: { baseCost: 100, maxLevel: 5, step: 1 }
};
页面只调用 getUpgradeQuote(type) 获取等级、价格和效果,购买时只传 type。这样展示与扣费不可能分叉。
九、把升级应用到下一局
玩家坦克创建后异步读取升级状态:
UpgradeManager.getInstance().getUpgradeState().then(state => {
if (!this.playerTank) return;
this.playerTank.upgradeSpeedMultiplier =
1.0 + state.speedLevel * 0.05;
this.playerTank.upgradeFireRateMultiplier =
1.0 + state.fireRateLevel * 0.05;
if (state.shieldLevel >= 1) {
this.playerTank.hasShield = true;
}
});
每级 5% 的速度和射速成长比较平滑。异步回调中再次检查 playerTank,防止玩家已经退出或引擎重置。
当前工程定义了 upgradeFireRateMultiplier,但实际开火限制主要由场上子弹数控制,尚未形成明确的冷却时间使用点。因此文章中应描述为“已读取倍率、仍需接入开火冷却”,而不能声称射速升级已完整生效。
十、商城页面的响应式更新
页面进入时读取状态,购买成功后刷新:
refreshData() {
UpgradeManager.getInstance().getUpgradeState().then(state => {
this.coins = state.coins;
this.speedLevel = state.speedLevel;
this.fireRateLevel = state.fireRateLevel;
this.shieldLevel = state.shieldLevel;
});
}
按钮根据余额决定颜色和是否可用,余额不足时显示 Toast。还应增加满级状态:隐藏价格或显示“已满级”,而不是继续给出不可购买按钮。
十一、数值设计不能只看公式
成长系统应建立可计算的经济表:
| 项目 | 需要观察的指标 |
|---|---|
| 单局产出 | 平均、P50、P90 晶石数 |
| 失败保留 | 玩家是否愿意冒险继续探索 |
| 升级成本 | 首次升级所需局数 |
| 效果强度 | 每级对胜率和时长的影响 |
| 满级周期 | 核心玩家多久完成成长 |
| 模式差异 | 是否存在刷取效率压倒其他模式 |
例如限时模式的晶石密度是普通模式的多倍,如果没有独立奖励系数,很可能成为唯一高效刷取模式。技术实现正确不代表经济平衡合理。
十二、防重复结算
结束条件可能连续多帧成立,或者异步计时器重复回调。项目使用 isGameOverSequenceStarted 防止多次加币:
if (!this.isGameOverSequenceStarted) {
this.isGameOverSequenceStarted = true;
UpgradeManager.getInstance().addCoins(coinsCollected);
// 进入结算
}
进一步可为每一局生成 sessionId,已结算 ID 写入短期记录。即使页面重复回调,也能做到幂等。
十三、测试清单 ✅
- 收集晶石后 HUD 是否立即更新;
- 退出、失败、正确出口、错误出口分别结算多少;
- 同一局是否可能重复加币;
- 页面展示价格是否等于真实扣费;
- 余额刚好等于价格时能否购买;
- 满级后是否禁止升级;
- 应用重启后金币和等级是否恢复;
- JSON 缺字段或等级越界时如何修复;
- 速度升级是否在不同屏幕缩放下保持比例;
- 射速倍率是否真正接入冷却逻辑。
十四、总结 ✨
一个最小但完整的成长系统需要:
- 当局资源与长期货币分离;
- 模式结束时统一结算;
- Manager 作为价格和扣费的权威来源;
- 状态持久化并支持版本迁移;
- 下一局读取并应用升级;
- 防重复结算和并发写入;
- UI 价格、真实扣费和效果配置来自同一份数据;
- 用实际产出和完成周期验证数值平衡。
成长循环让一次次独立关卡连接成长期体验,但越是看似简单的“加金币、点升级”,越需要严谨的一致性和结算边界。💎
推荐标签: HarmonyOS OpenHarmony ArkTS 游戏数值 商城系统 Preferences

更多推荐



所有评论(0)