using System.Collections.Generic; using RudderSdk.Unity; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; namespace LiveOpsExamples.Editor { /// /// Bakes the Cozy Collector demo scene (world + camera + game setup) and saves it /// to . UI is built by the CozyCollectorBake.Ui part. /// Re-running overwrites the same scene asset. /// 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.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(); renderer.sortingOrder = -10; var sprite = AssetDatabase.LoadAssetAtPath(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(); var playerGo = new GameObject("Player"); playerGo.transform.SetParent(worldGo.transform, false); playerRenderer = playerGo.AddComponent(); playerRenderer.sprite = AssetDatabase.LoadAssetAtPath(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(); body.bodyType = RigidbodyType2D.Kinematic; body.simulated = true; body.gravityScale = 0f; // Non-trigger collider so the snacks' trigger colliders fire OnTriggerEnter2D. var collider = playerGo.AddComponent(); 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(); return world; } private static CozyCollectorController BuildSceneGameSetup(GameWorld world) { var setupGo = new GameObject("GameSetup"); var rudder = setupGo.AddComponent(); var configuration = AssetDatabase.LoadAssetAtPath(ConfigurationPath); if (configuration == null) { Debug.LogError($"[CozyCollectorBake] RudderConfiguration not found at {ConfigurationPath}."); } else { SetSceneReference(rudder, "_configuration", configuration); } var controller = setupGo.AddComponent(); 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(ScorePopupPrefabPath); if (scorePopupPrefab != null) { SetSceneReference(world, "scorePopupPrefab", scorePopupPrefab); } var collectBurstPrefab = LoadScenePrefabComponent(CollectBurstPrefabPath); if (collectBurstPrefab != null) { SetSceneReference(world, "collectBurstPrefab", collectBurstPrefab); } } private static void WireSceneSpawner(SnackSpawner spawner, Transform snacksParent) { var snackPrefab = LoadScenePrefabComponent(SnackPrefabPath); if (snackPrefab != null) { SetSceneReference(spawner, "prefab", snackPrefab); } var library = AssetDatabase.LoadAssetAtPath(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(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(string path) where T : Component { var prefab = AssetDatabase.LoadAssetAtPath(path); var component = prefab != null ? prefab.GetComponent() : 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(); } } }