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
This commit is contained in:
edmand46
2026-09-04 14:23:38 +03:00
parent 22805173eb
commit 6d115f158e
71 changed files with 3338 additions and 1034 deletions
-1
View File
@@ -14,5 +14,4 @@ MonoBehaviour:
m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.RudderConfiguration
ProjectKey: YOUR_PROJECT_KEY
BaseUrl: https://api.rudder.build
RealtimeUrl: wss://realtime.rudder.build/api/realtime/ws
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
## 0.4.0
Breaking changes, following the Rudder.Core 0.4.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
Breaking changes, following the Rudder.Core 0.3.0 rework:
+37 -14
View File
@@ -1,10 +1,12 @@
# Rudder SDK for Unity
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:
`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
@@ -21,7 +23,7 @@ your project's `Packages/manifest.json`:
}
],
"dependencies": {
"rudder.sdk": "0.3.0",
"rudder.sdk": "0.4.0",
"com.unity.nuget.newtonsoft-json": "3.2.2"
}
}
@@ -33,26 +35,25 @@ Newtonsoft.Json and the package does not bundle the DLL.
## Quickstart
Create a configuration asset via `Assets > Create > Rudder > Configuration`,
set `ProjectKey`, `BaseUrl` and `RealtimeUrl`, then initialize once at
startup:
set `ProjectKey`, add the `Rudder` component to a startup scene, assign the
asset, then:
```csharp
using RudderSdk.Unity;
Rudder.Initialize(configuration); // or add the Rudder component to a scene
var client = await Rudder.Ready; // faults if initialization failed
var client = Rudder.Initialize();
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 stores = await client.Stores.ListAsync();
```
All features hang off the client: `Auth`, `Player`, `BattlePass`, `Quests`,
`Stores`, `Inventory`, `Leaderboards`, `RemoteConfig`, `Scenario`, `Storage`,
`Realtime`.
`Stores`, `Inventory`, `Leaderboards`, `RemoteConfig`, `Scenario`, `Effects`,
`Storage`.
```csharp
client.Scenario.OnStoreOffer += session => { /* show offer UI */ };
client.Effects.OnStoreOffer += effect => { /* show offer UI */ };
await client.Scenario.TriggerAsync("player_login");
var purchase = await client.Stores.PurchaseAsync("cozy-camp-shop", "moonberry-boost");
@@ -64,14 +65,36 @@ var top = await leaderboard.ListAsync(10);
## 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.LastError` — the initialization error when `State` is `Failed`.
- `Rudder.Client` — the initialized `RudderClient`; throws until `Ready`.
- `Rudder.Ready` — task that completes with the client or the initialization
error; `Rudder.Initialized` is the event-flavored sugar over it.
- `Rudder.Client` — the initialized `RudderClient`; throws until `Initialize`.
- HTTP timeout defaults to 10 seconds, configurable per
`RudderConfiguration.TimeoutSeconds`.
Errors surface as `RudderApiException` subclasses from `RudderSdk.Core`:
`RudderAuthException` (401, session over), `RudderNotFoundException` (404),
`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
{
BaseUrl = options.BaseUrl,
RealtimeUrl = options.RealtimeUrl,
ProjectKey = options.ProjectKey,
Logger = new UnityLoggerAdapter(options.LoggerSink ?? new UnityDebugLoggerSink()),
Transport = new UnityTransportAdapter(options.BaseUrl, executor),
TokenStore = new UnityTokenStoreAdapter(options.KeyValueStore),
DeviceIdProvider = new UnityDeviceIdProvider(),
Clock = new UnityClock(),
PlanStateStore = new UnityPlanStateStore(options.KeyValueStore),
Scheduler = new UnityPlanScheduler(),
RealtimeTransportFactory = new UnityRealtimeTransportFactory()
Clock = new UnityClock()
};
return new RudderClient(clientOptions);
@@ -3,7 +3,6 @@ namespace RudderSdk.Unity
public class RudderUnityClientOptions
{
public string BaseUrl { get; set; }
public string RealtimeUrl { get; set; }
public string ProjectKey { get; set; }
public int TimeoutSeconds { get; set; } = 10;
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>
/// Scene bootstrap MonoBehaviour and public entrypoint of the Rudder Unity SDK.
/// Add the component to a scene with a RudderConfiguration assigned, or call
/// Rudder.Initialize(configuration) and the hidden GameObject is created for you.
/// Put this on a startup scene object, assign a RudderConfiguration, then call
/// <see cref="Initialize"/>. Feature calls live on the returned Core client.
/// </summary>
[DefaultExecutionOrder(-1500), DisallowMultipleComponent]
public class Rudder : MonoBehaviour
@@ -43,33 +43,77 @@ namespace RudderSdk.Unity
if (State != RudderState.Ready || _client == null)
{
throw new InvalidOperationException(
"Rudder is not initialized. Add the Rudder component to the scene with a " +
"RudderConfiguration assigned, or call Rudder.Initialize(configuration).");
"Rudder is not initialized. Add the Rudder component to a scene with a " +
"RudderConfiguration assigned, then call Rudder.Initialize().");
}
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 void Initialize(RudderConfiguration configuration)
public static CoreClient Initialize()
{
if (configuration == null)
throw new ArgumentNullException(nameof(configuration));
if (State == RudderState.Ready && _client != null)
return _client;
if (State == RudderState.Initializing || State == RudderState.Ready)
throw new InvalidOperationException("Rudder is already initialized.");
if (State == RudderState.Failed)
{
_readyCompletion = NewCompletion();
LastError = null;
State = RudderState.NotInitialized;
}
if (_instance == null)
{
var host = new GameObject(nameof(Rudder));
_instance = host.AddComponent<Rudder>();
throw new InvalidOperationException(
"Rudder is not in the scene. Add the Rudder component to a startup scene " +
"and assign a RudderConfiguration.");
}
_instance._configuration = configuration;
_ = _instance.InitializeAsync();
if (_instance._configuration == null)
{
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)]
@@ -95,75 +139,16 @@ namespace RudderSdk.Unity
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()
{
_client?.Update(Time.deltaTime);
}
private void OnApplicationQuit()
{
DisconnectRealtime();
}
private void OnDestroy()
{
if (_instance != this)
return;
DisconnectRealtime();
_instance = null;
_client = null;
State = RudderState.NotInitialized;
@@ -172,25 +157,6 @@ namespace RudderSdk.Unity
_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()
{
return new TaskCompletionSource<CoreClient>(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -11,9 +11,6 @@ namespace RudderSdk.Unity
[Tooltip("API base URL, e.g. 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.")]
[Min(1)]
public int TimeoutSeconds = 10;
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 2dda93a7ed40941b3a831824b938fe46
guid: 53ede345941d4634ac87be4a7bbda9cd
folderAsset: yes
DefaultImporter:
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
guid: 3c34eb0ba13fe47eab84ba9a30d8bdb6
guid: 770e3347e0604c359d5ca59689007d42
folderAsset: yes
DefaultImporter:
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",
"version": "0.3.0",
"version": "0.4.0",
"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",
"author": {
"name": "Rudder",
@@ -15,5 +15,12 @@
"documentationUrl": "https://app.rudder.build",
"publishConfig": {
"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.
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`.
## 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. |
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
`Packages/rudder.sdk/AGENTS.md`.
## 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`
(the real asset is git-ignored because it holds your project key).
3. Set `ProjectKey` to your project key.
4. Confirm the endpoints:
- `BaseUrl`: `https://api.rudder.build`
- `RealtimeUrl`: `wss://realtime.rudder.build/api/realtime/ws`
5. Bake the demo assets (see below), then open `Assets/LiveOpsExamples/LiveOpsCozyCollector.unity`.
6. Press Play.
4. Confirm `BaseUrl` is `https://api.rudder.build`.
5. Open a feature sample under `Assets/Samples/Rudder SDK/` and press Play.
In this development project the samples are already linked there; do not
import them again from Package Manager.
## 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
Tools > LiveOps > Bake Cozy Collector (All)
```
| Scene | SDK calls |
| --- | --- |
| `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:
- `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:
The later scenes are more interesting with this staging data:
| 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. |
| 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. |
| Storage | No setup required; the demo writes type `cozy_fantasy_save`, id `main`. |
| Scenarios | Events `player_login` and/or `demo_round_finished`. |
| Storage | No setup required; the sample writes type `sample_save`, id `main`. |
## Project Structure
```text
Assets/LiveOpsExamples/
LiveOpsCozyCollector.unity (baked)
CozyCollectorSpriteLibrary.asset (baked)
Sprites/ (imported from liveops-phaser-demo)
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/Samples/Rudder SDK/ (symlink to the package Samples~)
Packages/rudder.sdk/
Runtime/ (Rudder.Unity + Rudder.Core.dll)
Samples~/ (feature sample scenes)
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
Build the C# SDK from the sibling repository and copy the assembly into the
+7 -10
View File
@@ -1,6 +1,6 @@
---
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, realtime.
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`)
@@ -13,7 +13,7 @@ adapters in `RudderSdk.Unity`. Public API surface:
`RudderState`, platform adapters.
- `RudderSdk.Core``RudderClient` and feature services (`Auth`, `Player`,
`RemoteConfig`, `Storage`, `ProjectStorage`, `Stores`, `Inventory`,
`Leaderboards`, `Quests`, `BattlePass`, `Scenario`, `Realtime`).
`Leaderboards`, `Quests`, `BattlePass`, `Scenario`, `Effects`).
- `RudderSdk.Core.Models.*` — DTOs (`PlayerProfile`, `Wallet`, `Offer`,
`Store`, `StorageItem`, `RankEntry`, `Quest`, `Reward`, ...).
@@ -47,7 +47,7 @@ the package does not bundle Newtonsoft.Json):
1. Create the configuration asset: `Assets > Create > Rudder > Configuration`.
2. Set `ProjectKey` (required). Defaults: `BaseUrl` `https://api.rudder.build`,
`RealtimeUrl` `wss://api.rudder.build/api/realtime/ws`, `TimeoutSeconds` 10.
`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
@@ -58,7 +58,7 @@ the package does not bundle Newtonsoft.Json):
using RudderSdk.Unity;
var client = Rudder.Initialize(); // synchronous, idempotent
await client.AuthorizeWithDeviceAsync("global", "en", nickname: "Player");
await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player");
```
`Rudder.Initialize()` throws if the component or configuration is missing.
@@ -79,9 +79,7 @@ you with `PlayerPrefsUnityKeyValueStore` + `UnityDebugLoggerSink`:
| `ITokenStore` | `UnityTokenStoreAdapter` | tokens in PlayerPrefs |
| `IDeviceIdProvider` | `UnityDeviceIdProvider` | GUID in PlayerPrefs (`rudder_device_id`) — not `SystemInfo.deviceUniqueIdentifier` |
| `IClock` | `UnityClock` | `DateTimeOffset.UtcNow` |
| `IPlanStateStore` | `UnityPlanStateStore` | scenario runs persist in PlayerPrefs |
| `IRudderLogger` | `UnityLoggerAdapter``IUnityLoggerSink` | default `UnityDebugLoggerSink``Debug.Log*` |
| `IRealtimeTransportFactory` | `UnityRealtimeTransportFactory` | WebGL: `JsWebSocketAdapter` (jslib); else `NativeWebSocketAdapter` |
## Capabilities
@@ -90,15 +88,14 @@ then read the reference file for exact signatures.
| Task | API | Reference |
| --- | --- | --- |
| Init, sign in, profile, wallets, errors | `Rudder.Initialize`, `client.AuthorizeWithDeviceAsync`, `Auth.*`, `Player.GetProfileAsync` | [reference/bootstrap.md](reference/bootstrap.md), [reference/authentication.md](reference/authentication.md) |
| 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`, `OnNotification`/`OnStoreOffer`/... | [reference/scenarios.md](reference/scenarios.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, `BattlePassSession` | [reference/battlepass.md](reference/battlepass.md) |
| Realtime websocket | `Realtime.ConnectAsync`/`DisconnectAsync` (relay not deployed) | [reference/realtime.md](reference/realtime.md) |
| Battle pass | `BattlePass.*` with scenario/node ids, `BattlePassEffect` | [reference/battlepass.md](reference/battlepass.md) |
## Hard rules
@@ -7,16 +7,16 @@ and `Samples~/Authentication/AuthenticationSample.cs`.
```csharp
var client = Rudder.Initialize();
await client.AuthorizeWithDeviceAsync("global", "en", nickname: "Player");
await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player");
```
`RudderClient.AuthorizeWithDeviceAsync(string region, string language,
`AuthService.LoginWithDeviceAsync(string region, string language,
string nickname = null, CancellationToken cancellationToken = default)`
returns `LoginViaDeviceResponse` (`AccessToken`, `RefreshToken`). It performs
device login and then restores persisted scenario runs. Tokens are saved to
PlayerPrefs by the Unity adapter.
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.
Lower-level alternative on `client.Auth` (login only, no scenario restore):
Auth service:
- `Task<LoginViaDeviceResponse> LoginWithDeviceAsync(string region, string language, string nickname = null, CancellationToken = default)`
- `Task<bool> RefreshAsync()` — explicit token refresh.
+15 -16
View File
@@ -3,31 +3,31 @@
`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 `BattlePassSession`
delivered by `Scenario.OnBattlePass` — it binds `scenarioId`/`nodeId`/
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.
## BattlePassSession (preferred)
## BattlePassEffect (preferred)
```csharp
client.Scenario.OnBattlePass += async session =>
client.Effects.OnBattlePass += async effect =>
{
var progress = await session.GetProgressAsync();
await session.AddXpAsync("kills", 100);
var claim = await session.ClaimRewardAsync(level, BattlePassService.TrackFree);
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)`
- `LevelUp()` / `LevelUpAsync()`, `MaxLevel()` / `MaxLevelAsync()`,
`Complete()` / `CompleteAsync()`resolve the scenario node.
- `Get<T>(key, fallback)`, `Id`, `Context` like other sessions.
- `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
`BattlePassLevelSession` (from `OnBattlePassLevel`) has `Claim()` /
`ClaimAsync()` and a `Level` property.
`BattlePassLevelEffect` (from `OnBattlePassLevel`) has `Claim()` /
`ClaimAsync()` (posts `onComplete`) and a `Level` property.
## BattlePassService (direct)
@@ -46,11 +46,10 @@ Request models carry `ScenarioId`, `NodeId`, and (except progress)
`PremiumOwned` (`bool?`), `ClaimedTiers` (`List<ClaimedTier>`: `Level`,
`Track`).
- `AddBattlePassXpResponse``Xp` (`long?`), `Level` (`int?`),
`LeveledUp` (`bool?`), `MaxLevel` (`bool?`), `Plan` (`ExecutionPlan`).
`LeveledUp` (`bool?`), `MaxLevel` (`bool?`).
- `ClaimBattlePassRewardResponse``Success` (`bool?`),
`AlreadyClaimed` (`bool?`), `Granted` (`List<Reward>`), `Error`.
- `PurchaseBattlePassPremiumResponse``Success` (`bool?`), `Error`,
`Plan` (`ExecutionPlan`).
- `PurchaseBattlePassPremiumResponse``Success` (`bool?`), `Error`.
Business failures come back in the response (`Success != true`, `Error`)
with codes from `RudderErrorCodes` (`LevelNotReached`,
+13 -11
View File
@@ -12,7 +12,6 @@ Create via `Assets > Create > Rudder > Configuration`.
| --- | --- | --- | --- |
| `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. |
| `RealtimeUrl` | `string` | `wss://api.rudder.build/api/realtime/ws` | Realtime websocket URL. The relay is not deployed yet; leave as-is. |
| `TimeoutSeconds` | `int` (`[Min(1)]`) | `10` | HTTP request timeout. |
Keep the asset with the real key out of git (this repo uses the git-ignored
@@ -23,8 +22,8 @@ Keep the asset with the real key out of git (this repo uses the git-ignored
`[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)` (scenarios and realtime need it — do not
call `client.Update` yourself). On quit/destroy it disconnects realtime.
`_client.Update(Time.deltaTime)` (effects heartbeat and wait-deadline checks
need it — do not call `client.Update` yourself).
Static API:
@@ -56,18 +55,21 @@ public class Bootstrap : MonoBehaviour
private async void Start()
{
var client = Rudder.Initialize();
await client.AuthorizeWithDeviceAsync("global", "en", nickname: "Player");
await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player");
}
}
```
`AuthorizeWithDeviceAsync` logs in with the SDK device id (a GUID persisted
in PlayerPrefs under `rudder_device_id`) and restores persisted scenario
runs. 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
`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`:
@@ -85,5 +87,5 @@ token refresh — game code does not implement that retry. Catch and surface
Removed APIs (do not use): `Rudder.Initialize(configuration)`,
`Rudder.Auth`-style static service shortcuts, `client.Scenarios`,
`BuyAsync`, `LoginViaDeviceAsync`, `Scenario.RestoreAsync` (folded into
`AuthorizeWithDeviceAsync`).
`BuyAsync`, `LoginViaDeviceAsync`, `AuthorizeWithDeviceAsync`,
`Scenario.RestoreAsync`.
+1 -1
View File
@@ -1,7 +1,7 @@
# Quests (global)
`client.Quests` (`QuestsService`) — the player's global quest list, distinct
from scenario `OnQuest` nodes (`QuestSession`, see
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.
@@ -1,36 +0,0 @@
# Realtime
`client.Realtime` (`RealtimeService`), verified against the bundled
`Rudder.Core.dll` and
`Packages/rudder.sdk/Runtime/Sources/Realtime/`.
**The realtime relay is not deployed yet.** `ConnectAsync` is available, but
do not build product features on it unless the user explicitly asks.
```csharp
var session = await client.Realtime.ConnectAsync();
session.MessageReceived += data => { /* ArraySegment<byte> payload */ };
await session.SendAsync(payload);
await client.Realtime.DisconnectAsync();
```
API:
- `Task<RealtimeSession> ConnectAsync(TimeSpan? timeout = null, CancellationToken = default)` — connects to `RudderConfiguration.RealtimeUrl` (default `wss://api.rudder.build/api/realtime/ws`).
- `Task<RealtimeSession> ConnectAsync(Uri uri, TimeSpan? timeout = null, CancellationToken = default)` — explicit URI.
- `Task DisconnectAsync(CancellationToken = default)` — also called by the
`Rudder` component on quit/destroy.
- `RealtimeSession Session { get; }`, `bool IsConnected { get; }`.
- `void Update(float deltaTime)` — pumped by the `Rudder` component.
`RealtimeSession`:
- `Task SendAsync(byte[] payload, CancellationToken = default)`
- `Task DisconnectAsync(CancellationToken = default)`
- `bool IsConnected { get; }`
- `event Action Closed`, `event Action<Exception> Error`,
`event Action<ArraySegment<byte>> MessageReceived`.
Transport selection is automatic (`UnityRealtimeTransportFactory`):
WebGL builds use a JavaScript websocket via `RudderWebSocket.jslib`
(`JsWebSocketAdapter`); all other platforms use `NativeWebSocketAdapter`.
@@ -21,6 +21,5 @@ API:
`Description`, `Environment`, `ProjectId`, `Active` (`bool?`), `CreatedAt`,
`UpdatedAt`.
Scenario `OnConfigChanged` sessions mean the cache was already patched —
re-read with `Get`, do not reload. A missing key returns the fallback; the
key must exist in the dashboard to have a server value.
A missing key returns the fallback; the key must exist in the dashboard to
have a server value.
+25 -30
View File
@@ -1,52 +1,47 @@
# Scenarios
`client.Scenario` (`ScenarioService`) — the LiveOps plan runtime. Verified
against the bundled `Rudder.Core.dll` and
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 `On*` events **before** triggering, then trigger an event name
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.Scenario.OnNotification += session => { /* show UI, then session.Complete() */ };
var runs = await client.Scenario.TriggerAsync("player_login");
client.Effects.OnNotification += effect => { /* show UI, then effect.Done() */ };
await client.Scenario.TriggerAsync("player_login");
```
API:
- `Task<IReadOnlyList<PlanRun>> TriggerAsync(string eventName, CancellationToken = default)`returns the runs it started.
- `Task RestoreAsync(CancellationToken = default)` — restore persisted runs.
Already called by `client.AuthorizeWithDeviceAsync`; do not call both.
- `void Update(float deltaTime)` — pumped by the `Rudder` component.
- `IReadOnlyList<PlanRun> ActiveRuns { get; }``PlanRun` exposes `RunId`,
`PlanId`, `ScenarioId`, `UserId`, `ActiveNodeIds`, `Plan`.
- `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.
Run state persists in PlayerPrefs (`UnityPlanStateStore`), so sessions
survive restarts.
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 sessions
## Events and effects
All session types expose `Id`, `ScenarioNodeContext Context`, and
`T Get<T>(string key, T defaultValue = default)` for node data.
`ScenarioNodeContext` exposes `Run`, `Node`, `RunId`, `PlanId`, `ScenarioId`,
`NodeId`, `Type`, `Data` (`JObject`), `AsObjectDictionary()`.
All effect types expose `RunId`, `ScenarioId`, `NodeId`, `Data` (`JObject`),
and `T Get<T>(string key, T defaultValue = default)` for node data.
| Event | Session | Game must |
| Event | Effect | Game must |
| --- | --- | --- |
| `OnNotification` | `NotificationSession` | Show UI, then `Complete()` / `CompleteAsync()`. |
| `OnStoreOffer` | `StoreOfferSession` | Show offer (`session.Data`, `session.Get("storeSlug", ...)`); on buy call `Stores.PurchaseAsync` then `Purchase()` / `PurchaseAsync()`, else `Decline()` / `DeclineAsync()`. `IsResolved` marks resolution. |
| `OnConfigChanged` | `ConfigChangedSession` | Re-read `RemoteConfig.Get` — patches are already applied to the cache. |
| `OnWait` | `WaitSession` | Deadline only (`DeadlineUtc`); the runtime completes it. |
| `OnQuest` | `QuestSession` | Scenario quest node: `AddProgress(counterKey, amount)` / `AddProgressAsync`, `Complete()` / `CompleteAsync()`, `Fail()` / `FailAsync()`. Not `client.Quests`. |
| `OnLeaderboard` | `LeaderboardSession` | `End()` / `EndAsync()`, `RewardClaimed()` / `RewardClaimedAsync()`. |
| `OnBattlePass` | `BattlePassSession` | Drive the session — see [battlepass.md](battlepass.md). |
| `OnBattlePassLevel` | `BattlePassLevelSession` | `Claim()` / `ClaimAsync()` for the level reward; `Level` prop. |
| `OnScenarioCompleted` | `PlanRun` | Log / surface. |
| `OnScenarioFailed` | `ScenarioFailedEvent` | `Run`, `NodeId`, `Exception`. Log / surface. |
| `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 starts zero runs.
configured in the dashboard — an unknown event simply produces no effects.