Add prototype gameplay loop with character/battlefield assets and server game APIs.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user