You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity游戏有限统计数据的最优存储方案技术问询

游戏统计系统序列化与键方案优化建议

问题背景

我正在开发一款Unity游戏,需要展示由「本地化统计名称」+「多类型统计值(int、float、string)」组成的列表/子列表。当前采用枚举作为键的方案(代码如下),但当统计项达到50-70个时会极度冗余。核心需求是:统计数据可序列化到磁盘,且能通过可序列化键便捷访问。现有枚举方案存在明显问题:序列化UI依赖元素顺序,迭代开发中增减统计项极易引发错误。

现有实现代码

[Serializable]
public enum GameStatisticsEnum
{
    TimesGameOpened,
    TimePlayed,
    TimeBattling,
    VersusBattleCount,
    VersusRoundCount,
    SpellCastCount,
    DeathCount,
}

[Serializable]
public class GameStatistics
{
    [SerializeField] public int TimesGameOpened;
    [SerializeField] public float TimePlayed;
    [SerializeField] public float TimeBattling;
    [SerializeField] public int VersusBattleCount;
    [SerializeField] public int VersusRoundCount;
    [SerializeField] public int SpellCastCount;
    [SerializeField] public int DeathCount;
    [SerializeField] public ZoneStats[] ZonesStats;
    [SerializeField] public CharacterStats[] CharactersStats;

    public string GetStatLocalizationID(GameStatisticsEnum stat)
    {
        return "GameStat_"+stat.ToString();
    }

    public float GetStatValue(GameStatisticsEnum stat)
    {
        switch (stat) {
            case GameStatisticsEnum.TimesGameOpened:
                return TimesGameOpened;
            case GameStatisticsEnum.TimePlayed:
                return TimePlayed;
            case GameStatisticsEnum.TimeBattling:
                return TimeBattling;
            case GameStatisticsEnum.VersusBattleCount:
                return VersusBattleCount;
            case GameStatisticsEnum.VersusRoundCount:
                return VersusRoundCount;
            case GameStatisticsEnum.SpellCastCount:
                return SpellCastCount;
            case GameStatisticsEnum.DeathCount:
                return DeathCount;
            default:
                throw new ArgumentOutOfRangeException(nameof(stat), stat, null);
        }
    }

    public void SetStat(GameStatisticsEnum stat, float value)
    {
        switch (stat) {
            case GameStatisticsEnum.TimesGameOpened:
                TimesGameOpened = (int)value;
                break;
            case GameStatisticsEnum.TimePlayed:
                TimePlayed = value;
                break;
            case GameStatisticsEnum.TimeBattling:
                TimeBattling = value;
                break;
            case GameStatisticsEnum.VersusBattleCount:
                VersusBattleCount = (int)value;
                break;
            case GameStatisticsEnum.VersusRoundCount:
                VersusRoundCount = (int)value;
                break;
            case GameStatisticsEnum.SpellCastCount:
                SpellCastCount = (int)value;
                break;
            case GameStatisticsEnum.DeathCount:
                DeathCount = (int)value;
                break;
            default:
                throw new ArgumentOutOfRangeException(nameof(stat), stat, null);
        }
    }
}

备选方案分析

方案一:ScriptableObject作为统计键

实现思路

为每个统计项创建一个继承自StatKeySO的ScriptableObject子类,存储本地化ID、值类型等元数据;GameStatistics用可序列化的List<StatEntry>存储键值对,初始化时转成字典缓存以提升访问效率。

优缺点

  • 优点:
    • 类型安全:编译时即可检查值类型匹配错误,避免枚举方案的switch冗余
    • 扩展性强:新增统计项仅需创建新SO,无需修改现有代码结构,彻底规避枚举顺序依赖问题
    • 元数据集中:本地化ID、显示格式(如时间转时分秒)等配置可直接存在SO内,管理更清晰
    • 编辑器友好:支持拖拽SO配置UI或统计逻辑,无需手动输入字符串/枚举值
  • 缺点:
    • 需维护大量SO资源文件,项目资源数量会增加
    • Unity原生不支持字典序列化,需用列表模拟或结合第三方序列化库(如Newtonsoft.Json)
    • 首次访问需遍历列表初始化缓存,但50-70个统计项的性能影响可忽略

