# Rudder Unity SDK — agent integration guide This file is for coding agents adding `rudder.sdk` to a Unity game. Read it before writing integration code. The public API is `Rudder` + `RudderClient`; verify signatures in this package and in `Rudder.Core.dll` (`RudderSdk.Core`), not from memory. Human-facing install notes live in `README.md`. Copy-paste scenes live in `Samples~/` (Package Manager → Rudder SDK → Feature Samples). ## Goal Wire LiveOps into the existing game: initialize once, log in with the device, then call the feature the user asked for. Do not build a second game, a debug dashboard, or a wrapper layer around the SDK. ## Install Add the scoped registry and both packages to `Packages/manifest.json`: ```json { "scopedRegistries": [ { "name": "Rudder", "url": "https://hub.rudder.build/api/packages/rudder/npm/", "scopes": ["rudder"] } ], "dependencies": { "rudder.sdk": "0.4.0", "com.unity.nuget.newtonsoft-json": "3.2.2" } } ``` `com.unity.nuget.newtonsoft-json` is required. The package does not bundle Newtonsoft. Unity `6000.0` or newer. Namespaces: - `RudderSdk.Unity` — `Rudder`, `RudderConfiguration`, `RudderState` - `RudderSdk.Core` — `RudderClient` and services - `RudderSdk.Core.Models.*` — `PlayerProfile`, `Offer`, `StorageItem`, `RankEntry`, … ## Bootstrap (do this once) 1. `Assets > Create > Rudder > Configuration` 2. Set `ProjectKey` (required). Defaults: `BaseUrl` `https://api.rudder.build`, timeout 10s. 3. Add the `Rudder` component to a startup scene and assign the asset. 4. Get the Core client, then authorize: ```csharp using RudderSdk.Unity; var client = Rudder.Initialize(); await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player"); ``` `Initialize()` is synchronous: it reads the serialized configuration and creates `RudderClient`. It does not create a GameObject — the component must already be in the scene. Calling it again returns the same client. `Auth.LoginWithDeviceAsync` logs in with the device id. After that, all features hang off `client` / `Rudder.Client`. Pending scenario effects are fetched automatically (heartbeat after sign-in, and immediately from `TriggerAsync`). Login uses the SDK device id (PlayerPrefs). Do not pass `SystemInfo.deviceUniqueIdentifier`. Tokens persist in PlayerPrefs; a second launch can still call `LoginWithDeviceAsync` (it refreshes the session). Do not call `client.Update`. The `Rudder` component pumps it every frame (effects heartbeat and wait-deadline checks need that). Subscribe to `client.Effects.On*` before `Scenario.TriggerAsync`. ## Feature map Call sites hang off `Rudder.Client`. Match the user's request to one row. Copy the matching scene under `Samples~/` if the shape is unclear. | Need | API | Sample | | --- | --- | --- | | Sign in / profile / wallets | `Auth.LoginWithDeviceAsync`, `Auth.Logout`, `Player.GetProfileAsync` | `Samples~/Authentication` | | Tunables | `RemoteConfig.LoadAsync`, `Get(key, fallback)` | `Samples~/RemoteConfig` | | Cloud save | `Storage.GetAsync` / `SaveAsync` / `DeleteAsync` | `Samples~/Storage` | | Shop + inventory | `Stores.ListAsync`, `PurchaseAsync`, `Inventory.GetAsync` | `Samples~/StoreInventory` | | High scores | `Leaderboards.FindBySlug(slug).SubmitAsync` / `ListAsync` | `Samples~/Leaderboards` | | LiveOps plans | `Scenario.TriggerAsync`, `Effects.OnNotification`, `Effects.OnStoreOffer`, … | `Samples~/Scenarios` | | Global quests | `Quests.ListAsync`, `ClaimAsync`, `ReportProgressAsync` | (no sample; see below) | | Battle pass | `BattlePass.*` with scenario/node ids, or `BattlePassEffect` | (no sample; see below) | ### Remote config ```csharp await client.RemoteConfig.LoadAsync(); var speed = client.RemoteConfig.Get("player_speed", 5f); ``` `Get` reads the cache. Call `LoadAsync` first (or `GetAsync`, which loads on first use). ### Storage `StorageItem` is `{ Type, Id, Data }` where `Data` is an opaque JSON string. `GetAsync(type)` pages; `SaveAsync` upserts; `DeleteAsync(type)` deletes that type. Prefer `JsonUtility` or Newtonsoft on `Data` — do not invent a second save format in a wrapper type. ### Stores and inventory ```csharp var stores = await client.Stores.ListAsync(); var result = await client.Stores.PurchaseAsync(storeSlug, offerSlug); if (result != null && result.Success != true) { /* result.Error */ } var items = await client.Inventory.GetAsync(); ``` `PurchaseAsync` generates an idempotency key when omitted. After a purchase, reload wallets (`Player.GetProfileAsync`) and inventory — there is no `onChange` callback. Use `Offer`, `Store`, `PlayerInventoryItem` as-is. ### Leaderboards ```csharp var board = client.Leaderboards.FindBySlug("my-board"); await board.SubmitAsync(score); var top = await board.ListAsync(10); ``` ### Scenarios Scenario graphs run on the server. The client triggers an event and surfaces pending effects. Subscribe first, then trigger an event name the project configured (examples: `player_login`, `demo_round_finished`). ```csharp client.Effects.OnNotification += effect => { /* show UI, then effect.Done() */ }; await client.Scenario.TriggerAsync("player_login"); ``` `Scenario.TriggerAsync(string eventName)` posts the event and ingests any effects the server returns. The `Rudder` component's per-frame `client.Update` also heartbeats `GET /sdk/v1/scenarios/pending` (every 30s while signed in) so effects that land later still arrive. | Event | Game must | | --- | --- | | `OnNotification` | Show UI, then `effect.Done()` / `DoneAsync()` | | `OnStoreOffer` | Show offer; `Stores.PurchaseAsync` then `effect.Purchase()`, or `effect.Decline()` | | `OnWait` | Deadline only (`DeadlineUtc`); the server advances the run | | `OnQuest` | Scenario quest node (`QuestEffect.ReportProgress`), not `client.Quests` | | `OnLeaderboard` | `effect.End()` / `effect.Claim()` | | `OnBattlePass` / `OnBattlePassLevel` | Drive the effect (`Claim` on a level effect) | | `OnScenarioCompleted` / `OnScenarioFailed` | Log / surface | Unsubscribe on destroy. `TriggerAsync` does not return runs — follow effects through the `On*` events. ### Quests (global) `client.Quests` is the player's quest list, distinct from `OnQuest` nodes. ```csharp var quests = await client.Quests.ListAsync(); await client.Quests.ClaimAsync(quest.Slug); var completedIds = await client.Quests.ReportProgressAsync("kills", 1); ``` Store purchases already report `purchase.offer:` / `purchase.item:`. ### Battle pass Progress is tied to a scenario battle-pass node. Prefer `BattlePassEffect` from `Effects.OnBattlePass` (it binds scenario/node/run ids). Direct `BattlePass.GetProgressAsync(scenarioSlug, nodeId)` needs those ids. Tracks: `BattlePassService.TrackFree` / `TrackPremium`. ## Errors Catch `RudderApiException` (and subclasses) from `RudderSdk.Core`: - `RudderAuthException` — 401; refresh failed → `AuthStateChanged(SignedOut)` - `RudderNotFoundException` — 404 - `RudderRateLimitException` — 429 - `RudderNetworkException` — no response / timeout Fields: `StatusCode`, `Code`, `RequestId`. Surface `exception.Message` in UI. Do not swallow errors. A 401 on an API call is retried once after refresh. Game code does not implement that retry. ## Unity rules for this SDK - Use the game's existing UI. Samples use IMGUI (`OnGUI`) only as a demo shell. Do not add UI Toolkit / `UIDocument` / `PanelRenderer` just to call Rudder. - Call the SDK from a `MonoBehaviour` (or the game's existing service object). Do not add a second singleton next to `Rudder`. - Use SDK model types in game code. Do not wrap `Offer` / `PlayerProfile` / `RankEntry` in project DTOs unless the user asks. - Do not declare consumer-local interfaces (`IRudderStore`, …) over `RudderClient`. - Do not edit `Runtime/Plugins/Rudder.Core.dll` or generated models. Change the C# SDK repo and copy the DLL if the API is wrong. - Do not add `replace` / local project references to `Rudder.Core`. - Game `await` must not use `ConfigureAwait(false)` so continuations return to the Unity sync context. - `Rudder.Initialize()` is idempotent: if `State` is `Ready`, it returns `Rudder.Client`. - Keep the project key out of git. Use a gitignored configuration asset (this repo uses `Assets/LiveOpsLocal.asset`). ## Do not - Invent APIs (`Rudder.Auth`, `client.Scenarios`, `BuyAsync`, `LoginViaDeviceAsync`, `AuthorizeWithDeviceAsync` are gone). Current names: `Rudder.Initialize()`, `client.Auth.LoginWithDeviceAsync`, `client.Scenario.TriggerAsync`, `client.Effects`, `PurchaseAsync`. - Port a Phaser/React store (event bus, phase machine, `Cozy*` types). - Generate scenes from editor bake scripts. - Call `Rudder.Initialize(configuration)` — that overload is gone. Put `Rudder` in a scene. - Block the main thread on HTTP. Use `async`/`await` for `LoginWithDeviceAsync` and feature calls. - Claim a feature works without a matching Rudder project entity (store slug, leaderboard slug, remote-config key, scenario event). Missing backend data is a 404, not an SDK bug. ## Checklist after integration 1. Configuration asset exists with a real `ProjectKey`. 2. `Rudder` is on a startup scene with that asset assigned. 3. `var client = Rudder.Initialize();` then `await client.Auth.LoginWithDeviceAsync(...)` before other calls. 4. The requested feature uses `client.*` / `Rudder.Client.*` with SDK types. 5. Failures hit `RudderApiException` and show in the game UI. 6. Play Mode: login succeeds and the feature call returns or shows a real API error (`LastError` / exception message), not a missing-component throw.