#if UNITY_EDITOR using System; using System.IO; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; [Serializable] public sealed class SceneReviewManifest { public int schemaVersion; public string sceneId; public string sourcePrefab; public string outputScene; public string reportPath; public string label; public SceneReviewVec3 cameraPosition; public SceneReviewVec3 cameraTarget; public float fieldOfView = 45f; } [Serializable] public sealed class SceneReviewVec3 { public float x; public float y; public float z; public Vector3 Value => new Vector3(x, y, z); } [Serializable] public sealed class SceneReviewReport { public int schemaVersion = 1; public string sceneId; public string sourcePrefab; public string outputScene; public string generatedAtUtc; public string[] reviewSteps; } /// /// Creates a persistent editor scene from an already-authored assembly prefab. /// This is an authoring/review step; no scene objects are created by runtime code. /// public static class BuildSceneReview { const string ManifestPath = "Doc/Design/Scene/scene_review_manifest.json"; static string RepoRoot => Directory.GetParent(Application.dataPath).Parent.FullName; static SceneReviewManifest ReadManifest() { var path = Path.GetFullPath(Path.Combine(RepoRoot, ManifestPath)); if (!File.Exists(path)) throw new FileNotFoundException("Missing scene review manifest", path); var manifest = JsonUtility.FromJson(File.ReadAllText(path)); if (manifest == null || string.IsNullOrEmpty(manifest.sceneId)) throw new InvalidOperationException("Scene review manifest is invalid."); if (string.IsNullOrEmpty(manifest.sourcePrefab) || string.IsNullOrEmpty(manifest.outputScene)) throw new InvalidOperationException("Scene review manifest requires sourcePrefab and outputScene."); if (manifest.cameraPosition == null || manifest.cameraTarget == null) throw new InvalidOperationException("Scene review manifest requires cameraPosition and cameraTarget."); return manifest; } static string NormalizeAssetPath(string path) { return path.Replace('\\', '/'); } static void EnsureFolderForAsset(string assetPath) { var absolute = Path.GetFullPath(Path.Combine(RepoRoot, NormalizeAssetPath(assetPath))); var directory = Path.GetDirectoryName(absolute); if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory); } static GameObject AddReviewCamera(SceneReviewManifest manifest) { var go = new GameObject("ReviewCamera"); go.tag = "MainCamera"; var camera = go.AddComponent(); camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.12f, 0.16f, 0.22f); camera.fieldOfView = manifest.fieldOfView > 1f ? manifest.fieldOfView : 45f; camera.nearClipPlane = 0.1f; camera.farClipPlane = 200f; go.AddComponent(); go.transform.position = manifest.cameraPosition.Value; go.transform.rotation = Quaternion.LookRotation(manifest.cameraTarget.Value - go.transform.position, Vector3.up); return go; } static GameObject AddReviewLight() { var go = new GameObject("ReviewKeyLight"); var light = go.AddComponent(); light.type = LightType.Directional; light.color = new Color(1f, 0.96f, 0.88f); light.intensity = 1.15f; light.shadows = LightShadows.Soft; go.transform.rotation = Quaternion.Euler(48f, -30f, 0f); return go; } static GameObject AddReviewLabel(SceneReviewManifest manifest) { var go = new GameObject("ReviewLabel"); var text = go.AddComponent(); text.text = string.IsNullOrEmpty(manifest.label) ? manifest.sceneId + " Review" : manifest.label; text.anchor = TextAnchor.UpperLeft; text.alignment = TextAlignment.Left; text.characterSize = 0.22f; text.fontSize = 42; text.color = Color.white; go.transform.position = manifest.cameraTarget.Value + new Vector3(-8f, 5f, 4f); go.transform.rotation = Quaternion.LookRotation(manifest.cameraPosition.Value - go.transform.position, Vector3.up); return go; } public static string ValidateOnly() { var manifest = ReadManifest(); var prefab = AssetDatabase.LoadAssetAtPath(NormalizeAssetPath(manifest.sourcePrefab)); if (prefab == null) throw new InvalidOperationException("Missing review source prefab " + manifest.sourcePrefab); if (string.IsNullOrEmpty(manifest.outputScene) || !manifest.outputScene.EndsWith(".unity", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("outputScene must be a .unity asset path."); return "PASS review manifest scene=" + manifest.sceneId + " prefab=" + manifest.sourcePrefab; } public static string Build() { if (EditorApplication.isPlaying) throw new InvalidOperationException("Build the review scene in Edit Mode."); var manifest = ReadManifest(); var prefabPath = NormalizeAssetPath(manifest.sourcePrefab); var scenePath = NormalizeAssetPath(manifest.outputScene); var prefab = AssetDatabase.LoadAssetAtPath(prefabPath); if (prefab == null) throw new InvalidOperationException("Missing review source prefab " + prefabPath); EnsureFolderForAsset(scenePath); var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab); instance.name = manifest.sceneId + "_ReviewRoot"; SceneManager.MoveGameObjectToScene(instance, scene); AddReviewCamera(manifest); AddReviewLight(); AddReviewLabel(manifest); RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Trilight; RenderSettings.ambientSkyColor = new Color(0.30f, 0.38f, 0.50f); RenderSettings.ambientEquatorColor = new Color(0.22f, 0.25f, 0.28f); RenderSettings.ambientGroundColor = new Color(0.12f, 0.12f, 0.12f); EditorSceneManager.SaveScene(scene, scenePath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); WriteReport(manifest, scenePath); return "Built review scene " + scenePath + " from " + prefabPath; } static void WriteReport(SceneReviewManifest manifest, string scenePath) { var reportPath = string.IsNullOrEmpty(manifest.reportPath) ? "Tools/output/scene_review/" + manifest.sceneId + "/review_report.json" : NormalizeAssetPath(manifest.reportPath); var absolute = Path.GetFullPath(Path.Combine(RepoRoot, reportPath)); var directory = Path.GetDirectoryName(absolute); if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory); var report = new SceneReviewReport { sceneId = manifest.sceneId, sourcePrefab = manifest.sourcePrefab, outputScene = scenePath, generatedAtUtc = DateTime.UtcNow.ToString("o"), reviewSteps = new[] { "Open the generated .unity scene in Unity.", "Check the assembled prefab hierarchy and component placement.", "Check the Game view from ReviewCamera and the Scene view for collisions.", "Run ValidateReview after any regeneration." } }; File.WriteAllText(absolute, JsonUtility.ToJson(report, true)); } public static string OpenReview() { var manifest = ReadManifest(); var scenePath = NormalizeAssetPath(manifest.outputScene); if (!File.Exists(Path.GetFullPath(Path.Combine(RepoRoot, scenePath)))) throw new FileNotFoundException("Review scene has not been built", scenePath); var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); var root = GameObject.Find(manifest.sceneId + "_ReviewRoot"); if (root != null) { Selection.activeGameObject = root; EditorGUIUtility.PingObject(root); } var sceneView = SceneView.lastActiveSceneView; if (sceneView != null) { sceneView.LookAt(manifest.cameraTarget.Value, Quaternion.Euler(32f, 0f, 0f), 24f); sceneView.Repaint(); } return "Opened review scene " + scene.path; } public static string BuildAndOpenBattlefieldReview() { BuildBattlefieldAssets.BuildAll(); BuildBattlefieldScene.Build(); BuildBattlefieldScene.VerifyBuilt(); var result = Build(); OpenReview(); return result + " and opened it for review."; } /// Build and open a review scene from the currently selected scene manifest. public static string BuildAndOpenReview() { BuildBattlefieldScene.Build(); var result = Build(); OpenReview(); return result + " and opened it for review."; } public static string ValidateReview() { var manifest = ReadManifest(); var scenePath = NormalizeAssetPath(manifest.outputScene); var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); var root = GameObject.Find(manifest.sceneId + "_ReviewRoot"); if (root == null) throw new InvalidOperationException("Missing review root " + manifest.sceneId + "_ReviewRoot"); if (GameObject.Find("ReviewCamera") == null) throw new InvalidOperationException("Missing ReviewCamera"); if (GameObject.Find("ReviewKeyLight") == null) throw new InvalidOperationException("Missing ReviewKeyLight"); return "PASS review scene=" + scene.path + " root=" + root.name; } } #endif