If your game can remember that a door was opened or a coin was collected, you already understand the core problem behind persistence in Unity: state has to survive a reload. The same pattern applies to NPCs, except instead of saving a boolean you are saving memory, mood, and relationships.
This guide walks the standard Unity persistence pattern first, with code you can drop into a project, and then shows how persistent NPC state fits into the exact same architecture.
Who this is for: Unity developers building save systems, interactive worlds, or NPCs that need to remember players across sessions.
1. Decide what actually needs to persist
Start with a list, not with code. Write down the parts of your scene that should survive a reload:
- Door open or closed state
- Collected items and pickups
- Quest flags and dialogue milestones
- Player inventory and position
- Any NPC facts or relationship changes
Then stop there. Avoid saving the whole scene if you only need a few values. Serializing every transform in a level is fragile, slow to debug, and breaks the moment you move a prop in the editor. Persisting the logical state instead of the scene graph keeps your save file small and your bug reports readable.
A useful rule: if you can reconstruct the value from something else you already saved, do not save it.
2. Store state in a save data model, not in the scene object itself
The mistake that causes most save-system rewrites is putting persistence logic inside the objects themselves. Keep the two apart. Gameplay code decides what happens, the persistence layer decides what gets restored.
Start with a plain serializable model:
using System;
using System.Collections.Generic;
[Serializable]
public class ObjectState
{
public string id;
public bool flag; // opened, collected, triggered
}
[Serializable]
public class SaveData
{
public List<ObjectState> objects = new List<ObjectState>();
public int coinsCollected;
public List<string> completedQuests = new List<string>();
}
Every persistent object needs a stable identifier. Not its name, not its sibling index, not its position. Those all change when you edit the scene, and when they change, your save file silently points at the wrong door.
A simple way to get stable IDs is a serialized GUID assigned once in the editor:
using UnityEngine;
[DisallowMultipleComponent]
public class PersistentId : MonoBehaviour
{
[SerializeField, HideInInspector] private string id;
public string Id => id;
#if UNITY_EDITOR
void OnValidate()
{
// Assign once, then never touch it again. Renaming the object is safe.
if (string.IsNullOrEmpty(id))
{
id = System.Guid.NewGuid().ToString();
UnityEditor.EditorUtility.SetDirty(this);
}
}
#endif
}
Now a door and a coin can both report their state against an ID that survives renames, reparenting, and prefab edits.
3. Save when the state changes, then load before gameplay logic runs
Two halves, and the ordering of the second half is where most one-frame glitches come from.
Write on change. When the player opens a door or picks up a coin, update the model immediately:
using UnityEngine;
public class Door : MonoBehaviour
{
[SerializeField] private PersistentId persistentId;
[SerializeField] private Animator animator;
private bool isOpen;
public void Open()
{
if (isOpen) return;
isOpen = true;
animator.SetBool("Open", true);
SaveSystem.SetFlag(persistentId.Id, true);
SaveSystem.Save();
}
public void Restore(bool open)
{
isOpen = open;
animator.SetBool("Open", open); // no transition, snap to state
}
}
Read before anything else runs. Load the file and apply it before AI, triggers, and interaction scripts initialize. In Unity the cleanest lever for this is script execution order or an explicit bootstrap step:
using UnityEngine;
// Set this to a very negative execution order so it runs before gameplay scripts.
[DefaultExecutionOrder(-1000)]
public class SaveBootstrap : MonoBehaviour
{
void Awake()
{
SaveSystem.Load();
SaveSystem.ApplyToScene();
}
}
If you apply saved state in Start() while your triggers also initialize in Start(), you are relying on undefined ordering. That is the bug where a collected coin flickers back into view for a single frame, or an already-open door plays its opening animation again on load.
4. Restore the scene by applying saved state to objects
Restoration is a lookup, not a rebuild. Walk the persistent objects in the scene, find their saved entry by ID, and push the value in:
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public static class SaveSystem
{
private static SaveData data = new SaveData();
private static readonly Dictionary<string, ObjectState> index =
new Dictionary<string, ObjectState>();
private static string Path =>
System.IO.Path.Combine(Application.persistentDataPath, "save.json");
public static void Load()
{
data = File.Exists(Path)
? JsonUtility.FromJson<SaveData>(File.ReadAllText(Path))
: new SaveData();
index.Clear();
foreach (var o in data.objects) index[o.id] = o;
}
public static void Save() => File.WriteAllText(Path, JsonUtility.ToJson(data));
public static bool GetFlag(string id) =>
index.TryGetValue(id, out var state) && state.flag;
public static void SetFlag(string id, bool value)
{
if (!index.TryGetValue(id, out var state))
{
state = new ObjectState { id = id };
index[id] = state;
data.objects.Add(state);
}
state.flag = value;
}
public static void ApplyToScene()
{
foreach (var door in Object.FindObjectsByType<Door>(FindObjectsSortMode.None))
door.Restore(GetFlag(door.GetComponent<PersistentId>().Id));
foreach (var coin in Object.FindObjectsByType<Coin>(FindObjectsSortMode.None))
if (GetFlag(coin.GetComponent<PersistentId>().Id)) coin.gameObject.SetActive(false);
}
}
That is the whole pattern. Stable ID, a model, write on change, apply before gameplay. Everything else in a save system is an elaboration on those four moves.
5. Extend the same pattern to NPC memory and relationships
Here is where the model starts to strain, and it is worth being precise about why.
A door has one bit of state. An NPC that feels alive does not. To make a shopkeeper remember a returning player, you would need to persist something like:
[Serializable]
public class NpcRelationship
{
public string npcId;
public string playerId;
public int trust;
public int patience;
public string mood;
public List<string> knownFacts; // "player betrayed the guild"
public List<string> conversationLog; // grows without bound
}
You can build this. Teams do. The part that does not scale is not the storage, it is everything downstream of it: deciding which of four hundred remembered facts belong in the current line of dialogue, updating trust from a conversation that was not on a scripted branch, keeping mood coherent over weeks of play, and hand-authoring the flags in the first place. A save system answers "what was the state." An NPC needs an answer to "what does this character know about this player, and which part of it matters right now."
That is the layer MistScale provides. Memory, per-player relationship state, and mood evolution are persisted server side and retrieved per message, so the Unity side stays about as complex as the door script above. The same NpcRelationship blob you were about to write by hand becomes state you read rather than state you maintain.
The transport is a WebSocket connection managed by the SDK, which means persistence works the way the rest of your save system does: state changes on the server as the conversation happens, and the next session picks it up on connect.
Wiring it up in Unity
The Unity SDK ships as a package (com.mistscale.unity-sdk) with its own WebSocket transport and no other dependencies. Setup is three steps:
- Install the package through the Unity Package Manager.
- Create the config asset via Assets → Create → Mistscale → Config, and put it in a
Resources/folder so it loads at runtime. - Log in from the editor with Mistscale → Login. Paste a project API key (keys start with
ms_) from Project Settings → API Keys, verify, and the window lists your NPCs with copyable IDs.
If you would rather not ship a config asset, initialize in code before any NPC connects:
using Mistscale.SDK;
using UnityEngine;
public class Bootstrap : MonoBehaviour
{
void Awake()
{
MistscaleSDK.InitializeWithKey("ms_your_key_here");
}
}
Then add the MistscaleNPC component to a GameObject, paste an NPC ID into its NPC Identity field, and talk to it. One method sends, one event receives:
using Mistscale.SDK;
using UnityEngine;
public class TavernKeeper : MonoBehaviour
{
[SerializeField] private MistscaleNPC mira; // drag in the Inspector
[SerializeField] private DialogueUI dialogueUI;
void Start()
{
mira.OnNPCResponseReceived += reply => dialogueUI.Show(reply);
}
public void Say(string playerMessage)
{
if (!mira.IsConnected) return; // connection opens in the background on Play
mira.SendChat(playerMessage);
}
}
Nothing in that script saves anything, and that is the point. The memory of this player, the NPC's mood, and its grounding against your lore all update on the server with every exchange.
Testing it end to end
The check that matters is the same one you use for doors and coins: does the state survive a reload?
- Create an NPC in the dashboard, give it a role and personality, and copy its ID.
- Drop
MistscaleNPCon a GameObject in a test scene, paste the ID, press Play. Enable logging in the config asset and watch the Console for the connected message. - Tell the NPC something specific and memorable. A name, a promise, a betrayal.
- Stop play mode. Exit the editor if you want to be strict about it.
- Press Play again and ask about it. A returning NPC recalls it. That is the whole test.
- Run the same scene twice with two different player identities and confirm the two relationships diverge.
On step 6, a detail worth setting up early: each running game instance gets its own player identity by default, which is convenient for testing and wrong for shipping. For a released game, pass your own stable player ID when the connection opens so a returning player is recognized across devices and installs. It is the same lesson as using GUIDs instead of object names, applied to players.
Common mistakes
- Saving only the visual state. Hiding a collected coin without recording that it was collected means the pickup logic reruns on reload.
- Using object names or hierarchy paths as IDs. The save file breaks the first time someone renames
Door_01in the editor. Use a serialized GUID. - Loading too late. Applying saved state after other scripts have initialized causes one-frame flickers and replayed animations. Load in a bootstrap with a negative execution order.
- Writing to disk on every frame. Save on state change, not in
Update(). On mobile, frequent writes will show up as hitches. - Treating NPCs like stateless dialogue boxes. If your NPC is a prefab with a fixed dialogue tree, no save system will make it feel like it knows the player. Memory has to be part of the character, not a flag next to it.
Frequently Asked Questions
Should I use PlayerPrefs for persistent object state in Unity?
PlayerPrefs is fine for tiny values like a tutorial-seen flag or an audio setting. It is not a good fit for doors, inventory, or NPC memory. It stores unstructured key-value pairs in the registry or a plist, it is trivially editable by players, and it has no versioning story. For anything structured, serialize to a JSON or binary save file under Application.persistentDataPath.
What is the best way to persist doors and coins between scene reloads? Give each object a stable ID, store its state in a save model keyed by that ID, and apply the saved values on scene load before gameplay scripts run. For coins, store whether each one was collected. For doors, store whether each one is open.
How do I keep saved state across additive scene loads?
Key everything by object ID rather than by scene, and run your restore pass when each scene finishes loading (SceneManager.sceneLoaded) instead of once at startup. The lookup does not care which scene an object came from.
How is NPC memory persistence different from normal save data? The pattern is the same, the data is richer. Instead of a boolean, you are persisting conversation history, learned facts, trust, mood, and relationship changes, per player. The hard part is not writing it to disk, it is selecting the relevant slice of it at the moment the NPC speaks. That is why teams tend to move from hand-rolled flags to a dedicated NPC cognition layer.
Does MistScale memory persist across sessions and restarts? Yes. Memory is stored server side per NPC and per player, so it survives play mode restarts, rebuilds, and reinstalls. Two players talking to the same NPC build two separate relationships with it.
Do I still need my own save system if I use MistScale? Yes. MistScale persists what NPCs know and how they feel about each player. Doors, coins, inventory, and quest flags are still yours. The two layers sit side by side.
If you already have a save system, you have done the harder architectural work. The next problem is usually the one that flags cannot solve: characters who should remember the player, not just the level. MistScale handles that layer, and there is a free tier you can point a test scene at in an afternoon.