成就与统计
追踪玩家统计数据,在达到阈值时自动解锁成就,将进度持久化到磁盘,并发布解锁事件。
在数据表中声明成就;框架负责追踪、解锁与保存。
| 接口 | IAchievementService |
| 关闭开关 | NullAchievementService |
| 程序集 | CommonGameSystem.Core |
| 启动 | 启动时自动注册;定义表由你加载 |
功能概览
成就与统计用于追踪玩家进度并给予认可。两者都在 ScriptableObject 表中声明:**统计(stat)**是一个具名计数器("enemies_defeated"、"playtime_seconds"),**成就(achievement)**则是一个 id 加显示文本,可选地绑定到某个统计并设定目标阈值。服务会记录统计变化、在统计变动的那一刻检查解锁条件、通过保存/加载服务把一切写入磁盘,并在成就解锁时在事件总线上触发 AchievementUnlocked 事件。
这样的分工是有意为之:框架负责追踪、解锁和持久化;弹窗或提示由你的 UI 负责。该模块完全无界面(headless)——不包含任何渲染代码,因此适配任何 UI 方案。
快速上手
加载一次表,然后在游戏逻辑代码中记录统计:
var achievements = ServiceLocator.Resolve<IAchievementService>();
achievements.AddToStat("enemies_defeated", 1);
订阅解锁事件来显示通知:
ServiceLocator.Resolve<IEventBus>().Subscribe<AchievementUnlocked>(evt =>
{
Debug.Log($"Unlocked: {evt.AchievementId}");
});
API 参考
表加载
void LoadTables(IReadOnlyList<AchievementTableAsset> achievements, IReadOnlyList<StatTableAsset> stats)— 必须最先调用。构建定义与查找索引。再次调用会替换全部内容并重置内存中的状态。
统计操作
long AddToStat(string statId, long delta)— 对统计做加法(或减法)。溢出安全,默认下限为 0。立即重新检查绑定到该统计的成就。返回新值。long SetStat(string statId, long value)— 把统计设为绝对值,应用同样的下限与阈值检查。返回实际存储的值。long GetStat(string statId)— 读取当前值。未知 id 返回0L。零分配。
解锁
void Unlock(string achievementId)— 直接解锁未绑定统计的成就(剧情里程碑、隐藏发现)。重复调用是安全的;已解锁的成就不会触发第二次事件。bool IsUnlocked(string achievementId)— 成就是否已解锁。未知 id 返回false。bool TryGetProgress(string achievementId, out AchievementProgress progress)— 填充一份进度快照(当前值、目标、完成比例、状态)。对任何已知成就返回true,无论是否绑定统计;仅对未知 id 返回false。
查询
void GetVisible(List<AchievementDefinition> results)— 把可见成就(未隐藏的,或虽隐藏但已解锁的)填入你的列表。会先清空列表。零分配——列表由你持有。void GetAll(List<AchievementDefinition> results)— 把所有定义填入你的列表,包括仍锁定的隐藏成就。会先清空列表。零分配。bool ContainsAchievement(string id)— 某个成就 id 是否已定义。bool ContainsStat(string id)— 某个统计 id 是否已定义。
持久化
SaveResult Save()— 通过保存/加载服务把内存状态写入磁盘。内容无变化时跳过写入。返回保存状态。void Load()— 从名为"achievements"的存档槽读取已保存的状态。存档缺失或损坏时静默加载为空状态。已解锁的成就不会重新触发其事件。void ResetAll()— 把所有统计清回初始值并锁定全部成就,仅作用于内存。之后调用Save()才会把重置持久化。
示例:完整设置
using System.Collections.Generic;
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)] // Run after the framework has started.
public class AchievementManager : MonoBehaviour
{
[SerializeField] private AchievementTableAsset[] _achievementTables; // assign in the Inspector
[SerializeField] private StatTableAsset[] _statTables; // assign in the Inspector
private IAchievementService _achievements;
private IEventBus _bus;
private void Start()
{
_achievements = ServiceLocator.Resolve<IAchievementService>();
_bus = ServiceLocator.Resolve<IEventBus>();
// Load the table definitions first, then the saved state from disk.
_achievements.LoadTables(_achievementTables, _statTables);
_achievements.Load();
// Subscribe to unlock events.
_bus.Subscribe<AchievementUnlocked>(OnAchievementUnlocked);
}
public void OnEnemyDefeated()
{
long newCount = _achievements.AddToStat("enemies_defeated", 1);
Debug.Log($"Enemies defeated: {newCount}");
}
private void OnAchievementUnlocked(AchievementUnlocked evt)
{
Debug.Log($"Achievement unlocked: {evt.AchievementId}");
// Show your toast or popup here.
}
}
关闭该服务
ServiceLocator.Replace<IAchievementService>(new NullAchievementService());
这会禁用整个模块:表加载不做任何事,所有修改都是空操作,GetStat 返回 0L,IsUnlocked 返回 false,不写磁盘,也不发布事件。可用于不带成就的测试,或发行最小化构建。
常见陷阱
LoadTables是必需的。 在任何统计或成就操作之前调用它。在空服务上调用AddToStat会记录一条警告,但不会崩溃。- 解锁默认立即写盘。
FlushOnUnlock选项默认为true,因此每次解锁都会通过保存/加载服务执行一次同步保存。如果可能出现大量成就集中解锁,请在AchievementOptions中设置FlushOnUnlock = false,并在检查点自行调用Save()。 - 阈值检查即时发生。
AddToStat或SetStat一执行,绑定的成就就会被重新评估。不存在延后的“稍后检查进度”——条件一满足事件就触发。 - 解锁是单向的。 成就一旦解锁就保持解锁。即使统计随后下降(开启
AllowNegativeDeltas选项时),成就也不会重新锁定。 - 进度事件需要显式开启。 在表中的成就定义上设置
PublishProgress = true才会启用AchievementProgressChanged事件。它默认关闭,以避免playtime_seconds这类高频统计刷爆事件。 - 自定义类型在 IL2CPP 下需要
link.xml。 模块自身的公开类型(如AchievementSaveData)已被保留。如果你在玩家构建中用JsonUtility序列化自己的类型,请在项目的link.xml中添加条目,防止 IL2CPP 构建裁剪它们。