Custom Remember Classes

This GitBook page shows you how to build a custom Remember component that plugs straight into Crystal Save’s pipeline: automatic registration, unique IDs, and fast binary serialization. We’ll derive from SaveableComponent, follow the same pattern as RememberCollider, and end with a working example + a flowchart you can paste into your docs.


What you’ll build

A RememberMyFeature component that:

  • Registers itself with the save system on Awake/OnEnable

  • Serializes just the state you care about into a compact byte[]

  • Deserializes and reapplies that state on load

  • Plays nicely with Remember Component (RememberComposite) and SaveablePrefab

  • Compiles only when Crystal Save + MemoryPack are present


1) Create the class and inherit from SaveableComponent

Wrap your script with the same compile-time symbols Crystal Save uses and add Unity attributes so it shows up in the Add‑Component menu and won’t allow duplicates. If your component “targets” some Unity component (like how RememberCollider targets Collider), you can also add [RememberTarget(typeof(...))] and optionally [RememberIcon("...")] for a nicer inspector.

#if ARAWN_REMEMBERME && MEMORYPACK
using UnityEngine;
using MemoryPack;
using Arawn.CrystalSave.Runtime; // namespace where SaveableComponent lives

[AddComponentMenu("Crystal Save/Remember Components/Remember My Feature")]
[DisallowMultipleComponent]
// [RememberTarget(typeof(MyUnityComponent))]   // optional: advertise your target type
// [RememberIcon("Some Unity Icon Name")]       // optional: show a nicer icon
public sealed class RememberMyFeature : SaveableComponent
{
    // (1) Toggle fields to control what you save
    [Header("What to save")]
    [SerializeField] private bool rememberEnabled = true;
    [SerializeField] private bool rememberSomeValue = true;

    // (2) Cache your target on Awake (and validate)
    private MyUnityComponent target;

    protected override void Awake()
    {
        base.Awake(); // IMPORTANT: ensures GameObject ID, component ID, registration, etc.

        target = GetComponent<MyUnityComponent>();
        if (target == null)
        {
            Logger.Log($"{nameof(RememberMyFeature)} requires {nameof(MyUnityComponent)} on '{gameObject.name}'.",
                       LogLevel.Error);
            enabled = false; // mirror existing Remember components: disable if target missing
        }
    }

    // (3) Serialize to bytes (compact & fast)
    protected override byte[] SerializeComponentData()
    {
        if (target == null) return System.Array.Empty<byte>();

        var payload = new Payload
        {
            Enabled = rememberEnabled ? target.enabled : default,
            SomeValue = rememberSomeValue ? target.someValue : default, // example field
        };

        return Serializer.Serialize(payload); // SaveDataSerializer.Instance aliased as Serializer in base
    }

    // (4) Deserialize and apply safely
    protected override void DeserializeComponentData(byte[] data)
    {
        if (data == null || data.Length == 0 || target == null) return;

        try
        {
            var payload = Serializer.Deserialize<Payload>(data);

            if (rememberEnabled)    target.enabled   = payload.Enabled;
            if (rememberSomeValue)  target.someValue = payload.SomeValue;
        }
        catch (System.Exception ex)
        {
            Logger.Log($"{nameof(RememberMyFeature)} deserialization error on '{gameObject.name}': {ex.Message}",
                       LogLevel.Error);
        }
    }

    // (5) DTO you actually serialize
    [MemoryPackable]
    public partial class Payload
    {
        public bool Enabled;
        public float SomeValue;

        // parameterless ctor is fine; add [MemoryPackConstructor] if you define your own
    }
}
#endif

Why these pieces matter

  • SaveableComponent is an abstract MonoBehaviour that defines the save/load contract and handles:

    • Unique IDs (GameObject + per-component)

    • Registration with SaveManager.ComponentManager

    • Optional “keep across scenes” logic

  • base.Awake() is essential: it wires up IDs + registration. Skipping it will break saving.

  • Guard rails: Disable the script if the target component is missing so your scene doesn’t spam errors.

  • Binary payloads: Using MemoryPack keeps files small & fast.


2) Store & validate your target in Awake

Follow the pattern from built-in components (e.g., RememberCollider): grab the component you want to remember and switch the script off if it isn’t present.

protected override void Awake()
{
    base.Awake();
    target = GetComponent<MyUnityComponent>();
    if (!target)
    {
        Logger.Log($"{nameof(RememberMyFeature)} requires {nameof(MyUnityComponent)}.", LogLevel.Error);
        enabled = false;
    }
}

3) Implement SerializeComponentData()

Package only the fields you need inside a payload object and return a byte[].

protected override byte[] SerializeComponentData()
{
    if (!target) return System.Array.Empty<byte>();

    var payload = new Payload
    {
        Enabled = rememberEnabled ? target.enabled : default,
        SomeValue = rememberSomeValue ? target.someValue : default
    };

    return Serializer.Serialize(payload);
}

