4 Commits

Author SHA1 Message Date
edmand46 abad50d5e5 Release 1.0.0
CI / publish (push) Failing after 6s
CI / check (push) Successful in 3s
2026-09-04 15:37:39 +03:00
edmand46 6d115f158e Server-side scenario execution: new Rudder.Core.dll, realtime/planstore glue removed
- Rudder.Core.dll rebuilt from csharp-sdk effects rewrite (96,768 bytes)
- Realtime/ adapters, UnityRealtimeTransportFactory, UnityPlanStateStore,
  UnityPlanScheduler, WebGL jslib and RealtimeUrl config deleted (with .meta)
- Scenarios sample rewired to client.Effects; samples login path updated
- AGENTS.md/README/CHANGELOG/package docs + agent skill updated; realtime.md skill doc removed
2026-09-04 14:23:38 +03:00
edmand46 22805173eb Add agent skill (SKILL.md + per-domain reference)
CI / check (push) Successful in 6s
CI / publish (push) Has been skipped
2026-08-29 11:46:25 +03:00
edmand46 b8e226722d examples: global quests usage in CozyCollector (list/claim/reportProgress), quests capability
CI / check (push) Successful in 6s
CI / publish (push) Has been skipped
2026-08-25 16:25:36 +03:00
76 changed files with 3876 additions and 921 deletions
@@ -23,6 +23,7 @@ namespace LiveOpsExamples
public const string LeaderboardSlug = "cozy-collector-score"; public const string LeaderboardSlug = "cozy-collector-score";
public const string StoreSlug = "cozy-camp-shop"; public const string StoreSlug = "cozy-camp-shop";
public const string OfferId = "moonberry-boost"; public const string OfferId = "moonberry-boost";
public const string QuestMetricCollectibles = "cozy_collectibles";
public const string StorageType = "cozy_fantasy_save"; public const string StorageType = "cozy_fantasy_save";
public const string StorageId = "main"; public const string StorageId = "main";
public const string NicknamePrefsKey = "rudder_demo_nickname"; public const string NicknamePrefsKey = "rudder_demo_nickname";
@@ -38,6 +39,7 @@ namespace LiveOpsExamples
public const string CapabilityStorage = "storage"; public const string CapabilityStorage = "storage";
public const string CapabilityInventory = "inventory"; public const string CapabilityInventory = "inventory";
public const string CapabilityWallet = "wallet"; public const string CapabilityWallet = "wallet";
public const string CapabilityQuests = "quests";
public const string CapabilitySystem = "system"; public const string CapabilitySystem = "system";
/// <summary>Color used by the event inspector to tag a capability line.</summary> /// <summary>Color used by the event inspector to tag a capability line.</summary>
@@ -53,6 +55,7 @@ namespace LiveOpsExamples
case CapabilityStorage: return new Color(0.56f, 0.87f, 0.87f); case CapabilityStorage: return new Color(0.56f, 0.87f, 0.87f);
case CapabilityInventory: return new Color(0.78f, 0.86f, 0.55f); case CapabilityInventory: return new Color(0.78f, 0.86f, 0.55f);
case CapabilityWallet: return new Color(1.00f, 0.88f, 0.63f); case CapabilityWallet: return new Color(1.00f, 0.88f, 0.63f);
case CapabilityQuests: return new Color(0.71f, 0.80f, 1.00f);
default: return new Color(0.75f, 0.75f, 0.75f); default: return new Color(0.75f, 0.75f, 0.75f);
} }
} }
@@ -7,6 +7,7 @@ using RudderSdk.Core;
using RudderSdk.Core.Models.Inventory; using RudderSdk.Core.Models.Inventory;
using RudderSdk.Core.Models.Leaderboards; using RudderSdk.Core.Models.Leaderboards;
using RudderSdk.Core.Models.Player; using RudderSdk.Core.Models.Player;
using RudderSdk.Core.Models.Quests;
using RudderSdk.Core.Models.Storage; using RudderSdk.Core.Models.Storage;
using RudderSdk.Core.Models.Stores; using RudderSdk.Core.Models.Stores;
using RudderSdk.Unity; using RudderSdk.Unity;
@@ -249,6 +250,7 @@ namespace LiveOpsExamples
await RefreshProfileAsync(); await RefreshProfileAsync();
await RefreshStoresAsync(); await RefreshStoresAsync();
await RefreshInventoryAsync(); await RefreshInventoryAsync();
await RefreshQuestsAsync();
try try
{ {
@@ -456,6 +458,11 @@ namespace LiveOpsExamples
try { await RefreshInventoryAsync(); } try { await RefreshInventoryAsync(); }
catch (Exception exception) { Log(CozyCollectorConsts.CapabilityInventory, "Inventory refresh failed", exception.Message); } catch (Exception exception) { Log(CozyCollectorConsts.CapabilityInventory, "Inventory refresh failed", exception.Message); }
// Purchase metrics (purchase.offer:* / purchase.item:*) are reported
// server-side by the purchase fan-out; re-list to pick up new progress.
try { await RefreshQuestsAsync(); }
catch (Exception exception) { Log(CozyCollectorConsts.CapabilityQuests, "Quest refresh failed", exception.Message); }
} }
// --------------------------------------------------------------- // ---------------------------------------------------------------
@@ -573,6 +580,63 @@ namespace LiveOpsExamples
return mapped; return mapped;
} }
// ---------------------------------------------------------------
// Global quests (distinct from scenario quest nodes / QuestSession)
// ---------------------------------------------------------------
private async Task RefreshQuestsAsync()
{
var quests = await Rudder.Client.Quests.ListAsync();
var activeCount = 0;
if (quests != null)
{
foreach (var quest in quests)
{
if (quest.Status == "claimed")
continue;
activeCount += 1;
Log(CozyCollectorConsts.CapabilityQuests, "Quest " + (quest.Status ?? "active"), QuestProgressSummary(quest));
if (quest.Status == "completed" && !string.IsNullOrEmpty(quest.Id))
await ClaimQuestAsync(quest);
}
}
Log(CozyCollectorConsts.CapabilityQuests, "Quests loaded", activeCount + " active");
}
private async Task ClaimQuestAsync(Quest quest)
{
var result = await Rudder.Client.Quests.ClaimAsync(quest.Id);
if (result == null || !result.Success)
{
Log(CozyCollectorConsts.CapabilityQuests, "Quest claim rejected", result?.Error);
return;
}
if (!result.AlreadyClaimed)
Toast("Quest complete", quest.Name ?? quest.Id, CozyToastTone.Success);
Log(CozyCollectorConsts.CapabilityQuests, "Quest claimed", quest.Name ?? quest.Id);
await RefreshWalletsAsync();
}
private static string QuestProgressSummary(Quest quest)
{
var name = quest.Name ?? quest.Id;
if (quest.Objectives == null || quest.Objectives.Count == 0)
return name;
var parts = new List<string>();
foreach (var objective in quest.Objectives)
{
parts.Add(objective.Metric + " " + objective.Current + "/" + objective.Target);
}
return name + " · " + string.Join(" · ", parts);
}
// --------------------------------------------------------------- // ---------------------------------------------------------------
// Round flow // Round flow
// --------------------------------------------------------------- // ---------------------------------------------------------------
@@ -625,6 +689,23 @@ namespace LiveOpsExamples
{ {
Log(CozyCollectorConsts.CapabilityScenario, "Trigger failed", exception.Message); Log(CozyCollectorConsts.CapabilityScenario, "Trigger failed", exception.Message);
} }
try
{
var completedIds = await Rudder.Client.Quests.ReportProgressAsync(
CozyCollectorConsts.QuestMetricCollectibles,
collected);
Log(
CozyCollectorConsts.CapabilityQuests,
"Quest progress reported",
CozyCollectorConsts.QuestMetricCollectibles + " +" + collected +
(completedIds.Count > 0 ? " · completed " + completedIds.Count + " quest(s)" : string.Empty));
await RefreshQuestsAsync();
}
catch (Exception exception)
{
Log(CozyCollectorConsts.CapabilityQuests, "Quest progress failed", exception.Message);
}
} }
public void PlayAgain() public void PlayAgain()
@@ -16,6 +16,7 @@ namespace LiveOpsExamples
{ "storage", new Color(0.624f, 0.690f, 0.769f) }, { "storage", new Color(0.624f, 0.690f, 0.769f) },
{ "inventory", new Color(0.898f, 0.604f, 0.796f) }, { "inventory", new Color(0.898f, 0.604f, 0.796f) },
{ "wallet", new Color(1f, 0.831f, 0.475f) }, { "wallet", new Color(1f, 0.831f, 0.475f) },
{ "quests", new Color(0.710f, 0.800f, 1f) },
{ "system", new Color(0.667f, 0.706f, 0.635f) }, { "system", new Color(0.667f, 0.706f, 0.635f) },
}; };
@@ -29,6 +30,7 @@ namespace LiveOpsExamples
{ "storage", "Storage" }, { "storage", "Storage" },
{ "inventory", "Items" }, { "inventory", "Items" },
{ "wallet", "Wallet" }, { "wallet", "Wallet" },
{ "quests", "Quests" },
{ "system", "System" }, { "system", "System" },
}; };
-1
View File
@@ -14,5 +14,4 @@ MonoBehaviour:
m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.RudderConfiguration m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.RudderConfiguration
ProjectKey: YOUR_PROJECT_KEY ProjectKey: YOUR_PROJECT_KEY
BaseUrl: https://api.rudder.build BaseUrl: https://api.rudder.build
RealtimeUrl: wss://realtime.rudder.build/api/realtime/ws
TimeoutSeconds: 10 TimeoutSeconds: 10
+243
View File
@@ -0,0 +1,243 @@
# 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, offerId);
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.Id);
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(scenarioId, 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.
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7c4e9a12b8d04f6a9e3c1f5d0a2b8c47
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+36
View File
@@ -1,5 +1,41 @@
# Changelog # Changelog
## 1.0.0
Breaking changes, following the Rudder.Core 1.0.0 rework:
- Bundled `Rudder.Core.dll` updated to 0.4.0. The local scenario graph engine
is gone. `client.Scenario` is only `TriggerAsync(eventName)`. Pending nodes
surface on `client.Effects` (`OnNotification`, `OnStoreOffer`,
`OnLeaderboard`, `OnWait`, `OnQuest`, `OnBattlePass`,
`OnBattlePassLevel`, `OnScenarioCompleted`, `OnScenarioFailed`).
- Session types (`NotificationSession`, `StoreOfferSession`, …) are now
`*Effect` types. Completion methods: `Done`/`DoneAsync`,
`Purchase`/`Decline`, `End`/`Claim`, `ReportProgress`, `LevelUp`.
- `UnityPlanScheduler` removed together with the core `IPlanScheduler`
abstraction and `RudderClientOptions.Scheduler`; the factory no longer
wires a scheduler.
- `UnityPlanStateStore`, `UnityRealtimeTransportFactory`,
`Runtime/Sources/Realtime/` websocket adapters, and
`Runtime/Plugins/WebGL/RudderWebSocket.jslib` removed together with Core
`IPlanStateStore` / `IRealtimeTransport` / `RealtimeService`.
- `RudderConfiguration.RealtimeUrl` and `RudderUnityClientOptions.RealtimeUrl`
removed. `Rudder` no longer disconnects a realtime socket on quit.
- `Rudder.Update` still pumps `client.Update(Time.deltaTime)` (effects
heartbeat and wait-deadline checks).
- Samples and docs use `Auth.LoginWithDeviceAsync`. Pending effects are
fetched after sign-in via the heartbeat; `Scenario.RestoreAsync` /
`AuthorizeWithDeviceAsync` are gone.
- Core model fields are nullable now; samples check
`PurchaseResponse.Success != true` instead of `!Success`.
- Feature sample scenes for authentication, remote config, storage, stores
and inventory, leaderboards, and scenarios (`Samples~`).
- Removed the Cozy Collector example project from the Unity SDK repo.
- Added `AGENTS.md` with Unity integration rules for coding agents.
- `Rudder.Initialize()` is now synchronous, takes no arguments, and returns
`RudderClient`. Put `Rudder` in a scene with a `RudderConfiguration`.
The old `Initialize(RudderConfiguration)` (hidden GameObject) is gone.
## 0.3.0 ## 0.3.0
Breaking changes, following the Rudder.Core 0.3.0 rework: Breaking changes, following the Rudder.Core 0.3.0 rework:
+37 -14
View File
@@ -1,10 +1,12 @@
# Rudder SDK for Unity # Rudder SDK for Unity
LiveOps SDK for Unity: device auth, player profile, remote config, stores, LiveOps SDK for Unity: device auth, player profile, remote config, stores,
inventory, leaderboards, battle pass, quests, storage, scenarios and realtime. inventory, leaderboards, battle pass, quests, storage and scenarios.
The package wraps `Rudder.Core.dll` (the .NET SDK) with Unity adapters: The package wraps `Rudder.Core.dll` (the .NET SDK) with Unity adapters:
`UnityWebRequest` transport, `PlayerPrefs` token storage, websocket realtime. `UnityWebRequest` transport and `PlayerPrefs` token storage.
Coding agents: read `AGENTS.md` before integrating this package into a game.
## Installation ## Installation
@@ -21,7 +23,7 @@ your project's `Packages/manifest.json`:
} }
], ],
"dependencies": { "dependencies": {
"rudder.sdk": "0.3.0", "rudder.sdk": "0.4.0",
"com.unity.nuget.newtonsoft-json": "3.2.2" "com.unity.nuget.newtonsoft-json": "3.2.2"
} }
} }
@@ -33,26 +35,25 @@ Newtonsoft.Json and the package does not bundle the DLL.
## Quickstart ## Quickstart
Create a configuration asset via `Assets > Create > Rudder > Configuration`, Create a configuration asset via `Assets > Create > Rudder > Configuration`,
set `ProjectKey`, `BaseUrl` and `RealtimeUrl`, then initialize once at set `ProjectKey`, add the `Rudder` component to a startup scene, assign the
startup: asset, then:
```csharp ```csharp
using RudderSdk.Unity; using RudderSdk.Unity;
Rudder.Initialize(configuration); // or add the Rudder component to a scene var client = Rudder.Initialize();
var client = await Rudder.Ready; // faults if initialization failed await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player");
var session = await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player");
var configs = await client.RemoteConfig.LoadAsync(); var configs = await client.RemoteConfig.LoadAsync();
var stores = await client.Stores.ListAsync(); var stores = await client.Stores.ListAsync();
``` ```
All features hang off the client: `Auth`, `Player`, `BattlePass`, `Quests`, All features hang off the client: `Auth`, `Player`, `BattlePass`, `Quests`,
`Stores`, `Inventory`, `Leaderboards`, `RemoteConfig`, `Scenario`, `Storage`, `Stores`, `Inventory`, `Leaderboards`, `RemoteConfig`, `Scenario`, `Effects`,
`Realtime`. `Storage`.
```csharp ```csharp
client.Scenario.OnStoreOffer += session => { /* show offer UI */ }; client.Effects.OnStoreOffer += effect => { /* show offer UI */ };
await client.Scenario.TriggerAsync("player_login"); await client.Scenario.TriggerAsync("player_login");
var purchase = await client.Stores.PurchaseAsync("cozy-camp-shop", "moonberry-boost"); var purchase = await client.Stores.PurchaseAsync("cozy-camp-shop", "moonberry-boost");
@@ -64,14 +65,36 @@ var top = await leaderboard.ListAsync(10);
## Lifecycle ## Lifecycle
- `Rudder.Initialize()` — synchronous. Reads the configuration on the scene
component and returns `RudderClient`. Idempotent. Throws if the component
or configuration is missing.
- `client.Auth.LoginWithDeviceAsync` — device login. Use this as the first
awaited call. Pending scenario effects are fetched after sign-in via the
per-frame `Update` heartbeat.
- `Rudder.State``NotInitialized` / `Initializing` / `Ready` / `Failed`. - `Rudder.State``NotInitialized` / `Initializing` / `Ready` / `Failed`.
- `Rudder.LastError` — the initialization error when `State` is `Failed`. - `Rudder.LastError` — the initialization error when `State` is `Failed`.
- `Rudder.Client` — the initialized `RudderClient`; throws until `Ready`. - `Rudder.Client` — the initialized `RudderClient`; throws until `Initialize`.
- `Rudder.Ready` — task that completes with the client or the initialization
error; `Rudder.Initialized` is the event-flavored sugar over it.
- HTTP timeout defaults to 10 seconds, configurable per - HTTP timeout defaults to 10 seconds, configurable per
`RudderConfiguration.TimeoutSeconds`. `RudderConfiguration.TimeoutSeconds`.
Errors surface as `RudderApiException` subclasses from `RudderSdk.Core`: Errors surface as `RudderApiException` subclasses from `RudderSdk.Core`:
`RudderAuthException` (401, session over), `RudderNotFoundException` (404), `RudderAuthException` (401, session over), `RudderNotFoundException` (404),
`RudderRateLimitException` (429) and `RudderNetworkException` (no response). `RudderRateLimitException` (429) and `RudderNetworkException` (no response).
## Samples
Import **Feature Samples** from the Package Manager (select the Rudder SDK
package, then Samples). Each scene is one API:
| Scene | SDK calls |
| --- | --- |
| Authentication | `Initialize`, `Auth.LoginWithDeviceAsync`, `Player.GetProfileAsync`, `Auth.Logout` |
| Remote Config | `RemoteConfig.LoadAsync`, `Get(key, fallback)` |
| Storage | `Storage.GetAsync`, `SaveAsync`, `DeleteAsync` |
| Store & Inventory | `Stores.ListAsync`, `PurchaseAsync`, `Inventory.GetAsync` |
| Leaderboards | `Leaderboards.FindBySlug`, `SubmitAsync`, `ListAsync` |
| Scenarios | `Scenario.TriggerAsync`, `Effects.OnNotification`, `Effects.OnStoreOffer`, … |
Assign a `RudderConfiguration` on the Sample object if the field is empty,
then press Play. In this repo the scenes are already visible at
`Assets/Samples/Rudder SDK/`.
Binary file not shown.
@@ -1,66 +0,0 @@
// WebSocket bridge for the Rudder Unity SDK (JsWebSocketAdapter).
// Implements the __Internal__ externals declared in Realtime/JsWebSocketAdapter.cs:
// RudderWebSocketCreate(gameObjectName, url)
// RudderWebSocketSend(gameObjectName, data, length)
// RudderWebSocketClose(gameObjectName)
// Events are routed back to the bridge GameObject via SendMessage:
// OnOpen(string), OnMessage(string base64), OnClose(string), OnError(string)
var RudderWebSockets = {};
function RudderWebSocketBase64(bytes) {
var chunks = [];
var chunkSize = 0x8000;
for (var i = 0; i < bytes.length; i += chunkSize) {
chunks.push(String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize)));
}
return btoa(chunks.join(''));
}
mergeInto(LibraryManager.library, {
RudderWebSocketCreate: function (gameObjectName, url) {
var name = UTF8ToString(gameObjectName);
var socket = new WebSocket(UTF8ToString(url));
socket.binaryType = 'arraybuffer';
RudderWebSockets[name] = socket;
socket.onopen = function () {
SendMessage(name, 'OnOpen', 'opened');
};
socket.onmessage = function (event) {
var bytes;
if (typeof event.data === 'string') {
// Text frame: UTF-8 encode, then base64 (the C# side decodes base64 to bytes).
bytes = new TextEncoder().encode(event.data);
} else {
bytes = new Uint8Array(event.data);
}
SendMessage(name, 'OnMessage', RudderWebSocketBase64(bytes));
};
socket.onerror = function () {
SendMessage(name, 'OnError', 'WebSocket error');
};
socket.onclose = function () {
delete RudderWebSockets[name];
SendMessage(name, 'OnClose', 'closed');
};
},
RudderWebSocketSend: function (gameObjectName, data, length) {
var socket = RudderWebSockets[UTF8ToString(gameObjectName)];
if (!socket || socket.readyState !== WebSocket.OPEN) return;
socket.send(HEAPU8.subarray(data, data + length));
},
RudderWebSocketClose: function (gameObjectName) {
var name = UTF8ToString(gameObjectName);
var socket = RudderWebSockets[name];
if (socket) {
delete RudderWebSockets[name];
socket.close();
}
}
});
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 3f90f5c02b3cd42e8a599649ae0a142d
@@ -11,16 +11,12 @@ namespace RudderSdk.Unity
var clientOptions = new RudderClientOptions var clientOptions = new RudderClientOptions
{ {
BaseUrl = options.BaseUrl, BaseUrl = options.BaseUrl,
RealtimeUrl = options.RealtimeUrl,
ProjectKey = options.ProjectKey, ProjectKey = options.ProjectKey,
Logger = new UnityLoggerAdapter(options.LoggerSink ?? new UnityDebugLoggerSink()), Logger = new UnityLoggerAdapter(options.LoggerSink ?? new UnityDebugLoggerSink()),
Transport = new UnityTransportAdapter(options.BaseUrl, executor), Transport = new UnityTransportAdapter(options.BaseUrl, executor),
TokenStore = new UnityTokenStoreAdapter(options.KeyValueStore), TokenStore = new UnityTokenStoreAdapter(options.KeyValueStore),
DeviceIdProvider = new UnityDeviceIdProvider(), DeviceIdProvider = new UnityDeviceIdProvider(),
Clock = new UnityClock(), Clock = new UnityClock()
PlanStateStore = new UnityPlanStateStore(options.KeyValueStore),
Scheduler = new UnityPlanScheduler(),
RealtimeTransportFactory = new UnityRealtimeTransportFactory()
}; };
return new RudderClient(clientOptions); return new RudderClient(clientOptions);
@@ -3,7 +3,6 @@ namespace RudderSdk.Unity
public class RudderUnityClientOptions public class RudderUnityClientOptions
{ {
public string BaseUrl { get; set; } public string BaseUrl { get; set; }
public string RealtimeUrl { get; set; }
public string ProjectKey { get; set; } public string ProjectKey { get; set; }
public int TimeoutSeconds { get; set; } = 10; public int TimeoutSeconds { get; set; } = 10;
public IUnityKeyValueStore KeyValueStore { get; set; } public IUnityKeyValueStore KeyValueStore { get; set; }
@@ -1,15 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using RudderSdk.Core.Abstractions;
namespace RudderSdk.Unity
{
internal class UnityPlanScheduler : IPlanScheduler
{
public Task ScheduleAsync(TimeSpan delay, CancellationToken cancellationToken = default)
{
return Task.Delay(delay, cancellationToken);
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: f4f8e4e8237be445c96672d96294c6c7
@@ -1,30 +0,0 @@
using RudderSdk.Core.Abstractions;
namespace RudderSdk.Unity
{
internal class UnityPlanStateStore : IPlanStateStore
{
private const string KEY = "liveops_plan_runner_state";
private readonly IUnityKeyValueStore _store;
public UnityPlanStateStore(IUnityKeyValueStore store)
{
_store = store;
}
public string State
{
get => _store.GetString(KEY);
set
{
if (string.IsNullOrEmpty(value))
_store.DeleteKey(KEY);
else
_store.SetString(KEY, value);
_store.Save();
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 118ea3286be8b4daab53eaf7885507a0
@@ -1,51 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using RudderSdk.Core.Abstractions;
namespace RudderSdk.Unity
{
internal sealed class UnityRealtimeTransportFactory : IRealtimeTransportFactory
{
public IRealtimeTransport Create()
{
#if UNITY_WEBGL && !UNITY_EDITOR
return new UnityRealtimeTransport(new JsWebSocketAdapter());
#else
return new UnityRealtimeTransport(new NativeWebSocketAdapter());
#endif
}
}
internal sealed class UnityRealtimeTransport : IRealtimeTransport
{
private readonly IWebSocketAdapter _adapter;
public UnityRealtimeTransport(IWebSocketAdapter adapter)
{
_adapter = adapter;
_adapter.Closed += () => Closed?.Invoke();
_adapter.Received += data => Received?.Invoke(data);
_adapter.ReceivedError += ex => Error?.Invoke(ex);
}
public event Action Closed;
public event Action<ArraySegment<byte>> Received;
public event Action<Exception> Error;
public bool IsConnected => _adapter.IsConnected;
public Task ConnectAsync(Uri uri, TimeSpan timeout, CancellationToken cancellationToken = default)
=> _adapter.ConnectAsync(uri, (int)Math.Ceiling(timeout.TotalSeconds), cancellationToken);
public Task SendAsync(ArraySegment<byte> data, CancellationToken cancellationToken = default)
=> _adapter.SendAsync(data, cancellationToken);
public Task CloseAsync(CancellationToken cancellationToken = default)
=> _adapter.CloseAsync();
public void Update(float deltaTime)
{
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: be75d4878806d40648e49338f0ecd508
@@ -1,22 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace RudderSdk.Unity
{
/// <summary>
/// Interface for WebSocket transport adapters used by the realtime transport bridge.
/// Provides connect, send, close, and main-thread event dispatch for WebSocket connections.
/// </summary>
public interface IWebSocketAdapter
{
event Action Closed;
event Action<ArraySegment<byte>> Received;
event Action<Exception> ReceivedError;
bool IsConnected { get; }
bool IsConnecting { get; }
Task ConnectAsync(Uri uri, int timeoutSeconds, CancellationToken cancellationToken = default);
Task CloseAsync();
Task SendAsync(ArraySegment<byte> data, CancellationToken cancellationToken = default);
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: a7216258d1bc14b1699fce5966a73073
@@ -1,312 +0,0 @@
#if UNITY_WEBGL && !UNITY_EDITOR
using System.Runtime.InteropServices;
#endif
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
namespace RudderSdk.Unity
{
/// <summary>
/// WebGL WebSocket adapter using JavaScript interop via DllImport("__Internal__").
/// Uses SendMessage callbacks from JS to route WebSocket events to Unity's main thread.
/// Only functional in WebGL builds; throws PlatformNotSupportedException elsewhere.
/// </summary>
public sealed class JsWebSocketAdapter : IWebSocketAdapter, IDisposable
{
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal__")]
private static extern void RudderWebSocketCreate(string gameObjectName, string url);
[DllImport("__Internal__")]
private static extern void RudderWebSocketSend(string gameObjectName, byte[] data, int length);
[DllImport("__Internal__")]
private static extern void RudderWebSocketClose(string gameObjectName);
#endif
private CancellationTokenSource _cts;
private readonly ConcurrentQueue<Action> _eventQueue = new();
private JsWebSocketBridge _bridge;
private TaskCompletionSource<bool> _connectionTcs;
private volatile bool _isConnected;
private volatile bool _isConnecting;
private bool _disposed;
/// <summary>
/// Fired when the WebSocket connection is closed.
/// </summary>
public event Action Closed;
/// <summary>
/// Fired when binary data is received from the server.
/// </summary>
public event Action<ArraySegment<byte>> Received;
/// <summary>
/// Fired when an error occurs on the WebSocket.
/// </summary>
public event Action<Exception> ReceivedError;
/// <summary>
/// True when the WebSocket connection is open.
/// </summary>
public bool IsConnected => _isConnected;
/// <summary>
/// True while the WebSocket connection is being established.
/// </summary>
public bool IsConnecting => _isConnecting;
public JsWebSocketAdapter()
{
var go = new GameObject("JsWebSocketBridge");
UnityEngine.Object.DontDestroyOnLoad(go);
_bridge = go.AddComponent<JsWebSocketBridge>();
_bridge.Init(this);
}
/// <summary>
/// Connects to the specified WebSocket URI via the JavaScript WebSocket API.
/// </summary>
/// <param name="uri">The WebSocket server URI (ws:// or wss://).</param>
/// <param name="timeoutSeconds">Connection timeout in seconds.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <exception cref="ArgumentNullException">Thrown when uri is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when timeoutSeconds is not positive.</exception>
/// <exception cref="TimeoutException">Thrown when the connection attempt times out.</exception>
/// <exception cref="PlatformNotSupportedException">Thrown when not running in WebGL.</exception>
public Task ConnectAsync(Uri uri, int timeoutSeconds, CancellationToken cancellationToken = default)
{
if (uri == null)
throw new ArgumentNullException(nameof(uri));
if (timeoutSeconds <= 0)
throw new ArgumentOutOfRangeException(nameof(timeoutSeconds), "Timeout must be greater than zero.");
#if UNITY_WEBGL && !UNITY_EDITOR
_isConnecting = true;
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_connectionTcs = new TaskCompletionSource<bool>();
// Register cancellation (including timeout)
_cts.Token.Register(() =>
{
if (_connectionTcs?.TrySetException(
new TimeoutException($"WebSocket connection timed out after {timeoutSeconds} seconds.")) == true)
{
_isConnecting = false;
}
});
_cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
RudderWebSocketCreate(_bridge.gameObject.name, uri.ToString());
return _connectionTcs.Task;
#else
// Not available outside WebGL builds; editor test uses NativeWebSocketAdapter.
return Task.FromException(new PlatformNotSupportedException(
"JsWebSocketAdapter is only supported on WebGL. Use NativeWebSocketAdapter for other platforms."));
#endif
}
/// <summary>
/// Gracefully closes the WebSocket connection via the JavaScript WebSocket API.
/// </summary>
public Task CloseAsync()
{
#if UNITY_WEBGL && !UNITY_EDITOR
RudderWebSocketClose(_bridge.gameObject.name);
#endif
_cts?.Cancel();
_isConnected = false;
_isConnecting = false;
EnqueueEvent(() => Closed?.Invoke());
return Task.CompletedTask;
}
/// <summary>
/// Sends binary data over the WebSocket via the JavaScript WebSocket API.
/// </summary>
public Task SendAsync(ArraySegment<byte> data, CancellationToken cancellationToken = default)
{
if (!_isConnected) return Task.CompletedTask;
#if UNITY_WEBGL && !UNITY_EDITOR
byte[] arr;
int offset;
int count;
if (data.Offset == 0 && data.Count == data.Array.Length)
{
arr = data.Array;
offset = 0;
count = data.Count;
}
else
{
// Copy to a contiguous buffer since DllImport needs a pinned array from offset 0
arr = new byte[data.Count];
System.Buffer.BlockCopy(data.Array, data.Offset, arr, 0, data.Count);
offset = 0;
count = data.Count;
}
RudderWebSocketSend(_bridge.gameObject.name, arr, count);
#endif
return Task.CompletedTask;
}
/// <summary>
/// Disposes the adapter, cleaning up the bridge GameObject.
/// </summary>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
#if UNITY_WEBGL && !UNITY_EDITOR
RudderWebSocketClose(_bridge.gameObject.name);
#endif
_cts?.Cancel();
_cts?.Dispose();
if (_bridge != null && _bridge.gameObject != null)
{
UnityEngine.Object.Destroy(_bridge.gameObject);
}
}
/// <summary>
/// Called by JsWebSocketBridge when the JS WebSocket onopen fires.
/// </summary>
internal void HandleOpen()
{
_isConnected = true;
_isConnecting = false;
_connectionTcs?.TrySetResult(true);
}
/// <summary>
/// Called by JsWebSocketBridge when the JS WebSocket onclose fires.
/// </summary>
internal void HandleClose()
{
_isConnected = false;
_isConnecting = false;
_connectionTcs?.TrySetCanceled();
Closed?.Invoke();
}
/// Called by JsWebSocketBridge when the JS WebSocket onerror fires.
/// </summary>
internal void HandleError(string errorMessage)
{
_isConnecting = false;
_connectionTcs?.TrySetException(new Exception(errorMessage));
ReceivedError?.Invoke(new Exception(errorMessage));
}
/// Called by JsWebSocketBridge when binary message data is received.
/// </summary>
internal void HandleMessage(ArraySegment<byte> data)
{
Received?.Invoke(data);
}
internal void HandleMessageError(Exception ex)
{
ReceivedError?.Invoke(ex);
}
internal void EnqueueEvent(Action action)
{
_eventQueue.Enqueue(action);
}
internal void ProcessEvents()
{
while (_eventQueue.TryDequeue(out var action))
{
try
{
action?.Invoke();
}
catch (Exception ex)
{
Debug.LogException(ex);
}
}
}
}
/// <summary>
/// MonoBehaviour that receives SendMessage callbacks from the JavaScript WebSocket bridge
/// and dispatches events on the Unity main thread.
/// </summary>
internal class JsWebSocketBridge : MonoBehaviour
{
private JsWebSocketAdapter _adapter;
public void Init(JsWebSocketAdapter adapter)
{
_adapter = adapter;
}
/// <summary>
/// Called from JavaScript via SendMessage when the WebSocket connection opens.
/// </summary>
public void OnOpen(string _)
{
_adapter?.EnqueueEvent(() => _adapter?.HandleOpen());
}
/// <summary>
/// Called from JavaScript via SendMessage when binary data is received.
/// Data is base64-encoded by the JS bridge and decoded here.
/// </summary>
public void OnMessage(string base64Data)
{
if (string.IsNullOrEmpty(base64Data) || _adapter == null) return;
try
{
var data = Convert.FromBase64String(base64Data);
var segment = new ArraySegment<byte>(data);
_adapter.EnqueueEvent(() => _adapter.HandleMessage(segment));
}
catch (FormatException ex)
{
Debug.LogError($"JsWebSocketBridge: Failed to decode base64 message: {ex.Message}");
_adapter.EnqueueEvent(() => _adapter.HandleMessageError(ex));
}
}
/// <summary>
/// Called from JavaScript via SendMessage when the WebSocket connection closes.
/// </summary>
public void OnClose(string _)
{
_adapter?.EnqueueEvent(() => _adapter?.HandleClose());
}
/// <summary>
/// Called from JavaScript via SendMessage when a WebSocket error occurs.
/// </summary>
public void OnError(string error)
{
_adapter?.EnqueueEvent(() => _adapter?.HandleError(error ?? "Unknown WebSocket error"));
}
private void Update()
{
_adapter?.ProcessEvents();
}
private void OnDestroy()
{
_adapter?.Dispose();
_adapter = null;
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: e53d8ae2e7487470280f0da178c56cf8
@@ -1,233 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
namespace RudderSdk.Unity
{
/// <summary>
/// Native WebSocket adapter using System.Net.WebSockets.ClientWebSocket.
/// Dispatches received events to the Unity main thread via a MonoBehaviour Update loop.
/// </summary>
public sealed class NativeWebSocketAdapter : IWebSocketAdapter, IDisposable
{
private ClientWebSocket _ws;
private CancellationTokenSource _cts;
private readonly WebSocketDispatcher _dispatcher;
private readonly ConcurrentQueue<Action> _eventQueue = new();
private bool _disposed;
/// <summary>
/// Fired when the WebSocket connection is closed.
/// </summary>
public event Action Closed;
/// <summary>
/// Fired when binary data is received from the server.
/// </summary>
public event Action<ArraySegment<byte>> Received;
/// <summary>
/// Fired when an error occurs on the receive loop.
/// </summary>
public event Action<Exception> ReceivedError;
/// <summary>
/// True when the WebSocket is in the Open state.
/// </summary>
public bool IsConnected => _ws?.State == WebSocketState.Open;
/// <summary>
/// True when the WebSocket is in the Connecting state.
/// </summary>
public bool IsConnecting => _ws?.State == WebSocketState.Connecting;
public NativeWebSocketAdapter()
{
var go = new GameObject("WebSocketDispatcher");
UnityEngine.Object.DontDestroyOnLoad(go);
_dispatcher = go.AddComponent<WebSocketDispatcher>();
_dispatcher.Init(this);
}
/// <summary>
/// Connects to the specified WebSocket URI.
/// </summary>
/// <param name="uri">The WebSocket server URI (ws:// or wss://).</param>
/// <param name="timeoutSeconds">Connection timeout in seconds.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <exception cref="ArgumentNullException">Thrown when uri is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when timeoutSeconds is not positive.</exception>
/// <exception cref="TimeoutException">Thrown when the connection attempt times out.</exception>
public async Task ConnectAsync(Uri uri, int timeoutSeconds, CancellationToken cancellationToken = default)
{
if (uri == null)
throw new ArgumentNullException(nameof(uri));
if (timeoutSeconds <= 0)
throw new ArgumentOutOfRangeException(nameof(timeoutSeconds), "Timeout must be greater than zero.");
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_ws = new ClientWebSocket();
_ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(30);
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds));
using var linked = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, timeoutCts.Token);
try
{
await _ws.ConnectAsync(uri, linked.Token);
_ = ReceiveLoop();
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
throw new TimeoutException($"WebSocket connection timed out after {timeoutSeconds} seconds.");
}
}
/// <summary>
/// Gracefully closes the WebSocket connection.
/// </summary>
public async Task CloseAsync()
{
if (_ws?.State == WebSocketState.Open)
{
try
{
await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client closing", CancellationToken.None);
}
catch
{
// Best-effort close
}
}
_cts?.Cancel();
EnqueueEvent(() => Closed?.Invoke());
}
/// <summary>
/// Sends binary data over the WebSocket. WebSocket transport is always reliable,
/// so no reliability parameter is needed.
/// </summary>
public async Task SendAsync(ArraySegment<byte> data, CancellationToken cancellationToken = default)
{
if (_ws?.State != WebSocketState.Open)
return;
await _ws.SendAsync(data, WebSocketMessageType.Binary, true, cancellationToken);
}
/// <summary>
/// Disposes the adapter, cancelling any in-flight operations and cleaning up the dispatcher GameObject.
/// </summary>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_cts?.Cancel();
_cts?.Dispose();
_ws?.Dispose();
if (_dispatcher != null && _dispatcher.gameObject != null)
{
UnityEngine.Object.Destroy(_dispatcher.gameObject);
}
}
private async Task ReceiveLoop()
{
var buffer = new byte[65536];
try
{
while (!_cts.IsCancellationRequested && _ws.State == WebSocketState.Open)
{
using (var ms = new MemoryStream())
{
WebSocketReceiveResult result;
do
{
result = await _ws.ReceiveAsync(new ArraySegment<byte>(buffer), _cts.Token);
ms.Write(buffer, 0, result.Count);
} while (!result.EndOfMessage);
if (result.MessageType == WebSocketMessageType.Close)
{
try
{
await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
}
catch
{
// Best-effort close
}
EnqueueEvent(() => Closed?.Invoke());
return;
}
var data = ms.ToArray();
EnqueueEvent(() => Received?.Invoke(new ArraySegment<byte>(data)));
}
}
}
catch (OperationCanceledException)
{
// Normal cancellation, no action needed
}
catch (Exception ex) when (!_disposed)
{
EnqueueEvent(() => ReceivedError?.Invoke(ex));
EnqueueEvent(() => Closed?.Invoke());
}
}
internal void EnqueueEvent(Action action)
{
_eventQueue.Enqueue(action);
}
internal void ProcessEvents()
{
while (_eventQueue.TryDequeue(out var action))
{
try
{
action?.Invoke();
}
catch (Exception ex)
{
Debug.LogException(ex);
}
}
}
}
/// <summary>
/// MonoBehaviour used to dispatch WebSocket events on the Unity main thread via Update.
/// Created and managed by NativeWebSocketAdapter.
/// </summary>
internal class WebSocketDispatcher : MonoBehaviour
{
private NativeWebSocketAdapter _adapter;
public void Init(NativeWebSocketAdapter adapter)
{
_adapter = adapter;
}
private void Update()
{
_adapter?.ProcessEvents();
}
private void OnDestroy()
{
_adapter?.Dispose();
_adapter = null;
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 508609c897805437dab5527c21c9f914
+58 -92
View File
@@ -15,8 +15,8 @@ namespace RudderSdk.Unity
/// <summary> /// <summary>
/// Scene bootstrap MonoBehaviour and public entrypoint of the Rudder Unity SDK. /// Scene bootstrap MonoBehaviour and public entrypoint of the Rudder Unity SDK.
/// Add the component to a scene with a RudderConfiguration assigned, or call /// Put this on a startup scene object, assign a RudderConfiguration, then call
/// Rudder.Initialize(configuration) and the hidden GameObject is created for you. /// <see cref="Initialize"/>. Feature calls live on the returned Core client.
/// </summary> /// </summary>
[DefaultExecutionOrder(-1500), DisallowMultipleComponent] [DefaultExecutionOrder(-1500), DisallowMultipleComponent]
public class Rudder : MonoBehaviour public class Rudder : MonoBehaviour
@@ -43,33 +43,77 @@ namespace RudderSdk.Unity
if (State != RudderState.Ready || _client == null) if (State != RudderState.Ready || _client == null)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
"Rudder is not initialized. Add the Rudder component to the scene with a " + "Rudder is not initialized. Add the Rudder component to a scene with a " +
"RudderConfiguration assigned, or call Rudder.Initialize(configuration)."); "RudderConfiguration assigned, then call Rudder.Initialize().");
} }
return _client; return _client;
} }
} }
/// Completes with the initialized client, or faults with the initialization error. /// Completes with the client after <see cref="Initialize"/>, or faults on
/// the initialization error. Prefer <see cref="Initialize"/> in game code.
public static Task<CoreClient> Ready => _readyCompletion.Task; public static Task<CoreClient> Ready => _readyCompletion.Task;
public static void Initialize(RudderConfiguration configuration) public static CoreClient Initialize()
{ {
if (configuration == null) if (State == RudderState.Ready && _client != null)
throw new ArgumentNullException(nameof(configuration)); return _client;
if (State == RudderState.Initializing || State == RudderState.Ready) if (State == RudderState.Failed)
throw new InvalidOperationException("Rudder is already initialized."); {
_readyCompletion = NewCompletion();
LastError = null;
State = RudderState.NotInitialized;
}
if (_instance == null) if (_instance == null)
{ {
var host = new GameObject(nameof(Rudder)); throw new InvalidOperationException(
_instance = host.AddComponent<Rudder>(); "Rudder is not in the scene. Add the Rudder component to a startup scene " +
"and assign a RudderConfiguration.");
} }
_instance._configuration = configuration; if (_instance._configuration == null)
_ = _instance.InitializeAsync(); {
throw new InvalidOperationException(
"Rudder has no configuration. Assign a RudderConfiguration on the Rudder " +
"component (Assets > Create > Rudder > Configuration).");
}
try
{
State = RudderState.Initializing;
LastError = null;
var options = new RudderUnityClientOptions
{
BaseUrl = _instance._configuration.BaseUrl,
ProjectKey = _instance._configuration.ProjectKey,
TimeoutSeconds = _instance._configuration.TimeoutSeconds,
KeyValueStore = new PlayerPrefsUnityKeyValueStore(),
LoggerSink = new UnityDebugLoggerSink()
};
_client = RudderUnityClientFactory.Create(options);
}
catch (Exception exception)
{
_client = null;
State = RudderState.Failed;
LastError = exception;
_readyCompletion.TrySetException(exception);
Debug.LogError(
"[Rudder] Failed to initialize the Rudder SDK. Check the RudderConfiguration asset " +
$"(ProjectKey and BaseUrl must be set). {exception.Message}");
throw;
}
State = RudderState.Ready;
Debug.Log("[Rudder] Rudder SDK initialized.");
_readyCompletion.TrySetResult(_client);
Initialized?.Invoke(_client);
return _client;
} }
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
@@ -95,75 +139,16 @@ namespace RudderSdk.Unity
DontDestroyOnLoad(gameObject); DontDestroyOnLoad(gameObject);
} }
private void Start()
{
if (_instance == this && _configuration != null && State == RudderState.NotInitialized)
_ = InitializeAsync();
}
private async Task InitializeAsync()
{
State = RudderState.Initializing;
LastError = null;
try
{
var options = new RudderUnityClientOptions
{
BaseUrl = _configuration.BaseUrl,
RealtimeUrl = _configuration.RealtimeUrl,
ProjectKey = _configuration.ProjectKey,
TimeoutSeconds = _configuration.TimeoutSeconds,
KeyValueStore = new PlayerPrefsUnityKeyValueStore(),
LoggerSink = new UnityDebugLoggerSink()
};
_client = RudderUnityClientFactory.Create(options);
try
{
await _client.Scenario.RestoreAsync();
}
catch (Exception restoreException)
{
Debug.LogWarning("[Rudder] Failed to restore scenario state: " + restoreException.Message);
}
}
catch (Exception exception)
{
_client = null;
State = RudderState.Failed;
LastError = exception;
_readyCompletion.TrySetException(exception);
Debug.LogError(
"[Rudder] Failed to initialize the Rudder SDK. Check the RudderConfiguration asset " +
$"(ProjectKey and BaseUrl must be set). {exception.Message}");
return;
}
State = RudderState.Ready;
Debug.Log("[Rudder] Rudder SDK initialized.");
_readyCompletion.TrySetResult(_client);
Initialized?.Invoke(_client);
}
private void Update() private void Update()
{ {
_client?.Update(Time.deltaTime); _client?.Update(Time.deltaTime);
} }
private void OnApplicationQuit()
{
DisconnectRealtime();
}
private void OnDestroy() private void OnDestroy()
{ {
if (_instance != this) if (_instance != this)
return; return;
DisconnectRealtime();
_instance = null; _instance = null;
_client = null; _client = null;
State = RudderState.NotInitialized; State = RudderState.NotInitialized;
@@ -172,25 +157,6 @@ namespace RudderSdk.Unity
_readyCompletion = NewCompletion(); _readyCompletion = NewCompletion();
} }
private void DisconnectRealtime()
{
var client = _client;
if (client == null)
return;
try
{
_ = client.Realtime.DisconnectAsync().ContinueWith(
task => Debug.LogWarning(
"[Rudder] Realtime disconnect failed: " + task.Exception?.GetBaseException().Message),
TaskContinuationOptions.OnlyOnFaulted);
}
catch (Exception exception)
{
Debug.LogWarning("[Rudder] Realtime disconnect failed: " + exception.Message);
}
}
private static TaskCompletionSource<CoreClient> NewCompletion() private static TaskCompletionSource<CoreClient> NewCompletion()
{ {
return new TaskCompletionSource<CoreClient>(TaskCreationOptions.RunContinuationsAsynchronously); return new TaskCompletionSource<CoreClient>(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -11,9 +11,6 @@ namespace RudderSdk.Unity
[Tooltip("API base URL, e.g. https://api.rudder.build.")] [Tooltip("API base URL, e.g. https://api.rudder.build.")]
public string BaseUrl = "https://api.rudder.build"; public string BaseUrl = "https://api.rudder.build";
[Tooltip("Realtime websocket URL. The relay is not deployed yet; set this only once it is.")]
public string RealtimeUrl = "wss://api.rudder.build/api/realtime/ws";
[Tooltip("HTTP request timeout in seconds.")] [Tooltip("HTTP request timeout in seconds.")]
[Min(1)] [Min(1)]
public int TimeoutSeconds = 10; public int TimeoutSeconds = 10;
@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 2dda93a7ed40941b3a831824b938fe46 guid: 53ede345941d4634ac87be4a7bbda9cd
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}
@@ -0,0 +1,303 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 0000000000000000d000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 2
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &100001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 100003}
- component: {fileID: 100002}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!20 &100002
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100001}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 2
m_BackGroundColor: {r: 0.12, g: 0.13, b: 0.15, a: 1}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 1
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &100003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &200001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 200003}
- component: {fileID: 200002}
m_Layer: 0
m_Name: Sample
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &200002
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 200001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: ef14aea70c3b46e0a15e09dce6e0e810, type: 3}
m_Name:
m_EditorClassIdentifier: Rudder.Unity.Samples::RudderSdk.Unity.Samples.AuthenticationSample
nickname: Player
region: global
language: en
--- !u!4 &200003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 200001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &300001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 300003}
- component: {fileID: 300002}
m_Layer: 0
m_Name: Rudder
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &300002
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 300001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: efb778515f4ca44289f365faa2acecd7, type: 3}
m_Name:
m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.Rudder
_configuration: {fileID: 11400000, guid: 98d531126f0d748ceacfb85bdd959e81, type: 2}
--- !u!4 &300003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 300001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 100003}
- {fileID: 300003}
- {fileID: 200003}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 90eddd30462947fca53cffee662972fa
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,87 @@
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using RudderSdk.Unity;
namespace RudderSdk.Unity.Samples
{
public class AuthenticationSample : MonoBehaviour
{
[SerializeField] string nickname = "Player";
[SerializeField] string region = "global";
[SerializeField] string language = "en";
readonly SamplePage _page = new SamplePage(
"Authentication",
"Initialize the SDK, sign in with the device id, then load the player profile.");
void OnGUI()
{
_page.Draw(() =>
{
GUILayout.BeginHorizontal();
_page.Label("Nickname");
nickname = GUILayout.TextField(nickname, GUILayout.MaxWidth(200f));
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
if (_page.Button("Login with device"))
_page.Run(this, Login);
if (_page.Button("Load profile"))
_page.Run(this, LoadProfile);
if (_page.Button("Logout"))
Logout();
GUILayout.EndHorizontal();
});
}
async Task Login()
{
var client = Rudder.Initialize();
_page.Log("Auth.LoginWithDeviceAsync(" + region + ", " + language + ", " + nickname + ")");
await client.Auth.LoginWithDeviceAsync(region, language, nickname);
_page.SetStatus("Signed in");
await LoadProfile();
}
async Task LoadProfile()
{
var client = Rudder.Initialize();
_page.Log("Player.GetProfileAsync()");
var profile = await client.Player.GetProfileAsync();
var player = profile?.Player;
var text = new StringBuilder();
text.AppendLine("Player id: " + (player?.Id ?? "—"));
text.AppendLine("Nickname: " + (player?.Nickname ?? "—"));
text.AppendLine("Region: " + (player?.Region ?? "—"));
text.AppendLine("Language: " + (player?.Language ?? "—"));
if (profile?.Wallets == null || profile.Wallets.Count == 0)
{
text.Append("Wallets: none");
}
else
{
text.Append("Wallets:");
foreach (var wallet in profile.Wallets)
text.Append("\n " + wallet.Balance + " " + wallet.Currency);
}
_page.SetOutput(text.ToString());
_page.SetStatus("Profile loaded");
}
void Logout()
{
if (Rudder.State != RudderState.Ready)
{
_page.SetStatus("SDK is not initialized.", true);
return;
}
Rudder.Client.Auth.Logout();
_page.SetOutput(string.Empty);
_page.SetStatus("Signed out");
_page.Log("Auth.Logout()");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ef14aea70c3b46e0a15e09dce6e0e810
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 3c34eb0ba13fe47eab84ba9a30d8bdb6 guid: 770e3347e0604c359d5ca59689007d42
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}
@@ -0,0 +1,304 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 0000000000000000d000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 2
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &100001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 100003}
- component: {fileID: 100002}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!20 &100002
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100001}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 2
m_BackGroundColor: {r: 0.12, g: 0.13, b: 0.15, a: 1}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 1
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &100003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &200001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 200003}
- component: {fileID: 200002}
m_Layer: 0
m_Name: Sample
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &200002
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 200001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6cbf51073b96453cba7f019ab7b790be, type: 3}
m_Name:
m_EditorClassIdentifier: Rudder.Unity.Samples::RudderSdk.Unity.Samples.LeaderboardsSample
nickname: Player
region: global
language: en
slug: cozy-collector-score
--- !u!4 &200003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 200001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &300001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 300003}
- component: {fileID: 300002}
m_Layer: 0
m_Name: Rudder
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &300002
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 300001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: efb778515f4ca44289f365faa2acecd7, type: 3}
m_Name:
m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.Rudder
_configuration: {fileID: 11400000, guid: 98d531126f0d748ceacfb85bdd959e81, type: 2}
--- !u!4 &300003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 300001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 100003}
- {fileID: 300003}
- {fileID: 200003}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7e30e842ebc54b63a9885d5200c91557
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,93 @@
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using RudderSdk.Unity;
namespace RudderSdk.Unity.Samples
{
public class LeaderboardsSample : MonoBehaviour
{
[SerializeField] string nickname = "Player";
[SerializeField] string region = "global";
[SerializeField] string language = "en";
[SerializeField] string slug = "cozy-collector-score";
readonly SamplePage _page = new SamplePage(
"Leaderboards",
"Submit a score to a board by slug, then list the top entries.");
string _scoreText = "10";
void Start()
{
_page.Run(this, async () =>
{
await Rudder.Initialize().Auth.LoginWithDeviceAsync(region, language, nickname);
_page.Log("Signed in");
await List();
});
}
void OnGUI()
{
_page.Draw(() =>
{
GUILayout.BeginHorizontal();
_page.Label("Score");
_scoreText = GUILayout.TextField(_scoreText, GUILayout.MaxWidth(120f));
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
if (_page.Button("Submit"))
_page.Run(this, Submit);
if (_page.Button("List top 10"))
_page.Run(this, List);
GUILayout.EndHorizontal();
});
}
async Task Submit()
{
if (!int.TryParse(_scoreText, out var score))
{
_page.SetStatus("Score must be an integer.", true);
return;
}
var client = Rudder.Initialize();
var board = client.Leaderboards.FindBySlug(slug);
_page.Log("Leaderboards.FindBySlug(\"" + slug + "\").SubmitAsync(" + score + ")");
await board.SubmitAsync(score);
_page.SetStatus("Submitted " + score);
await List();
}
async Task List()
{
var client = Rudder.Initialize();
var board = client.Leaderboards.FindBySlug(slug);
_page.Log("FindBySlug(\"" + slug + "\").ListAsync(10)");
var entries = await board.ListAsync(10);
var text = new StringBuilder();
text.AppendLine(slug);
if (entries == null || entries.Count == 0)
{
text.Append("No entries yet.");
}
else
{
foreach (var entry in entries)
{
var name = entry.PlayerName;
if (string.IsNullOrEmpty(name))
name = entry.PlayerId;
text.AppendLine("#" + entry.Rank + " " + name + " " + entry.Score);
}
}
_page.SetOutput(text.ToString());
_page.SetStatus("Listed " + (entries == null ? 0 : entries.Count) + " entries");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6cbf51073b96453cba7f019ab7b790be
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+20
View File
@@ -0,0 +1,20 @@
# Rudder feature samples
Small scenes that show one Rudder API each. Open a scene, assign a
`RudderConfiguration` if the field is empty, press Play.
| Scene | Shows |
| --- | --- |
| `Authentication/Authentication.unity` | `Rudder.Initialize`, `Auth.LoginWithDeviceAsync`, `Player.GetProfileAsync`, `Auth.Logout` |
| `RemoteConfig/RemoteConfig.unity` | `RemoteConfig.LoadAsync`, `Get(key, fallback)` |
| `Storage/Storage.unity` | `Storage.GetAsync`, `SaveAsync`, `DeleteAsync` |
| `StoreInventory/StoreInventory.unity` | `Stores.ListAsync`, `PurchaseAsync`, `Inventory.GetAsync` |
| `Leaderboards/Leaderboards.unity` | `Leaderboards.FindBySlug`, `SubmitAsync`, `ListAsync` |
| `Scenarios/Scenarios.unity` | `Scenario.TriggerAsync` and `Effects.OnNotification` / `Effects.OnStoreOffer` / … |
Each scene has a `Rudder` component using `Assets/LiveOpsLocal.asset`.
Login uses the device id owned by the SDK (PlayerPrefs).
Stage data that makes the later scenes interesting is listed in the project
README (store `cozy-camp-shop`, leaderboard `cozy-collector-score`, scenario
events `player_login` / `demo_round_finished`).
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 94f8aa6a797a488581772bb70594a153
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 964c9816fce94c8392cc1c85d5d21a83
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,305 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 0000000000000000d000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 2
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &100001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 100003}
- component: {fileID: 100002}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!20 &100002
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100001}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 2
m_BackGroundColor: {r: 0.12, g: 0.13, b: 0.15, a: 1}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 1
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &100003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &200001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 200003}
- component: {fileID: 200002}
m_Layer: 0
m_Name: Sample
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &200002
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 200001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 75288bf5899a4ee2a805b1de8d640fc0, type: 3}
m_Name:
m_EditorClassIdentifier: Rudder.Unity.Samples::RudderSdk.Unity.Samples.RemoteConfigSample
nickname: Player
region: global
language: en
roundSecondsKey: demo_round_seconds
playerSpeedKey: demo_player_speed
--- !u!4 &200003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 200001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &300001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 300003}
- component: {fileID: 300002}
m_Layer: 0
m_Name: Rudder
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &300002
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 300001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: efb778515f4ca44289f365faa2acecd7, type: 3}
m_Name:
m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.Rudder
_configuration: {fileID: 11400000, guid: 98d531126f0d748ceacfb85bdd959e81, type: 2}
--- !u!4 &300003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 300001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 100003}
- {fileID: 300003}
- {fileID: 200003}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 232ad1be9e3840e9ae3f64b58db8992d
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,60 @@
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using RudderSdk.Unity;
namespace RudderSdk.Unity.Samples
{
public class RemoteConfigSample : MonoBehaviour
{
[SerializeField] string nickname = "Player";
[SerializeField] string region = "global";
[SerializeField] string language = "en";
[SerializeField] string roundSecondsKey = "demo_round_seconds";
[SerializeField] string playerSpeedKey = "demo_player_speed";
readonly SamplePage _page = new SamplePage(
"Remote Config",
"Load the project's remote config cache, then read typed values with fallbacks.");
void Start()
{
_page.Run(this, async () =>
{
await Rudder.Initialize().Auth.LoginWithDeviceAsync(region, language, nickname);
_page.Log("Signed in");
await Load();
});
}
void OnGUI()
{
_page.Draw(() =>
{
if (_page.Button("Reload"))
_page.Run(this, Load);
});
}
async Task Load()
{
var client = Rudder.Initialize();
_page.Log("RemoteConfig.LoadAsync()");
var configs = await client.RemoteConfig.LoadAsync();
var roundSeconds = client.RemoteConfig.Get(roundSecondsKey, 150f);
var playerSpeed = client.RemoteConfig.Get(playerSpeedKey, 260f);
var text = new StringBuilder();
text.AppendLine(roundSecondsKey + " = " + roundSeconds);
text.AppendLine(playerSpeedKey + " = " + playerSpeed);
text.AppendLine();
text.AppendLine("All keys (" + configs.Count + "):");
foreach (var pair in configs)
text.AppendLine(" " + pair.Key + " = " + pair.Value.Value + " (" + pair.Value.ValueType + ")");
_page.SetOutput(text.ToString());
_page.SetStatus("Loaded " + configs.Count + " config(s)");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 75288bf5899a4ee2a805b1de8d640fc0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
{
"name": "Rudder.Unity.Samples",
"rootNamespace": "RudderSdk.Unity.Samples",
"references": [
"Rudder.Unity"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9b15bdad283e494c9f77a456c0f06d07
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 15b529225eaf43f093572b9d8938c6fe
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,305 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 0000000000000000d000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 2
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &100001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 100003}
- component: {fileID: 100002}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!20 &100002
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100001}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 2
m_BackGroundColor: {r: 0.12, g: 0.13, b: 0.15, a: 1}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 1
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &100003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &200001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 200003}
- component: {fileID: 200002}
m_Layer: 0
m_Name: Sample
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &200002
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 200001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 72ab1e68bc9f48588c945a033026622e, type: 3}
m_Name:
m_EditorClassIdentifier: Rudder.Unity.Samples::RudderSdk.Unity.Samples.ScenariosSample
nickname: Player
region: global
language: en
loginEvent: player_login
roundEvent: demo_round_finished
--- !u!4 &200003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 200001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &300001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 300003}
- component: {fileID: 300002}
m_Layer: 0
m_Name: Rudder
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &300002
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 300001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: efb778515f4ca44289f365faa2acecd7, type: 3}
m_Name:
m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.Rudder
_configuration: {fileID: 11400000, guid: 98d531126f0d748ceacfb85bdd959e81, type: 2}
--- !u!4 &300003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 300001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 100003}
- {fileID: 300003}
- {fileID: 200003}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d57ceb109f374f93862dd52f93d5961f
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,201 @@
using System;
using System.Threading.Tasks;
using RudderSdk.Core;
using UnityEngine;
using RudderSdk.Unity;
namespace RudderSdk.Unity.Samples
{
public class ScenariosSample : MonoBehaviour
{
[SerializeField] string nickname = "Player";
[SerializeField] string region = "global";
[SerializeField] string language = "en";
[SerializeField] string loginEvent = "player_login";
[SerializeField] string roundEvent = "demo_round_finished";
readonly SamplePage _page = new SamplePage(
"Scenarios",
"Subscribe to scenario effects, trigger an event, then resolve nodes from UI.");
StoreOfferEffect _offer;
bool _subscribed;
void Start()
{
_page.Run(this, async () =>
{
await Rudder.Initialize().Auth.LoginWithDeviceAsync(region, language, nickname);
Subscribe();
_page.SetStatus("Signed in, listening for scenario effects");
_page.Log("Subscribed to Effects.On*");
});
}
void OnDestroy()
{
Unsubscribe();
}
void OnGUI()
{
_page.Draw(() =>
{
GUILayout.BeginHorizontal();
if (_page.Button("Trigger " + loginEvent))
_page.Run(this, () => Trigger(loginEvent));
if (_page.Button("Trigger " + roundEvent))
_page.Run(this, () => Trigger(roundEvent));
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
if (_page.Button("Buy offer"))
_page.Run(this, BuyOffer);
if (_page.Button("Decline offer"))
DeclineOffer();
GUILayout.EndHorizontal();
});
}
async Task Trigger(string eventName)
{
var client = Rudder.Initialize();
Subscribe();
_page.Log("Scenario.TriggerAsync(\"" + eventName + "\")");
await client.Scenario.TriggerAsync(eventName);
_page.SetStatus("Triggered " + eventName);
}
async Task BuyOffer()
{
if (_offer == null)
{
_page.SetStatus("No store-offer effect.", true);
return;
}
var client = Rudder.Initialize();
var storeSlug = _offer.Get("storeSlug", "cozy-camp-shop");
var store = await client.Stores.GetAsync(storeSlug);
var offer = store?.Offers != null && store.Offers.Count > 0 ? store.Offers[0] : null;
if (offer == null)
throw new InvalidOperationException("Store " + storeSlug + " has no offers.");
_page.Log("Stores.PurchaseAsync(" + storeSlug + ", " + offer.Id + ")");
var response = await client.Stores.PurchaseAsync(storeSlug, offer.Id);
if (response != null && response.Success != true)
throw new InvalidOperationException(response.Error ?? "Purchase rejected");
_offer.Purchase();
_offer = null;
_page.SetStatus("Offer purchased");
_page.Log("StoreOfferEffect.Purchase()");
}
void DeclineOffer()
{
if (_offer == null)
{
_page.SetStatus("No store-offer effect.", true);
return;
}
_offer.Decline();
_offer = null;
_page.SetStatus("Offer declined");
_page.Log("StoreOfferEffect.Decline()");
}
void Subscribe()
{
if (_subscribed || Rudder.State != RudderState.Ready)
return;
var effects = Rudder.Client.Effects;
effects.OnNotification += OnNotification;
effects.OnStoreOffer += OnStoreOffer;
effects.OnWait += OnWait;
effects.OnQuest += OnQuest;
effects.OnLeaderboard += OnLeaderboard;
effects.OnBattlePass += OnBattlePass;
effects.OnBattlePassLevel += OnBattlePassLevel;
effects.OnScenarioCompleted += OnCompleted;
effects.OnScenarioFailed += OnFailed;
_subscribed = true;
}
void Unsubscribe()
{
if (!_subscribed || Rudder.State != RudderState.Ready)
return;
var effects = Rudder.Client.Effects;
effects.OnNotification -= OnNotification;
effects.OnStoreOffer -= OnStoreOffer;
effects.OnWait -= OnWait;
effects.OnQuest -= OnQuest;
effects.OnLeaderboard -= OnLeaderboard;
effects.OnBattlePass -= OnBattlePass;
effects.OnBattlePassLevel -= OnBattlePassLevel;
effects.OnScenarioCompleted -= OnCompleted;
effects.OnScenarioFailed -= OnFailed;
_subscribed = false;
}
void OnNotification(NotificationEffect effect)
{
var title = effect.Title;
var message = effect.Message;
_page.Log("OnNotification: " + title + " " + message);
_page.SetOutput(title + "\n" + message);
effect.Done();
}
void OnStoreOffer(StoreOfferEffect effect)
{
_offer = effect;
var storeSlug = effect.Get("storeSlug", "cozy-camp-shop");
_page.Log("OnStoreOffer: " + storeSlug);
_page.SetOutput("Store offer from " + storeSlug + "\nUse Buy offer or Decline offer.");
_page.SetStatus("Store offer waiting");
}
void OnWait(WaitEffect effect)
{
_page.Log("OnWait until " + effect.DeadlineUtc.ToString("O"));
}
void OnQuest(QuestEffect effect)
{
_page.Log("OnQuest: " + effect.NodeId);
}
void OnLeaderboard(LeaderboardEffect effect)
{
_page.Log("OnLeaderboard: " + effect.NodeId);
effect.End();
}
void OnBattlePass(BattlePassEffect effect)
{
_page.Log("OnBattlePass: " + effect.NodeId);
}
void OnBattlePassLevel(BattlePassLevelEffect effect)
{
_page.Log("OnBattlePassLevel: " + effect.NodeId);
}
void OnCompleted(ScenarioCompletedEffect effect)
{
_page.Log("OnScenarioCompleted: " + effect.RunId);
_page.SetStatus("Scenario completed");
}
void OnFailed(ScenarioFailedEffect failed)
{
_page.Log("OnScenarioFailed: " + failed.NodeId + " " + failed.Exception.Message);
_page.SetStatus(failed.Exception.Message, true);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 72ab1e68bc9f48588c945a033026622e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4b81c103928547eba13e72280ba03a08
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
namespace RudderSdk.Unity.Samples
{
sealed class SamplePage
{
const int MaxLogLines = 40;
readonly string _title;
readonly string _subtitle;
readonly List<string> _logLines = new List<string>();
string _status = "Idle";
bool _error;
string _output = string.Empty;
bool _busy;
Vector2 _outputScroll;
Vector2 _logScroll;
GUIStyle _titleStyle;
GUIStyle _bodyStyle;
GUIStyle _statusStyle;
public SamplePage(string title, string subtitle)
{
_title = title;
_subtitle = subtitle;
}
public void Draw(Action body)
{
EnsureStyles();
const float pad = 16f;
GUILayout.BeginArea(new Rect(pad, pad, Screen.width - pad * 2f, Screen.height - pad * 2f));
GUILayout.Label(_title, _titleStyle);
GUILayout.Label(_subtitle, _bodyStyle);
_statusStyle.normal.textColor = _error ? new Color(1f, 0.45f, 0.4f) : Color.white;
GUILayout.Label(_status, _statusStyle);
GUILayout.Space(8f);
var enabled = GUI.enabled;
GUI.enabled = enabled && !_busy;
body();
GUI.enabled = enabled;
if (!string.IsNullOrEmpty(_output))
{
GUILayout.Space(8f);
_outputScroll = GUILayout.BeginScrollView(_outputScroll, GUILayout.MinHeight(120f));
GUILayout.Label(_output, _bodyStyle);
GUILayout.EndScrollView();
}
GUILayout.Space(8f);
GUILayout.Label("Log", _titleStyle);
_logScroll = GUILayout.BeginScrollView(_logScroll, GUILayout.MinHeight(100f));
if (_logLines.Count == 0)
GUILayout.Label("(empty)", _bodyStyle);
else
GUILayout.Label(string.Join("\n", _logLines), _bodyStyle);
GUILayout.EndScrollView();
GUILayout.EndArea();
}
public bool Button(string text)
{
return GUILayout.Button(text, GUILayout.Height(28f), GUILayout.MaxWidth(280f));
}
public void Label(string text)
{
GUILayout.Label(text, _bodyStyle);
}
public void SetStatus(string text, bool error = false)
{
_status = text;
_error = error;
}
public void SetOutput(string text)
{
_output = text ?? string.Empty;
}
public void Log(string line)
{
_logLines.Insert(0, DateTime.Now.ToString("HH:mm:ss") + " " + line);
if (_logLines.Count > MaxLogLines)
_logLines.RemoveAt(_logLines.Count - 1);
}
public async void Run(MonoBehaviour host, Func<Task> action)
{
if (_busy)
return;
_busy = true;
SetStatus("Working…");
try
{
await action();
}
catch (Exception exception)
{
await Awaitable.MainThreadAsync();
if (host == null)
return;
SetStatus(exception.Message, true);
Log(exception.Message);
}
finally
{
_busy = false;
}
}
void EnsureStyles()
{
if (_titleStyle != null)
return;
_titleStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 20,
fontStyle = FontStyle.Bold,
wordWrap = true
};
_titleStyle.normal.textColor = Color.white;
_bodyStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 14,
wordWrap = true
};
_bodyStyle.normal.textColor = Color.white;
_statusStyle = new GUIStyle(_bodyStyle);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5b306e86aff644e0adfa093a21ca4d97
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f9ab4042bed849c6a532bb712b2b863c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,305 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 0000000000000000d000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 2
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &100001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 100003}
- component: {fileID: 100002}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!20 &100002
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100001}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 2
m_BackGroundColor: {r: 0.12, g: 0.13, b: 0.15, a: 1}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 1
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &100003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &200001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 200003}
- component: {fileID: 200002}
m_Layer: 0
m_Name: Sample
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &200002
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 200001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 8fb25d5ed4f44c4f88b064592d30d17b, type: 3}
m_Name:
m_EditorClassIdentifier: Rudder.Unity.Samples::RudderSdk.Unity.Samples.StorageSample
nickname: Player
region: global
language: en
storageType: sample_save
storageId: main
--- !u!4 &200003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 200001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &300001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 300003}
- component: {fileID: 300002}
m_Layer: 0
m_Name: Rudder
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &300002
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 300001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: efb778515f4ca44289f365faa2acecd7, type: 3}
m_Name:
m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.Rudder
_configuration: {fileID: 11400000, guid: 98d531126f0d748ceacfb85bdd959e81, type: 2}
--- !u!4 &300003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 300001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 100003}
- {fileID: 300003}
- {fileID: 200003}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 0e058d0347144fb5acff8c1d22ab6367
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,105 @@
using System.Threading.Tasks;
using RudderSdk.Core.Models.Storage;
using UnityEngine;
using RudderSdk.Unity;
namespace RudderSdk.Unity.Samples
{
public class StorageSample : MonoBehaviour
{
[SerializeField] string nickname = "Player";
[SerializeField] string region = "global";
[SerializeField] string language = "en";
[SerializeField] string storageType = "sample_save";
[SerializeField] string storageId = "main";
readonly SamplePage _page = new SamplePage(
"Storage",
"Read and write a player storage item. Data is an opaque JSON string.");
string _data = "{\"bestScore\":0}";
void Start()
{
_page.Run(this, async () =>
{
await Rudder.Initialize().Auth.LoginWithDeviceAsync(region, language, nickname);
_page.Log("Signed in");
await Load();
});
}
void OnGUI()
{
_page.Draw(() =>
{
_page.Label("Data");
_data = GUILayout.TextArea(_data, GUILayout.MinHeight(72f));
GUILayout.BeginHorizontal();
if (_page.Button("Load"))
_page.Run(this, Load);
if (_page.Button("Save"))
_page.Run(this, Save);
if (_page.Button("Delete type"))
_page.Run(this, Delete);
GUILayout.EndHorizontal();
});
}
async Task Load()
{
var client = Rudder.Initialize();
_page.Log("Storage.GetAsync(type: " + storageType + ")");
var response = await client.Storage.GetAsync(storageType);
StorageItem item = null;
if (response?.Items != null)
{
foreach (var candidate in response.Items)
{
if (candidate.Id == storageId)
{
item = candidate;
break;
}
}
}
if (item == null)
{
_page.SetOutput("No item " + storageType + "/" + storageId + " yet.");
_page.SetStatus("Empty");
return;
}
_data = item.Data;
_page.SetOutput("Loaded " + item.Type + "/" + item.Id + "\n" + item.Data);
_page.SetStatus("Loaded");
}
async Task Save()
{
var client = Rudder.Initialize();
_page.Log("Storage.SaveAsync(" + storageType + "/" + storageId + ")");
await client.Storage.SaveAsync(new[]
{
new StorageItem
{
Type = storageType,
Id = storageId,
Data = _data
}
});
_page.SetOutput("Saved " + storageType + "/" + storageId);
_page.SetStatus("Saved");
}
async Task Delete()
{
var client = Rudder.Initialize();
_page.Log("Storage.DeleteAsync(" + storageType + ")");
await client.Storage.DeleteAsync(storageType);
_page.SetOutput("Deleted all items of type " + storageType);
_page.SetStatus("Deleted");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8fb25d5ed4f44c4f88b064592d30d17b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ef7f15e047164a2e97b85f62bd9e38da
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,303 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 0000000000000000d000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 2
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &100001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 100003}
- component: {fileID: 100002}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!20 &100002
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100001}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 2
m_BackGroundColor: {r: 0.12, g: 0.13, b: 0.15, a: 1}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 1
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &100003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &200001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 200003}
- component: {fileID: 200002}
m_Layer: 0
m_Name: Sample
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &200002
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 200001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7e51322b24f541848b432de2202ac5b2, type: 3}
m_Name:
m_EditorClassIdentifier: Rudder.Unity.Samples::RudderSdk.Unity.Samples.StoreInventorySample
nickname: Player
region: global
language: en
--- !u!4 &200003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 200001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &300001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 300003}
- component: {fileID: 300002}
m_Layer: 0
m_Name: Rudder
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &300002
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 300001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: efb778515f4ca44289f365faa2acecd7, type: 3}
m_Name:
m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.Rudder
_configuration: {fileID: 11400000, guid: 98d531126f0d748ceacfb85bdd959e81, type: 2}
--- !u!4 &300003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 300001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 100003}
- {fileID: 300003}
- {fileID: 200003}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 2c93fb60baaf4d5b936a324c8161529f
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using RudderSdk.Core.Models.Stores;
using UnityEngine;
using RudderSdk.Unity;
namespace RudderSdk.Unity.Samples
{
public class StoreInventorySample : MonoBehaviour
{
[SerializeField] string nickname = "Player";
[SerializeField] string region = "global";
[SerializeField] string language = "en";
readonly SamplePage _page = new SamplePage(
"Store & Inventory",
"List store offers, purchase one, then refresh wallets and inventory.");
IReadOnlyList<Store> _stores = Array.Empty<Store>();
void Start()
{
_page.Run(this, async () =>
{
await Rudder.Initialize().Auth.LoginWithDeviceAsync(region, language, nickname);
_page.Log("Signed in");
await LoadCatalog();
await LoadInventory();
});
}
void OnGUI()
{
_page.Draw(() =>
{
GUILayout.BeginHorizontal();
if (_page.Button("Refresh catalog"))
_page.Run(this, LoadCatalog);
if (_page.Button("Refresh inventory"))
_page.Run(this, LoadInventory);
GUILayout.EndHorizontal();
foreach (var store in _stores)
{
GUILayout.Space(6f);
_page.Label(store.Name + " (" + store.Slug + ")");
if (store.Offers == null || store.Offers.Count == 0)
{
_page.Label(" no offers");
continue;
}
foreach (var offer in store.Offers)
{
var slug = store.Slug;
var offerId = offer.Id;
var name = string.IsNullOrEmpty(offer.Name) ? offer.Id : offer.Name;
var price = offer.Price == null ? "free" : offer.Price.Amount + " " + offer.Price.Currency;
if (_page.Button("Buy " + name + " — " + price))
_page.Run(this, () => Buy(slug, offerId, name));
}
}
});
}
async Task LoadCatalog()
{
var client = Rudder.Initialize();
_page.Log("Stores.ListAsync()");
var stores = await client.Stores.ListAsync();
_stores = stores ?? Array.Empty<Store>();
_page.SetStatus(_stores.Count == 0 ? "No stores" : "Loaded " + _stores.Count + " store(s)");
}
async Task Buy(string storeSlug, string offerId, string name)
{
var client = Rudder.Initialize();
_page.Log("Stores.PurchaseAsync(" + storeSlug + ", " + offerId + ")");
var response = await client.Stores.PurchaseAsync(storeSlug, offerId);
if (response != null && response.Success != true)
throw new InvalidOperationException(response.Error ?? "Purchase rejected");
_page.SetStatus("Purchased " + name);
await LoadInventory();
}
async Task LoadInventory()
{
var client = Rudder.Initialize();
_page.Log("Inventory.GetAsync() / Player.GetProfileAsync()");
var items = await client.Inventory.GetAsync();
var profile = await client.Player.GetProfileAsync();
var text = new StringBuilder();
text.Append("Wallets:");
if (profile?.Wallets == null || profile.Wallets.Count == 0)
{
text.Append(" none");
}
else
{
foreach (var wallet in profile.Wallets)
text.Append("\n " + wallet.Balance + " " + wallet.Currency);
}
text.Append("\n\nInventory:");
if (items == null || items.Count == 0)
{
text.Append(" empty");
}
else
{
foreach (var item in items)
{
var name = string.IsNullOrEmpty(item.NameOverride) ? item.Slug : item.NameOverride;
text.Append("\n " + name + " × " + item.Amount);
}
}
_page.SetOutput(text.ToString());
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7e51322b24f541848b432de2202ac5b2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+10 -3
View File
@@ -1,8 +1,8 @@
{ {
"name": "rudder.sdk", "name": "rudder.sdk",
"version": "0.3.0", "version": "1.0.0",
"displayName": "Rudder SDK", "displayName": "Rudder SDK",
"description": "LiveOps SDK for Unity — player auth, remote config, stores, inventory, leaderboards, scenarios, realtime.", "description": "LiveOps SDK for Unity — player auth, remote config, stores, inventory, leaderboards, scenarios.",
"unity": "6000.0", "unity": "6000.0",
"author": { "author": {
"name": "Rudder", "name": "Rudder",
@@ -15,5 +15,12 @@
"documentationUrl": "https://app.rudder.build", "documentationUrl": "https://app.rudder.build",
"publishConfig": { "publishConfig": {
"registry": "https://hub.rudder.build/api/packages/rudder/npm/" "registry": "https://hub.rudder.build/api/packages/rudder/npm/"
} },
"samples": [
{
"displayName": "Feature Samples",
"description": "Small scenes that show one Rudder API each: authentication, remote config, storage, stores and inventory, leaderboards, and scenarios.",
"path": "Samples~"
}
]
} }
+27 -57
View File
@@ -1,21 +1,8 @@
# LiveOps Unity Cozy Collector Demo # Rudder Unity SDK
This Unity project is a playable port of the Cozy Collector web demo (`liveops-phaser-demo`). It uses the same arcade loop — move around the camp, collect spawning snacks before the round timer runs out — to show how a game can use LiveOps services without shipping a client update. This project hosts the `rudder.sdk` UPM package and the scenes that show how
to call it. Agents integrating the SDK into a game should read
The demo is assembled like a regular Unity game: the scene, world, and UI live in baked assets (scene, prefabs, ScriptableObjects, imported sprites), and runtime scripts only contain logic wired through the inspector. All assets are generated by Editor bake scripts under `Tools > LiveOps`. `Packages/rudder.sdk/AGENTS.md`.
## Demonstrated Services
| Service | Demo use |
| --- | --- |
| Authorization | Device login with the configured project key. |
| Player | Loads player profile and wallets after login; session card shows nickname, region, wallets. |
| Remote Config | Controls round duration, player speed, spawn interval, collectible score; live updates highlight changed keys. |
| Storage | Saves best score and last round state (`cozy_fantasy_save` / `main`). |
| Stores | Shop modal lists the `cozy-camp-shop` catalog; purchases debit wallets. |
| Inventory | Backpack modal lists collected items via `Rudder.Client.Inventory`. |
| Leaderboards | Submits round score to `cozy-collector-score` and shows the top entries in the round-result modal. |
| Scenarios | Sends `player_login` after login and `demo_round_finished` after a round; scenario offers show a Buy/Decline modal, `remote_config_override` applies live. |
## Requirements ## Requirements
@@ -29,62 +16,45 @@ The demo is assembled like a regular Unity game: the scene, world, and UI live i
2. Copy `Assets/LiveOpsLocal.asset.example` to `Assets/LiveOpsLocal.asset` 2. Copy `Assets/LiveOpsLocal.asset.example` to `Assets/LiveOpsLocal.asset`
(the real asset is git-ignored because it holds your project key). (the real asset is git-ignored because it holds your project key).
3. Set `ProjectKey` to your project key. 3. Set `ProjectKey` to your project key.
4. Confirm the endpoints: 4. Confirm `BaseUrl` is `https://api.rudder.build`.
- `BaseUrl`: `https://api.rudder.build` 5. Open a feature sample under `Assets/Samples/Rudder SDK/` and press Play.
- `RealtimeUrl`: `wss://realtime.rudder.build/api/realtime/ws` In this development project the samples are already linked there; do not
5. Bake the demo assets (see below), then open `Assets/LiveOpsExamples/LiveOpsCozyCollector.unity`. import them again from Package Manager.
6. Press Play.
## Baking The Demo Assets ## Feature Samples
All demo assets are generated from the Unity menu: Each scene is one Rudder API. The scripts live in
`Packages/rudder.sdk/Samples~` and are the copy-paste examples for the SDK.
```text | Scene | SDK calls |
Tools > LiveOps > Bake Cozy Collector (All) | --- | --- |
``` | `Authentication/Authentication.unity` | `Rudder.Initialize`, `Auth.LoginWithDeviceAsync`, `Player.GetProfileAsync`, `Auth.Logout` |
| `RemoteConfig/RemoteConfig.unity` | `RemoteConfig.LoadAsync`, `Get(key, fallback)` |
| `Storage/Storage.unity` | `Storage.GetAsync`, `SaveAsync`, `DeleteAsync` |
| `StoreInventory/StoreInventory.unity` | `Stores.ListAsync`, `PurchaseAsync`, `Inventory.GetAsync` |
| `Leaderboards/Leaderboards.unity` | `Leaderboards.FindBySlug`, `SubmitAsync`, `ListAsync` |
| `Scenarios/Scenarios.unity` | `Scenario.TriggerAsync`, `Effects.OnNotification`, `Effects.OnStoreOffer`, … |
The bake scripts (`Assets/LiveOpsExamples/Editor/CozyCollectorBake.*.cs`) create or overwrite: The later scenes are more interesting with this staging data:
- `Assets/LiveOpsExamples/CozyCollectorSpriteLibrary.asset` — sprite references imported from `Sprites/`.
- `Assets/LiveOpsExamples/Prefabs/` — snack, score popup, collect burst, and UI row prefabs.
- `Assets/LiveOpsExamples/LiveOpsCozyCollector.unity` — camera, world, canvas, panels; assigns `Assets/LiveOpsLocal.asset` to `Rudder`; adds the scene to Build Settings.
The bake is idempotent: re-running it rewrites the same asset paths, so run it again after changing runtime scripts or sprites.
## Stage Data Checklist
The demo handles missing backend data with visible setup messages, but the strongest run needs these staging entities:
| Feature | Expected setup | | Feature | Expected setup |
| --- | --- | | --- | --- |
| Remote Config | Optional keys: `demo_round_seconds`, `demo_player_speed`, `demo_spawn_interval_ms`, `demo_collectible_score`. Defaults are used when keys are missing. | | Remote Config | Optional keys: `demo_round_seconds`, `demo_player_speed`. Defaults are used when keys are missing. |
| Stores | A store with slug `cozy-camp-shop` with at least one offer. | | Stores | A store with slug `cozy-camp-shop` with at least one offer. |
| Leaderboards | A leaderboard with slug `cozy-collector-score`. | | Leaderboards | A leaderboard with slug `cozy-collector-score`. |
| Scenarios | Events `player_login` and/or `demo_round_finished`; offer `moonberry-boost` for the scenario offer flow. | | Scenarios | Events `player_login` and/or `demo_round_finished`. |
| Storage | No setup required; the demo writes type `cozy_fantasy_save`, id `main`. | | Storage | No setup required; the sample writes type `sample_save`, id `main`. |
## Project Structure ## Project Structure
```text ```text
Assets/LiveOpsExamples/ Assets/Samples/Rudder SDK/ (symlink to the package Samples~)
LiveOpsCozyCollector.unity (baked) Packages/rudder.sdk/
CozyCollectorSpriteLibrary.asset (baked) Runtime/ (Rudder.Unity + Rudder.Core.dll)
Sprites/ (imported from liveops-phaser-demo) Samples~/ (feature sample scenes)
Prefabs/ (baked)
Scripts/ (runtime logic only)
CozyCollectorController.cs
GameWorld.cs, SnackSpawner.cs, Snack.cs, ScorePopupView.cs
CozyCollectorConsts.cs, CozyCollectorSpriteLibrary.cs
UI/ (panels and row views)
Sandbox/ (legacy sandbox controllers)
Editor/
CozyCollectorBake.*.cs (asset/prefab/scene/UI bake)
Assets/LiveOpsLocal.asset (git-ignored; copy from LiveOpsLocal.asset.example) Assets/LiveOpsLocal.asset (git-ignored; copy from LiveOpsLocal.asset.example)
Packages/rudder.sdk/ (Rudder SDK UPM package: Runtime/Sources + Runtime/Plugins/Rudder.Core.dll)
``` ```
Runtime scripts never construct UI or scene objects; everything visual comes from the baked scene and prefabs, with references wired via `[SerializeField]`.
## Updating Core DLLs ## Updating Core DLLs
Build the C# SDK from the sibling repository and copy the assembly into the Build the C# SDK from the sibling repository and copy the assembly into the
+115
View File
@@ -0,0 +1,115 @@
---
name: rudder-unity-sdk
description: Use when working with the Rudder Unity SDK rudder.sdk in a Unity project — install, bootstrap, auth, remote config, storage, stores, inventory, leaderboards, quests, battle pass, scenarios.
---
# Rudder Unity SDK (`rudder.sdk`)
UPM package `rudder.sdk` (v0.4.0): LiveOps SDK for Unity. It wraps
`Rudder.Core.dll` (namespace `RudderSdk.Core`, the .NET SDK) with Unity
adapters in `RudderSdk.Unity`. Public API surface:
- `RudderSdk.Unity``Rudder` (MonoBehaviour bootstrap), `RudderConfiguration`,
`RudderState`, platform adapters.
- `RudderSdk.Core``RudderClient` and feature services (`Auth`, `Player`,
`RemoteConfig`, `Storage`, `ProjectStorage`, `Stores`, `Inventory`,
`Leaderboards`, `Quests`, `BattlePass`, `Scenario`, `Effects`).
- `RudderSdk.Core.Models.*` — DTOs (`PlayerProfile`, `Wallet`, `Offer`,
`Store`, `StorageItem`, `RankEntry`, `Quest`, `Reward`, ...).
Full integration rules live in `Packages/rudder.sdk/AGENTS.md`; copy-paste
sample scenes live in `Packages/rudder.sdk/Samples~/`. Verify signatures
against those, never from memory.
## Install
Unity 6000.0+. Add the scoped registry and both dependencies to
`Packages/manifest.json` (`com.unity.nuget.newtonsoft-json` is required —
the package does not bundle Newtonsoft.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"
}
}
```
## Bootstrap
1. Create the configuration asset: `Assets > Create > Rudder > Configuration`.
2. Set `ProjectKey` (required). Defaults: `BaseUrl` `https://api.rudder.build`,
`TimeoutSeconds` 10.
3. Add the `Rudder` component to a startup scene object and assign the asset.
`Rudder` is `[DefaultExecutionOrder(-1500), DisallowMultipleComponent]`,
uses `DontDestroyOnLoad`, and pumps `client.Update(Time.deltaTime)` every
frame — never call `client.Update` yourself.
4. Initialize and sign in:
```csharp
using RudderSdk.Unity;
var client = Rudder.Initialize(); // synchronous, idempotent
await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player");
```
`Rudder.Initialize()` throws if the component or configuration is missing.
`Rudder.State` (`NotInitialized`/`Initializing`/`Ready`/`Failed`),
`Rudder.LastError`, `Rudder.Client`, `Rudder.Ready` (Task) and
`Rudder.Initialized` (event) expose lifecycle. Details:
[reference/bootstrap.md](reference/bootstrap.md).
## Platform adapters
`RudderUnityClientFactory.Create(RudderUnityClientOptions)` wires Core
abstractions to Unity implementations; `Rudder.Initialize()` does this for
you with `PlayerPrefsUnityKeyValueStore` + `UnityDebugLoggerSink`:
| Core abstraction | Unity adapter | Notes |
| --- | --- | --- |
| `IRudderTransport` | `UnityTransportAdapter` over `UnityWebRequestExecutor` | `UnityWebRequest`, timeout from config |
| `ITokenStore` | `UnityTokenStoreAdapter` | tokens in PlayerPrefs |
| `IDeviceIdProvider` | `UnityDeviceIdProvider` | GUID in PlayerPrefs (`rudder_device_id`) — not `SystemInfo.deviceUniqueIdentifier` |
| `IClock` | `UnityClock` | `DateTimeOffset.UtcNow` |
| `IRudderLogger` | `UnityLoggerAdapter``IUnityLoggerSink` | default `UnityDebugLoggerSink``Debug.Log*` |
## Capabilities
All calls hang off `client` / `Rudder.Client`. Match the task to one row,
then read the reference file for exact signatures.
| Task | API | Reference |
| --- | --- | --- |
| Init, sign in, profile, wallets, errors | `Rudder.Initialize`, `client.Auth.LoginWithDeviceAsync`, `Auth.*`, `Player.GetProfileAsync` | [reference/bootstrap.md](reference/bootstrap.md), [reference/authentication.md](reference/authentication.md) |
| Remote tunables | `RemoteConfig.LoadAsync`, `Get<T>(key, fallback)`, `GetAsync` | [reference/remote-config.md](reference/remote-config.md) |
| Player cloud save / project storage | `Storage.GetAsync`/`SaveAsync`/`DeleteAsync`, `ProjectStorage.*` | [reference/storage.md](reference/storage.md) |
| Shop + inventory | `Stores.ListAsync`/`GetAsync`/`PurchaseAsync`, `Inventory.GetAsync` | [reference/stores-inventory.md](reference/stores-inventory.md) |
| High scores | `Leaderboards.FindBySlug(slug)``SubmitAsync`/`ListAsync` | [reference/leaderboards.md](reference/leaderboards.md) |
| LiveOps scenarios | `Scenario.TriggerAsync`, `Effects.OnNotification`/`OnStoreOffer`/... | [reference/scenarios.md](reference/scenarios.md) |
| Global quests | `Quests.ListAsync`/`ClaimAsync`/`ReportProgressAsync` | [reference/quests.md](reference/quests.md) |
| Battle pass | `BattlePass.*` with scenario/node ids, `BattlePassEffect` | [reference/battlepass.md](reference/battlepass.md) |
## Hard rules
- Call the SDK from a `MonoBehaviour` or the game's service object; no second
singleton next to `Rudder`, no wrapper layers, no consumer-local interfaces
over `RudderClient`.
- Use SDK model types directly; do not re-wrap `Offer`/`PlayerProfile`/
`RankEntry` in project DTOs unless asked.
- No `ConfigureAwait(false)` in game code — continuations must return to the
Unity synchronization context.
- Catch `RudderApiException` subclasses and surface `exception.Message`;
do not swallow errors. A 401 is retried once after a token refresh by the
client itself.
- Backend entities must exist: store slug, leaderboard slug, remote-config
key, scenario event. Missing data is a 404, not an SDK bug.
- Every feature call requires a matching dashboard entity; the API base is
`https://api.rudder.build`, dashboard `https://app.rudder.build/`.
@@ -0,0 +1,46 @@
# Authentication and player profile
Namespace `RudderSdk.Core`. Verified against the bundled `Rudder.Core.dll`
and `Samples~/Authentication/AuthenticationSample.cs`.
## Sign in
```csharp
var client = Rudder.Initialize();
await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player");
```
`AuthService.LoginWithDeviceAsync(string region, string language,
string nickname = null, CancellationToken cancellationToken = default)`
returns `LoginViaDeviceResponse` (`AccessToken`, `RefreshToken`). Tokens are
saved to PlayerPrefs by the Unity adapter. Pending scenario effects are
fetched after sign-in by the effects heartbeat.
Auth service:
- `Task<LoginViaDeviceResponse> LoginWithDeviceAsync(string region, string language, string nickname = null, CancellationToken = default)`
- `Task<bool> RefreshAsync()` — explicit token refresh.
- `void Logout()` — clears tokens locally.
- `event Action<RudderAuthState> AuthStateChanged``RudderAuthState.SignedIn` / `SignedOut`. `SignedOut` also fires when a refresh fails.
Login uses the SDK device id from PlayerPrefs. Do not pass
`SystemInfo.deviceUniqueIdentifier`.
## Profile and wallets
```csharp
var profile = await client.Player.GetProfileAsync();
var nickname = profile.Player.Nickname;
foreach (var wallet in profile.Wallets)
Debug.Log($"{wallet.Currency}: {wallet.Balance}");
```
- `Task<PlayerProfile> PlayerService.GetProfileAsync(CancellationToken = default)`
- `PlayerProfile``Player Player`, `List<Wallet> Wallets`.
- `Player``Id`, `ProjectId`, `Nickname`, `Region`, `Language`,
`CreatedAt` (`DateTimeOffset?`).
- `Wallet``Currency` (string), `Balance` (`long?`).
Models are nullable since 0.4.0 — check fields before use. After any
purchase or reward grant, reload the profile to see new wallet balances;
there is no change callback.
@@ -0,0 +1,57 @@
# Battle pass
`client.BattlePass` (`BattlePassService`), verified against the bundled
`Rudder.Core.dll`. There is no sample scene for this feature.
Progress is tied to a scenario battle-pass node. Prefer `BattlePassEffect`
delivered by `Effects.OnBattlePass` — it binds `scenarioId`/`nodeId`/
`runId` for you. Direct `BattlePassService` calls need those ids manually.
## BattlePassEffect (preferred)
```csharp
client.Effects.OnBattlePass += async effect =>
{
var progress = await effect.GetProgressAsync();
await effect.AddXpAsync("kills", 100);
var claim = await effect.ClaimRewardAsync(level, BattlePassService.TrackFree);
};
```
- `Task<GetBattlePassProgressResponse> GetProgressAsync(CancellationToken = default)`
- `Task<AddBattlePassXpResponse> AddXpAsync(string source, long amount, CancellationToken = default)`
- `Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(int level, string track, CancellationToken = default)`
- `Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(CancellationToken = default)` — posts `onPremiumPurchase` on success
- `LevelUp()` / `LevelUpAsync()` — posts `onLevelUp`
- `End()` / `EndAsync()` — posts `onComplete`
- `Get<T>(key, fallback)`, `RunId`, `ScenarioId`, `NodeId`, `Data` like other effects
`BattlePassLevelEffect` (from `OnBattlePassLevel`) has `Claim()` /
`ClaimAsync()` (posts `onComplete`) and a `Level` property.
## BattlePassService (direct)
- `Task<GetBattlePassProgressResponse> GetProgressAsync(string scenarioId, string nodeId, CancellationToken = default)`
- `Task<AddBattlePassXpResponse> AddXpAsync(AddBattlePassXpRequest request, CancellationToken = default)`
- `Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(ClaimBattlePassRewardRequest request, CancellationToken = default)`
- `Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(PurchaseBattlePassPremiumRequest request, CancellationToken = default)`
- Track constants: `BattlePassService.TrackFree`, `BattlePassService.TrackPremium` (static fields).
Request models carry `ScenarioId`, `NodeId`, and (except progress)
`RunId`; `PurchaseBattlePassPremiumRequest` also takes `IdempotencyKey`.
## Response models
- `GetBattlePassProgressResponse``Level` (`int?`), `Xp` (`long?`),
`PremiumOwned` (`bool?`), `ClaimedTiers` (`List<ClaimedTier>`: `Level`,
`Track`).
- `AddBattlePassXpResponse``Xp` (`long?`), `Level` (`int?`),
`LeveledUp` (`bool?`), `MaxLevel` (`bool?`).
- `ClaimBattlePassRewardResponse``Success` (`bool?`),
`AlreadyClaimed` (`bool?`), `Granted` (`List<Reward>`), `Error`.
- `PurchaseBattlePassPremiumResponse``Success` (`bool?`), `Error`.
Business failures come back in the response (`Success != true`, `Error`)
with codes from `RudderErrorCodes` (`LevelNotReached`,
`ObjectivesIncomplete`, `NodeNotActive`, `RunExpired`, ...); transport
failures throw `RudderApiException` subclasses.
@@ -0,0 +1,91 @@
# Bootstrap and lifecycle
Namespace `RudderSdk.Unity`. Verified against
`Packages/rudder.sdk/Runtime/Sources/Rudder.cs`,
`RudderConfiguration.cs` and the bundled `Rudder.Core.dll`.
## RudderConfiguration (ScriptableObject)
Create via `Assets > Create > Rudder > Configuration`.
| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `ProjectKey` | `string` | — | Required. Issued in the Rudder admin panel. Empty key logs a warning in `OnValidate`. |
| `BaseUrl` | `string` | `https://api.rudder.build` | API base URL. |
| `TimeoutSeconds` | `int` (`[Min(1)]`) | `10` | HTTP request timeout. |
Keep the asset with the real key out of git (this repo uses the git-ignored
`Assets/LiveOpsLocal.asset` pattern).
## Rudder (MonoBehaviour)
`[DefaultExecutionOrder(-1500), DisallowMultipleComponent]`. Put it on a
startup scene object, assign the configuration asset. `Awake` keeps the first
instance (`DontDestroyOnLoad`) and destroys duplicates. `Update` pumps
`_client.Update(Time.deltaTime)` (effects heartbeat and wait-deadline checks
need it — do not call `client.Update` yourself).
Static API:
- `RudderState State``NotInitialized` / `Initializing` / `Ready` / `Failed`.
- `Exception LastError` — initialization error when `State` is `Failed`.
- `RudderClient Client` — the Core client; throws `InvalidOperationException`
until `State` is `Ready`.
- `Task<RudderClient> Ready` — completes with the client after `Initialize`,
faults on initialization error. Prefer `Initialize()` in game code.
- `event Action<RudderClient> Initialized` — attach point for add-on
packages; late subscribers must check `State` themselves.
- `RudderClient Initialize()` — synchronous. Reads the serialized
configuration, creates `RudderClient` via `RudderUnityClientFactory`.
Idempotent: returns the same client when already `Ready`; resets and
retries after `Failed`. Throws if the component is not in the scene or no
configuration is assigned. Does not create a GameObject.
Statics reset on Enter Play Mode without domain reload
(`RuntimeInitializeOnLoadMethod(SubsystemRegistration)`).
## Minimal startup
```csharp
using RudderSdk.Unity;
using UnityEngine;
public class Bootstrap : MonoBehaviour
{
private async void Start()
{
var client = Rudder.Initialize();
await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player");
}
}
```
`LoginWithDeviceAsync` logs in with the SDK device id (a GUID persisted
in PlayerPrefs under `rudder_device_id`). It is the first awaited call;
afterwards use `client.*` / `Rudder.Client.*`. Tokens persist in PlayerPrefs,
so a second launch can call it again (it refreshes the session). See
[authentication.md](authentication.md) for the Auth service.
Pending scenario effects are fetched after sign-in by the effects heartbeat
(`client.Update`, pumped by `Rudder`). Subscribe to `client.Effects.On*`
before `Scenario.TriggerAsync`.
## Errors
All API errors are `RudderApiException` subclasses from `RudderSdk.Core`:
- `RudderAuthException` — 401; refresh failed, session over
(`AuthStateChanged(SignedOut)` fires).
- `RudderNotFoundException` — 404.
- `RudderRateLimitException` — 429.
- `RudderNetworkException` — no response / timeout.
`RudderApiException` exposes `StatusCode` (int), `Code` (string),
`RequestId` (string). The client retries a 401 once after a single-flight
token refresh — game code does not implement that retry. Catch and surface
`exception.Message`; do not swallow errors.
Removed APIs (do not use): `Rudder.Initialize(configuration)`,
`Rudder.Auth`-style static service shortcuts, `client.Scenarios`,
`BuyAsync`, `LoginViaDeviceAsync`, `AuthorizeWithDeviceAsync`,
`Scenario.RestoreAsync`.
@@ -0,0 +1,26 @@
# Leaderboards
`client.Leaderboards` (`LeaderboardsService`), verified against the bundled
`Rudder.Core.dll` and `Samples~/Leaderboards/LeaderboardsSample.cs`.
```csharp
var board = client.Leaderboards.FindBySlug("my-board");
await board.SubmitAsync(score);
var top = await board.ListAsync(10);
```
API:
- `LeaderboardHandle FindBySlug(string slug)` — no network call; returns a
handle bound to the slug.
- `LeaderboardHandle.SubmitAsync(double score, CancellationToken = default)`
- `LeaderboardHandle.ListAsync(int limit = 100, CancellationToken = default)`
`IReadOnlyList<RankEntry>`.
- `LeaderboardHandle.Slug { get; }`
`RankEntry``PlayerId`, `PlayerName`, `Rank` (`long?`), `Score`
(`double?`).
The leaderboard slug must exist in the dashboard, else the calls 404. For
scenario leaderboard nodes see `OnLeaderboard` in
[scenarios.md](scenarios.md).
@@ -0,0 +1,33 @@
# Quests (global)
`client.Quests` (`QuestsService`) — the player's global quest list, distinct
from scenario `OnQuest` nodes (`QuestEffect`, see
[scenarios.md](scenarios.md)). Verified against the bundled
`Rudder.Core.dll`. There is no sample scene for this feature.
```csharp
var quests = await client.Quests.ListAsync();
var claim = await client.Quests.ClaimAsync(quest.Id);
var completedIds = await client.Quests.ReportProgressAsync("kills", 1);
```
API:
- `Task<IReadOnlyList<Quest>> ListAsync(CancellationToken = default)`
- `Task<ClaimQuestResponse> ClaimAsync(string questId, CancellationToken = default)`
- `Task<IReadOnlyList<string>> ReportProgressAsync(string metric, long amount, CancellationToken = default)` — returns the ids of quests completed by this report.
Models:
- `Quest``Id`, `Name`, `Status`, `Objectives`
(`List<QuestObjectiveProgress>`), `Rewards` (`List<Reward>`).
- `QuestObjectiveProgress``ObjectiveId`, `Metric`, `Current` (`long?`),
`Target` (`long?`), `Completed` (`bool?`).
- `Reward``Currency`, `ItemId`, `Amount` (`long?`).
- `ClaimQuestResponse``Success` (`bool?`), `AlreadyClaimed` (`bool?`),
`Granted` (`List<Reward>`), `Error`.
Store purchases already report metrics automatically; the metric strings are
built by `QuestMetrics.PurchaseOffer(offerId)``purchase.offer:<offerId>`
and `QuestMetrics.PurchaseItem(itemId)``purchase.item:<itemId>`
(`RudderSdk.Core.QuestMetrics`, static).
@@ -0,0 +1,25 @@
# Remote config
`client.RemoteConfig` (`RemoteConfigService`, namespace `RudderSdk.Core`).
Verified against the bundled `Rudder.Core.dll` and
`Samples~/RemoteConfig/RemoteConfigSample.cs`.
```csharp
await client.RemoteConfig.LoadAsync();
var speed = client.RemoteConfig.Get("player_speed", 5f);
```
API:
- `Task<IReadOnlyDictionary<string, RemoteConfig>> LoadAsync(CancellationToken = default)` — fetches all configs into the cache.
- `T Get<T>(string key, T defaultValue = default)` — reads the cache synchronously. Call `LoadAsync` first (or use `GetAsync`).
- `Task<T> GetAsync<T>(string key, T defaultValue = default, CancellationToken = default)` — loads on first use, then reads.
- `Task<RemoteConfig> GetConfigAsync(string key, CancellationToken = default)` — full config entry.
- `bool IsLoaded { get; }`, `IReadOnlyDictionary<string, RemoteConfig> Configs { get; }` — cache state.
`RemoteConfig` model: `Id`, `Key`, `Value` (string), `ValueType`,
`Description`, `Environment`, `ProjectId`, `Active` (`bool?`), `CreatedAt`,
`UpdatedAt`.
A missing key returns the fallback; the key must exist in the dashboard to
have a server value.
@@ -0,0 +1,47 @@
# Scenarios
Scenario graphs execute on the server. `client.Scenario` (`ScenarioService`)
only triggers an event; pending nodes surface through `client.Effects`
(`EffectsService`). Verified against the bundled `Rudder.Core.dll` and
`Samples~/Scenarios/ScenariosSample.cs`.
## Flow
Subscribe to `Effects.On*` **before** triggering, then trigger an event name
configured in the dashboard (examples: `player_login`,
`demo_round_finished`). Unsubscribe on destroy.
```csharp
client.Effects.OnNotification += effect => { /* show UI, then effect.Done() */ };
await client.Scenario.TriggerAsync("player_login");
```
API:
- `Task ScenarioService.TriggerAsync(string eventName, CancellationToken = default)` — posts the event and ingests any effects the response contains. Does not return runs.
- `void RudderClient.Update(float deltaTime)` — pumped by the `Rudder` component. Drives the 30s pending-effects heartbeat and wait-deadline checks. Do not call it from game code.
After `Auth.LoginWithDeviceAsync`, the heartbeat fetches pending effects for
the signed-in player. Effects that arrive later (or after a wait deadline)
are dispatched the same way.
## Events and effects
All effect types expose `RunId`, `ScenarioId`, `NodeId`, `Data` (`JObject`),
and `T Get<T>(string key, T defaultValue = default)` for node data.
| Event | Effect | Game must |
| --- | --- | --- |
| `OnNotification` | `NotificationEffect` | Show UI (`Title`, `Message`), then `Done()` / `DoneAsync()`. |
| `OnStoreOffer` | `StoreOfferEffect` | Show offer (`StoreSlug`, `Get("storeSlug", ...)`); on buy call `Stores.PurchaseAsync` then `Purchase()` / `PurchaseAsync()`, else `Decline()` / `DeclineAsync()`. `IsResolved` marks resolution. |
| `OnWait` | `WaitEffect` | Deadline only (`DeadlineUtc`); the server advances the run. |
| `OnQuest` | `QuestEffect` | Scenario quest node: `ReportProgress(objectiveId, amount)` / `ReportProgressAsync`. The server auto-completes the node once every objective is satisfied. Not `client.Quests`. |
| `OnLeaderboard` | `LeaderboardEffect` | `End()` / `EndAsync()`, `Claim()` / `ClaimAsync()`. `IsResolved` marks resolution. |
| `OnBattlePass` | `BattlePassEffect` | Drive the effect — see [battlepass.md](battlepass.md). |
| `OnBattlePassLevel` | `BattlePassLevelEffect` | `Claim()` / `ClaimAsync()` for the level reward; `Level` prop. |
| `OnScenarioCompleted` | `ScenarioCompletedEffect` | `RunId`, `ScenarioId`. Log / surface. |
| `OnScenarioFailed` | `ScenarioFailedEffect` | `RunId`, `ScenarioId`, `NodeId`, `Exception`. Log / surface. |
Async and sync variants are equivalent; the sync variants fire-and-forget
the same server callback. The event name must match a scenario trigger
configured in the dashboard — an unknown event simply produces no effects.
@@ -0,0 +1,37 @@
# Storage
Two services, both verified against the bundled `Rudder.Core.dll` and
`Samples~/Storage/StorageSample.cs`.
## Player storage — `client.Storage` (`StorageService`)
Per-player key-value items. `StorageItem` is `{ Type, Id, Data }` where
`Data` is an opaque JSON string — serialize your save object with
`JsonUtility` or Newtonsoft; do not invent a second save format.
- `Task<GetStorageResponse> GetAsync(string type = null, int limit = 100, string cursor = null, CancellationToken = default)` — pages items; response has `Items` (`List<StorageItem>`) and `NextCursor`.
- `IAsyncEnumerable<StorageItem> ListAllAsync(string type = null, int limit = 100, CancellationToken = default)` — follows cursors for you.
- `Task SaveAsync(IEnumerable<StorageItem> items, CancellationToken = default)` — upserts by `Type`+`Id`.
- `Task DeleteAsync(string type, CancellationToken = default)` — deletes all items of that type.
```csharp
await client.Storage.SaveAsync(new[]
{
new StorageItem { Type = "sample_save", Id = "main", Data = json }
});
var response = await client.Storage.GetAsync("sample_save");
await client.Storage.DeleteAsync("sample_save");
```
## Project storage — `client.ProjectStorage` (`ProjectStorageService`)
Global key-value storage shared across players (read from the SDK; writes
respect item permissions).
- `Task<GetProjectStorageResponse> GetAsync(string type = null, int limit = 100, string cursor = null, CancellationToken = default)``Items` + `NextCursor`.
- `IAsyncEnumerable<ProjectStorageItem> ListAllAsync(string type = null, int limit = 100, CancellationToken = default)`
- `Task SaveAsync(IEnumerable<ProjectStorageUpdateItem> items, string idempotencyKey = null, CancellationToken = default)`
`ProjectStorageItem`: `Type`, `Id`, `Data` (JSON string), `ReadPermission`,
`WritePermission`, `Version` (`long?`), `Size` (`long?`), `ExpiresAt`,
`UpdatedAt`. `ProjectStorageUpdateItem`: `{ Type, Data }`.
@@ -0,0 +1,44 @@
# Stores and inventory
Verified against the bundled `Rudder.Core.dll` and
`Samples~/StoreInventory/StoreInventorySample.cs`.
## Stores — `client.Stores` (`StoresService`)
```csharp
var stores = await client.Stores.ListAsync();
var response = await client.Stores.PurchaseAsync(storeSlug, offerId);
if (response != null && response.Success != true)
throw new InvalidOperationException(response.Error ?? "Purchase rejected");
```
- `Task<IReadOnlyList<Store>> ListAsync(CancellationToken = default)`
- `Task<Store> GetAsync(string slug, CancellationToken = default)`
- `Task<PurchaseOfferResponse> PurchaseAsync(string storeSlug, string offerId, string idempotencyKey = null, CancellationToken = default)` — generates an idempotency key when omitted.
Models:
- `Store``Id`, `Slug`, `Name`, `Description`, `Status`, `Environment`,
`ProjectId`, `ScenarioId`, `Data` (`JToken`), `Offers` (`List<Offer>`),
`CreatedAt`, `UpdatedAt`.
- `Offer``Id`, `Name`, `Price` (`OfferPrice`), `Contents`
(`List<OfferContent>`), `MaxPurchases` (`int?`), `CreatedAt`, `UpdatedAt`.
- `OfferPrice``Currency`, `Amount` (`long?`).
- `OfferContent``ItemId`, `Amount` (`long?`).
- `PurchaseOfferResponse``Success` (`bool?`), `Error`, `PurchaseId`.
Check `Success != true` (nullable bool), not `!Success`. After a purchase,
reload wallets (`Player.GetProfileAsync`) and inventory — there is no
`onChange` callback. Purchases auto-report quest metrics
`purchase.offer:<offerId>` / `purchase.item:<itemId>` (see
[quests.md](quests.md)). The store slug must exist in the dashboard, else 404.
## Inventory — `client.Inventory` (`InventoryService`)
```csharp
var items = await client.Inventory.GetAsync();
```
- `Task<IReadOnlyList<PlayerInventoryItem>> GetAsync(CancellationToken = default)`
- `PlayerInventoryItem``Slug`, `Amount` (`long?`), `NameOverride`,
`PropertiesOverride` (`JToken`), `UpdatedAt`.