241 lines
9.9 KiB
C#
241 lines
9.9 KiB
C#
#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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<SceneReviewManifest>(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>();
|
|
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<AudioListener>();
|
|
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>();
|
|
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<TextMesh>();
|
|
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<GameObject>(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<GameObject>(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.";
|
|
}
|
|
|
|
/// <summary>Build and open a review scene from the currently selected scene manifest.</summary>
|
|
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
|