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:
@@ -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.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c4e9a12b8d04f6a9e3c1f5d0a2b8c47
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
-1
@@ -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
-1
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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~"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user