Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| abad50d5e5 | |||
| 6d115f158e | |||
| 22805173eb | |||
| b8e226722d |
@@ -23,6 +23,7 @@ namespace LiveOpsExamples
|
||||
public const string LeaderboardSlug = "cozy-collector-score";
|
||||
public const string StoreSlug = "cozy-camp-shop";
|
||||
public const string OfferId = "moonberry-boost";
|
||||
public const string QuestMetricCollectibles = "cozy_collectibles";
|
||||
public const string StorageType = "cozy_fantasy_save";
|
||||
public const string StorageId = "main";
|
||||
public const string NicknamePrefsKey = "rudder_demo_nickname";
|
||||
@@ -38,6 +39,7 @@ namespace LiveOpsExamples
|
||||
public const string CapabilityStorage = "storage";
|
||||
public const string CapabilityInventory = "inventory";
|
||||
public const string CapabilityWallet = "wallet";
|
||||
public const string CapabilityQuests = "quests";
|
||||
public const string CapabilitySystem = "system";
|
||||
|
||||
/// <summary>Color used by the event inspector to tag a capability line.</summary>
|
||||
@@ -53,6 +55,7 @@ namespace LiveOpsExamples
|
||||
case CapabilityStorage: return new Color(0.56f, 0.87f, 0.87f);
|
||||
case CapabilityInventory: return new Color(0.78f, 0.86f, 0.55f);
|
||||
case CapabilityWallet: return new Color(1.00f, 0.88f, 0.63f);
|
||||
case CapabilityQuests: return new Color(0.71f, 0.80f, 1.00f);
|
||||
default: return new Color(0.75f, 0.75f, 0.75f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using RudderSdk.Core;
|
||||
using RudderSdk.Core.Models.Inventory;
|
||||
using RudderSdk.Core.Models.Leaderboards;
|
||||
using RudderSdk.Core.Models.Player;
|
||||
using RudderSdk.Core.Models.Quests;
|
||||
using RudderSdk.Core.Models.Storage;
|
||||
using RudderSdk.Core.Models.Stores;
|
||||
using RudderSdk.Unity;
|
||||
@@ -249,6 +250,7 @@ namespace LiveOpsExamples
|
||||
await RefreshProfileAsync();
|
||||
await RefreshStoresAsync();
|
||||
await RefreshInventoryAsync();
|
||||
await RefreshQuestsAsync();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -456,6 +458,11 @@ namespace LiveOpsExamples
|
||||
|
||||
try { await RefreshInventoryAsync(); }
|
||||
catch (Exception exception) { Log(CozyCollectorConsts.CapabilityInventory, "Inventory refresh failed", exception.Message); }
|
||||
|
||||
// Purchase metrics (purchase.offer:* / purchase.item:*) are reported
|
||||
// server-side by the purchase fan-out; re-list to pick up new progress.
|
||||
try { await RefreshQuestsAsync(); }
|
||||
catch (Exception exception) { Log(CozyCollectorConsts.CapabilityQuests, "Quest refresh failed", exception.Message); }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
@@ -573,6 +580,63 @@ namespace LiveOpsExamples
|
||||
return mapped;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Global quests (distinct from scenario quest nodes / QuestSession)
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private async Task RefreshQuestsAsync()
|
||||
{
|
||||
var quests = await Rudder.Client.Quests.ListAsync();
|
||||
|
||||
var activeCount = 0;
|
||||
if (quests != null)
|
||||
{
|
||||
foreach (var quest in quests)
|
||||
{
|
||||
if (quest.Status == "claimed")
|
||||
continue;
|
||||
|
||||
activeCount += 1;
|
||||
Log(CozyCollectorConsts.CapabilityQuests, "Quest " + (quest.Status ?? "active"), QuestProgressSummary(quest));
|
||||
|
||||
if (quest.Status == "completed" && !string.IsNullOrEmpty(quest.Id))
|
||||
await ClaimQuestAsync(quest);
|
||||
}
|
||||
}
|
||||
|
||||
Log(CozyCollectorConsts.CapabilityQuests, "Quests loaded", activeCount + " active");
|
||||
}
|
||||
|
||||
private async Task ClaimQuestAsync(Quest quest)
|
||||
{
|
||||
var result = await Rudder.Client.Quests.ClaimAsync(quest.Id);
|
||||
if (result == null || !result.Success)
|
||||
{
|
||||
Log(CozyCollectorConsts.CapabilityQuests, "Quest claim rejected", result?.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.AlreadyClaimed)
|
||||
Toast("Quest complete", quest.Name ?? quest.Id, CozyToastTone.Success);
|
||||
Log(CozyCollectorConsts.CapabilityQuests, "Quest claimed", quest.Name ?? quest.Id);
|
||||
await RefreshWalletsAsync();
|
||||
}
|
||||
|
||||
private static string QuestProgressSummary(Quest quest)
|
||||
{
|
||||
var name = quest.Name ?? quest.Id;
|
||||
if (quest.Objectives == null || quest.Objectives.Count == 0)
|
||||
return name;
|
||||
|
||||
var parts = new List<string>();
|
||||
foreach (var objective in quest.Objectives)
|
||||
{
|
||||
parts.Add(objective.Metric + " " + objective.Current + "/" + objective.Target);
|
||||
}
|
||||
|
||||
return name + " · " + string.Join(" · ", parts);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Round flow
|
||||
// ---------------------------------------------------------------
|
||||
@@ -625,6 +689,23 @@ namespace LiveOpsExamples
|
||||
{
|
||||
Log(CozyCollectorConsts.CapabilityScenario, "Trigger failed", exception.Message);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var completedIds = await Rudder.Client.Quests.ReportProgressAsync(
|
||||
CozyCollectorConsts.QuestMetricCollectibles,
|
||||
collected);
|
||||
Log(
|
||||
CozyCollectorConsts.CapabilityQuests,
|
||||
"Quest progress reported",
|
||||
CozyCollectorConsts.QuestMetricCollectibles + " +" + collected +
|
||||
(completedIds.Count > 0 ? " · completed " + completedIds.Count + " quest(s)" : string.Empty));
|
||||
await RefreshQuestsAsync();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log(CozyCollectorConsts.CapabilityQuests, "Quest progress failed", exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public void PlayAgain()
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace LiveOpsExamples
|
||||
{ "storage", new Color(0.624f, 0.690f, 0.769f) },
|
||||
{ "inventory", new Color(0.898f, 0.604f, 0.796f) },
|
||||
{ "wallet", new Color(1f, 0.831f, 0.475f) },
|
||||
{ "quests", new Color(0.710f, 0.800f, 1f) },
|
||||
{ "system", new Color(0.667f, 0.706f, 0.635f) },
|
||||
};
|
||||
|
||||
@@ -29,6 +30,7 @@ namespace LiveOpsExamples
|
||||
{ "storage", "Storage" },
|
||||
{ "inventory", "Items" },
|
||||
{ "wallet", "Wallet" },
|
||||
{ "quests", "Quests" },
|
||||
{ "system", "System" },
|
||||
};
|
||||
|
||||
|
||||
@@ -14,5 +14,4 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.RudderConfiguration
|
||||
ProjectKey: YOUR_PROJECT_KEY
|
||||
BaseUrl: https://api.rudder.build
|
||||
RealtimeUrl: wss://realtime.rudder.build/api/realtime/ws
|
||||
TimeoutSeconds: 10
|
||||
|
||||
@@ -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
|
||||
|
||||
## 1.0.0
|
||||
|
||||
Breaking changes, following the Rudder.Core 1.0.0 rework:
|
||||
|
||||
- Bundled `Rudder.Core.dll` updated to 0.4.0. The local scenario graph engine
|
||||
is gone. `client.Scenario` is only `TriggerAsync(eventName)`. Pending nodes
|
||||
surface on `client.Effects` (`OnNotification`, `OnStoreOffer`,
|
||||
`OnLeaderboard`, `OnWait`, `OnQuest`, `OnBattlePass`,
|
||||
`OnBattlePassLevel`, `OnScenarioCompleted`, `OnScenarioFailed`).
|
||||
- Session types (`NotificationSession`, `StoreOfferSession`, …) are now
|
||||
`*Effect` types. Completion methods: `Done`/`DoneAsync`,
|
||||
`Purchase`/`Decline`, `End`/`Claim`, `ReportProgress`, `LevelUp`.
|
||||
- `UnityPlanScheduler` removed together with the core `IPlanScheduler`
|
||||
abstraction and `RudderClientOptions.Scheduler`; the factory no longer
|
||||
wires a scheduler.
|
||||
- `UnityPlanStateStore`, `UnityRealtimeTransportFactory`,
|
||||
`Runtime/Sources/Realtime/` websocket adapters, and
|
||||
`Runtime/Plugins/WebGL/RudderWebSocket.jslib` removed together with Core
|
||||
`IPlanStateStore` / `IRealtimeTransport` / `RealtimeService`.
|
||||
- `RudderConfiguration.RealtimeUrl` and `RudderUnityClientOptions.RealtimeUrl`
|
||||
removed. `Rudder` no longer disconnects a realtime socket on quit.
|
||||
- `Rudder.Update` still pumps `client.Update(Time.deltaTime)` (effects
|
||||
heartbeat and wait-deadline checks).
|
||||
- Samples and docs use `Auth.LoginWithDeviceAsync`. Pending effects are
|
||||
fetched after sign-in via the heartbeat; `Scenario.RestoreAsync` /
|
||||
`AuthorizeWithDeviceAsync` are gone.
|
||||
- Core model fields are nullable now; samples check
|
||||
`PurchaseResponse.Success != true` instead of `!Success`.
|
||||
- Feature sample scenes for authentication, remote config, storage, stores
|
||||
and inventory, leaderboards, and scenarios (`Samples~`).
|
||||
- Removed the Cozy Collector example project from the Unity SDK repo.
|
||||
- Added `AGENTS.md` with Unity integration rules for coding agents.
|
||||
- `Rudder.Initialize()` is now synchronous, takes no arguments, and returns
|
||||
`RudderClient`. Put `Rudder` in a scene with a `RudderConfiguration`.
|
||||
The old `Initialize(RudderConfiguration)` (hidden GameObject) is gone.
|
||||
|
||||
## 0.3.0
|
||||
|
||||
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": "1.0.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~"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
# LiveOps Unity Cozy Collector Demo
|
||||
# Rudder Unity SDK
|
||||
|
||||
This Unity project is a playable port of the Cozy Collector web demo (`liveops-phaser-demo`). It uses the same arcade loop — move around the camp, collect spawning snacks before the round timer runs out — to show how a game can use LiveOps services without shipping a client update.
|
||||
|
||||
The demo is assembled like a regular Unity game: the scene, world, and UI live in baked assets (scene, prefabs, ScriptableObjects, imported sprites), and runtime scripts only contain logic wired through the inspector. All assets are generated by Editor bake scripts under `Tools > LiveOps`.
|
||||
|
||||
## Demonstrated Services
|
||||
|
||||
| Service | Demo use |
|
||||
| --- | --- |
|
||||
| Authorization | Device login with the configured project key. |
|
||||
| Player | Loads player profile and wallets after login; session card shows nickname, region, wallets. |
|
||||
| Remote Config | Controls round duration, player speed, spawn interval, collectible score; live updates highlight changed keys. |
|
||||
| Storage | Saves best score and last round state (`cozy_fantasy_save` / `main`). |
|
||||
| Stores | Shop modal lists the `cozy-camp-shop` catalog; purchases debit wallets. |
|
||||
| Inventory | Backpack modal lists collected items via `Rudder.Client.Inventory`. |
|
||||
| Leaderboards | Submits round score to `cozy-collector-score` and shows the top entries in the round-result modal. |
|
||||
| Scenarios | Sends `player_login` after login and `demo_round_finished` after a round; scenario offers show a Buy/Decline modal, `remote_config_override` applies live. |
|
||||
This project hosts the `rudder.sdk` UPM package and the scenes that show how
|
||||
to call it. Agents integrating the SDK into a game should read
|
||||
`Packages/rudder.sdk/AGENTS.md`.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -29,62 +16,45 @@ The demo is assembled like a regular Unity game: the scene, world, and UI live i
|
||||
2. Copy `Assets/LiveOpsLocal.asset.example` to `Assets/LiveOpsLocal.asset`
|
||||
(the real asset is git-ignored because it holds your project key).
|
||||
3. Set `ProjectKey` to your project key.
|
||||
4. Confirm the endpoints:
|
||||
- `BaseUrl`: `https://api.rudder.build`
|
||||
- `RealtimeUrl`: `wss://realtime.rudder.build/api/realtime/ws`
|
||||
5. Bake the demo assets (see below), then open `Assets/LiveOpsExamples/LiveOpsCozyCollector.unity`.
|
||||
6. Press Play.
|
||||
4. Confirm `BaseUrl` is `https://api.rudder.build`.
|
||||
5. Open a feature sample under `Assets/Samples/Rudder SDK/` and press Play.
|
||||
In this development project the samples are already linked there; do not
|
||||
import them again from Package Manager.
|
||||
|
||||
## Baking The Demo Assets
|
||||
## Feature Samples
|
||||
|
||||
All demo assets are generated from the Unity menu:
|
||||
Each scene is one Rudder API. The scripts live in
|
||||
`Packages/rudder.sdk/Samples~` and are the copy-paste examples for the SDK.
|
||||
|
||||
```text
|
||||
Tools > LiveOps > Bake Cozy Collector (All)
|
||||
```
|
||||
| Scene | SDK calls |
|
||||
| --- | --- |
|
||||
| `Authentication/Authentication.unity` | `Rudder.Initialize`, `Auth.LoginWithDeviceAsync`, `Player.GetProfileAsync`, `Auth.Logout` |
|
||||
| `RemoteConfig/RemoteConfig.unity` | `RemoteConfig.LoadAsync`, `Get(key, fallback)` |
|
||||
| `Storage/Storage.unity` | `Storage.GetAsync`, `SaveAsync`, `DeleteAsync` |
|
||||
| `StoreInventory/StoreInventory.unity` | `Stores.ListAsync`, `PurchaseAsync`, `Inventory.GetAsync` |
|
||||
| `Leaderboards/Leaderboards.unity` | `Leaderboards.FindBySlug`, `SubmitAsync`, `ListAsync` |
|
||||
| `Scenarios/Scenarios.unity` | `Scenario.TriggerAsync`, `Effects.OnNotification`, `Effects.OnStoreOffer`, … |
|
||||
|
||||
The bake scripts (`Assets/LiveOpsExamples/Editor/CozyCollectorBake.*.cs`) create or overwrite:
|
||||
|
||||
- `Assets/LiveOpsExamples/CozyCollectorSpriteLibrary.asset` — sprite references imported from `Sprites/`.
|
||||
- `Assets/LiveOpsExamples/Prefabs/` — snack, score popup, collect burst, and UI row prefabs.
|
||||
- `Assets/LiveOpsExamples/LiveOpsCozyCollector.unity` — camera, world, canvas, panels; assigns `Assets/LiveOpsLocal.asset` to `Rudder`; adds the scene to Build Settings.
|
||||
|
||||
The bake is idempotent: re-running it rewrites the same asset paths, so run it again after changing runtime scripts or sprites.
|
||||
|
||||
## Stage Data Checklist
|
||||
|
||||
The demo handles missing backend data with visible setup messages, but the strongest run needs these staging entities:
|
||||
The later scenes are more interesting with this staging data:
|
||||
|
||||
| Feature | Expected setup |
|
||||
| --- | --- |
|
||||
| Remote Config | Optional keys: `demo_round_seconds`, `demo_player_speed`, `demo_spawn_interval_ms`, `demo_collectible_score`. Defaults are used when keys are missing. |
|
||||
| Remote Config | Optional keys: `demo_round_seconds`, `demo_player_speed`. Defaults are used when keys are missing. |
|
||||
| Stores | A store with slug `cozy-camp-shop` with at least one offer. |
|
||||
| Leaderboards | A leaderboard with slug `cozy-collector-score`. |
|
||||
| Scenarios | Events `player_login` and/or `demo_round_finished`; offer `moonberry-boost` for the scenario offer flow. |
|
||||
| Storage | No setup required; the demo writes type `cozy_fantasy_save`, id `main`. |
|
||||
| Scenarios | Events `player_login` and/or `demo_round_finished`. |
|
||||
| Storage | No setup required; the sample writes type `sample_save`, id `main`. |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
Assets/LiveOpsExamples/
|
||||
LiveOpsCozyCollector.unity (baked)
|
||||
CozyCollectorSpriteLibrary.asset (baked)
|
||||
Sprites/ (imported from liveops-phaser-demo)
|
||||
Prefabs/ (baked)
|
||||
Scripts/ (runtime logic only)
|
||||
CozyCollectorController.cs
|
||||
GameWorld.cs, SnackSpawner.cs, Snack.cs, ScorePopupView.cs
|
||||
CozyCollectorConsts.cs, CozyCollectorSpriteLibrary.cs
|
||||
UI/ (panels and row views)
|
||||
Sandbox/ (legacy sandbox controllers)
|
||||
Editor/
|
||||
CozyCollectorBake.*.cs (asset/prefab/scene/UI bake)
|
||||
Assets/Samples/Rudder SDK/ (symlink to the package Samples~)
|
||||
Packages/rudder.sdk/
|
||||
Runtime/ (Rudder.Unity + Rudder.Core.dll)
|
||||
Samples~/ (feature sample scenes)
|
||||
Assets/LiveOpsLocal.asset (git-ignored; copy from LiveOpsLocal.asset.example)
|
||||
Packages/rudder.sdk/ (Rudder SDK UPM package: Runtime/Sources + Runtime/Plugins/Rudder.Core.dll)
|
||||
```
|
||||
|
||||
Runtime scripts never construct UI or scene objects; everything visual comes from the baked scene and prefabs, with references wired via `[SerializeField]`.
|
||||
|
||||
## Updating Core DLLs
|
||||
|
||||
Build the C# SDK from the sibling repository and copy the assembly into the
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
name: rudder-unity-sdk
|
||||
description: Use when working with the Rudder Unity SDK rudder.sdk in a Unity project — install, bootstrap, auth, remote config, storage, stores, inventory, leaderboards, quests, battle pass, scenarios.
|
||||
---
|
||||
|
||||
# Rudder Unity SDK (`rudder.sdk`)
|
||||
|
||||
UPM package `rudder.sdk` (v0.4.0): LiveOps SDK for Unity. It wraps
|
||||
`Rudder.Core.dll` (namespace `RudderSdk.Core`, the .NET SDK) with Unity
|
||||
adapters in `RudderSdk.Unity`. Public API surface:
|
||||
|
||||
- `RudderSdk.Unity` — `Rudder` (MonoBehaviour bootstrap), `RudderConfiguration`,
|
||||
`RudderState`, platform adapters.
|
||||
- `RudderSdk.Core` — `RudderClient` and feature services (`Auth`, `Player`,
|
||||
`RemoteConfig`, `Storage`, `ProjectStorage`, `Stores`, `Inventory`,
|
||||
`Leaderboards`, `Quests`, `BattlePass`, `Scenario`, `Effects`).
|
||||
- `RudderSdk.Core.Models.*` — DTOs (`PlayerProfile`, `Wallet`, `Offer`,
|
||||
`Store`, `StorageItem`, `RankEntry`, `Quest`, `Reward`, ...).
|
||||
|
||||
Full integration rules live in `Packages/rudder.sdk/AGENTS.md`; copy-paste
|
||||
sample scenes live in `Packages/rudder.sdk/Samples~/`. Verify signatures
|
||||
against those, never from memory.
|
||||
|
||||
## Install
|
||||
|
||||
Unity 6000.0+. Add the scoped registry and both dependencies to
|
||||
`Packages/manifest.json` (`com.unity.nuget.newtonsoft-json` is required —
|
||||
the package does not bundle Newtonsoft.Json):
|
||||
|
||||
```json
|
||||
{
|
||||
"scopedRegistries": [
|
||||
{
|
||||
"name": "Rudder",
|
||||
"url": "https://hub.rudder.build/api/packages/rudder/npm/",
|
||||
"scopes": ["rudder"]
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"rudder.sdk": "0.4.0",
|
||||
"com.unity.nuget.newtonsoft-json": "3.2.2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Bootstrap
|
||||
|
||||
1. Create the configuration asset: `Assets > Create > Rudder > Configuration`.
|
||||
2. Set `ProjectKey` (required). Defaults: `BaseUrl` `https://api.rudder.build`,
|
||||
`TimeoutSeconds` 10.
|
||||
3. Add the `Rudder` component to a startup scene object and assign the asset.
|
||||
`Rudder` is `[DefaultExecutionOrder(-1500), DisallowMultipleComponent]`,
|
||||
uses `DontDestroyOnLoad`, and pumps `client.Update(Time.deltaTime)` every
|
||||
frame — never call `client.Update` yourself.
|
||||
4. Initialize and sign in:
|
||||
|
||||
```csharp
|
||||
using RudderSdk.Unity;
|
||||
|
||||
var client = Rudder.Initialize(); // synchronous, idempotent
|
||||
await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player");
|
||||
```
|
||||
|
||||
`Rudder.Initialize()` throws if the component or configuration is missing.
|
||||
`Rudder.State` (`NotInitialized`/`Initializing`/`Ready`/`Failed`),
|
||||
`Rudder.LastError`, `Rudder.Client`, `Rudder.Ready` (Task) and
|
||||
`Rudder.Initialized` (event) expose lifecycle. Details:
|
||||
[reference/bootstrap.md](reference/bootstrap.md).
|
||||
|
||||
## Platform adapters
|
||||
|
||||
`RudderUnityClientFactory.Create(RudderUnityClientOptions)` wires Core
|
||||
abstractions to Unity implementations; `Rudder.Initialize()` does this for
|
||||
you with `PlayerPrefsUnityKeyValueStore` + `UnityDebugLoggerSink`:
|
||||
|
||||
| Core abstraction | Unity adapter | Notes |
|
||||
| --- | --- | --- |
|
||||
| `IRudderTransport` | `UnityTransportAdapter` over `UnityWebRequestExecutor` | `UnityWebRequest`, timeout from config |
|
||||
| `ITokenStore` | `UnityTokenStoreAdapter` | tokens in PlayerPrefs |
|
||||
| `IDeviceIdProvider` | `UnityDeviceIdProvider` | GUID in PlayerPrefs (`rudder_device_id`) — not `SystemInfo.deviceUniqueIdentifier` |
|
||||
| `IClock` | `UnityClock` | `DateTimeOffset.UtcNow` |
|
||||
| `IRudderLogger` | `UnityLoggerAdapter` → `IUnityLoggerSink` | default `UnityDebugLoggerSink` → `Debug.Log*` |
|
||||
|
||||
## Capabilities
|
||||
|
||||
All calls hang off `client` / `Rudder.Client`. Match the task to one row,
|
||||
then read the reference file for exact signatures.
|
||||
|
||||
| Task | API | Reference |
|
||||
| --- | --- | --- |
|
||||
| Init, sign in, profile, wallets, errors | `Rudder.Initialize`, `client.Auth.LoginWithDeviceAsync`, `Auth.*`, `Player.GetProfileAsync` | [reference/bootstrap.md](reference/bootstrap.md), [reference/authentication.md](reference/authentication.md) |
|
||||
| Remote tunables | `RemoteConfig.LoadAsync`, `Get<T>(key, fallback)`, `GetAsync` | [reference/remote-config.md](reference/remote-config.md) |
|
||||
| Player cloud save / project storage | `Storage.GetAsync`/`SaveAsync`/`DeleteAsync`, `ProjectStorage.*` | [reference/storage.md](reference/storage.md) |
|
||||
| Shop + inventory | `Stores.ListAsync`/`GetAsync`/`PurchaseAsync`, `Inventory.GetAsync` | [reference/stores-inventory.md](reference/stores-inventory.md) |
|
||||
| High scores | `Leaderboards.FindBySlug(slug)` → `SubmitAsync`/`ListAsync` | [reference/leaderboards.md](reference/leaderboards.md) |
|
||||
| LiveOps scenarios | `Scenario.TriggerAsync`, `Effects.OnNotification`/`OnStoreOffer`/... | [reference/scenarios.md](reference/scenarios.md) |
|
||||
| Global quests | `Quests.ListAsync`/`ClaimAsync`/`ReportProgressAsync` | [reference/quests.md](reference/quests.md) |
|
||||
| Battle pass | `BattlePass.*` with scenario/node ids, `BattlePassEffect` | [reference/battlepass.md](reference/battlepass.md) |
|
||||
|
||||
## Hard rules
|
||||
|
||||
- Call the SDK from a `MonoBehaviour` or the game's service object; no second
|
||||
singleton next to `Rudder`, no wrapper layers, no consumer-local interfaces
|
||||
over `RudderClient`.
|
||||
- Use SDK model types directly; do not re-wrap `Offer`/`PlayerProfile`/
|
||||
`RankEntry` in project DTOs unless asked.
|
||||
- No `ConfigureAwait(false)` in game code — continuations must return to the
|
||||
Unity synchronization context.
|
||||
- Catch `RudderApiException` subclasses and surface `exception.Message`;
|
||||
do not swallow errors. A 401 is retried once after a token refresh by the
|
||||
client itself.
|
||||
- Backend entities must exist: store slug, leaderboard slug, remote-config
|
||||
key, scenario event. Missing data is a 404, not an SDK bug.
|
||||
- Every feature call requires a matching dashboard entity; the API base is
|
||||
`https://api.rudder.build`, dashboard `https://app.rudder.build/`.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Authentication and player profile
|
||||
|
||||
Namespace `RudderSdk.Core`. Verified against the bundled `Rudder.Core.dll`
|
||||
and `Samples~/Authentication/AuthenticationSample.cs`.
|
||||
|
||||
## Sign in
|
||||
|
||||
```csharp
|
||||
var client = Rudder.Initialize();
|
||||
await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player");
|
||||
```
|
||||
|
||||
`AuthService.LoginWithDeviceAsync(string region, string language,
|
||||
string nickname = null, CancellationToken cancellationToken = default)`
|
||||
returns `LoginViaDeviceResponse` (`AccessToken`, `RefreshToken`). Tokens are
|
||||
saved to PlayerPrefs by the Unity adapter. Pending scenario effects are
|
||||
fetched after sign-in by the effects heartbeat.
|
||||
|
||||
Auth service:
|
||||
|
||||
- `Task<LoginViaDeviceResponse> LoginWithDeviceAsync(string region, string language, string nickname = null, CancellationToken = default)`
|
||||
- `Task<bool> RefreshAsync()` — explicit token refresh.
|
||||
- `void Logout()` — clears tokens locally.
|
||||
- `event Action<RudderAuthState> AuthStateChanged` — `RudderAuthState.SignedIn` / `SignedOut`. `SignedOut` also fires when a refresh fails.
|
||||
|
||||
Login uses the SDK device id from PlayerPrefs. Do not pass
|
||||
`SystemInfo.deviceUniqueIdentifier`.
|
||||
|
||||
## Profile and wallets
|
||||
|
||||
```csharp
|
||||
var profile = await client.Player.GetProfileAsync();
|
||||
var nickname = profile.Player.Nickname;
|
||||
foreach (var wallet in profile.Wallets)
|
||||
Debug.Log($"{wallet.Currency}: {wallet.Balance}");
|
||||
```
|
||||
|
||||
- `Task<PlayerProfile> PlayerService.GetProfileAsync(CancellationToken = default)`
|
||||
- `PlayerProfile` — `Player Player`, `List<Wallet> Wallets`.
|
||||
- `Player` — `Id`, `ProjectId`, `Nickname`, `Region`, `Language`,
|
||||
`CreatedAt` (`DateTimeOffset?`).
|
||||
- `Wallet` — `Currency` (string), `Balance` (`long?`).
|
||||
|
||||
Models are nullable since 0.4.0 — check fields before use. After any
|
||||
purchase or reward grant, reload the profile to see new wallet balances;
|
||||
there is no change callback.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Battle pass
|
||||
|
||||
`client.BattlePass` (`BattlePassService`), verified against the bundled
|
||||
`Rudder.Core.dll`. There is no sample scene for this feature.
|
||||
|
||||
Progress is tied to a scenario battle-pass node. Prefer `BattlePassEffect`
|
||||
delivered by `Effects.OnBattlePass` — it binds `scenarioId`/`nodeId`/
|
||||
`runId` for you. Direct `BattlePassService` calls need those ids manually.
|
||||
|
||||
## BattlePassEffect (preferred)
|
||||
|
||||
```csharp
|
||||
client.Effects.OnBattlePass += async effect =>
|
||||
{
|
||||
var progress = await effect.GetProgressAsync();
|
||||
await effect.AddXpAsync("kills", 100);
|
||||
var claim = await effect.ClaimRewardAsync(level, BattlePassService.TrackFree);
|
||||
};
|
||||
```
|
||||
|
||||
- `Task<GetBattlePassProgressResponse> GetProgressAsync(CancellationToken = default)`
|
||||
- `Task<AddBattlePassXpResponse> AddXpAsync(string source, long amount, CancellationToken = default)`
|
||||
- `Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(int level, string track, CancellationToken = default)`
|
||||
- `Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(CancellationToken = default)` — posts `onPremiumPurchase` on success
|
||||
- `LevelUp()` / `LevelUpAsync()` — posts `onLevelUp`
|
||||
- `End()` / `EndAsync()` — posts `onComplete`
|
||||
- `Get<T>(key, fallback)`, `RunId`, `ScenarioId`, `NodeId`, `Data` like other effects
|
||||
|
||||
`BattlePassLevelEffect` (from `OnBattlePassLevel`) has `Claim()` /
|
||||
`ClaimAsync()` (posts `onComplete`) and a `Level` property.
|
||||
|
||||
## BattlePassService (direct)
|
||||
|
||||
- `Task<GetBattlePassProgressResponse> GetProgressAsync(string scenarioId, string nodeId, CancellationToken = default)`
|
||||
- `Task<AddBattlePassXpResponse> AddXpAsync(AddBattlePassXpRequest request, CancellationToken = default)`
|
||||
- `Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(ClaimBattlePassRewardRequest request, CancellationToken = default)`
|
||||
- `Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(PurchaseBattlePassPremiumRequest request, CancellationToken = default)`
|
||||
- Track constants: `BattlePassService.TrackFree`, `BattlePassService.TrackPremium` (static fields).
|
||||
|
||||
Request models carry `ScenarioId`, `NodeId`, and (except progress)
|
||||
`RunId`; `PurchaseBattlePassPremiumRequest` also takes `IdempotencyKey`.
|
||||
|
||||
## Response models
|
||||
|
||||
- `GetBattlePassProgressResponse` — `Level` (`int?`), `Xp` (`long?`),
|
||||
`PremiumOwned` (`bool?`), `ClaimedTiers` (`List<ClaimedTier>`: `Level`,
|
||||
`Track`).
|
||||
- `AddBattlePassXpResponse` — `Xp` (`long?`), `Level` (`int?`),
|
||||
`LeveledUp` (`bool?`), `MaxLevel` (`bool?`).
|
||||
- `ClaimBattlePassRewardResponse` — `Success` (`bool?`),
|
||||
`AlreadyClaimed` (`bool?`), `Granted` (`List<Reward>`), `Error`.
|
||||
- `PurchaseBattlePassPremiumResponse` — `Success` (`bool?`), `Error`.
|
||||
|
||||
Business failures come back in the response (`Success != true`, `Error`)
|
||||
with codes from `RudderErrorCodes` (`LevelNotReached`,
|
||||
`ObjectivesIncomplete`, `NodeNotActive`, `RunExpired`, ...); transport
|
||||
failures throw `RudderApiException` subclasses.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Bootstrap and lifecycle
|
||||
|
||||
Namespace `RudderSdk.Unity`. Verified against
|
||||
`Packages/rudder.sdk/Runtime/Sources/Rudder.cs`,
|
||||
`RudderConfiguration.cs` and the bundled `Rudder.Core.dll`.
|
||||
|
||||
## RudderConfiguration (ScriptableObject)
|
||||
|
||||
Create via `Assets > Create > Rudder > Configuration`.
|
||||
|
||||
| Field | Type | Default | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `ProjectKey` | `string` | — | Required. Issued in the Rudder admin panel. Empty key logs a warning in `OnValidate`. |
|
||||
| `BaseUrl` | `string` | `https://api.rudder.build` | API base URL. |
|
||||
| `TimeoutSeconds` | `int` (`[Min(1)]`) | `10` | HTTP request timeout. |
|
||||
|
||||
Keep the asset with the real key out of git (this repo uses the git-ignored
|
||||
`Assets/LiveOpsLocal.asset` pattern).
|
||||
|
||||
## Rudder (MonoBehaviour)
|
||||
|
||||
`[DefaultExecutionOrder(-1500), DisallowMultipleComponent]`. Put it on a
|
||||
startup scene object, assign the configuration asset. `Awake` keeps the first
|
||||
instance (`DontDestroyOnLoad`) and destroys duplicates. `Update` pumps
|
||||
`_client.Update(Time.deltaTime)` (effects heartbeat and wait-deadline checks
|
||||
need it — do not call `client.Update` yourself).
|
||||
|
||||
Static API:
|
||||
|
||||
- `RudderState State` — `NotInitialized` / `Initializing` / `Ready` / `Failed`.
|
||||
- `Exception LastError` — initialization error when `State` is `Failed`.
|
||||
- `RudderClient Client` — the Core client; throws `InvalidOperationException`
|
||||
until `State` is `Ready`.
|
||||
- `Task<RudderClient> Ready` — completes with the client after `Initialize`,
|
||||
faults on initialization error. Prefer `Initialize()` in game code.
|
||||
- `event Action<RudderClient> Initialized` — attach point for add-on
|
||||
packages; late subscribers must check `State` themselves.
|
||||
- `RudderClient Initialize()` — synchronous. Reads the serialized
|
||||
configuration, creates `RudderClient` via `RudderUnityClientFactory`.
|
||||
Idempotent: returns the same client when already `Ready`; resets and
|
||||
retries after `Failed`. Throws if the component is not in the scene or no
|
||||
configuration is assigned. Does not create a GameObject.
|
||||
|
||||
Statics reset on Enter Play Mode without domain reload
|
||||
(`RuntimeInitializeOnLoadMethod(SubsystemRegistration)`).
|
||||
|
||||
## Minimal startup
|
||||
|
||||
```csharp
|
||||
using RudderSdk.Unity;
|
||||
using UnityEngine;
|
||||
|
||||
public class Bootstrap : MonoBehaviour
|
||||
{
|
||||
private async void Start()
|
||||
{
|
||||
var client = Rudder.Initialize();
|
||||
await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`LoginWithDeviceAsync` logs in with the SDK device id (a GUID persisted
|
||||
in PlayerPrefs under `rudder_device_id`). It is the first awaited call;
|
||||
afterwards use `client.*` / `Rudder.Client.*`. Tokens persist in PlayerPrefs,
|
||||
so a second launch can call it again (it refreshes the session). See
|
||||
[authentication.md](authentication.md) for the Auth service.
|
||||
|
||||
Pending scenario effects are fetched after sign-in by the effects heartbeat
|
||||
(`client.Update`, pumped by `Rudder`). Subscribe to `client.Effects.On*`
|
||||
before `Scenario.TriggerAsync`.
|
||||
|
||||
## Errors
|
||||
|
||||
All API errors are `RudderApiException` subclasses from `RudderSdk.Core`:
|
||||
|
||||
- `RudderAuthException` — 401; refresh failed, session over
|
||||
(`AuthStateChanged(SignedOut)` fires).
|
||||
- `RudderNotFoundException` — 404.
|
||||
- `RudderRateLimitException` — 429.
|
||||
- `RudderNetworkException` — no response / timeout.
|
||||
|
||||
`RudderApiException` exposes `StatusCode` (int), `Code` (string),
|
||||
`RequestId` (string). The client retries a 401 once after a single-flight
|
||||
token refresh — game code does not implement that retry. Catch and surface
|
||||
`exception.Message`; do not swallow errors.
|
||||
|
||||
Removed APIs (do not use): `Rudder.Initialize(configuration)`,
|
||||
`Rudder.Auth`-style static service shortcuts, `client.Scenarios`,
|
||||
`BuyAsync`, `LoginViaDeviceAsync`, `AuthorizeWithDeviceAsync`,
|
||||
`Scenario.RestoreAsync`.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Leaderboards
|
||||
|
||||
`client.Leaderboards` (`LeaderboardsService`), verified against the bundled
|
||||
`Rudder.Core.dll` and `Samples~/Leaderboards/LeaderboardsSample.cs`.
|
||||
|
||||
```csharp
|
||||
var board = client.Leaderboards.FindBySlug("my-board");
|
||||
await board.SubmitAsync(score);
|
||||
var top = await board.ListAsync(10);
|
||||
```
|
||||
|
||||
API:
|
||||
|
||||
- `LeaderboardHandle FindBySlug(string slug)` — no network call; returns a
|
||||
handle bound to the slug.
|
||||
- `LeaderboardHandle.SubmitAsync(double score, CancellationToken = default)`
|
||||
- `LeaderboardHandle.ListAsync(int limit = 100, CancellationToken = default)`
|
||||
→ `IReadOnlyList<RankEntry>`.
|
||||
- `LeaderboardHandle.Slug { get; }`
|
||||
|
||||
`RankEntry` — `PlayerId`, `PlayerName`, `Rank` (`long?`), `Score`
|
||||
(`double?`).
|
||||
|
||||
The leaderboard slug must exist in the dashboard, else the calls 404. For
|
||||
scenario leaderboard nodes see `OnLeaderboard` in
|
||||
[scenarios.md](scenarios.md).
|
||||
@@ -0,0 +1,33 @@
|
||||
# Quests (global)
|
||||
|
||||
`client.Quests` (`QuestsService`) — the player's global quest list, distinct
|
||||
from scenario `OnQuest` nodes (`QuestEffect`, see
|
||||
[scenarios.md](scenarios.md)). Verified against the bundled
|
||||
`Rudder.Core.dll`. There is no sample scene for this feature.
|
||||
|
||||
```csharp
|
||||
var quests = await client.Quests.ListAsync();
|
||||
var claim = await client.Quests.ClaimAsync(quest.Id);
|
||||
var completedIds = await client.Quests.ReportProgressAsync("kills", 1);
|
||||
```
|
||||
|
||||
API:
|
||||
|
||||
- `Task<IReadOnlyList<Quest>> ListAsync(CancellationToken = default)`
|
||||
- `Task<ClaimQuestResponse> ClaimAsync(string questId, CancellationToken = default)`
|
||||
- `Task<IReadOnlyList<string>> ReportProgressAsync(string metric, long amount, CancellationToken = default)` — returns the ids of quests completed by this report.
|
||||
|
||||
Models:
|
||||
|
||||
- `Quest` — `Id`, `Name`, `Status`, `Objectives`
|
||||
(`List<QuestObjectiveProgress>`), `Rewards` (`List<Reward>`).
|
||||
- `QuestObjectiveProgress` — `ObjectiveId`, `Metric`, `Current` (`long?`),
|
||||
`Target` (`long?`), `Completed` (`bool?`).
|
||||
- `Reward` — `Currency`, `ItemId`, `Amount` (`long?`).
|
||||
- `ClaimQuestResponse` — `Success` (`bool?`), `AlreadyClaimed` (`bool?`),
|
||||
`Granted` (`List<Reward>`), `Error`.
|
||||
|
||||
Store purchases already report metrics automatically; the metric strings are
|
||||
built by `QuestMetrics.PurchaseOffer(offerId)` → `purchase.offer:<offerId>`
|
||||
and `QuestMetrics.PurchaseItem(itemId)` → `purchase.item:<itemId>`
|
||||
(`RudderSdk.Core.QuestMetrics`, static).
|
||||
@@ -0,0 +1,25 @@
|
||||
# Remote config
|
||||
|
||||
`client.RemoteConfig` (`RemoteConfigService`, namespace `RudderSdk.Core`).
|
||||
Verified against the bundled `Rudder.Core.dll` and
|
||||
`Samples~/RemoteConfig/RemoteConfigSample.cs`.
|
||||
|
||||
```csharp
|
||||
await client.RemoteConfig.LoadAsync();
|
||||
var speed = client.RemoteConfig.Get("player_speed", 5f);
|
||||
```
|
||||
|
||||
API:
|
||||
|
||||
- `Task<IReadOnlyDictionary<string, RemoteConfig>> LoadAsync(CancellationToken = default)` — fetches all configs into the cache.
|
||||
- `T Get<T>(string key, T defaultValue = default)` — reads the cache synchronously. Call `LoadAsync` first (or use `GetAsync`).
|
||||
- `Task<T> GetAsync<T>(string key, T defaultValue = default, CancellationToken = default)` — loads on first use, then reads.
|
||||
- `Task<RemoteConfig> GetConfigAsync(string key, CancellationToken = default)` — full config entry.
|
||||
- `bool IsLoaded { get; }`, `IReadOnlyDictionary<string, RemoteConfig> Configs { get; }` — cache state.
|
||||
|
||||
`RemoteConfig` model: `Id`, `Key`, `Value` (string), `ValueType`,
|
||||
`Description`, `Environment`, `ProjectId`, `Active` (`bool?`), `CreatedAt`,
|
||||
`UpdatedAt`.
|
||||
|
||||
A missing key returns the fallback; the key must exist in the dashboard to
|
||||
have a server value.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Scenarios
|
||||
|
||||
Scenario graphs execute on the server. `client.Scenario` (`ScenarioService`)
|
||||
only triggers an event; pending nodes surface through `client.Effects`
|
||||
(`EffectsService`). Verified against the bundled `Rudder.Core.dll` and
|
||||
`Samples~/Scenarios/ScenariosSample.cs`.
|
||||
|
||||
## Flow
|
||||
|
||||
Subscribe to `Effects.On*` **before** triggering, then trigger an event name
|
||||
configured in the dashboard (examples: `player_login`,
|
||||
`demo_round_finished`). Unsubscribe on destroy.
|
||||
|
||||
```csharp
|
||||
client.Effects.OnNotification += effect => { /* show UI, then effect.Done() */ };
|
||||
await client.Scenario.TriggerAsync("player_login");
|
||||
```
|
||||
|
||||
API:
|
||||
|
||||
- `Task ScenarioService.TriggerAsync(string eventName, CancellationToken = default)` — posts the event and ingests any effects the response contains. Does not return runs.
|
||||
- `void RudderClient.Update(float deltaTime)` — pumped by the `Rudder` component. Drives the 30s pending-effects heartbeat and wait-deadline checks. Do not call it from game code.
|
||||
|
||||
After `Auth.LoginWithDeviceAsync`, the heartbeat fetches pending effects for
|
||||
the signed-in player. Effects that arrive later (or after a wait deadline)
|
||||
are dispatched the same way.
|
||||
|
||||
## Events and effects
|
||||
|
||||
All effect types expose `RunId`, `ScenarioId`, `NodeId`, `Data` (`JObject`),
|
||||
and `T Get<T>(string key, T defaultValue = default)` for node data.
|
||||
|
||||
| Event | Effect | Game must |
|
||||
| --- | --- | --- |
|
||||
| `OnNotification` | `NotificationEffect` | Show UI (`Title`, `Message`), then `Done()` / `DoneAsync()`. |
|
||||
| `OnStoreOffer` | `StoreOfferEffect` | Show offer (`StoreSlug`, `Get("storeSlug", ...)`); on buy call `Stores.PurchaseAsync` then `Purchase()` / `PurchaseAsync()`, else `Decline()` / `DeclineAsync()`. `IsResolved` marks resolution. |
|
||||
| `OnWait` | `WaitEffect` | Deadline only (`DeadlineUtc`); the server advances the run. |
|
||||
| `OnQuest` | `QuestEffect` | Scenario quest node: `ReportProgress(objectiveId, amount)` / `ReportProgressAsync`. The server auto-completes the node once every objective is satisfied. Not `client.Quests`. |
|
||||
| `OnLeaderboard` | `LeaderboardEffect` | `End()` / `EndAsync()`, `Claim()` / `ClaimAsync()`. `IsResolved` marks resolution. |
|
||||
| `OnBattlePass` | `BattlePassEffect` | Drive the effect — see [battlepass.md](battlepass.md). |
|
||||
| `OnBattlePassLevel` | `BattlePassLevelEffect` | `Claim()` / `ClaimAsync()` for the level reward; `Level` prop. |
|
||||
| `OnScenarioCompleted` | `ScenarioCompletedEffect` | `RunId`, `ScenarioId`. Log / surface. |
|
||||
| `OnScenarioFailed` | `ScenarioFailedEffect` | `RunId`, `ScenarioId`, `NodeId`, `Exception`. Log / surface. |
|
||||
|
||||
Async and sync variants are equivalent; the sync variants fire-and-forget
|
||||
the same server callback. The event name must match a scenario trigger
|
||||
configured in the dashboard — an unknown event simply produces no effects.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Storage
|
||||
|
||||
Two services, both verified against the bundled `Rudder.Core.dll` and
|
||||
`Samples~/Storage/StorageSample.cs`.
|
||||
|
||||
## Player storage — `client.Storage` (`StorageService`)
|
||||
|
||||
Per-player key-value items. `StorageItem` is `{ Type, Id, Data }` where
|
||||
`Data` is an opaque JSON string — serialize your save object with
|
||||
`JsonUtility` or Newtonsoft; do not invent a second save format.
|
||||
|
||||
- `Task<GetStorageResponse> GetAsync(string type = null, int limit = 100, string cursor = null, CancellationToken = default)` — pages items; response has `Items` (`List<StorageItem>`) and `NextCursor`.
|
||||
- `IAsyncEnumerable<StorageItem> ListAllAsync(string type = null, int limit = 100, CancellationToken = default)` — follows cursors for you.
|
||||
- `Task SaveAsync(IEnumerable<StorageItem> items, CancellationToken = default)` — upserts by `Type`+`Id`.
|
||||
- `Task DeleteAsync(string type, CancellationToken = default)` — deletes all items of that type.
|
||||
|
||||
```csharp
|
||||
await client.Storage.SaveAsync(new[]
|
||||
{
|
||||
new StorageItem { Type = "sample_save", Id = "main", Data = json }
|
||||
});
|
||||
var response = await client.Storage.GetAsync("sample_save");
|
||||
await client.Storage.DeleteAsync("sample_save");
|
||||
```
|
||||
|
||||
## Project storage — `client.ProjectStorage` (`ProjectStorageService`)
|
||||
|
||||
Global key-value storage shared across players (read from the SDK; writes
|
||||
respect item permissions).
|
||||
|
||||
- `Task<GetProjectStorageResponse> GetAsync(string type = null, int limit = 100, string cursor = null, CancellationToken = default)` — `Items` + `NextCursor`.
|
||||
- `IAsyncEnumerable<ProjectStorageItem> ListAllAsync(string type = null, int limit = 100, CancellationToken = default)`
|
||||
- `Task SaveAsync(IEnumerable<ProjectStorageUpdateItem> items, string idempotencyKey = null, CancellationToken = default)`
|
||||
|
||||
`ProjectStorageItem`: `Type`, `Id`, `Data` (JSON string), `ReadPermission`,
|
||||
`WritePermission`, `Version` (`long?`), `Size` (`long?`), `ExpiresAt`,
|
||||
`UpdatedAt`. `ProjectStorageUpdateItem`: `{ Type, Data }`.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Stores and inventory
|
||||
|
||||
Verified against the bundled `Rudder.Core.dll` and
|
||||
`Samples~/StoreInventory/StoreInventorySample.cs`.
|
||||
|
||||
## Stores — `client.Stores` (`StoresService`)
|
||||
|
||||
```csharp
|
||||
var stores = await client.Stores.ListAsync();
|
||||
var response = await client.Stores.PurchaseAsync(storeSlug, offerId);
|
||||
if (response != null && response.Success != true)
|
||||
throw new InvalidOperationException(response.Error ?? "Purchase rejected");
|
||||
```
|
||||
|
||||
- `Task<IReadOnlyList<Store>> ListAsync(CancellationToken = default)`
|
||||
- `Task<Store> GetAsync(string slug, CancellationToken = default)`
|
||||
- `Task<PurchaseOfferResponse> PurchaseAsync(string storeSlug, string offerId, string idempotencyKey = null, CancellationToken = default)` — generates an idempotency key when omitted.
|
||||
|
||||
Models:
|
||||
|
||||
- `Store` — `Id`, `Slug`, `Name`, `Description`, `Status`, `Environment`,
|
||||
`ProjectId`, `ScenarioId`, `Data` (`JToken`), `Offers` (`List<Offer>`),
|
||||
`CreatedAt`, `UpdatedAt`.
|
||||
- `Offer` — `Id`, `Name`, `Price` (`OfferPrice`), `Contents`
|
||||
(`List<OfferContent>`), `MaxPurchases` (`int?`), `CreatedAt`, `UpdatedAt`.
|
||||
- `OfferPrice` — `Currency`, `Amount` (`long?`).
|
||||
- `OfferContent` — `ItemId`, `Amount` (`long?`).
|
||||
- `PurchaseOfferResponse` — `Success` (`bool?`), `Error`, `PurchaseId`.
|
||||
|
||||
Check `Success != true` (nullable bool), not `!Success`. After a purchase,
|
||||
reload wallets (`Player.GetProfileAsync`) and inventory — there is no
|
||||
`onChange` callback. Purchases auto-report quest metrics
|
||||
`purchase.offer:<offerId>` / `purchase.item:<itemId>` (see
|
||||
[quests.md](quests.md)). The store slug must exist in the dashboard, else 404.
|
||||
|
||||
## Inventory — `client.Inventory` (`InventoryService`)
|
||||
|
||||
```csharp
|
||||
var items = await client.Inventory.GetAsync();
|
||||
```
|
||||
|
||||
- `Task<IReadOnlyList<PlayerInventoryItem>> GetAsync(CancellationToken = default)`
|
||||
- `PlayerInventoryItem` — `Slug`, `Amount` (`long?`), `NameOverride`,
|
||||
`PropertiesOverride` (`JToken`), `UpdatedAt`.
|
||||
Reference in New Issue
Block a user