Skip to content

Commit 99bf8c2

Browse files
feat(audience-quickstart): drop-in QuickStart sample covering every SDK API (SDK-49)
Adds a Samples/QuickStart folder (no tilde — Unity auto-includes it when the package is installed). Drop AudienceDemo on any GameObject or open the bundled scene, set the publishable key, press Play. IMGUI overlay (zero scene setup): - Boxed SDK-status panel reads the public diagnostic getters live (Init, Environment, Consent, Pub key, User ID, Anon ID, Session ID, Queued). Full GUIDs; Copy buttons write raw values to the system clipboard. Pub key row flags the REPLACE_ME placeholder in red so misconfiguration is obvious before the backend 401s. - Sections: SDK lifecycle (Init / Shutdown / Flush), Typed events, Custom event, Identity, Privacy consent, Advanced (GDPR erasure). Each has a plain-English description so a zero-context reader understands what the buttons do without opening source. - Two-line buttons: action sentence on top ("Player started a level"), SDK API in parens below ("(Progression.Start)"). - Buttons gate on live SDK state — Track-family disables below Consent.None, Identify/Alias require Full consent, each Consent button disables at its own level, Init enables only when not initialised, Shutdown/Flush/Delete only when initialised. - No auto-init: user presses Start the SDK explicitly; every SDK-dependent button stays disabled until they do. - Panel caps at 800px and centres horizontally so it reads as a focused card on wide screens and fills narrow Game views. Sample payloads use familiar fantasy terminology (overworld, stone_age, gold, diamond_sword, dragon_defeated) — cold-readable without gaming background, replaced during integration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4c01094 commit 99bf8c2

6 files changed

Lines changed: 661 additions & 0 deletions

