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 SaveablePrefabCompiles only when Crystal Save + MemoryPack are present
1) Create the class and inherit from SaveableComponent
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 abstractMonoBehaviour
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
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()
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)
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
Add your
RememberMyFeature
to a GameObject (often alongside Remember Component /RememberComposite
).Configure the toggles in the inspector (which fields to track).
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
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 callbase.Awake()
.Null target → always cache & validate the target in
Awake
andenabled = 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 type →
RememberComposite
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()
calledTarget component cached & validated; script disables if missing
Payload type marked
[MemoryPackable]
SerializeComponentData()
andDeserializeComponentData(...)
implemented
Last updated