方案二:带自定义属性抽屉的字符串键

实现思路

定义可序列化的StatKey结构体,包含字符串键值;编写自定义PropertyDrawer,在编辑器中提供统计键下拉选择,避免手动输入错误;统计数据用可序列化的键值对列表或字典(结合第三方序列化)存储,元数据(本地化ID、值类型)单独维护在配置类/SO中。

优缺点

  • 优点:
    • 轻量无额外资源:无需创建SO文件,仅用字符串存储键
    • 序列化灵活:列表形式可被Unity原生序列化,字典可通过Newtonsoft.Json实现序列化
    • 配置便捷:新增统计项仅需在配置中添加条目,无需修改代码结构
  • 缺点:
    • 类型不安全:字符串易出现拼写错误,虽有编辑器抽屉辅助,但运行时仍需额外错误处理
    • 元数据分散:统计键与对应元数据需分开维护,易出现不一致问题
    • 需编写自定义PropertyDrawer,增加编辑器代码的维护成本

推荐方案

若团队规模不大、统计项增长稳定,优先选择ScriptableObject方案——类型安全和编辑器友好性能大幅降低迭代中的错误概率,虽需维护SO资源,但管理成本远低于枚举方案的冗余代码。若追求轻量灵活,或项目已引入Newtonsoft.Json等序列化库,字符串键+自定义抽屉方案也是可行选择。

优化示例(ScriptableObject方案)

// 统计键基类
public abstract class StatKeySO : ScriptableObject
{
    public string LocalizationID;
    public abstract Type ValueType { get; }
}

// 不同类型的统计键子类
public class IntStatKeySO : StatKeySO
{
    public override Type ValueType => typeof(int);
}

public class FloatStatKeySO : StatKeySO
{
    public override Type ValueType => typeof(float);
}

public class StringStatKeySO : StatKeySO
{
    public override Type ValueType => typeof(string);
}

// 可序列化的统计条目
[Serializable]
public class StatEntry
{
    public StatKeySO Key;
    public int IntValue;
    public float FloatValue;
    public string StringValue;

    public object GetValue()
    {
        return Key switch
        {
            IntStatKeySO => IntValue,
            FloatStatKeySO => FloatValue,
            StringStatKeySO => StringValue,
            _ => null
        };
    }

    public void SetValue(object value)
    {
        switch (Key)
        {
            case IntStatKeySO:
                IntValue = (int)value;
                break;
            case FloatStatKeySO:
                FloatValue = (float)value;
                break;
            case StringStatKeySO:
                StringValue = (string)value;
                break;
        }
    }
}

// 统计数据容器
[Serializable]
public class GameStatistics
{
    [SerializeField] private List<StatEntry> _stats = new();
    private Dictionary<StatKeySO, StatEntry> _statCache;

    public void Initialize()
    {
        _statCache = _stats.ToDictionary(entry => entry.Key, entry => entry);
    }

    public T GetStatValue<T>(StatKeySO key)
    {
        if (_statCache.TryGetValue(key, out var entry))
        {
            return (T)entry.GetValue();
        }
        return default;
    }

    public void SetStatValue(StatKeySO key, object value)
    {
        if (_statCache.TryGetValue(key, out var entry))
        {
            entry.SetValue(value);
        }
        else
        {
            var newEntry = new StatEntry { Key = key };
            newEntry.SetValue(value);
            _stats.Add(newEntry);
            _statCache.Add(key, newEntry);
        }
    }
}

内容的提问来源于stack exchange,提问作者Santiago14

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 05:20:21