File tree

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
using System.Collections.Generic;
2+
using UnityEngine;
3+
4+
namespace Immutable.Audience.Samples.QuickStart
5+
{
6+
public sealed class AudienceDemo : MonoBehaviour
7+
{
8+
[Header("Publishable key")]
9+
[Tooltip("Your publishable key. Test keys start with pk_imapik-test-.")]
10+
public string PublishableKey = "pk_imapik-test-REPLACE_ME";
11+
12+
[Header("Environment")]
13+
[Tooltip("Which Immutable backend to send events to. Sandbox is the safe default " +
14+
"for development; switch to Production explicitly when shipping to live " +
15+
"players. Dev is reserved for Immutable engineers.")]
16+
public AudienceEnvironment Environment = AudienceEnvironment.Sandbox;
17+
18+
[Header("Starting consent")]
19+
[Tooltip("Starting consent level. Studios normally collect this from the player.")]
20+
public ConsentLevel StartingConsent = ConsentLevel.Anonymous;
21+
22+
[Header("Distribution platform")]
23+
[Tooltip("Optional — use DistributionPlatforms.Steam / .Epic / .GOG / .Itch / .Standalone for autocomplete, " +
24+
"or any custom string. Sent as a property on game_launch. " +
25+
"Defaults to Standalone so first-run sample data does not falsely tag every integrator as Steam.")]
26+
public string DistributionPlatform = DistributionPlatforms.Standalone;
27+
28+
[Tooltip("Enable ambient [ImmutableAudience] log lines.")]
29+
public bool DebugLogging = true;
30+
31+
public void InitSdk()
32+
{
33+
ImmutableAudience.Init(new AudienceConfig
34+
{
35+
PublishableKey = PublishableKey,
36+
Environment = Environment,
37+
Consent = StartingConsent,
38+
DistributionPlatform = DistributionPlatform,
39+
Debug = DebugLogging,
40+
OnError = err => Debug.LogWarning($"[AudienceDemo] SDK error: {err.Code}{err.Message}"),
41+
});
42+
}
43+
44+
public void ShutdownSdk() => ImmutableAudience.Shutdown();
45+
46+
// async void swallows exceptions; try/catch routes them through OnError instead.
47+
public async void FlushNow()
48+
{
49+
try
50+
{
51+
await ImmutableAudience.FlushAsync();
52+
}
53+
catch (System.Exception ex)
54+
{
55+
Debug.LogWarning($"[AudienceDemo] FlushAsync threw: {ex.Message}");
56+
}
57+
}
58+
59+
public void RequestGdprErasure() => ImmutableAudience.DeleteData();
60+
61+
public void FireProgressionStart() => ImmutableAudience.Track(new Progression
62+
{
63+
Status = ProgressionStatus.Start,
64+
World = "overworld",
65+
Level = "stone_age",
66+
});
67+
68+
public void FireProgressionComplete() => ImmutableAudience.Track(new Progression
69+
{
70+
Status = ProgressionStatus.Complete,
71+
World = "overworld",
72+
Level = "stone_age",
73+
Score = 1500,
74+
DurationSec = 120f,
75+
});
76+
77+
public void FireResourceEarn() => ImmutableAudience.Track(new Resource
78+
{
79+
Flow = ResourceFlow.Source,
80+
Currency = "gold",
81+
Amount = 100,
82+
ItemType = "monster_kill",
83+
ItemId = "zombie",
84+
});
85+
86+
public void FireResourceSpend() => ImmutableAudience.Track(new Resource
87+
{
88+
Flow = ResourceFlow.Sink,
89+
Currency = "gold",
90+
Amount = 50,
91+
ItemType = "weapon",
92+
ItemId = "diamond_sword",
93+
});
94+
95+
// Production: use the payment provider's stable order id, not a fresh GUID.
96+
public void FirePurchase() => ImmutableAudience.Track(new Purchase
97+
{
98+
Currency = "USD",
99+
Value = 9.99m,
100+
ItemId = "skin_pack_knight",
101+
ItemName = "Knight Skin Pack",
102+
Quantity = 1,
103+
TransactionId = System.Guid.NewGuid().ToString(),
104+
});
105+
106+
public void FireMilestone() => ImmutableAudience.Track(new MilestoneReached
107+
{
108+
Name = "dragon_defeated",
109+
});
110+
111+
public void FireCustomEvent() => ImmutableAudience.Track("crafting_started", new Dictionary<string, object>
112+
{
113+
["recipe_id"] = "diamond_sword",
114+
["station"] = "crafting_table",
115+
["player_level"] = 20,
116+
});
117+
118+
public void IdentifyAsSteam() =>
119+
ImmutableAudience.Identify("76561198012345", IdentityType.Steam);
120+
121+
public void AliasSteamToPassport() => ImmutableAudience.Alias(
122+
fromId: "76561198012345", fromType: IdentityType.Steam,
123+
toId: "user_abc", toType: IdentityType.Passport);
124+
125+
public void ResetIdentity() => ImmutableAudience.Reset();
126+
127+
public void ConsentNone() => ImmutableAudience.SetConsent(ConsentLevel.None);
128+
public void ConsentAnonymous() => ImmutableAudience.SetConsent(ConsentLevel.Anonymous);
129+
public void ConsentFull() => ImmutableAudience.SetConsent(ConsentLevel.Full);
130+
131+
private void OnGUI()
132+
{
133+
const float padding = 8f;
134+
const float buttonHeight = 44f;
135+
const float maxPanelWidth = 800f;
136+
var panelWidth = Mathf.Min(Screen.width - padding * 2, maxPanelWidth);
137+
var panelX = (Screen.width - panelWidth) * 0.5f;
138+
139+
EnsureStyles();
140+
141+
var init = ImmutableAudience.Initialized;
142+
var consent = ImmutableAudience.CurrentConsent;
143+
var canTrack = init && consent != ConsentLevel.None;
144+
var canIdentify = init && consent == ConsentLevel.Full;
145+
146+
GUILayout.BeginArea(new Rect(panelX, padding, panelWidth, Screen.height - padding * 2));
147+
_scroll = GUILayout.BeginScrollView(_scroll);
148+
149+
GUILayout.Label("Immutable Audience — QuickStart", _titleStyle);
150+
GUILayout.Label(
151+
"Press a button to send a sample event. The panel below shows what the SDK is doing. " +
152+
"Check the Unity Console for log output.",
153+
_introStyle);
154+
155+
DrawStatusPanel();
156+
157+
DrawSection("SDK lifecycle",
158+
"Start, stop, and flush the SDK. Press Start the SDK first — every " +
159+
"other button stays disabled until the SDK is initialised.");
160+
TwoColumnButtons(buttonHeight,
161+
("Start the SDK\n(Init)", InitSdk, !init),
162+
("Turn off the SDK\n(Shutdown)", ShutdownSdk, init),
163+
("Send queued events now\n(FlushAsync)", FlushNow, init));
164+
165+
DrawSection("Typed events",
166+
"Standard event types Immutable's dashboards chart automatically: " +
167+
"player progression, currency in/out, purchases, achievements.");
168+
TwoColumnButtons(buttonHeight,
169+
("Player started a level\n(Progression.Start)", FireProgressionStart, canTrack),
170+
("Player finished a level\n(Progression.Complete)", FireProgressionComplete, canTrack),
171+
("Player earned currency\n(Resource.Source)", FireResourceEarn, canTrack),
172+
("Player spent currency\n(Resource.Sink)", FireResourceSpend, canTrack),
173+
("Player made a purchase\n(Purchase)", FirePurchase, canTrack),
174+
("Player reached a milestone\n(MilestoneReached)", FireMilestone, canTrack));
175+
176+
DrawSection("Custom event",
177+
"Send any event you want. You pick the name and the data — Immutable stores both.");
178+
TwoColumnButtons(buttonHeight,
179+
("Send a custom event\n(Track(\"crafting_started\"))", FireCustomEvent, canTrack));
180+
181+
DrawSection("Identity",
182+
"Tell Immutable who's playing. Identify links events to a player. " +
183+
"Alias merges two accounts into one. Reset clears the link.");
184+
TwoColumnButtons(buttonHeight,
185+
("Identify player by Steam ID\n(Identify)", IdentifyAsSteam, canIdentify),
186+
("Link Steam → Passport\n(Alias)", AliasSteamToPassport, canIdentify),
187+
("⚠ Forget who's playing\n(Reset)", ResetIdentity, init));
188+
189+
DrawSection("Privacy consent",
190+
"What can Immutable track? None: nothing. Anonymous: counts only, no player id. " +
191+
"Full: keep the player id alongside events.");
192+
TwoColumnButtons(buttonHeight,
193+
("Stop tracking\n(SetConsent(None))", ConsentNone, init && consent != ConsentLevel.None),
194+
("Track anonymously\n(SetConsent(Anonymous))", ConsentAnonymous, init && consent != ConsentLevel.Anonymous),
195+
("Track with player ID\n(SetConsent(Full))", ConsentFull, init && consent != ConsentLevel.Full));
196+
197+
DrawSection("Advanced",
198+
"Danger zone. Deleting a player's data asks Immutable to erase " +
199+
"everything the backend has stored for that player — GDPR / " +
200+
"right-to-be-forgotten territory, not recoverable.");
201+
TwoColumnButtons(buttonHeight,
202+
("⚠ Delete this player's data (GDPR)\n(DeleteData)", RequestGdprErasure, init));
203+
204+
GUILayout.EndScrollView();
205+
GUILayout.EndArea();
206+
}
207+
208+
private Vector2 _scroll;
209+
210+
private static GUIStyle _titleStyle;
211+
private static GUIStyle _introStyle;
212+
private static GUIStyle _sectionHeaderStyle;
213+
private static GUIStyle _sectionDescStyle;
214+
private static GUIStyle _statusBoxStyle;
215+
private static GUIStyle _statusLabelStyle;
216+
private static GUIStyle _statusValueStyle;
217+
private static GUIStyle _statusValueWarnStyle;
218+
private static GUIStyle _copyButtonStyle;
219+
220+
private static void EnsureStyles()
221+
{
222+
if (_titleStyle != null) return;
223+
224+
_titleStyle = new GUIStyle(GUI.skin.label)
225+
{
226+
fontStyle = FontStyle.Bold,
227+
fontSize = 15,
228+
};
229+
_introStyle = new GUIStyle(GUI.skin.label)
230+
{
231+
wordWrap = true,
232+
fontStyle = FontStyle.Italic,
233+
};
234+
_sectionHeaderStyle = new GUIStyle(GUI.skin.label)
235+
{
236+
fontStyle = FontStyle.Bold,
237+
fontSize = 13,
238+
margin = new RectOffset(0, 0, 6, 2),
239+
};
240+
_sectionDescStyle = new GUIStyle(GUI.skin.label)
241+
{
242+
wordWrap = true,
243+
fontStyle = FontStyle.Italic,
244+
margin = new RectOffset(0, 0, 0, 4),
245+
};
246+
_statusBoxStyle = new GUIStyle(GUI.skin.box)
247+
{
248+
padding = new RectOffset(8, 8, 6, 6),
249+
};
250+
_statusLabelStyle = new GUIStyle(GUI.skin.label)
251+
{
252+
fontStyle = FontStyle.Bold,
253+
};
254+
_statusValueStyle = new GUIStyle(GUI.skin.label)
255+
{
256+
wordWrap = true,
257+
};
258+
_statusValueWarnStyle = new GUIStyle(GUI.skin.label)
259+
{
260+
wordWrap = true,
261+
normal = { textColor = new Color(1f, 0.55f, 0.4f) },
262+
};
263+
_copyButtonStyle = new GUIStyle(GUI.skin.button)
264+
{
265+
fontSize = 10,
266+
padding = new RectOffset(4, 4, 2, 2),
267+
margin = new RectOffset(4, 0, 2, 0),
268+
};
269+
}
270+
271+
private void DrawSection(string title, string description)
272+
{
273+
GUILayout.Label(title, _sectionHeaderStyle);
274+
GUILayout.Label(description, _sectionDescStyle);
275+
}
276+
277+
private void DrawStatusPanel()
278+
{
279+
GUILayout.Space(4);
280+
GUILayout.BeginVertical(_statusBoxStyle);
281+
282+
GUILayout.Label("SDK status", _sectionHeaderStyle);
283+
284+
DrawStatusRow("Initialized", ImmutableAudience.Initialized ? "yes" : "no");
285+
DrawStatusRow("Environment", ImmutableAudience.CurrentEnvironment.ToString());
286+
DrawStatusRow("Consent", ImmutableAudience.CurrentConsent.ToString());
287+
DrawStatusRow("Pub key", FormatPublishableKey(out var pubKeyIsWarning), pubKeyIsWarning,
288+
copyValue: pubKeyIsWarning ? null : PublishableKey);
289+
DrawStatusRow("User ID", ImmutableAudience.UserId ?? "(none)",
290+
copyValue: ImmutableAudience.UserId);
291+
DrawStatusRow("Anon ID", ImmutableAudience.AnonymousId ?? "(none — needs consent above None)",
292+
copyValue: ImmutableAudience.AnonymousId);
293+
DrawStatusRow("Session ID", ImmutableAudience.SessionId ?? "(none)",
294+
copyValue: ImmutableAudience.SessionId);
295+
DrawStatusRow("Queued", ImmutableAudience.QueueSize.ToString());
296+
297+
GUILayout.EndVertical();
298+
GUILayout.Space(4);
299+
}
300+
301+
private static void DrawStatusRow(string label, string value, bool warn = false, string copyValue = null)
302+
{
303+
GUILayout.BeginHorizontal();
304+
GUILayout.Label(label, _statusLabelStyle, GUILayout.Width(100));
305+
GUILayout.Label(value, warn ? _statusValueWarnStyle : _statusValueStyle);
306+
if (!string.IsNullOrEmpty(copyValue))
307+
{
308+
if (GUILayout.Button("Copy", _copyButtonStyle, GUILayout.Width(50)))
309+
{
310+
GUIUtility.systemCopyBuffer = copyValue;
311+
}
312+
}
313+
GUILayout.EndHorizontal();
314+
}
315+
316+
private string FormatPublishableKey(out bool isWarning)
317+
{
318+
if (string.IsNullOrEmpty(PublishableKey))
319+
{
320+
isWarning = true;
321+
return "⚠ (not set — set in Inspector)";
322+
}
323+
if (PublishableKey.EndsWith("REPLACE_ME"))
324+
{
325+
isWarning = true;
326+
return "⚠ " + PublishableKey + " — set your real key in the Inspector";
327+
}
328+
isWarning = false;
329+
return PublishableKey;
330+
}
331+
332+
private static void TwoColumnButtons(float buttonHeight, params (string label, System.Action action, bool enabled)[] buttons)
333+
{
334+
for (var i = 0; i < buttons.Length; i += 2)
335+
{
336+
GUILayout.BeginHorizontal();
337+
DrawCellButton(buttons[i], buttonHeight);
338+
if (i + 1 < buttons.Length)
339+
{
340+
DrawCellButton(buttons[i + 1], buttonHeight);
341+
}
342+
else
343+
{
344+
GUILayout.Box(GUIContent.none, GUIStyle.none,
345+
GUILayout.Height(buttonHeight),
346+
GUILayout.ExpandWidth(true));
347+
}
348+
GUILayout.EndHorizontal();
349+
}
350+
}
351+
352+
private static void DrawCellButton((string label, System.Action action, bool enabled) button, float buttonHeight)
353+
{
354+
var prev = GUI.enabled;
355+
GUI.enabled = prev && button.enabled;
356+
if (GUILayout.Button(button.label,
357+
GUILayout.Height(buttonHeight),
358+
GUILayout.ExpandWidth(true)))
359+
{
360+
button.action();
361+
}
362+
GUI.enabled = prev;
363+
}
364+
}
365+
}

src/Packages/Audience/Samples/QuickStart/AudienceDemo.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)