Under the hood, Crystal Save will call your SaveData() (from the base) which calls this method and stores the bytes under the component’s unique identifier.


4) Implement DeserializeComponentData(byte[] data)

Validate input, deserialize, and reapply to the target. Keep it defensive (null checks, try/catch).

protected override void DeserializeComponentData(byte[] data)
{
    if (data == null || data.Length == 0 || !target) return;

    var payload = Serializer.Deserialize<Payload>(data);

    if (rememberEnabled)    target.enabled   = payload.Enabled;
    if (rememberSomeValue)  target.someValue = payload.SomeValue;
}

5) Define your MemoryPack payload types

These are simple DTOs that declare exactly what you save (and restore). Keep them stable over time; if you change them later, consider versioning/migration.

[MemoryPackable]
public partial class Payload
{
    public bool  Enabled;
    public float SomeValue;

    // If you need your own ctor:
    // [MemoryPackConstructor] public Payload() { }
}

6) Build & use your component

  1. Add your RememberMyFeature to a GameObject (often alongside Remember Component / RememberComposite).

  2. Configure the toggles in the inspector (which fields to track).

  3. That’s it—Crystal Save will:

    • Register the helper during Awake/OnEnable

    • Call your serializer on save

    • Call your deserializer on load

If this lives under a SaveablePrefab: its bytes are bundled with the prefab entry and reapplied when the prefab is re-instantiated. If it’s a scene object: the component bytes are stored under <GameObjectUniqueID>_<ComponentID> and re-applied to the live instance.


Optional add‑ons you can copy from RememberCollider

  • Target hint & custom icon

    [RememberTarget(typeof(Collider))]
    [RememberIcon("SphereCollider Icon")]
    public sealed class RememberCollider : SaveableComponent { ... }
    
  • Use this style in your own component to declare a “natural” pairing in the inspector and provide a nicer icon.

  • Material/reference lookups If you persist asset names (e.g. materials), load them back with your project’s AssetProvider or equivalent and handle “not found” gracefully.


Common gotchas (and fixes)

  • Forgot base.Awake() → the component won’t have valid IDs or register; saving won’t include it. Always call base.Awake().

  • Null target → always cache & validate the target in Awake and enabled = false if missing.

  • Changed payload schema → if you add/remove fields later, plan a migration step or keep backward‑compatible defaults.

  • Multiple helpers of same typeRememberComposite already handles de‑duping and preserves component IDs. If you manually add duplicates, you might overwrite state; prefer the composite UI.


Example: A minimal “Remember Light” (complete file)

Paste this and tweak as needed.

#if ARAWN_REMEMBERME && MEMORYPACK
using UnityEngine;
using MemoryPack;
using Arawn.CrystalSave.Runtime;

[AddComponentMenu("Crystal Save/Remember Components/Remember Light")]
[DisallowMultipleComponent]
[RememberTarget(typeof(Light))]
[RememberIcon("Light Icon")]
public sealed class RememberLight : SaveableComponent
{
    [Header("What to save")]
    [SerializeField] private bool rememberEnabled = true;
    [SerializeField] private bool rememberIntensity = true;
    [SerializeField] private bool rememberColor = true;

    private Light target;

    protected override void Awake()
    {
        base.Awake();
        target = GetComponent<Light>();
        if (!target)
        {
            Logger.Log($"{nameof(RememberLight)} requires {nameof(Light)} on '{gameObject.name}'.",
                       LogLevel.Error);
            enabled = false;
        }
    }

    protected override byte[] SerializeComponentData()
    {
        if (!target) return System.Array.Empty<byte>();
        var p = new Payload
        {
            Enabled   = rememberEnabled   ? target.enabled   : default,
            Intensity = rememberIntensity ? target.intensity : default,
            R = rememberColor ? target.color.r : 0f,
            G = rememberColor ? target.color.g : 0f,
            B = rememberColor ? target.color.b : 0f,
            A = rememberColor ? target.color.a : 0f,
        };
        return Serializer.Serialize(p);
    }

    protected override void DeserializeComponentData(byte[] data)
    {
        if (data == null || data.Length == 0 || !target) return;
        var p = Serializer.Deserialize<Payload>(data);
        if (rememberEnabled)   target.enabled   = p.Enabled;
        if (rememberIntensity) target.intensity = p.Intensity;
        if (rememberColor)     target.color     = new Color(p.R, p.G, p.B, p.A);
    }

    [MemoryPackable]
    public partial class Payload
    {
        public bool  Enabled;
        public float Intensity;
        public float R, G, B, A;
    }
}
#endif

How it flows (save ↔ load)

Checklist before you ship

  • Script guarded by #if ARAWN_REMEMBERME && MEMORYPACK

  • Class derives from SaveableComponent

  • base.Awake() called

  • Target component cached & validated; script disables if missing

  • Payload type marked [MemoryPackable]

  • SerializeComponentData() and DeserializeComponentData(...) implemented

Last updated