account-achievement-system
在无服务器架构的 Web 端游戏(通常基于 HTML5 / TypeScript / React / Vue / Pixi.js 等前端技术栈)中,成就系统的设计必须在模块化解耦、存储安全性、性能优化三个方面进行深度考量。
以下是针对《同福客栈》的本地化账号成就系统设计与技术实现方案。
《同福客栈》账号成就系统设计与技术实现方案
一、 系统架构设计
为了保证游戏在挂机和高频数值变动时不会卡顿,且成就判定逻辑不侵入核心经营代码,系统采用 “订阅/发布事件总线(Event Bus)” 架构。
┌────────────────────────────────────────────────────────────────────────┐ │ 1. 游戏核心业务逻辑 (Business Logic) │ ├────────────────────────────────────────────────────────────────────────┤ │ ┌─────────────────────────┐ 派发事件 ┌───────────────────────────┐ │ │ │ 收银/防盗/清洁/员工交互 │ ─────────► │ 事件总线 (Event Bus) │ │ │ └─────────────────────────┘ └─────────────┬─────────────┘ │ └──────────────────────────────────────────────────────│─────────────────┘ │ ▼ 触发监听器 (Trigger) ┌────────────────────────────────────────────────────────────────────────┐ │ 2. 成就系统管理器 (AchievementManager) │ ├────────────────────────────────────────────────────────────────────────┤ │ ┌─────────────────────────┐ ┌──────────────────────────┐ │ │ │ 成就定义静态配置 │ ◄─────────► │ 全局计数器处理器 │ │ │ │ (AchievementConfigs) │ │ (Global Counters) │ │ │ └─────────────────────────┘ └───────────┬──────────────┘ │ │ │ │ │ ▼ 满足条件 │ │ ┌──────────────────────────┐ │ │ │ 解锁并写入全局存档 │ │ │ │ (GlobalProfile DTO) │ │ │ └──────────────────────────┘ │ └────────────────────────────────────────────────────────────────────────┘
二、 成就系统数据结构定义 (TypeScript)
2.1 基础数据模型 (DTO)
// 1. 全局账号存档数据结构 export interface GlobalProfile { legacyPoints: number; // 全局累积的江湖阅历点 unlockedAchievements: string[]; // 已解锁成就ID列表 (ACH_XXX) globalCounters: Record<string, number>; // 全局跨周目累积计数器 (例如 {"silver_gained": 5000}) purchasedLegacies: string[]; // 已购买的永久特权 lastSavedAt: number; // 上次保存时间戳 signature: string; // 存档防篡改数字签名 }
// 2. 单条成就静态配置接口 export interface AchievementConfig { id: string; name: string; desc: string; category: 'CUMULATIVE' | 'THRESHOLD' | 'SPECIAL'; // 累积型、阈值型、特殊条件型 triggerEvent: string; // 绑定的事件总线事件名称 conditionParam: string; // 判定所需的数值字段 targetValue: number; // 判定目标值 rewardLegacyPoints: number; // 奖励的江湖阅历 }
2.2 成就配置静态数据集 (JSON 示例)
[ { "id": "ACH_MON_100W", "name": "富甲关中", "desc": "累计赚取 1,000,000 两银子(跨周目计算)", "category": "CUMULATIVE", "triggerEvent": "EVENT_SILVER_CHANGE", "conditionParam": "silver_gained", "targetValue": 1000000, "rewardLegacyPoints": 300 }, { "id": "ACH_LBA_DOU", "name": "盗圣在此谁敢造次", "desc": "单局内使用白展堂点住 100 次小偷", "category": "CUMULATIVE", "triggerEvent": "EVENT_THIEF_CAUGHT", "conditionParam": "thieves_caught_single_run", "targetValue": 100, "rewardLegacyPoints": 150 } ]
三、 技术实现方案
3.1 核心事件总线 (Event Bus) 实现
通过 Pub/Sub 模式解耦经营系统与成就系统。
type EventCallback = (data: any) => void;
export class GameEventBus { private static instance: GameEventBus; private listeners: Map<string, EventCallback[]> = new Map();
public static getInstance(): GameEventBus { if (!this.instance) this.instance = new GameEventBus(); return this.instance; }
// 监听事件 public on(event: string, callback: EventCallback) { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event)!.push(callback); }
// 派发事件 public emit(event: string, data: any) { const callbacks = this.listeners.get(event); if (callbacks) { callbacks.forEach(cb => cb(data)); } } }
3.2 成就管理器 (Achievement Manager) 核心逻辑
管理器负责监听事件、更新计数器、评估是否解锁,并将状态同步至全局存档中。
import { GameEventBus } from './GameEventBus'; import { GlobalProfile, AchievementConfig } from './Interfaces'; import { ProfileStorage } from './ProfileStorage';
export class AchievementManager { private static instance: AchievementManager; private profile: GlobalProfile; private configs: AchievementConfig[] = [];
private constructor() { this.profile = ProfileStorage.loadProfile(); this.initListeners(); }
public static getInstance(): AchievementManager { if (!this.instance) this.instance = new AchievementManager(); return this.instance; }
// 加载成就配置 public loadConfigs(configs: AchievementConfig[]) { this.configs = configs; this.initListeners(); }
// 初始化事件监听 private initListeners() { // 动态绑定所有在 JSON 配置文件中声明了 triggerEvent 的事件 this.configs.forEach(config => { GameEventBus.getInstance().on(config.triggerEvent, (eventData) => { this.evaluateAchievement(config, eventData); }); }); }
// 评估成就达成状态 private evaluateAchievement(config: AchievementConfig, eventData: any) { // 若已解锁,直接返回 if (this.profile.unlockedAchievements.includes(config.id)) return;
let currentValue = 0;
if (config.category === 'CUMULATIVE') {
// 累积型成就:将事件带来的增量值累加到全局计数器
const delta = eventData.delta || 0;
const currentCount = this.profile.globalCounters[config.conditionParam] || 0;
currentValue = currentCount + delta;
this.profile.globalCounters[config.conditionParam] = currentValue;
} else if (config.category === 'THRESHOLD') {
// 阈值型成就:直接读取当前环境的即时状态(如清洁度等)
currentValue = eventData.value || 0;
}
// 判定是否达成
if (currentValue >= config.targetValue) {
this.unlockAchievement(config);
} else {
// 未解锁但数值更新,亦需保存计数器数据
ProfileStorage.saveProfile(this.profile);
}
}
// 解锁成就核心逻辑 private unlockAchievement(config: AchievementConfig) { if (this.profile.unlockedAchievements.includes(config.id)) return;
this.profile.unlockedAchievements.push(config.id);
this.profile.legacyPoints += config.rewardLegacyPoints;
// 保存进度
ProfileStorage.saveProfile(this.profile);
// 向 UI 系统发布解锁成功事件(用于播放跑马灯动画)
GameEventBus.getInstance().emit('UI_ACHIEVEMENT_UNLOCKED', {
id: config.id,
name: config.name,
desc: config.desc,
reward: config.rewardLegacyPoints
});
} }
四、 本地化安全与签名机制 (Anti-Cheat Scheme)
由于是纯前端运行,玩家可通过 F12 控制台或直接修改浏览器的 LocalStorage 绕过游戏机制直接解锁全部成就。为了提高作弊门槛并建立安全的“存档导出与备份”机制,需要引入基于密钥哈希的校验和机制 (HMAC / Signature) [1, 2]。
存档 JSON 字符串 ──► 拼接待盐 (Salt) ──► MD5/SHA256 哈希 ──► 产生 Signature │ ▼ 最终写入 LocalStorage: { "legacyPoints": 50, ..., "signature": "e59a6..." }
4.1 存档保存与防篡改验证类
由于此代码在客户端运行,我们将密钥(Salt)进行简单混淆防止被直接搜索字符串:
import { GlobalProfile } from './Interfaces';
// 简单混淆密钥,增大搜索难度(可根据需要进行替换或使用其他加密算法) const KEY_PARTS = ['T', 'o', 'n', 'g', 'F', 'u', '2', '0', '2', '6', '_', 'S', 'e', 'c', 'r', 'e', 't']; const SALT = KEY_PARTS.join('');
export class ProfileStorage { private static readonly LOCAL_STORAGE_KEY = 'tongfu_profile_v1';
// 1. 生成防篡改数字签名 private static generateSignature(profile: Omit<GlobalProfile, 'signature'>): string { const rawData = JSON.stringify({ legacyPoints: profile.legacyPoints, unlockedAchievements: profile.unlockedAchievements, globalCounters: profile.globalCounters, purchasedLegacies: profile.purchasedLegacies, lastSavedAt: profile.lastSavedAt });
// 使用简单的哈希方法(此处可用标准的 Crypto-JS md5/sha256 库代替)
return this.hashString(rawData + SALT);
}
// 2. 加载与验证 public loadProfile(): GlobalProfile { const rawData = localStorage.getItem(this.LOCAL_STORAGE_KEY); if (!rawData) { return this.initNewProfile(); }
try {
const profile: GlobalProfile = JSON.parse(rawData);
const { signature, ...dataToVerify } = profile;
// 校验签名
const expectedSignature = this.generateSignature(dataToVerify);
if (signature !== expectedSignature) {
console.warn("警告:检测到存档异常篡改!已重置为安全备份或最新合法数值。");
return this.initNewProfile(); // 判定篡改,执行降级/重置策略
}
return profile;
} catch (e) {
return this.initNewProfile();
}
}
// 3. 保存 public saveProfile(profile: Omit<GlobalProfile, 'signature'>): void { profile.lastSavedAt = Date.now(); const signature = this.generateSignature(profile); const fullProfile: GlobalProfile = { ...profile, signature };
localStorage.setItem(this.LOCAL_STORAGE_KEY, JSON.stringify(fullProfile));
}
// 4. 导出为导出字符串(用于备份及多设备互导,采用 Base64 编码增加阅读障碍) public exportProfileString(profile: GlobalProfile): string { const jsonString = JSON.stringify(profile); return btoa(encodeURIComponent(jsonString)); // Base64 混淆 }
// 5. 导入验证 public importProfileString(base64Str: string): boolean { try { const decodedJson = decodeURIComponent(atob(base64Str)); const profile: GlobalProfile = JSON.parse(decodedJson); const { signature, ...dataToVerify } = profile;
const expectedSignature = this.generateSignature(dataToVerify);
if (signature === expectedSignature) {
localStorage.setItem(this.LOCAL_STORAGE_KEY, decodedJson);
return true; // 导入成功
}
} catch (e) {
return false; // 导入失败,数据损坏
}
return false;
}
private static initNewProfile(): GlobalProfile { return { legacyPoints: 0, unlockedAchievements: [], globalCounters: {}, purchasedLegacies: [], lastSavedAt: Date.now(), signature: '' }; }
// JS自带的轻量级非标准 Hash 算法实现(免除额外引入大型外部依赖) private static hashString(str: string): string { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash |= 0; // 转换为 32位 整数 } return hash.toString(16); } }
五、 开发与对接实践要点(如何集成到游戏流程中)
5.1 经营事件发布示例
当玩家在游戏中赚到银两时,业务代码仅需一行事件派发即可驱动整个成就系统:
// 业务层代码:收银结算逻辑 public collectPayment(amount: number) { this.currentSilver += amount;
// 派发事件:通知成就系统( delta 增量用于累积型成就 ) GameEventBus.getInstance().emit('EVENT_SILVER_CHANGE', { delta: amount, currentTotal: this.currentSilver }); }
5.2 UI 层“成就解锁弹窗”对接
UI(例如 React 或 Vue 组件)只需在挂载时订阅通知事件即可:
// React 组件生命周期示例
useEffect(() => {
const handleUnlock = (data: { name: string; desc: string; reward: number }) => {
// 弹出炫酷的“成就达成”横幅
MyToast.show({
title: '✨ 功成名就',
message: 恭喜达成成就:【${data.name}】!获得江湖阅历 ${data.reward} 点。
});
};
GameEventBus.getInstance().on('UI_ACHIEVEMENT_UNLOCKED', handleUnlock); }, []);
5.3 离线与挂机结算处理
对于放置挂机期间的长跨度结算,建议采用“聚合派发”。
- 在计算出离线 8 小时总收益(如:120,000 两银子)后,在玩家上线时,一次性调用: GameEventBus.getInstance().emit('EVENT_SILVER_CHANGE', { delta: 120000 })。
- 系统会依次触发关联的累加逻辑,避免了挂机期间多频次 Tick 计算带来的 JS 堆栈开销,提高性能。