Initial commit

This commit is contained in:
rudder
2026-08-12 14:03:44 +03:00
commit 9a7b4a57dc
304 changed files with 82311 additions and 0 deletions
@@ -0,0 +1,60 @@
using UnityEngine;
namespace LiveOpsExamples
{
/// <summary>
/// Shared constants for the Cozy Collector demo: remote config keys with their
/// defaults, LiveOps slugs, and the inspector capability palette.
/// Mirrors liveops-phaser-demo/src/config.ts.
/// </summary>
public static class CozyCollectorConsts
{
// Remote config keys (mirror DemoRemoteConfig in the Phaser demo).
public const string ConfigRoundSeconds = "demo_round_seconds";
public const string ConfigPlayerSpeed = "demo_player_speed";
public const string ConfigSpawnIntervalMs = "demo_spawn_interval_ms";
public const string ConfigCollectibleScore = "demo_collectible_score";
public const float DefaultRoundSeconds = 150f;
public const float DefaultPlayerSpeed = 260f;
public const float DefaultSpawnIntervalMs = 900f;
public const float DefaultCollectibleScore = 10f;
public const string LeaderboardSlug = "cozy-collector-score";
public const string StoreSlug = "cozy-camp-shop";
public const string OfferId = "moonberry-boost";
public const string StorageType = "cozy_fantasy_save";
public const string StorageId = "main";
public const string NicknamePrefsKey = "rudder_demo_nickname";
public const string Region = "global";
public const string Language = "en";
// Inspector capability tags (same tags as the Phaser demo).
public const string CapabilityAuth = "auth";
public const string CapabilityConfig = "config";
public const string CapabilityScenario = "scenario";
public const string CapabilityStore = "store";
public const string CapabilityLeaderboard = "leaderboard";
public const string CapabilityStorage = "storage";
public const string CapabilityInventory = "inventory";
public const string CapabilityWallet = "wallet";
public const string CapabilitySystem = "system";
/// <summary>Color used by the event inspector to tag a capability line.</summary>
public static Color CapabilityColor(string capability)
{
switch (capability)
{
case CapabilityAuth: return new Color(0.55f, 0.76f, 1.00f);
case CapabilityConfig: return new Color(0.61f, 0.90f, 0.69f);
case CapabilityScenario: return new Color(0.85f, 0.68f, 1.00f);
case CapabilityStore: return new Color(1.00f, 0.82f, 0.51f);
case CapabilityLeaderboard: return new Color(1.00f, 0.72f, 0.58f);
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);
default: return new Color(0.75f, 0.75f, 0.75f);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f0f8000a396a64ea7890de20a70265ce
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 05ab47060c2ef4476800002c4a956b85
@@ -0,0 +1,42 @@
using UnityEngine;
namespace LiveOpsExamples
{
/// <summary>
/// Baked sprite references for the Cozy Collector demo. Mirrors the Phaser
/// texture keys; <see cref="ForSlug"/> falls back to the star sprite for
/// unknown slugs, same as the Phaser backpack icon mapping.
/// </summary>
[CreateAssetMenu(menuName = "LiveOps Examples/Cozy Collector Sprite Library")]
public class CozyCollectorSpriteLibrary : ScriptableObject
{
[SerializeField] private Sprite background;
[SerializeField] private Sprite player;
[SerializeField] private Sprite moonberry;
[SerializeField] private Sprite acorn;
[SerializeField] private Sprite star;
[SerializeField] private Sprite snack;
public Sprite Background => background;
public Sprite Player => player;
public Sprite Moonberry => moonberry;
public Sprite Acorn => acorn;
public Sprite Star => star;
public Sprite Snack => snack;
public Sprite ForSlug(string slug)
{
switch (slug)
{
case "background":
case "camp-bg": return background;
case "player": return player;
case "moonberry": return moonberry;
case "acorn": return acorn;
case "snack": return snack;
case "star":
default: return star;
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8cd851b86da394b9ca06595f889a49b3
+149
View File
@@ -0,0 +1,149 @@
using System;
using UnityEngine;
namespace LiveOpsExamples
{
/// <summary>
/// Gameplay world for the Cozy Collector demo — the Unity port of the Phaser
/// GameScene.ts. Renders only the game world: player movement (WASD/arrows),
/// round timer, spawn cadence and pickups. All chrome lives in the UI panels,
/// all SDK calls in <see cref="CozyCollectorController"/>.
///
/// The Phaser field is 1280×720 px at 100 PPU, i.e. 12.8×7.2 world units
/// centered on the origin; tuning speeds/intervals stay in Phaser units
/// (px/s, ms) and are converted here.
/// </summary>
public class GameWorld : MonoBehaviour
{
private const float PixelsPerUnit = 100f;
[SerializeField] private SpriteRenderer player;
[SerializeField] private SnackSpawner spawner;
[SerializeField] private CozyCollectorController controller;
[SerializeField] private ScorePopupView scorePopupPrefab;
[SerializeField] private ParticleSystem collectBurstPrefab;
[SerializeField] private Vector2 moveMin = new Vector2(-6.0f, -3.2f);
[SerializeField] private Vector2 moveMax = new Vector2(6.0f, 3.2f);
private bool _paused;
private float _spawnElapsedMs;
private int _lastSecond = -1;
private bool _finishRaised;
public int Score { get; private set; }
public int Collected { get; private set; }
public float TimeLeftSeconds { get; private set; }
public bool RoundActive { get; private set; }
public event Action StateChanged;
public event Action<int, int> RoundFinished;
private void Awake()
{
spawner.Player = player;
spawner.Collected += OnSnackCollected;
}
private void OnDestroy()
{
spawner.Collected -= OnSnackCollected;
}
private void Update()
{
MovePlayer(_paused || !RoundActive);
if (!RoundActive || _paused)
return;
TimeLeftSeconds = Mathf.Max(0f, TimeLeftSeconds - Time.deltaTime);
_spawnElapsedMs += Time.deltaTime * 1000f;
if (_spawnElapsedMs >= controller.Tuning.SpawnIntervalMs)
{
_spawnElapsedMs = 0f;
spawner.Spawn();
}
var second = Mathf.CeilToInt(TimeLeftSeconds);
if (second != _lastSecond)
{
_lastSecond = second;
StateChanged?.Invoke();
}
if (TimeLeftSeconds <= 0f)
FinishRound();
}
public void SetPaused(bool paused)
{
_paused = paused;
}
public void RestartRound()
{
Score = 0;
Collected = 0;
TimeLeftSeconds = controller.Tuning.RoundSeconds;
_spawnElapsedMs = 0f;
_lastSecond = -1;
_finishRaised = false;
spawner.Clear();
RoundActive = true;
spawner.Spawn();
StateChanged?.Invoke();
}
private void MovePlayer(bool frozen)
{
if (frozen)
return;
var vx = (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A) ? -1 : 0) +
(Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D) ? 1 : 0);
var vy = (Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.S) ? -1 : 0) +
(Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.W) ? 1 : 0);
if (vx == 0 && vy == 0)
return;
var direction = new Vector2(vx, vy).normalized;
var speed = controller.Tuning.PlayerSpeed / PixelsPerUnit;
var position = player.transform.position;
position += (Vector3)(direction * speed * Time.deltaTime);
position.x = Mathf.Clamp(position.x, moveMin.x, moveMax.x);
position.y = Mathf.Clamp(position.y, moveMin.y, moveMax.y);
player.transform.position = position;
if (vx != 0)
player.flipX = vx < 0;
}
private void OnSnackCollected(Snack snack)
{
var position = snack.transform.position;
var gain = Mathf.RoundToInt(controller.Tuning.CollectibleScore);
Score += gain;
Collected += 1;
var popup = Instantiate(scorePopupPrefab, position, Quaternion.identity);
popup.Set(gain);
Instantiate(collectBurstPrefab, position, Quaternion.identity);
StateChanged?.Invoke();
}
private void FinishRound()
{
if (_finishRaised)
return;
_finishRaised = true;
RoundActive = false;
spawner.Clear();
StateChanged?.Invoke();
RoundFinished?.Invoke(Score, Collected);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 952f52af0af8546eeab332b11e2e4bb7
@@ -0,0 +1,43 @@
using System.Collections;
using TMPro;
using UnityEngine;
namespace LiveOpsExamples
{
/// <summary>
/// World-space "+N" score popup. Floats up ~0.5 world units and fades out over
/// 0.7 s (Cubic.easeOut, like the Phaser floatScore tween), then destroys itself.
/// </summary>
public class ScorePopupView : MonoBehaviour
{
private const float FloatDistance = 0.5f;
private const float LifetimeSeconds = 0.7f;
[SerializeField] private TextMeshPro label;
public void Set(int gain)
{
label.text = "+" + gain;
StartCoroutine(Animate());
}
private IEnumerator Animate()
{
var start = transform.position;
var color = label.color;
var elapsed = 0f;
while (elapsed < LifetimeSeconds)
{
elapsed += Time.deltaTime;
var t = Mathf.Clamp01(elapsed / LifetimeSeconds);
var eased = 1f - Mathf.Pow(1f - t, 3f);
transform.position = start + new Vector3(0f, FloatDistance * eased, 0f);
label.color = new Color(color.r, color.g, color.b, color.a * (1f - eased));
yield return null;
}
Destroy(gameObject);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b1db4bf03192148bf95ea374d128f349
+45
View File
@@ -0,0 +1,45 @@
using System;
using UnityEngine;
namespace LiveOpsExamples
{
/// <summary>
/// A collectible snack. Bobs gently (sine, 0.08 world units amplitude, 650 ms
/// half-period — mirrors the Phaser yoyo tween) and raises <see cref="Collected"/>
/// when the player overlaps its trigger, then destroys itself.
/// </summary>
[RequireComponent(typeof(SpriteRenderer))]
public class Snack : MonoBehaviour
{
private const float BobAmplitude = 0.08f;
private const float BobHalfPeriodSeconds = 0.65f;
public SpriteRenderer Player { get; set; }
public event Action<Snack> Collected;
private Vector3 _basePosition;
private float _elapsed;
private void Start()
{
_basePosition = transform.position;
}
private void Update()
{
_elapsed += Time.deltaTime;
var offset = Mathf.Sin(_elapsed * Mathf.PI / BobHalfPeriodSeconds) * BobAmplitude;
transform.position = _basePosition + new Vector3(0f, offset, 0f);
}
private void OnTriggerEnter2D(Collider2D other)
{
if (Player == null || other.gameObject != Player.gameObject)
return;
Collected?.Invoke(this);
Destroy(gameObject);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 36b117cefc78541b9b42188266102562
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace LiveOpsExamples
{
/// <summary>
/// Spawns collectible snacks at random positions inside the configured bounds,
/// picking a random collectible slug (moonberry/acorn/star/snack) and applying
/// its sprite from the library. Mirrors GameScene.spawnCollectible.
///
/// Phaser spawn margins x∈[115,1165], y∈[118,625] px on a 1280×720 field at
/// 100 PPU map to x∈[-5.25,5.25], y∈[-2.65,2.42] world units (y is flipped).
/// </summary>
public class SnackSpawner : MonoBehaviour
{
private static readonly string[] CollectibleSlugs = { "moonberry", "acorn", "star", "snack" };
[SerializeField] private Snack prefab;
[SerializeField] private CozyCollectorSpriteLibrary library;
[SerializeField] private Vector2 spawnMin = new Vector2(-5.25f, -2.65f);
[SerializeField] private Vector2 spawnMax = new Vector2(5.25f, 2.42f);
[SerializeField] private Transform parent;
private readonly List<Snack> _spawned = new List<Snack>();
public SpriteRenderer Player { get; set; }
public event Action<Snack> Collected;
public Snack Spawn()
{
var slug = CollectibleSlugs[UnityEngine.Random.Range(0, CollectibleSlugs.Length)];
var position = new Vector3(
UnityEngine.Random.Range(spawnMin.x, spawnMax.x),
UnityEngine.Random.Range(spawnMin.y, spawnMax.y),
0f);
var snack = Instantiate(prefab, position, Quaternion.identity, parent);
var sprite = library.ForSlug(slug);
snack.GetComponent<SpriteRenderer>().sprite = sprite;
// Phaser setDisplaySize(52, 52) at 100 PPU -> 0.52 x 0.52 world units.
var spriteBounds = sprite.bounds.size;
var scale = new Vector3(0.52f / spriteBounds.x, 0.52f / spriteBounds.y, 1f);
snack.transform.localScale = scale;
// The prefab collider radius is tuned to snack.png; a circle collider's
// world radius scales by the largest transform axis, so retune per sprite.
snack.GetComponent<CircleCollider2D>().radius = 0.22f / Mathf.Max(scale.x, scale.y);
snack.Player = Player;
snack.Collected += OnSnackCollected;
_spawned.Add(snack);
return snack;
}
public void Clear()
{
foreach (var snack in _spawned)
{
if (snack == null)
continue;
snack.Collected -= OnSnackCollected;
Destroy(snack.gameObject);
}
_spawned.Clear();
}
private void OnSnackCollected(Snack snack)
{
snack.Collected -= OnSnackCollected;
_spawned.Remove(snack);
Collected?.Invoke(snack);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ec58b83d93d5b435abaaa64ecc05fe1a
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c21c943a5e1bd4f79a2d6bcb2e3f30fb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,81 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class BackpackModalPanel : MonoBehaviour
{
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
[SerializeField] private VisualTreeAsset backpackItemRowTemplate;
[SerializeField] private CozyCollectorSpriteLibrary spriteLibrary;
private VisualElement _root;
private ScrollView _itemsScroll;
private VisualElement _emptyNote;
private Button _closeButton;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.ModalChanged += ApplyVisibility;
controller.BackpackChanged += Rebuild;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.ModalChanged -= ApplyVisibility;
controller.BackpackChanged -= Rebuild;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_root = root.Q<VisualElement>("backpack-modal");
_itemsScroll = _root.Q<ScrollView>("backpack-items-scroll");
_emptyNote = _root.Q<VisualElement>("backpack-empty");
_closeButton = _root.Q<Button>("backpack-close-button");
_closeButton.clicked -= HandleCloseClicked;
_closeButton.clicked += HandleCloseClicked;
ApplyVisibility();
Rebuild();
}
private void ApplyVisibility()
{
_root.style.display = controller.ActiveModal == CozyModal.Backpack ? DisplayStyle.Flex : DisplayStyle.None;
}
private void Rebuild()
{
_itemsScroll.contentContainer.Clear();
var items = controller.BackpackItems;
_emptyNote.style.display = items == null || items.Count == 0 ? DisplayStyle.Flex : DisplayStyle.None;
if (items == null)
{
return;
}
foreach (var item in items)
{
var rowRoot = backpackItemRowTemplate.CloneTree();
_itemsScroll.contentContainer.Add(rowRoot);
var row = new BackpackItemRowView(rowRoot);
row.Set(
spriteLibrary.ForSlug(item.Slug),
string.IsNullOrEmpty(item.Name) ? item.Slug : item.Name,
item.Slug,
item.Amount);
}
}
private void HandleCloseClicked()
{
controller.CloseModal();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0c73aae57f5c749b88f5bf2c22538af5
@@ -0,0 +1,70 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class BootOverlayPanel : MonoBehaviour
{
private const float SpinnerDegreesPerSecond = 180f;
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
private VisualElement _overlay;
private Label _statusLabel;
private VisualElement _spinner;
private IVisualElementScheduledItem _spinSchedule;
private float _angle;
private float _lastTick;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.PhaseChanged += ApplyVisibility;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.PhaseChanged -= ApplyVisibility;
_spinSchedule?.Pause();
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_overlay = root.Q<VisualElement>("boot-overlay");
_statusLabel = root.Q<Label>("boot-status");
_spinner = root.Q<VisualElement>("boot-spinner");
_spinSchedule = _spinner.schedule.Execute(Spin).Every(16);
ApplyVisibility();
}
private void Update()
{
_statusLabel.text = controller.StatusMessage;
}
private void Spin()
{
var now = Time.unscaledTime;
_angle = (_angle + SpinnerDegreesPerSecond * (now - _lastTick)) % 360f;
_lastTick = now;
_spinner.style.rotate = new Rotate(Angle.Degrees(_angle));
}
private void ApplyVisibility()
{
var visible = controller.Phase == CozyPhase.Booting || controller.Phase == CozyPhase.Connecting;
_overlay.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
if (visible)
{
_lastTick = Time.unscaledTime;
_spinSchedule.Resume();
}
else
{
_spinSchedule.Pause();
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f54172384bff5402686bb558e2abb5b7
@@ -0,0 +1,89 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class EventInspectorPanel : MonoBehaviour
{
private static readonly Dictionary<string, Color> CapabilityColors = new Dictionary<string, Color>
{
{ "auth", new Color(0.486f, 0.769f, 1f) },
{ "config", new Color(0.945f, 0.776f, 0.490f) },
{ "scenario", new Color(0.769f, 0.635f, 1f) },
{ "store", new Color(1f, 0.698f, 0.490f) },
{ "leaderboard", new Color(0.482f, 0.878f, 0.690f) },
{ "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) },
{ "system", new Color(0.667f, 0.706f, 0.635f) },
};
private static readonly Dictionary<string, string> CapabilityLabels = new Dictionary<string, string>
{
{ "auth", "Auth" },
{ "config", "Config" },
{ "scenario", "Scenario" },
{ "store", "Store" },
{ "leaderboard", "Board" },
{ "storage", "Storage" },
{ "inventory", "Items" },
{ "wallet", "Wallet" },
{ "system", "System" },
};
private static readonly Color FallbackColor = new Color(0.667f, 0.706f, 0.635f);
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
[SerializeField] private VisualTreeAsset logRowTemplate;
private ScrollView _scroll;
private VisualElement _emptyNote;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.LogAdded += Rebuild;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.LogAdded -= Rebuild;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_scroll = root.Q<ScrollView>("event-log-scroll");
_emptyNote = root.Q("event-log-empty");
Rebuild();
}
private void Rebuild()
{
var container = _scroll.contentContainer;
container.Clear();
var entries = controller.LogEntries;
_emptyNote.style.display = entries == null || entries.Count == 0 ? DisplayStyle.Flex : DisplayStyle.None;
if (entries == null)
{
return;
}
foreach (var entry in entries)
{
var rowRoot = logRowTemplate.CloneTree();
container.Add(rowRoot);
string key = entry.Capability == null ? string.Empty : entry.Capability.ToLowerInvariant();
string label = CapabilityLabels.TryGetValue(key, out var known) ? known : entry.Capability;
Color color = CapabilityColors.TryGetValue(key, out var knownColor) ? knownColor : FallbackColor;
string detail = string.IsNullOrEmpty(entry.Detail) ? string.Empty : $" — {entry.Detail}";
new LogRowView(rowRoot).Set(entry.Timestamp.ToString("HH:mm:ss"), label, color, entry.Message, detail);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a134e625b6e114c4b80b9f55a14e48b3
@@ -0,0 +1,119 @@
using System.Text;
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class HudPanel : MonoBehaviour
{
private const string TimerLowClass = "timer-low";
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
[SerializeField] private GameWorld world;
private Label _scoreLabel;
private Label _bestLabel;
private Label _timeLeftLabel;
private VisualElement _timerFill;
private Label _walletLabel;
private Button _shopButton;
private Button _backpackButton;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
world.StateChanged += Refresh;
world.RoundFinished += HandleRoundFinished;
controller.WalletsChanged += RefreshWallet;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
world.StateChanged -= Refresh;
world.RoundFinished -= HandleRoundFinished;
controller.WalletsChanged -= RefreshWallet;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_scoreLabel = root.Q<Label>("hud-score");
_bestLabel = root.Q<Label>("hud-best");
_timeLeftLabel = root.Q<Label>("hud-time-left");
_timerFill = root.Q("hud-timer-fill");
_walletLabel = root.Q<Label>("hud-wallet");
_shopButton = root.Q<Button>("hud-shop-button");
_backpackButton = root.Q<Button>("hud-backpack-button");
_shopButton.clicked -= HandleShopClicked;
_shopButton.clicked += HandleShopClicked;
_backpackButton.clicked -= HandleBackpackClicked;
_backpackButton.clicked += HandleBackpackClicked;
Refresh();
RefreshWallet();
}
private void Update()
{
RefreshTimer();
}
private void Refresh()
{
_scoreLabel.text = world.Score.ToString();
_bestLabel.text = controller.Save != null ? controller.Save.BestScore.ToString() : "0";
RefreshTimer();
}
private void RefreshTimer()
{
float roundSeconds = controller.Tuning != null ? Mathf.Max(1f, controller.Tuning.RoundSeconds) : 1f;
float fraction = Mathf.Clamp01(world.TimeLeftSeconds / roundSeconds);
_timeLeftLabel.text = $"{Mathf.CeilToInt(Mathf.Max(0f, world.TimeLeftSeconds))}s";
_timerFill.style.width = Length.Percent(fraction * 100f);
_timerFill.EnableInClassList(TimerLowClass, fraction <= 0.2f);
}
private void RefreshWallet()
{
_walletLabel.text = WalletLabel();
}
private string WalletLabel()
{
var wallets = controller.Wallets;
if (wallets == null || wallets.Count == 0)
{
return "—";
}
var builder = new StringBuilder();
foreach (var wallet in wallets)
{
if (builder.Length > 0)
{
builder.Append(" · ");
}
builder.Append($"{wallet.Balance:0.##} {wallet.Currency}".TrimEnd());
}
return builder.ToString();
}
private void HandleRoundFinished(int score, int collected)
{
Refresh();
}
private void HandleShopClicked()
{
controller.OpenModal(CozyModal.Shop);
}
private void HandleBackpackClicked()
{
controller.OpenModal(CozyModal.Backpack);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2e5e5cb2ad54545a6847ad8c90d1f7d8
@@ -0,0 +1,102 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class OfferModalPanel : MonoBehaviour
{
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
private VisualElement root;
private Label titleLabel;
private Label nameLabel;
private Label priceLabel;
private Button buyButton;
private Button declineButton;
private Button closeButton;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.ModalChanged += ApplyVisibility;
controller.OfferChanged += Refresh;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.ModalChanged -= ApplyVisibility;
controller.OfferChanged -= Refresh;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
this.root = root.Q<VisualElement>("offer-modal");
titleLabel = this.root.Q<Label>("offer-title");
nameLabel = this.root.Q<Label>("offer-name");
priceLabel = this.root.Q<Label>("offer-price");
buyButton = this.root.Q<Button>("offer-buy-button");
declineButton = this.root.Q<Button>("offer-decline-button");
closeButton = this.root.Q<Button>("offer-close-button");
buyButton.clicked -= HandleBuyClicked;
buyButton.clicked += HandleBuyClicked;
declineButton.clicked -= HandleDeclineClicked;
declineButton.clicked += HandleDeclineClicked;
closeButton.clicked -= HandleCloseClicked;
closeButton.clicked += HandleCloseClicked;
ApplyVisibility();
Refresh();
}
private void ApplyVisibility()
{
root.style.display = controller.ActiveModal == CozyModal.Offer ? DisplayStyle.Flex : DisplayStyle.None;
}
private void Refresh()
{
var offerInfo = controller.CurrentOffer;
if (offerInfo == null)
{
titleLabel.text = "Offer";
nameLabel.text = "—";
priceLabel.text = string.Empty;
buyButton.SetEnabled(false);
return;
}
titleLabel.text = string.IsNullOrEmpty(offerInfo.StoreName) ? offerInfo.StoreSlug : offerInfo.StoreName;
var offer = offerInfo.Offers != null && offerInfo.Offers.Count > 0 ? offerInfo.Offers[0] : null;
if (offer == null)
{
nameLabel.text = "—";
priceLabel.text = string.Empty;
buyButton.SetEnabled(false);
return;
}
nameLabel.text = string.IsNullOrEmpty(offer.Name) ? offer.OfferId : offer.Name;
priceLabel.text = ShopModalPanel.PriceLabel(offer.Price, offer.Currency);
buyButton.SetEnabled(true);
}
private void HandleBuyClicked()
{
controller.BuyScenarioOffer();
}
private void HandleDeclineClicked()
{
controller.DeclineScenarioOffer();
}
private void HandleCloseClicked()
{
controller.CloseModal();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b996708622cc54d328cac546b893e111
@@ -0,0 +1,92 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class RemoteConfigPanel : MonoBehaviour
{
private const string FlashClass = "flash";
private const long FlashMilliseconds = 600;
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
private Label _playerSpeedValue;
private Label _spawnIntervalValue;
private Label _collectibleScoreValue;
private Label _roundSecondsValue;
private VisualElement _playerSpeedRow;
private VisualElement _spawnIntervalRow;
private VisualElement _collectibleScoreRow;
private VisualElement _roundSecondsRow;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.TuningChanged += HandleTuningChanged;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.TuningChanged -= HandleTuningChanged;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_playerSpeedValue = root.Q<Label>("rc-value-player-speed");
_spawnIntervalValue = root.Q<Label>("rc-value-spawn-interval");
_collectibleScoreValue = root.Q<Label>("rc-value-collectible-score");
_roundSecondsValue = root.Q<Label>("rc-value-round-seconds");
_playerSpeedRow = root.Q("rc-row-player-speed");
_spawnIntervalRow = root.Q("rc-row-spawn-interval");
_collectibleScoreRow = root.Q("rc-row-collectible-score");
_roundSecondsRow = root.Q("rc-row-round-seconds");
Refresh();
}
private void Refresh()
{
var tuning = controller.Tuning;
if (tuning == null)
{
return;
}
_playerSpeedValue.text = $"{Mathf.RoundToInt(tuning.PlayerSpeed)}";
_spawnIntervalValue.text = $"{Mathf.RoundToInt(tuning.SpawnIntervalMs)} ms";
_collectibleScoreValue.text = $"+{Mathf.RoundToInt(tuning.CollectibleScore)}";
_roundSecondsValue.text = $"{Mathf.RoundToInt(tuning.RoundSeconds)} s";
}
private void HandleTuningChanged(string key)
{
Refresh();
string normalized = key == null ? string.Empty : key.ToLowerInvariant().Replace("_", string.Empty).Replace("-", string.Empty).Replace(" ", string.Empty);
if (normalized.Contains("playerspeed"))
{
Flash(_playerSpeedRow);
}
else if (normalized.Contains("spawninterval"))
{
Flash(_spawnIntervalRow);
}
else if (normalized.Contains("collectiblescore"))
{
Flash(_collectibleScoreRow);
}
else if (normalized.Contains("roundseconds"))
{
Flash(_roundSecondsRow);
}
}
private static void Flash(VisualElement row)
{
row.AddToClassList(FlashClass);
row.schedule.Execute(() => row.RemoveFromClassList(FlashClass)).StartingIn(FlashMilliseconds);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1d57e6a031317461cb98e41f6940c8d3
@@ -0,0 +1,117 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class RoundResultModalPanel : MonoBehaviour
{
private const int MaxLeaderboardRows = 10;
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
[SerializeField] private VisualTreeAsset leaderboardRowTemplate;
private VisualElement root;
private Label scoreLabel;
private Label bestLabel;
private Label collectedLabel;
private ScrollView leaderboardScroll;
private VisualElement emptyNote;
private Button playAgainButton;
private Button claimButton;
private Button endButton;
private Button closeButton;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.ModalChanged += ApplyVisibility;
controller.RoundResultChanged += Refresh;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.ModalChanged -= ApplyVisibility;
controller.RoundResultChanged -= Refresh;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
this.root = root.Q<VisualElement>("round-result-modal");
scoreLabel = this.root.Q<Label>("rr-score");
bestLabel = this.root.Q<Label>("rr-best");
collectedLabel = this.root.Q<Label>("rr-collected");
leaderboardScroll = this.root.Q<ScrollView>("rr-leaderboard-scroll");
emptyNote = this.root.Q<VisualElement>("rr-empty");
playAgainButton = this.root.Q<Button>("rr-play-again-button");
claimButton = this.root.Q<Button>("rr-claim-button");
endButton = this.root.Q<Button>("rr-end-button");
closeButton = this.root.Q<Button>("rr-close-button");
playAgainButton.clicked -= HandlePlayAgainClicked;
playAgainButton.clicked += HandlePlayAgainClicked;
claimButton.clicked -= HandleClaimClicked;
claimButton.clicked += HandleClaimClicked;
endButton.clicked -= HandleEndClicked;
endButton.clicked += HandleEndClicked;
closeButton.clicked -= HandleCloseClicked;
closeButton.clicked += HandleCloseClicked;
ApplyVisibility();
Refresh();
}
private void ApplyVisibility()
{
root.style.display = controller.ActiveModal == CozyModal.RoundResult ? DisplayStyle.Flex : DisplayStyle.None;
}
private void Refresh()
{
var result = controller.RoundResult;
scoreLabel.text = result != null ? result.Score.ToString() : "0";
bestLabel.text = result != null ? result.Best.ToString() : "0";
collectedLabel.text = result != null ? result.Collected.ToString() : "0";
leaderboardScroll.Clear();
var entries = result != null ? result.TopEntries : null;
emptyNote.style.display = entries == null || entries.Count == 0 ? DisplayStyle.Flex : DisplayStyle.None;
if (entries == null)
{
return;
}
int count = Mathf.Min(entries.Count, MaxLeaderboardRows);
for (int i = 0; i < count; i++)
{
var entry = entries[i];
var rowRoot = leaderboardRowTemplate.CloneTree();
var row = new LeaderboardRowView(rowRoot);
row.Set(entry.Rank, string.IsNullOrEmpty(entry.PlayerName) ? "player" : entry.PlayerName, entry.Score);
leaderboardScroll.Add(rowRoot);
}
}
private void HandlePlayAgainClicked()
{
controller.PlayAgain();
}
private void HandleClaimClicked()
{
controller.ClaimScenarioLeaderboardReward();
}
private void HandleEndClicked()
{
controller.EndScenarioLeaderboard();
}
private void HandleCloseClicked()
{
controller.CloseModal();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6f3916e69e6ce4c78b909c659160ad95
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cde562fa676ee420795c27bd66549cab
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,29 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class BackpackItemRowView
{
private readonly VisualElement _icon;
private readonly Label _nameLabel;
private readonly Label _metaLabel;
private readonly Label _amountLabel;
public BackpackItemRowView(VisualElement root)
{
_icon = root.Q<VisualElement>("item-icon");
_nameLabel = root.Q<Label>("item-name");
_metaLabel = root.Q<Label>("item-meta");
_amountLabel = root.Q<Label>("item-amount");
}
public void Set(Sprite icon, string itemName, string slug, int amount)
{
_icon.style.backgroundImage = new StyleBackground(icon);
_nameLabel.text = itemName;
_metaLabel.text = slug;
_amountLabel.text = $"×{amount}";
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d7a25568da97f48f3b07abf321ad2f82
@@ -0,0 +1,25 @@
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class LeaderboardRowView
{
private readonly Label rankLabel;
private readonly Label playerLabel;
private readonly Label scoreLabel;
public LeaderboardRowView(VisualElement root)
{
rankLabel = root.Q<Label>("lb-rank");
playerLabel = root.Q<Label>("lb-player");
scoreLabel = root.Q<Label>("lb-score");
}
public void Set(int rank, string playerName, int score)
{
rankLabel.text = rank > 0 ? $"#{rank}" : "—";
playerLabel.text = playerName;
scoreLabel.text = score.ToString();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 29788c49080d642ff919c1d25b5dd979
@@ -0,0 +1,31 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class LogRowView
{
private readonly Label _timeLabel;
private readonly Label _tagLabel;
private readonly Label _messageLabel;
private readonly Label _detailLabel;
public LogRowView(VisualElement root)
{
_timeLabel = root.Q<Label>("log-time");
_tagLabel = root.Q<Label>("log-tag");
_messageLabel = root.Q<Label>("log-message");
_detailLabel = root.Q<Label>("log-detail");
}
public void Set(string time, string tag, Color tagColor, string message, string detail)
{
_timeLabel.text = time;
_tagLabel.text = tag;
_tagLabel.style.color = tagColor;
_tagLabel.style.backgroundColor = new Color(tagColor.r, tagColor.g, tagColor.b, 0.18f);
_messageLabel.text = message;
_detailLabel.text = detail;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ffa213acafee84d818a3cf631f0bb01b
@@ -0,0 +1,26 @@
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class ShopOfferRowView
{
private readonly Label _nameLabel;
private readonly Label _priceLabel;
private readonly Button _buyButton;
public Button BuyButton => _buyButton;
public ShopOfferRowView(VisualElement root)
{
_nameLabel = root.Q<Label>("offer-name");
_priceLabel = root.Q<Label>("offer-price");
_buyButton = root.Q<Button>("offer-buy-button");
}
public void Set(string offerName, string price)
{
_nameLabel.text = offerName;
_priceLabel.text = price;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 94bf1e4d6fb50418487ed96aa4d65d4b
@@ -0,0 +1,32 @@
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class ToastView
{
private const string ToneInfoClass = "tone-info";
private const string ToneSuccessClass = "tone-success";
private const string ToneErrorClass = "tone-error";
private readonly Label _titleLabel;
private readonly Label _messageLabel;
public ToastView(VisualElement root)
{
Root = root;
_titleLabel = root.Q<Label>("toast-title");
_messageLabel = root.Q<Label>("toast-message");
}
public VisualElement Root { get; }
public void Set(string title, string message, CozyToastTone tone)
{
_titleLabel.text = title;
_messageLabel.text = message;
Root.EnableInClassList(ToneInfoClass, tone == CozyToastTone.Info);
Root.EnableInClassList(ToneSuccessClass, tone == CozyToastTone.Success);
Root.EnableInClassList(ToneErrorClass, tone == CozyToastTone.Error);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5d468efab7b7645f78186c03b82f5d32
@@ -0,0 +1,87 @@
using System.Text;
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class SessionCardPanel : MonoBehaviour
{
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
private Label _nicknameLabel;
private Label _playerIdLabel;
private Label _deviceIdLabel;
private Label _regionLabel;
private Label _projectKeyLabel;
private Label _walletsLabel;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.PhaseChanged += Refresh;
controller.WalletsChanged += RefreshWallets;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.PhaseChanged -= Refresh;
controller.WalletsChanged -= RefreshWallets;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_nicknameLabel = root.Q<Label>("session-nickname");
_playerIdLabel = root.Q<Label>("session-player-id");
_deviceIdLabel = root.Q<Label>("session-device-id");
_regionLabel = root.Q<Label>("session-region");
_projectKeyLabel = root.Q<Label>("session-project-key");
_walletsLabel = root.Q<Label>("session-wallets");
Refresh();
}
private void Refresh()
{
var identity = controller.Identity;
if (identity == null)
{
_nicknameLabel.text = "—";
_playerIdLabel.text = "—";
_deviceIdLabel.text = "—";
_regionLabel.text = "—";
_projectKeyLabel.text = "—";
}
else
{
_nicknameLabel.text = string.IsNullOrEmpty(identity.Nickname)
? $"Player {identity.PlayerId.Substring(0, Mathf.Min(6, identity.PlayerId.Length))}"
: identity.Nickname;
_playerIdLabel.text = identity.PlayerId;
_deviceIdLabel.text = identity.DeviceId;
_regionLabel.text = string.IsNullOrEmpty(identity.Region) ? "global" : identity.Region;
_projectKeyLabel.text = identity.ProjectKey;
}
RefreshWallets();
}
private void RefreshWallets()
{
var wallets = controller.Wallets;
if (wallets == null || wallets.Count == 0)
{
_walletsLabel.text = "No wallets";
return;
}
var builder = new StringBuilder();
foreach (var wallet in wallets)
{
builder.AppendLine($"{wallet.Balance:0.##} {wallet.Currency}".TrimEnd());
}
_walletsLabel.text = builder.ToString().TrimEnd();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 900b9e31987914b56a3bba037e2a830c
@@ -0,0 +1,43 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class SetupOverlayPanel : MonoBehaviour
{
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
private VisualElement _overlay;
private Label _errorLabel;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.PhaseChanged += ApplyVisibility;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.PhaseChanged -= ApplyVisibility;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_overlay = root.Q<VisualElement>("setup-overlay");
_errorLabel = root.Q<Label>("setup-error");
ApplyVisibility();
}
private void Update()
{
_errorLabel.text = controller.SetupError;
}
private void ApplyVisibility()
{
_overlay.style.display = controller.Phase == CozyPhase.Setup ? DisplayStyle.Flex : DisplayStyle.None;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6de014257693b4ced830566d18e49444
@@ -0,0 +1,115 @@
using System.Text;
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class ShopModalPanel : MonoBehaviour
{
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
[SerializeField] private VisualTreeAsset shopOfferRowTemplate;
private VisualElement _root;
private Label _walletLabel;
private ScrollView _offersScroll;
private VisualElement _emptyNote;
private Button _closeButton;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.ModalChanged += ApplyVisibility;
controller.ShopChanged += Rebuild;
controller.WalletsChanged += RefreshWallet;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.ModalChanged -= ApplyVisibility;
controller.ShopChanged -= Rebuild;
controller.WalletsChanged -= RefreshWallet;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_root = root.Q<VisualElement>("shop-modal");
_walletLabel = _root.Q<Label>("shop-wallet");
_offersScroll = _root.Q<ScrollView>("shop-offers-scroll");
_emptyNote = _root.Q<VisualElement>("shop-empty");
_closeButton = _root.Q<Button>("shop-close-button");
_closeButton.clicked -= HandleCloseClicked;
_closeButton.clicked += HandleCloseClicked;
ApplyVisibility();
RefreshWallet();
Rebuild();
}
private void ApplyVisibility()
{
_root.style.display = controller.ActiveModal == CozyModal.Shop ? DisplayStyle.Flex : DisplayStyle.None;
}
private void RefreshWallet()
{
var wallets = controller.Wallets;
if (wallets == null || wallets.Count == 0)
{
_walletLabel.text = "Wallet: —";
return;
}
var builder = new StringBuilder("Wallet: ");
foreach (var wallet in wallets)
{
if (builder.Length > "Wallet: ".Length)
{
builder.Append(" · ");
}
builder.Append($"{wallet.Balance:0.##} {wallet.Currency}".TrimEnd());
}
_walletLabel.text = builder.ToString();
}
private void Rebuild()
{
_offersScroll.contentContainer.Clear();
var offers = controller.ShopOffers;
_emptyNote.style.display = offers == null || offers.Count == 0 ? DisplayStyle.Flex : DisplayStyle.None;
if (offers == null)
{
return;
}
foreach (var offer in offers)
{
var rowRoot = shopOfferRowTemplate.CloneTree();
_offersScroll.contentContainer.Add(rowRoot);
var row = new ShopOfferRowView(rowRoot);
row.Set(string.IsNullOrEmpty(offer.Name) ? offer.OfferId : offer.Name, PriceLabel(offer.Price, offer.Currency));
string offerId = offer.OfferId;
row.BuyButton.clicked += () => controller.BuyShopOffer(offerId);
}
}
internal static string PriceLabel(double price, string currency)
{
if (price <= 0)
{
return "Free";
}
return $"{price:0.##} {currency}".TrimEnd();
}
private void HandleCloseClicked()
{
controller.CloseModal();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 36789e549043d400a92f3a84849b4038
@@ -0,0 +1,78 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public class ToastsPanel : MonoBehaviour
{
private const float ToastLifetimeSeconds = 4.2f;
[SerializeField] private PanelRenderer panelRenderer;
[SerializeField] private CozyCollectorController controller;
[SerializeField] private VisualTreeAsset toastTemplate;
private readonly List<ToastView> _toasts = new List<ToastView>();
private readonly List<float> _dismissAt = new List<float>();
private VisualElement _container;
private void OnEnable()
{
panelRenderer.RegisterUIReloadCallback(OnUIReload);
controller.ToastRequested += HandleToastRequested;
}
private void OnDisable()
{
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
controller.ToastRequested -= HandleToastRequested;
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_container = root.Q("toasts");
}
private void Update()
{
for (int i = _toasts.Count - 1; i >= 0; i--)
{
if (Time.unscaledTime >= _dismissAt[i])
{
Dismiss(i);
}
}
}
private void HandleToastRequested(string title, string message, CozyToastTone tone)
{
var root = toastTemplate.CloneTree();
_container.Add(root);
var toast = new ToastView(root);
toast.Set(title, message, tone);
toast.Root.RegisterCallback<ClickEvent>(_ => Dismiss(toast));
_toasts.Add(toast);
_dismissAt.Add(Time.unscaledTime + ToastLifetimeSeconds);
}
private void Dismiss(ToastView toast)
{
int index = _toasts.IndexOf(toast);
if (index >= 0)
{
Dismiss(index);
}
}
private void Dismiss(int index)
{
var toast = _toasts[index];
_toasts.RemoveAt(index);
_dismissAt.RemoveAt(index);
toast.Root.RemoveFromHierarchy();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 57e0ed949960344bd80072dd54a52c4f
@@ -0,0 +1,134 @@
using System.Collections;
using System.Text;
using UnityEngine;
using UnityEngine.UIElements;
namespace LiveOpsExamples
{
public static class UiDebugProbe
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void Install()
{
new GameObject("UiDebugProbe").AddComponent<Probe>();
}
private sealed class Probe : MonoBehaviour
{
private PanelRenderer _panelRenderer;
private VisualElement _root;
private IEnumerator Start()
{
yield return null;
yield return null;
_panelRenderer = FindFirstObjectByType<PanelRenderer>();
if (_panelRenderer == null)
{
Debug.Log("[UiProbe] PanelRenderer: NOT FOUND");
}
else
{
_panelRenderer.RegisterUIReloadCallback(OnUIReload);
}
if (_root == null)
{
Dump("after-scene-load");
}
while (true)
{
yield return new WaitForSecondsRealtime(10f);
Dump("periodic");
}
}
private void OnDestroy()
{
if (_panelRenderer != null)
{
_panelRenderer.UnregisterUIReloadCallback(OnUIReload);
}
}
private void OnUIReload(PanelRenderer renderer, VisualElement root)
{
_root = root;
Dump("ui-reload");
}
private void Dump(string tag)
{
var sb = new StringBuilder();
sb.AppendLine($"[UiProbe] --- {tag} ---");
sb.AppendLine($"screen={Screen.width}x{Screen.height}");
if (_panelRenderer == null)
{
sb.AppendLine("PanelRenderer: NOT FOUND");
Debug.Log(sb.ToString());
return;
}
var ps = _panelRenderer.panelSettings;
sb.AppendLine(ps == null
? "panelSettings: NULL"
: $"panelSettings={ps.name} scaleMode={ps.scaleMode} ref={ps.referenceResolution.x}x{ps.referenceResolution.y} match={ps.match} scale={ps.scale}");
if (_root == null)
{
sb.AppendLine("root: NULL (reload callback not fired yet)");
Debug.Log(sb.ToString());
return;
}
sb.AppendLine($"root worldBound={_root.worldBound} children={_root.childCount}");
var uiRoot = _root.childCount > 0 ? _root[0] : null;
sb.AppendLine($"uiRoot={uiRoot?.name} children={uiRoot?.childCount}");
if (uiRoot != null)
{
foreach (var child in uiRoot.Children())
{
Describe(sb, child);
}
}
var renderers = FindObjectsByType<PanelRenderer>(FindObjectsSortMode.None);
sb.AppendLine($"panelRenderers={renderers.Length} panel={(_root.panel == null ? "null" : "ok")}");
Describe(sb, _root.Q("hud-score"));
Describe(sb, _root.Q("hud-timer-fill"));
Describe(sb, _root.Q("session-card"));
Describe(sb, _root.Q("remote-config"));
Describe(sb, _root.Q("event-log-scroll"));
var controller = FindFirstObjectByType<CozyCollectorController>();
sb.AppendLine(controller == null
? "controller: NOT FOUND"
: $"controller phase={controller.Phase} activeModal={controller.ActiveModal} status='{controller.StatusMessage}'");
var cam = Camera.main;
sb.AppendLine(cam == null
? "camera: NOT FOUND"
: $"camera rect={cam.rect} targetTexture={(cam.targetTexture == null ? "null" : cam.targetTexture.name)} enabled={cam.enabled} clearFlags={cam.clearFlags} time={Time.time:0.#} timeScale={Time.timeScale}");
Debug.Log(sb.ToString());
}
private static void Describe(StringBuilder sb, VisualElement element)
{
if (element == null)
{
sb.AppendLine(" <element not found>");
return;
}
var r = element.resolvedStyle;
sb.AppendLine($" {element.name}: display={r.display} visibility={r.visibility} opacity={r.opacity} " +
$"size={r.width:0.#}x{r.height:0.#} worldBound={element.worldBound} classes='{string.Join(",", element.GetClasses())}'");
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b70a6370f0b8b48aabebe030ae087adf