成就状态需绑定用户唯一标识存储,服务端判定解锁条件,前端仅上报行为;响应式展示应统一状态管理并监听存储变更;聚合成就须原子更新标志位并带版本控制。

成就状态怎么存才不会丢、不冲突
本地存成就最常见坑是直接用 localStorage 存一个扁平对象,结果多人账号切换或跨设备时数据错乱。真正靠谱的做法是把成就状态和用户唯一标识绑定,哪怕只是单机游戏,也建议加一层命名空间前缀。
- 别用
localStorage.setItem('achievements', JSON.stringify(data))这种裸存法 - 改成
localStorage.setItem(`ach_${userId}`, JSON.stringify(data)),userId可以是登录 token 截取、设备 ID 或本地生成的 UUID - 如果用 IndexedDB,表名别叫
achievements,叫achievements_v2_user_123更稳妥(版本号+用户标识) - Web 游戏若支持离线,记得在同步成功后才更新本地标记,否则网络失败会导致“已解锁”变回“未解锁”
怎么判断一个成就该不该解锁
不是所有触发条件都适合实时轮询或监听事件。比如“连续登录 7 天”,靠前端计时器根本不可靠;而“击杀 100 只哥布林”这种,必须确保只算一次,且不能被重复提交。
- 优先用服务端判定:前端只上报行为(如
reportEvent('kill_goblin')),由后端检查累计值并返回是否解锁 - 客户端做轻量校验可以,但别做最终决定——例如前端可缓存当前击杀数,但解锁逻辑必须和服务端一致
- 时间类成就(如“凌晨 2 点通关”)务必用服务端时间,
Date.now()在用户改系统时间时会失效 - 避免用
localStorage计数器 + 自增,容易因页面刷新、多标签页导致漏计或重复计
React/Vue 里怎么响应式显示成就进度
成就进度条不是简单绑定个数字就行。用户可能在多个组件里查看同一成就,还可能中途跳转、重进页面,状态必须可恢复且不重复触发通知。
- 别在组件内部用
useState或ref存进度值——它和持久化存储不同步 - 统一用状态管理(如
zustand的useAchievementStore)或自定义 Hook(如useAchievement('kill_goblin'))封装读写逻辑 - 进度更新时,用
useEffect监听存储变更(storage事件)或 WebSocket 推送,而不是 setInterval 轮询 - 解锁提示弹窗这类副作用,一定要加防抖,避免同一成就多次触发(比如用户快速点击 10 次提交按钮)
“全成就达成”这种聚合型成就怎么安全检测
它看起来只是个布尔值,但背后涉及所有子成就的状态一致性。常见错误是每次打开成就页就遍历全部、重新计算,既慢又容易漏掉未加载的成就项。
- 不要每次渲染都调用
Object.values(achievements).every(a => a.unlocked) - 在每次子成就解锁时,单独维护一个
all_unlocked标志位,并原子更新(比如用 Redis 的INCR+ 总数比对) - 前端缓存这个标志时,必须带版本号或时间戳,防止旧缓存覆盖新状态(例如用户从 v1.2 升级到 v1.3,新增了成就,但本地仍显示“全成就达成”)
- 如果成就列表动态加载(如 DLC 追加),聚合检测必须等全部元数据就绪后再执行,不能只看当前已知的那几项
最麻烦的永远不是“怎么点亮图标”,而是“怎么确认点亮那一刻,整个系统里没有其他地方还在用过期状态”。尤其当成就数据分散在本地存储、内存、服务端、甚至第三方分析 SDK 里时,一致性得靠设计约束,不是靠代码补丁。











