244 lines
9.5 KiB
Markdown
244 lines
9.5 KiB
Markdown
# 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<T>(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:<id>` / `purchase.item:<id>`.
|
|
|
|
### 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.
|