Getting started — the first-hour LivingSim SDK
For Unity / C# game developers. The fastest way into LivingSim: record one player action, and an NPC remembers it, feels about it, can explain why, and changes what it is inclined to do — deterministic, offline, no LLM. This is the demo-first path through the
LivingSimSdkfacade. For the deeper engine (authoring whole scenarios, the full Core API) see../START_HERE.md.
What it is
A deterministic NPC memory & social-consequence SDK for Unity / C# games. You
record typed social actions through one facade type — LivingSimSdk — and
read back what an NPC remembers, how it feels, the why behind that feeling, and
its next inclination. The facade is grouped by area: sdk.Characters, sdk.Places,
sdk.Items, sdk.Actions, sdk.Relationships, sdk.Time and sdk.Events, plus
sdk.Save() / sdk.Load(json) for the whole world. Same seed + same calls → the same result on every run. No
network, no model calls. It is a plain .NET library (net8.0 and
netstandard2.1 for Unity 2022.3 LTS).
What it is not
- Not a behaviour tree or GOAP framework you hand-author. Behaviour emerges from
accumulated state, never scripted per-NPC. By default the engine drives each character on
its own (run a turn with
sdk.Time.Step()); set a characterAutonomyLevel.ReactiveorAutonomyLevel.Controlledwithsdk.Characters.SetAutonomyand act on the projected inclination yourself. - Not an LLM / chat layer. No prompts, no generated text — simulation truth is typed data, deterministic from a seed.
- Not a Unity scene framework. No rendering, physics, input, or prefabs.
- Not a full game template. It is the memory-and-consequence layer your game calls into.
The first-hour loop
Set the world up (places first, then the people in them), then one player action flows through seven facade calls:
| Step | Facade call | What you get |
|---|---|---|
| set up | sdk.Places.Add, sdk.Characters.Add |
the rooms, and who is in them (a setup mistake such as a reused id or an unknown place throws) |
| act | sdk.Actions.TryRecord |
record a typed social action (Help, Insult, Betray, …) |
| remember | sdk.Characters.GetMemory |
what the NPC remembers, with importance + source |
| feel | sdk.Relationships.Get |
the dyad's six relationship axes (affinity, trust, …) |
| why | sdk.Relationships.Explain |
the ranked remembered events behind the feeling |
| next move | sdk.Characters.GetNextIntent |
the NPC's coarse inclination — approach / avoid / confront |
| continue | sdk.Time.Advance |
age the world deterministically (memory fades, the bond endures) |
| branch | sdk.Save / sdk.Load |
snapshot the whole social world, reload to branch or revert |
Characters.GetNextIntentprojects an inclination: a read of what the NPC holds right now. It does not plan or act, whatever the character's autonomy.Beside the relationship,
sdk.Characters.GetEmotionsreads the NPC's own feelings (eleven, each 0 to 100, fading with time), andsdk.Characters.GetHabits/SetHabitread and give the habits (a gossip, someone who gets even) that tilt an autonomous character's choices.Acting on its own is what
sdk.Time.Step()does. It runs an active simulation turn, whilesdk.Time.Advanceonly ages the world. AStep()lets every autonomous character (the default for a new character) commit goals, plan and carry them out;sdk.Characters.ExplainDecisionthen reads the committed decision with its typed why. Set a characterAutonomyLevel.Reactive(a background character who pursues nothing of their own) orAutonomyLevel.Controlled(your player, or anyone your own AI drives: the engine authors no act in their name) withsdk.Characters.SetAutonomy.
See it in 60 seconds (the demo)
The Unity Inspector demo drives this whole loop through only the facade — no
Core internals. Drop the package into a Unity 2022.3+ project, add the
LivingSimWorld, LivingSimAgent, LivingSimEventSource and LivingSimDebugPanel components, press
Play, and click Fire social action. LivingSimWorld sets the world up from the
Inspector: its seed, its places (kitchen, haven), then its cast, each with an id, a
name, a starting place and an autonomy level (by default the player is controlled, and
Nora and Ada are autonomous). Time moves only when you move it, unless you set its
Step Every Seconds: then it runs one step each time that much game time has built up,
at most one a frame, so the count depends on frame timing.
| You do | The panel shows |
|---|---|
| Fire Help (player → Nora) | Nora remembers the Help · affinity/trust rise · why: Help by player · next: approach player |
| Fire Insult instead | Nora remembers the Insult · affinity/trust drop · why: Insult by player · next: avoid/confront player |
| Save, fire again, Load | the world snaps back to the saved social state — the action is undone |
Full scene setup is in the Unity package README:
the Unity package's README (§5; it ships inside the package as README.md).
Recipes
All three use only the LivingSimSdk facade. (Snippets use Console.WriteLine
to show what you read; in Unity, use Debug.Log. Example output values — importance,
strength — are illustrative; the engine computes the exact numbers deterministically
from the seed.)
Recipe 1 — Record a player social action and read NPC memory
using System;
using LivingSim.Sdk;
var sdk = new LivingSimSdk(seed: 0);
sdk.Places.Add("kitchen", name: "Kitchen"); // places first; the name is for display
sdk.Characters.Add(new() { Id = "player", Place = "kitchen" }); // explicit setup — a write against an unknown id is rejected, never a phantom NPC
sdk.Characters.Add(new() { Id = "nora", Name = "Nora", Place = "kitchen" });
sdk.Characters.SetAutonomy("player", AutonomyLevel.Controlled); // the engine never acts for your player
// act: the player helps Nora. (Help / Thank / Comfort / Apologize / Insult /
// Accuse / Threaten / Betray / Lie / Steal are the curated SocialActionKinds.)
TryRecordSocialActionResult result = sdk.Actions.TryRecord(
new TryRecordSocialActionRequest("player", "nora", "kitchen", SocialActionKind.Help, Impact: 20));
// result.Success == true; on a rejection result.ReasonCode carries a stable code (e.g. "target_not_present").
// Fluent equivalent — same validation, same result. At is optional (defaults to the actor's
// place), and so is Impact (defaults to the verb's calibrated severity, ActionSeverity's
// standard tier). An impact of 0 is refused with "impact_zero" on either path.
// Sign it as harm or kindness: negative for harm (Insult -25), positive for kindness
// (Help 50); ActionSeverity.Get(EventType.Insult) gives the calibrated number.
// sdk.Actions.By("player").Toward("nora").At("kitchen").Kind(SocialActionKind.Help).Impact(20).TryRecord();
// remember: read what Nora now holds about it (copy-safe, never throws)
NpcMemoryView memory = sdk.Characters.GetMemory("nora");
foreach (MemoryEntryView m in memory.Memories)
{
// → Help by player (importance ~27, Direct) // illustrative; appraisal raises the importance of what she felt
Console.WriteLine($"{m.EventType} by {m.ActorId} (importance {m.Importance}, {m.Source})");
}
Recipe 2 — Explain why an NPC feels differently, and show its next intent
// feel: how Nora regards the player — the dyad's six relationship axes
RelationshipView rel = sdk.Relationships.Get("nora", "player");
// rel.Affinity / rel.Trust have moved off neutral because of the Help.
// why: the remembered events that account for that feeling, ranked most-influential-first
RelationshipExplanationView why = sdk.Relationships.Explain("nora", "player");
foreach (MemoryEntryView e in why.Evidence)
{
// → Help by player — this is ranked evidence, not a per-axis causal claim
Console.WriteLine($"because: {e.EventType} by {e.ActorId}");
}
// next move: the coarse inclination this leaves her with — an inclination, NOT an executed action
NextIntentView intent = sdk.Characters.GetNextIntent("nora");
Console.WriteLine($"next: {intent.Kind} {intent.TargetId} (strength {intent.Strength})");
// After a Help → Approach player. After an Insult / Betray / Lie → Avoid / Confront player.
Recipe 3 — Save before an action, change the world, then Load to branch / revert
// branch: snapshot the whole social world before a consequence
string save = sdk.Save();
// change the world — the player insults Nora
sdk.Actions.TryRecord(
new TryRecordSocialActionRequest("player", "nora", "kitchen", SocialActionKind.Insult, Impact: -30));
// Nora now remembers the insult, her feeling sours, and her next intent flips toward Avoid / Confront.
// continue (optional): let time pass — the memory of the insult fades, the relationship endures
sdk.Time.Advance(10);
// revert: reload the saved point — every consequence since the save is undone
sdk.Load(save);
// Nora remembers nothing of the insult again; the relationship is exactly as it was when saved.
Recipe 4 — React when something happens, without polling
// subscribe once; each event is raised after the call that caused it, with copied values
sdk.Events.OnGossip += g =>
{
if (g.ListenerId != "player") return;
// a rumour reached the player: g.TellerId told them about log entry g.Memory.EventIndex
// (g.Memory.EventType by g.Memory.ActorId, believed at g.Memory.Confidence%)
};
sdk.Events.OnDecision += d => Console.WriteLine($"{d.CharacterId} decided: {d.Decision.ReasonSummary}");
sdk.Events.OnPlaceChanged += m => Console.WriteLine($"{m.CharacterId}: {m.FromPlaceId} -> {m.ToPlaceId}");
sdk.Time.Step(); // the turn runs, then its events are raised in the engine's order
Handlers may read anything (GetPlace, ExplainDecision, Save, …) and may write nothing: a write
from inside a handler (Step, TryRecord, Move, Load, …) throws InvalidOperationException;
queue it and make it after Step returns. Nothing is saved, Load replays nothing, and
subscribers change no outcome: the same seed gives the same world and the same event sequence with
or without them. The five events (OnEvent, OnGossip, OnDecision, OnPlanStep,
OnPlaceChanged), their order and their views are in ../QUICKSTART.md.
Recipe 5 — Record your game's own verbs, and get them back in your own words
// once, at setup: your word, the engine verb it means, how heavy it is, the phrase to show
sdk.Actions.RegisterVerb(new HostVerbOptions
{
Id = "spill-drink", Kind = SocialActionKind.Insult, Severity = SeverityTier.Light, Label = "spilled a drink on",
});
// record by your word; the engine records an Insult at the registered weight
var spilled = sdk.Actions.By("player").Toward("nora").Verb("spill-drink").TryRecord();
// or: sdk.Actions.TryRecord(new TryRecordHostVerbRequest("player", "nora", PlaceId: null, VerbId: "spill-drink"));
// every view of the act carries your word and your phrase
var memory = sdk.Characters.GetMemory("nora").Memories.Single(m => m.EventIndex == spilled.EventIndex);
Console.WriteLine($"{memory.ActorId} {memory.HostVerbLabel ?? memory.EventType.ToString()} {memory.TargetId}");
// -> "player spilled a drink on nora"
An act recorded through a registered verb has exactly the consequences of the engine verb it maps
to, at the registered weight (or the impact the call gives). The id and the label travel with the
event into every memory, relay and view (MemoryEntryView, WorldEventView, the reaction, place
and decision evidence, ExplainDecision's Why.SourceMemoryHostVerbLabel), and the registry is
saved with the world, so a loaded save needs no re-registration. RegisterVerb is a setup call like
Places.Add: a reused id, an unknown kind or a zero weight throws; recording an id no verb was
registered under returns unknown_host_verb. GetVerbs() lists what the world has.
Where to go next
| If you want… | Read |
|---|---|
| The Unity package — import, scene setup, the playable demo | the package's own README.md (it ships inside the .unitypackage) |
| Plug it into your game — behaviour / dialogue / movement / generative adapters over the facade | adapters.md |
The SDK facade API — every LivingSimSdk area and its calls + the view types |
the LivingSim.Sdk reference, generated from the facade's XML docs (the facade source is src/LivingSim.Sdk/ in the repository; its XML docs publish to the API-reference site) |
| The facade design & shipped status — what each call guarantees, what is still planned | the design note docs/product/first-hour-facade.md in the repository |
| The deeper engine & architecture — the full Core API, authoring whole worlds | ../START_HERE.md (and docs/NORTH_STAR.md in the repository) |
| Worked scenarios / the canonical proof | docs/reference-scenarios.md in the repository, the LivingSim.ProductProof tool |