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 LiveOpsExamples;
using UnityEditor;
using UnityEngine;
namespace LiveOpsExamples.Editor
{
/// <summary>
/// Bakes the Cozy Collector ScriptableObject assets. Idempotent: writes to
/// fixed asset paths and overwrites whatever is already there.
/// </summary>
public static partial class CozyCollectorBake
{
private const string SpriteLibraryAssetPath = "Assets/LiveOpsExamples/CozyCollectorSpriteLibrary.asset";
private const string SpriteFolderPath = "Assets/LiveOpsExamples/Sprites";
[MenuItem("Tools/LiveOps/Bake Cozy Collector Assets")]
public static void BakeAssets()
{
var library = AssetDatabase.LoadAssetAtPath<CozyCollectorSpriteLibrary>(SpriteLibraryAssetPath);
if (library == null)
{
library = ScriptableObject.CreateInstance<CozyCollectorSpriteLibrary>();
AssetDatabase.CreateAsset(library, SpriteLibraryAssetPath);
}
var serialized = new SerializedObject(library);
AssignLibrarySprite(serialized, "background", "cozy-camp-bg.png");
AssignLibrarySprite(serialized, "player", "player.png");
AssignLibrarySprite(serialized, "moonberry", "moonberry.png");
AssignLibrarySprite(serialized, "acorn", "acorn.png");
AssignLibrarySprite(serialized, "star", "star.png");
AssignLibrarySprite(serialized, "snack", "snack.png");
serialized.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(library);
AssetDatabase.SaveAssets();
Debug.Log($"Cozy Collector sprite library baked: {SpriteLibraryAssetPath}");
}
private static void AssignLibrarySprite(SerializedObject serialized, string fieldName, string fileName)
{
var spritePath = $"{SpriteFolderPath}/{fileName}";
var sprite = AssetDatabase.LoadAssetAtPath<Sprite>(spritePath);
if (sprite == null)
{
Debug.LogError($"CozyCollectorBake: sprite not found at {spritePath}");
return;
}
var property = serialized.FindProperty(fieldName);
if (property == null)
{
Debug.LogError($"CozyCollectorBake: field '{fieldName}' not found on {nameof(CozyCollectorSpriteLibrary)}");
return;
}
property.objectReferenceValue = sprite;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 640a29c33f5384caebacc7de21235ab8
@@ -0,0 +1,166 @@
using System.IO;
using LiveOpsExamples;
using TMPro;
using UnityEditor;
using UnityEngine;
namespace LiveOpsExamples.Editor
{
/// <summary>
/// Bakes the Cozy Collector gameplay prefabs. Idempotent: every prefab is
/// rebuilt from code and saved over the same fixed asset path.
/// </summary>
public static partial class CozyCollectorBake
{
private const string PrefabFolderPath = "Assets/LiveOpsExamples/Prefabs";
private const string SnackSpritePath = "Assets/LiveOpsExamples/Sprites/snack.png";
private static readonly Color AccentColor = ParseHexColor("#FFD27D");
[MenuItem("Tools/LiveOps/Bake Cozy Collector Prefabs")]
public static void BakePrefabs()
{
EnsureAssetFolder(PrefabFolderPath);
SaveBakedPrefab(BuildSnackPrefab(), $"{PrefabFolderPath}/Snack.prefab");
SaveBakedPrefab(BuildScorePopupPrefab(), $"{PrefabFolderPath}/ScorePopup.prefab");
SaveBakedPrefab(BuildCollectBurstPrefab(), $"{PrefabFolderPath}/CollectBurst.prefab");
AssetDatabase.SaveAssets();
Debug.Log($"Cozy Collector prefabs baked under {PrefabFolderPath}");
}
private static GameObject BuildSnackPrefab()
{
var go = new GameObject("Snack");
var collider = go.AddComponent<CircleCollider2D>();
collider.isTrigger = true;
collider.radius = 0.3f;
var renderer = go.AddComponent<SpriteRenderer>();
var sprite = AssetDatabase.LoadAssetAtPath<Sprite>(SnackSpritePath);
if (sprite == null)
{
Debug.LogError($"CozyCollectorBake: sprite not found at {SnackSpritePath}");
}
else
{
renderer.sprite = sprite;
// Phaser setDisplaySize(52, 52) at 100 PPU -> 0.52 x 0.52 world units.
var scale = new Vector3(
0.52f / sprite.bounds.size.x,
0.52f / sprite.bounds.size.y,
1f);
go.transform.localScale = scale;
// Phaser pickup radius = 0.42 * 52px ~= 0.22 world units; a circle
// collider's world radius scales by the largest transform axis.
collider.radius = 0.22f / Mathf.Max(scale.x, scale.y);
}
go.AddComponent<Snack>();
return go;
}
private static GameObject BuildScorePopupPrefab()
{
var go = new GameObject("ScorePopup");
var label = go.AddComponent<TextMeshPro>();
AssignDefaultFont(label);
label.text = "+10";
label.fontSize = 2.5f;
label.alignment = TextAlignmentOptions.Center;
label.color = Color.white;
var view = go.AddComponent<ScorePopupView>();
WireObjectFields(view, ("label", label));
return go;
}
private static GameObject BuildCollectBurstPrefab()
{
var go = new GameObject("CollectBurst");
var particles = go.AddComponent<ParticleSystem>();
var main = particles.main;
main.duration = 0.5f;
main.loop = false;
main.playOnAwake = true;
main.startLifetime = 0.45f;
main.startSpeed = 2f;
main.startSize = 0.15f;
main.gravityModifier = 0f;
main.startColor = AccentColor;
main.stopAction = ParticleSystemStopAction.Destroy;
var emission = particles.emission;
emission.rateOverTime = 0f;
emission.SetBursts(new[] { new ParticleSystem.Burst(0f, 9) });
var shape = particles.shape;
shape.shapeType = ParticleSystemShapeType.Sphere;
shape.radius = 0.1f;
var renderer = particles.GetComponent<ParticleSystemRenderer>();
renderer.renderMode = ParticleSystemRenderMode.Billboard;
var material = AssetDatabase.GetBuiltinExtraResource<Material>("Default-Particle.mat");
if (material == null)
material = AssetDatabase.GetBuiltinExtraResource<Material>("Sprites-Default.mat");
if (material == null)
Debug.LogWarning("CozyCollectorBake: no builtin particle material resolved, assign one manually.");
else
renderer.sharedMaterial = material;
return go;
}
private static Color ParseHexColor(string hex)
{
return ColorUtility.TryParseHtmlString(hex, out var color) ? color : Color.magenta;
}
private static void EnsureAssetFolder(string folderPath)
{
if (AssetDatabase.IsValidFolder(folderPath))
return;
var parent = Path.GetDirectoryName(folderPath)?.Replace('\\', '/');
if (!string.IsNullOrEmpty(parent))
EnsureAssetFolder(parent);
AssetDatabase.CreateFolder(parent, Path.GetFileName(folderPath));
}
private static void SaveBakedPrefab(GameObject go, string path)
{
PrefabUtility.SaveAsPrefabAsset(go, path);
Object.DestroyImmediate(go);
}
private static void WireObjectFields(Object component, params (string Field, Object Value)[] fields)
{
var serialized = new SerializedObject(component);
foreach (var (field, value) in fields)
{
var property = serialized.FindProperty(field);
if (property == null)
{
Debug.LogError($"CozyCollectorBake: field '{field}' not found on {component.GetType().Name}");
continue;
}
property.objectReferenceValue = value;
}
serialized.ApplyModifiedPropertiesWithoutUndo();
}
private static void AssignDefaultFont(TMP_Text text)
{
if (TMP_Settings.defaultFontAsset != null)
text.font = TMP_Settings.defaultFontAsset;
else
Debug.LogWarning("CozyCollectorBake: TMP default font asset is not set, assign a font manually.");
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5d0f038b4a90c45a8a95bf4f37221e78
@@ -0,0 +1,258 @@
using System.Collections.Generic;
using RudderSdk.Unity;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
namespace LiveOpsExamples.Editor
{
/// <summary>
/// Bakes the Cozy Collector demo scene (world + camera + game setup) and saves it
/// to <see cref="ScenePath"/>. UI is built by the CozyCollectorBake.Ui part.
/// Re-running overwrites the same scene asset.
/// </summary>
public static partial class CozyCollectorBake
{
private const string ScenePath = "Assets/LiveOpsExamples/LiveOpsCozyCollector.unity";
private const string BackgroundSpritePath = "Assets/LiveOpsExamples/Sprites/cozy-camp-bg.png";
private const string PlayerSpritePath = "Assets/LiveOpsExamples/Sprites/player.png";
private const string ConfigurationPath = "Assets/LiveOpsLocal.asset";
private const string SpriteLibraryPath = "Assets/LiveOpsExamples/CozyCollectorSpriteLibrary.asset";
private const string SnackPrefabPath = "Assets/LiveOpsExamples/Prefabs/Snack.prefab";
private const string ScorePopupPrefabPath = "Assets/LiveOpsExamples/Prefabs/ScorePopup.prefab";
private const string CollectBurstPrefabPath = "Assets/LiveOpsExamples/Prefabs/CollectBurst.prefab";
// The Phaser field is 1280x720 px at 100 PPU -> 12.8x7.2 world units.
private const float FieldWidthUnits = 12.8f;
private const float FieldHeightUnits = 7.2f;
private static readonly Color CameraBackgroundColor = new Color(0.10f, 0.08f, 0.13f);
[MenuItem("Tools/LiveOps/Bake Cozy Collector (All)")]
public static void BakeAll()
{
BakeAssets();
BakePrefabs();
BakeScene();
AssetDatabase.SaveAssets();
}
[MenuItem("Tools/LiveOps/Bake Cozy Collector Scene")]
public static void BakeScene()
{
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
BuildSceneCamera();
BuildSceneBackground();
var world = BuildSceneWorld(out var playerRenderer, out var spawner, out var snacksParent);
var controller = BuildSceneGameSetup(world);
WireSceneWorld(world, playerRenderer, spawner, controller);
WireSceneSpawner(spawner, snacksParent);
BuildUi(controller, world);
EditorSceneManager.SaveScene(scene, ScenePath);
AddSceneToBuildSettings();
AssetDatabase.SaveAssets();
Debug.Log($"[CozyCollectorBake] Scene saved to {ScenePath}.");
}
private static void BuildSceneCamera()
{
var cameraGo = new GameObject("Main Camera");
cameraGo.tag = "MainCamera";
cameraGo.transform.position = new Vector3(0f, 0f, -10f);
var camera = cameraGo.AddComponent<Camera>();
camera.orthographic = true;
camera.orthographicSize = FieldHeightUnits / 2f;
camera.clearFlags = CameraClearFlags.SolidColor;
camera.backgroundColor = CameraBackgroundColor;
// Game field occupies the left 75% of the screen; the right side hosts the UI rail.
camera.rect = new Rect(0f, 0f, 0.75f, 1f);
}
private static void BuildSceneBackground()
{
var backgroundGo = new GameObject("Background");
var renderer = backgroundGo.AddComponent<SpriteRenderer>();
renderer.sortingOrder = -10;
var sprite = AssetDatabase.LoadAssetAtPath<Sprite>(BackgroundSpritePath);
if (sprite == null)
{
Debug.LogError($"[CozyCollectorBake] Background sprite not found at {BackgroundSpritePath}.");
return;
}
renderer.sprite = sprite;
var size = sprite.bounds.size;
backgroundGo.transform.localScale = new Vector3(
size.x > 0f ? FieldWidthUnits / size.x : 1f,
size.y > 0f ? FieldHeightUnits / size.y : 1f,
1f);
}
private static GameWorld BuildSceneWorld(
out SpriteRenderer playerRenderer,
out SnackSpawner spawner,
out Transform snacksParent)
{
var worldGo = new GameObject("World");
var world = worldGo.AddComponent<GameWorld>();
var playerGo = new GameObject("Player");
playerGo.transform.SetParent(worldGo.transform, false);
playerRenderer = playerGo.AddComponent<SpriteRenderer>();
playerRenderer.sprite = AssetDatabase.LoadAssetAtPath<Sprite>(PlayerSpritePath);
var colliderRadius = 0.3f;
if (playerRenderer.sprite == null)
{
Debug.LogError($"[CozyCollectorBake] Player sprite not found at {PlayerSpritePath}.");
}
else
{
// Phaser setDisplaySize(82, 108) at 100 PPU -> 0.82 x 1.08 world units.
var bounds = playerRenderer.sprite.bounds.size;
var scale = new Vector3(0.82f / bounds.x, 1.08f / bounds.y, 1f);
playerGo.transform.localScale = scale;
// Phaser body is ~0.45x0.55 world units; a circle collider's world
// radius scales by the largest transform axis.
colliderRadius = 0.24f / Mathf.Max(scale.x, scale.y);
}
var body = playerGo.AddComponent<Rigidbody2D>();
body.bodyType = RigidbodyType2D.Kinematic;
body.simulated = true;
body.gravityScale = 0f;
// Non-trigger collider so the snacks' trigger colliders fire OnTriggerEnter2D.
var collider = playerGo.AddComponent<CircleCollider2D>();
collider.radius = colliderRadius;
collider.isTrigger = false;
var snacksGo = new GameObject("Snacks");
snacksGo.transform.SetParent(worldGo.transform, false);
snacksParent = snacksGo.transform;
var spawnerGo = new GameObject("Spawner");
spawnerGo.transform.SetParent(worldGo.transform, false);
spawner = spawnerGo.AddComponent<SnackSpawner>();
return world;
}
private static CozyCollectorController BuildSceneGameSetup(GameWorld world)
{
var setupGo = new GameObject("GameSetup");
var rudder = setupGo.AddComponent<Rudder>();
var configuration = AssetDatabase.LoadAssetAtPath<RudderConfiguration>(ConfigurationPath);
if (configuration == null)
{
Debug.LogError($"[CozyCollectorBake] RudderConfiguration not found at {ConfigurationPath}.");
}
else
{
SetSceneReference(rudder, "_configuration", configuration);
}
var controller = setupGo.AddComponent<CozyCollectorController>();
SetSceneReference(controller, "world", world);
return controller;
}
private static void WireSceneWorld(
GameWorld world,
SpriteRenderer playerRenderer,
SnackSpawner spawner,
CozyCollectorController controller)
{
SetSceneReference(world, "player", playerRenderer);
SetSceneReference(world, "spawner", spawner);
SetSceneReference(world, "controller", controller);
// moveMin/moveMax keep the component defaults (-6,-3.2)/(6,3.2).
var scorePopupPrefab = LoadScenePrefabComponent<ScorePopupView>(ScorePopupPrefabPath);
if (scorePopupPrefab != null)
{
SetSceneReference(world, "scorePopupPrefab", scorePopupPrefab);
}
var collectBurstPrefab = LoadScenePrefabComponent<ParticleSystem>(CollectBurstPrefabPath);
if (collectBurstPrefab != null)
{
SetSceneReference(world, "collectBurstPrefab", collectBurstPrefab);
}
}
private static void WireSceneSpawner(SnackSpawner spawner, Transform snacksParent)
{
var snackPrefab = LoadScenePrefabComponent<Snack>(SnackPrefabPath);
if (snackPrefab != null)
{
SetSceneReference(spawner, "prefab", snackPrefab);
}
var library = AssetDatabase.LoadAssetAtPath<CozyCollectorSpriteLibrary>(SpriteLibraryPath);
if (library == null)
{
Debug.LogError($"[CozyCollectorBake] Sprite library not found at {SpriteLibraryPath}. " +
"Run Tools/LiveOps/Bake Cozy Collector (All) first.");
}
else
{
SetSceneReference(spawner, "library", library);
}
SetSceneReference(spawner, "parent", snacksParent);
// spawnMin/spawnMax keep the component defaults.
}
private static void AddSceneToBuildSettings()
{
var scenes = new List<EditorBuildSettingsScene>(EditorBuildSettings.scenes);
foreach (var entry in scenes)
{
if (entry.path == ScenePath)
{
return;
}
}
scenes.Add(new EditorBuildSettingsScene(ScenePath, true));
EditorBuildSettings.scenes = scenes.ToArray();
}
private static T LoadScenePrefabComponent<T>(string path) where T : Component
{
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
var component = prefab != null ? prefab.GetComponent<T>() : null;
if (component == null)
{
Debug.LogError($"[CozyCollectorBake] Prefab with {typeof(T).Name} not found at {path}. " +
"Run Tools/LiveOps/Bake Cozy Collector (All) first; the scene is saved with empty references.");
}
return component;
}
private static void SetSceneReference(Object target, string propertyName, Object value)
{
var serialized = new SerializedObject(target);
var property = serialized.FindProperty(propertyName);
if (property == null)
{
Debug.LogError($"[CozyCollectorBake] Property '{propertyName}' not found on {target.GetType().Name}.");
return;
}
property.objectReferenceValue = value;
serialized.ApplyModifiedPropertiesWithoutUndo();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9b06747933b8c458b8262e8516634b14
@@ -0,0 +1,144 @@
using System;
using System.Reflection;
using LiveOpsExamples;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using Object = UnityEngine.Object;
namespace LiveOpsExamples.Editor
{
/// <summary>
/// Builds the Cozy Collector UI: a GameObject with a PanelRenderer (PanelSettings +
/// CozyCollectorUI.uxml) and all panel components wired through serialized
/// references. The PanelSettings asset is created programmatically when missing.
/// Called from <see cref="CozyCollectorBake.BakeScene"/>.
/// </summary>
public static partial class CozyCollectorBake
{
private const string UiFolderPath = "Assets/LiveOpsExamples/UI";
private const string UiPanelSettingsPath = UiFolderPath + "/CozyCollectorPanelSettings.asset";
private const string UiDocumentAssetPath = UiFolderPath + "/CozyCollectorUI.uxml";
private const string UiLogRowTemplatePath = UiFolderPath + "/LogRow.uxml";
private const string UiToastTemplatePath = UiFolderPath + "/ToastRow.uxml";
private const string UiShopOfferRowTemplatePath = UiFolderPath + "/ShopOfferRow.uxml";
private const string UiBackpackItemRowTemplatePath = UiFolderPath + "/BackpackItemRow.uxml";
private const string UiLeaderboardRowTemplatePath = UiFolderPath + "/LeaderboardRow.uxml";
private static void BuildUi(CozyCollectorController controller, GameWorld world)
{
var panelSettings = GetOrCreatePanelSettings();
var visualTree = LoadUiAsset<VisualTreeAsset>(UiDocumentAssetPath);
var uiGo = new GameObject("UI");
var panelRenderer = uiGo.AddComponent<PanelRenderer>();
panelRenderer.panelSettings = panelSettings;
if (visualTree != null)
{
panelRenderer.visualTreeAsset = visualTree;
}
WirePanel(uiGo.AddComponent<SessionCardPanel>(), panelRenderer, controller);
WirePanel(uiGo.AddComponent<RemoteConfigPanel>(), panelRenderer, controller);
var eventInspector = uiGo.AddComponent<EventInspectorPanel>();
WirePanel(eventInspector, panelRenderer, controller);
WireTemplate(eventInspector, "logRowTemplate", UiLogRowTemplatePath);
var hud = uiGo.AddComponent<HudPanel>();
WirePanel(hud, panelRenderer, controller);
SetSceneReference(hud, "world", world);
var toasts = uiGo.AddComponent<ToastsPanel>();
WirePanel(toasts, panelRenderer, controller);
WireTemplate(toasts, "toastTemplate", UiToastTemplatePath);
WirePanel(uiGo.AddComponent<BootOverlayPanel>(), panelRenderer, controller);
WirePanel(uiGo.AddComponent<SetupOverlayPanel>(), panelRenderer, controller);
var shop = uiGo.AddComponent<ShopModalPanel>();
WirePanel(shop, panelRenderer, controller);
WireTemplate(shop, "shopOfferRowTemplate", UiShopOfferRowTemplatePath);
var backpack = uiGo.AddComponent<BackpackModalPanel>();
WirePanel(backpack, panelRenderer, controller);
WireTemplate(backpack, "backpackItemRowTemplate", UiBackpackItemRowTemplatePath);
var spriteLibrary = AssetDatabase.LoadAssetAtPath<CozyCollectorSpriteLibrary>(SpriteLibraryPath);
if (spriteLibrary == null)
{
Debug.LogError($"[CozyCollectorBake] Sprite library not found at {SpriteLibraryPath}. " +
"Run Tools/LiveOps/Bake Cozy Collector (All) first.");
}
else
{
SetSceneReference(backpack, "spriteLibrary", spriteLibrary);
}
WirePanel(uiGo.AddComponent<OfferModalPanel>(), panelRenderer, controller);
var roundResult = uiGo.AddComponent<RoundResultModalPanel>();
WirePanel(roundResult, panelRenderer, controller);
WireTemplate(roundResult, "leaderboardRowTemplate", UiLeaderboardRowTemplatePath);
}
private static void WirePanel(MonoBehaviour panel, PanelRenderer panelRenderer, CozyCollectorController controller)
{
SetSceneReference(panel, "panelRenderer", panelRenderer);
SetSceneReference(panel, "controller", controller);
}
private static void WireTemplate(MonoBehaviour panel, string fieldName, string templatePath)
{
var template = LoadUiAsset<VisualTreeAsset>(templatePath);
if (template != null)
{
SetSceneReference(panel, fieldName, template);
}
}
private static T LoadUiAsset<T>(string path) where T : Object
{
var asset = AssetDatabase.LoadAssetAtPath<T>(path);
if (asset == null)
{
Debug.LogError($"[CozyCollectorBake] {typeof(T).Name} not found at {path}. " +
"The scene is saved with an empty reference.");
}
return asset;
}
private static PanelSettings GetOrCreatePanelSettings()
{
var settings = AssetDatabase.LoadAssetAtPath<PanelSettings>(UiPanelSettingsPath);
if (settings != null)
{
return settings;
}
EnsureAssetFolder(UiFolderPath);
settings = ScriptableObject.CreateInstance<PanelSettings>();
settings.scaleMode = PanelScaleMode.ScaleWithScreenSize;
settings.referenceResolution = new Vector2Int(1600, 900);
settings.screenMatchMode = PanelScreenMatchMode.MatchWidthOrHeight;
settings.match = 0.5f;
var defaultTheme = LoadDefaultRuntimeTheme();
if (defaultTheme != null)
{
settings.themeStyleSheet = defaultTheme;
}
AssetDatabase.CreateAsset(settings, UiPanelSettingsPath);
return settings;
}
private static ThemeStyleSheet LoadDefaultRuntimeTheme()
{
var themeUtility = Type.GetType("UnityEditor.UIElements.ThemeUtility, UnityEditor.UIElementsModule");
var property = themeUtility?.GetProperty("builtInDefaultRuntimeTheme", BindingFlags.Static | BindingFlags.NonPublic);
return property?.GetValue(null) as ThemeStyleSheet;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4ffeef7dc98af46a8b8bf186f4e2073b