№ · 设计 BACKLOG CH.06.22
主页 · 📋 设计 Backlog · account-achievement-system

account-achievement-system

📅 2026-06-22 · 📦 15.4 KB · 📋 设计 Backlog · 📝 MD 文档 🏷️ 员工🏷️ 设计

在无服务器架构的 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 堆栈开销,提高性能。