Skip to content

存档序列化

自定义组件最关键的一步是让数据在保存/读取时不丢失。ONI 有一套基于属性的序列化系统,大部分情况下你只需要加几个标记。

为什么需要关心

不加序列化标记的字段,读取存档后会回到默认值。建筑的状态机、计时器、进度条、储存内容——这些如果丢了,玩家体验非常差。

三步启用序列化

1. 类级别标记

csharp
[SerializationConfig(MemberSerialization.OptIn)]
public class MyComponent : KMonoBehaviour
{
}

MemberSerialization.OptIn 表示"只序列化我明确标记的字段"。这是游戏里绝大多数组件的默认选择。

两个可用值:

含义
OptIn只序列化标了 [Serialize] 的成员(推荐)
OptOut自动序列化所有 public 字段/属性,除非标了 [NonSerialized]

如果整个类层级都没有 [SerializationConfig],默认是 OptOut。这意味着所有 public 字段都会被保存。为了避免意外保存大量无关数据,新组件推荐始终明确声明 OptIn

2. 字段级别标记

csharp
[SerializationConfig(MemberSerialization.OptIn)]
public class MyComponent : KMonoBehaviour
{
    [Serialize]
    private int workCount;

    [Serialize]
    public float progress;

    [Serialize]
    public bool isEnabled;
}

[Serialize] 是一个空标记属性,不限制访问级别——privateprotectedpublic 都可以。

3. 可序列化的类型

[Serialize] 支持的字段类型:

类别类型
数值intfloatdoublelongshortbytebool
字符串string
Unity 结构Vector2Vector3ColorVector2I
集合List<T>Dictionary<K,V>HashSet<T>Queue<T>T[]
自定义类/结构任何未继承 MonoBehaviour 的类,且自身也有 [SerializationConfig][Serialize] 标记的字段

不支持直接序列化: GameObjectComponentdelegateTexture2D。引用其他对象需要用 Ref<T>ResourceRef<T>

完整示例

做一个带计数器和状态的组件:

csharp
using KSerialization;
using UnityEngine;

namespace MyMod
{
    [SerializationConfig(MemberSerialization.OptIn)]
    public class MyStorage : KMonoBehaviour
    {
        [Serialize]
        private int itemsProcessed;

        [Serialize]
        private float nextProcessTime;

        [Serialize]
        public bool isActive;

        [Serialize]
        private Vector3 lastPosition;

        protected override void OnSpawn()
        {
            base.OnSpawn();
            // 存读档后这些字段的值不会丢失
        }
    }
}

关键:using KSerialization; 不要漏。[Serialize][SerializationConfig] 都在 KSerialization 命名空间里。

不支持直接序列化的解决方案

引用其他 GameObject:Ref<T>

csharp
[Serialize]
public Ref<MinionAssignablesProxy> assignableProxy;

Ref<T> 通过对象的 InstanceID 保存引用。读取存档时,即使 GameObject 被销毁重建,只要 InstanceID 匹配就能恢复引用。

引用数据库资源:ResourceRef<T>

csharp
[Serialize]
private ResourceRef<Accessory> targetAccessory;

// 写入前自动存 guid
[OnSerializing]
private void OnSerializing()
{
    targetAccessory.Set(actualAccessory);
}

// 读取后从 guid 恢复引用
[OnDeserialized]
private void OnDeserialized()
{
    actualAccessory = targetAccessory.Get();
}

[OnSerializing][OnDeserialized] 是 C# 标准序列化回调,ONI 完全支持。

SkipSaveFileSerialization

有些字段你希望在内存中保留,但不需要存进存档(比如运行时缓存、临时计算结果):

csharp
[SkipSaveFileSerialization]
private float cachedValue;

这个属性告诉序列化系统跳过该字段。只在 OptOut 模式下有意义——OptIn 模式下没标 [Serialize] 就不会被保存。

常见保存读取流程

  1. 游戏保存时,SaveManager 遍历所有场景对象。
  2. SaveLoadRoot 找到 GameObject 上所有 ISaveLoadable 组件(所有 KMonoBehaviour 都自动实现)。
  3. 序列化系统用反射找到每个组件的 [SerializationConfig][Serialize] 标记。
  4. 收集字段值,写入 BinaryWriter
  5. 读取时,先还原类型模板,再逐字段填入保存的值。

存档和代码版本绑定的关键: 序列化是按字段名匹配的,不是按字段顺序。改名要小心——旧存档里的 oldFieldName 找不到对应的新字段,值会丢失。

状态机和序列化

状态机组件自带序列化支持。StateMachineComponent 基类已经声明了 [SerializationConfig(MemberSerialization.OptIn)]

状态机实例中的 [Serialize] 字段会被保存。StateMachine 本身有 Serializable 标记控制状态保存粒度:

csharp
// 在 InitializeStates 中设置保存策略
base.serializable = StateMachine.SerializeType.ParamsOnly; // 只保存参数
// 或
base.serializable = StateMachine.SerializeType.Both_DEPRECATED; // 保存状态+参数(不推荐新项目使用)

存档版本迁移

如果改动了数据结构,可能需要处理旧存档。简单方案是添加新字段而不删旧字段:

csharp
[Serialize]
private float oldValue;      // 保留,旧存档能读

[Serialize]
private float newValue;      // 新逻辑用这个

[OnDeserialized]
private void OnDeserialized()
{
    if (newValue == 0f && oldValue != 0f)
    {
        newValue = oldValue * 2f;  // 迁移逻辑
    }
}

更复杂的迁移需要自定义 ISaveLoadableDetails,但这已经超出入门范围。

调试

存档出问题时先做这几个检查:

csharp
// 1. 确认字段被标了 [Serialize]
// 2. 确认字段类型是受支持的类型
// 3. 确认类级别有 [SerializationConfig(MemberSerialization.OptIn)]

[OnDeserialized]
private void OnDeserialized()
{
    Debug.Log($"[MyComponent] Loaded: items={itemsProcessed}, progress={nextProcessTime}");
}

常见崩溃排查:

  • [Serialize] 标记了 GameObjectComponent 直接引用——改用 Ref<T>
  • List<T>T 是不支持序列化的类型——确保 T 是可序列化类型。
  • 字段改名后旧存档读取报错——保留旧字段名,新增字段替代。
  • 忘了加 using KSerialization;——编译通过但序列化静默失效。

常见问题

加了 [Serialize] 但读档后还是默认值 — 先确认类级别有没有 [SerializationConfig(MemberSerialization.OptIn)]。在 OptOut 模式下,[Serialize] 标记不影响行为;在 OptIn 模式下,没标记的字段不会被保存,标记了的才会。

存档文件突然变大很多 — 可能是 OptOut 模式下意外保存了大型 ListDictionary。切换回 OptIn 并明确标记需要的字段。

字段改名后存档崩 — 序列化按名字匹配。改名字等同于删除旧字段 + 新增新字段,旧值丢失是正常的。想兼容旧存档,保留旧字段名。

不要序列化静态字段[Serialize] 只对实例成员有效。静态字段属于类型不属于实例,序列化系统不会处理。