Files
AIGame/AIClient/AgentScripts/BuildBattlefieldScene.cs

355 lines
19 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
[Serializable] public sealed class BattlefieldVec3 { public float x, y, z; public Vector3 Value => new Vector3(x, y, z); }
[Serializable] public sealed class BattlefieldGroundSpec { public string type, gamePrefab, requirements; public int targetTriangles; }
[Serializable] public sealed class BattlefieldPlacement { public string name; public BattlefieldVec3 position, rotation, scale; }
[Serializable] public sealed class BattlefieldComponentSpec { public string type, prefab, label; public int targetTriangles; public bool placeInScene; public BattlefieldVec3 position, rotation, scale; public BattlefieldPlacement[] placements; }
[Serializable] public sealed class BattlefieldAssemblySpec { public string outputPrefab, componentFolder, markerFolder, materialsFolder; }
[Serializable] public sealed class BattlefieldManifest
{
public int schemaVersion, componentTypeLimit;
public string sceneId, selectedDesign;
public BattlefieldGroundSpec ground;
public BattlefieldComponentSpec[] components;
public BattlefieldAssemblySpec assembly;
}
/// <summary>Pipeline authoring entry for the selected battlefield design. It never creates gameplay scenery at runtime.</summary>
public static class BuildBattlefieldScene
{
const string ManifestPath = "Doc/Design/Scene/Battlefield/battlefield_scene_manifest.json";
static string RepoRoot => Directory.GetParent(Application.dataPath).Parent.FullName;
static string ProjectPath(string path) => path.StartsWith("Assets/", StringComparison.Ordinal) ? path : path;
static BattlefieldManifest ReadManifest()
{
string path = Path.GetFullPath(Path.Combine(RepoRoot, ManifestPath));
if (!File.Exists(path)) throw new FileNotFoundException("Missing battlefield manifest", path);
string text = File.ReadAllText(path);
// JsonUtility drops nested objects in this manifest, so each object is parsed on its own.
var data = new BattlefieldManifest
{
schemaVersion = ExtractInt(text, "schemaVersion"),
sceneId = ExtractString(text, "sceneId"),
selectedDesign = ExtractString(text, "selectedDesign"),
componentTypeLimit = ExtractInt(text, "componentTypeLimit"),
ground = JsonUtility.FromJson<BattlefieldGroundSpec>(SliceObject(text, "ground")),
assembly = JsonUtility.FromJson<BattlefieldAssemblySpec>(SliceObject(text, "assembly")),
components = ParseComponents(SliceArray(text, "components"))
};
if (data.ground == null) throw new Exception("Invalid battlefield manifest");
return data;
}
static string ExtractString(string json, string key)
{
var match = Regex.Match(json, "\"" + key + "\"\\s*:\\s*\"([^\"]*)\"");
return match.Success ? match.Groups[1].Value : "";
}
static int ExtractInt(string json, string key)
{
var match = Regex.Match(json, "\"" + key + "\"\\s*:\\s*(-?\\d+)");
return match.Success ? int.Parse(match.Groups[1].Value) : 0;
}
static BattlefieldVec3 ReadVec(string json, string key)
{
var match = Regex.Match(json, "\"" + key + "\"\\s*:\\s*\\{\\s*\"x\"\\s*:\\s*(-?[0-9.]+)\\s*,\\s*\"y\"\\s*:\\s*(-?[0-9.]+)\\s*,\\s*\"z\"\\s*:\\s*(-?[0-9.]+)");
if (!match.Success) return null;
return new BattlefieldVec3
{
x = float.Parse(match.Groups[1].Value, System.Globalization.CultureInfo.InvariantCulture),
y = float.Parse(match.Groups[2].Value, System.Globalization.CultureInfo.InvariantCulture),
z = float.Parse(match.Groups[3].Value, System.Globalization.CultureInfo.InvariantCulture)
};
}
static BattlefieldComponentSpec[] ParseComponents(string arrayJson)
{
var items = new List<BattlefieldComponentSpec>();
int depth = 0, start = -1;
for (int i = 0; i < arrayJson.Length; i++)
{
if (arrayJson[i] == '{') { if (depth == 0) start = i; depth++; }
else if (arrayJson[i] == '}')
{
depth--;
if (depth == 0 && start >= 0)
{
string slice = arrayJson.Substring(start, i - start + 1);
var item = JsonUtility.FromJson<BattlefieldComponentSpec>(slice);
if (item != null)
{
item.position = ReadVec(slice, "position") ?? item.position;
item.rotation = ReadVec(slice, "rotation") ?? item.rotation;
item.scale = ReadVec(slice, "scale") ?? item.scale;
item.placements = ReadPlacements(slice);
var place = Regex.Match(slice, "\"placeInScene\"\\s*:\\s*(true|false)");
item.placeInScene = !place.Success || place.Groups[1].Value == "true";
items.Add(item);
}
start = -1;
}
}
}
return items.ToArray();
}
static BattlefieldPlacement[] ReadPlacements(string slice)
{
if (slice.IndexOf("\"placements\"", StringComparison.Ordinal) < 0) return null;
string arrayJson = SliceArray(slice, "placements");
var items = new List<BattlefieldPlacement>();
int depth = 0, start = -1;
for (int i = 0; i < arrayJson.Length; i++)
{
if (arrayJson[i] == '{') { if (depth == 0) start = i; depth++; }
else if (arrayJson[i] == '}')
{
depth--;
if (depth == 0 && start >= 0)
{
string place = arrayJson.Substring(start, i - start + 1);
items.Add(new BattlefieldPlacement
{
name = Regex.Match(place, "\"name\"\\s*:\\s*\"([^\"]*)\"").Groups[1].Value,
position = ReadVec(place, "position"),
rotation = ReadVec(place, "rotation"),
scale = ReadVec(place, "scale")
});
start = -1;
}
}
}
return items.Count == 0 ? null : items.ToArray();
}
static void Validate(BattlefieldManifest data)
{
if (string.IsNullOrEmpty(data.selectedDesign) || (data.selectedDesign != "A" && data.selectedDesign != "B" && data.selectedDesign != "C"))
throw new Exception("Choose battlefield design A/B/C before assembling models.");
if (data.ground == null || data.ground.type != "ground" || string.IsNullOrEmpty(data.ground.gamePrefab))
throw new Exception("Ground must be a separate authored prefab.");
if (data.components == null) data.components = Array.Empty<BattlefieldComponentSpec>();
var types = new HashSet<string>();
foreach (var item in data.components)
{
if (item == null || string.IsNullOrEmpty(item.type) || !types.Add(item.type)) throw new Exception("Component types must be unique.");
if (types.Count > 15 || types.Count > data.componentTypeLimit) throw new Exception("Battlefield component type limit exceeded.");
if (item.type == "ground") throw new Exception("Ground cannot be listed as a decoration component.");
if (string.IsNullOrEmpty(item.prefab)) throw new Exception("Missing prefab for component " + item.type);
if (AssetDatabase.LoadAssetAtPath<GameObject>(item.prefab) == null) throw new Exception("Missing component prefab " + item.prefab);
}
var groundPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(data.ground.gamePrefab);
if (groundPrefab == null) throw new Exception("Missing ground prefab " + data.ground.gamePrefab);
var groundMeshes = groundPrefab.GetComponentsInChildren<MeshFilter>(true);
if (groundMeshes.Length == 0 || groundMeshes.Any(m => m.sharedMesh == null)) throw new Exception("Ground prefab has no readable mesh " + data.ground.gamePrefab);
var groundColliders = groundPrefab.GetComponentsInChildren<MeshCollider>(true);
if (groundColliders.Length == 0 || groundColliders.Any(c => c.sharedMesh == null || c.convex)) throw new Exception("Ground prefab requires a non-convex MeshCollider " + data.ground.gamePrefab);
if (data.assembly == null || string.IsNullOrEmpty(data.assembly.outputPrefab)) throw new Exception("Missing assembly.outputPrefab");
}
static Transform Group(Transform parent, string name)
{
var go = new GameObject(name); go.layer = 30; go.transform.SetParent(parent, false); return go.transform;
}
static void SetLayer(GameObject go, int layer)
{
foreach (var transform in go.GetComponentsInChildren<Transform>(true)) transform.gameObject.layer = layer;
}
static GameObject AddPrefab(string assetPath, Transform parent, string name, BattlefieldVec3 position, BattlefieldVec3 rotation, BattlefieldVec3 scale)
{
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
instance.name = name; instance.transform.SetParent(parent, false);
SetLayer(instance, 30);
if (position != null) instance.transform.localPosition = position.Value;
if (rotation != null) instance.transform.localEulerAngles = rotation.Value;
if (scale != null) instance.transform.localScale = scale.Value;
return instance;
}
/// <summary>
/// The baked ground is a tall slab facing Z. Lay it on XZ, cover the three lanes, and sit the top surface on y=0.
/// </summary>
static void LayGroundFlat(GameObject groundMesh)
{
groundMesh.transform.localRotation = Quaternion.Euler(-90f, 0f, 0f);
groundMesh.transform.localScale = Vector3.one;
groundMesh.transform.localPosition = Vector3.zero;
var bounds = RendererBounds(groundMesh);
const float targetWidth = 22f;
const float targetDepth = 26f;
float scale = Mathf.Max(targetWidth / Mathf.Max(0.01f, bounds.size.x), targetDepth / Mathf.Max(0.01f, bounds.size.z));
groundMesh.transform.localScale = Vector3.one * scale;
bounds = RendererBounds(groundMesh);
var footprintCenter = new Vector3(0f, 0f, 3.5f);
groundMesh.transform.position += new Vector3(footprintCenter.x - bounds.center.x, -bounds.max.y, footprintCenter.z - bounds.center.z);
}
static Bounds RendererBounds(GameObject go)
{
var renderers = go.GetComponentsInChildren<Renderer>(true);
if (renderers.Length == 0) throw new Exception("Ground prefab has no renderer.");
var bounds = renderers[0].bounds;
for (int i = 1; i < renderers.Length; i++) bounds.Encapsulate(renderers[i].bounds);
return bounds;
}
static string SliceObject(string text, string key)
{
int i = text.IndexOf("\"" + key + "\"", StringComparison.Ordinal);
if (i < 0) return "";
int start = text.IndexOf('{', i);
int depth = 0;
for (int c = start; c < text.Length; c++)
{
if (text[c] == '{') depth++;
else if (text[c] == '}') { depth--; if (depth == 0) return text.Substring(start, c - start + 1); }
}
return "";
}
static string SliceArray(string text, string key)
{
int i = text.IndexOf("\"" + key + "\"", StringComparison.Ordinal);
int start = text.IndexOf('[', i);
int depth = 0;
for (int c = start; c < text.Length; c++)
{
if (text[c] == '[') depth++;
else if (text[c] == ']') { depth--; if (depth == 0) return text.Substring(start, c - start + 1); }
}
return "[]";
}
public static string ValidateOnly()
{
var data = ReadManifest(); Validate(data);
return "PASS battlefield manifest design=" + data.selectedDesign + " components=" + (data.components?.Length ?? 0);
}
public static string Build()
{
if (EditorApplication.isPlaying) throw new Exception("Build battlefield in Edit Mode.");
var data = ReadManifest(); Validate(data);
string output = data.assembly.outputPrefab;
string folder = Path.GetDirectoryName(output).Replace('\\', '/');
if (!AssetDatabase.IsValidFolder(folder)) throw new Exception("Create the authored battlefield folder first: " + folder);
var root = new GameObject(data.sceneId);
root.layer = 30;
try
{
var ground = Group(root.transform, "Ground");
LayGroundFlat(AddPrefab(data.ground.gamePrefab, ground, "GroundMesh", null, null, null));
var routes = Group(root.transform, "Routes");
var gameplay = Group(root.transform, "Gameplay"); var spawns = Group(gameplay, "SpawnPoints"); var goals = Group(gameplay, "GoalPoints");
for (int lane = 0; lane < 3; lane++) { var route = Group(routes, "Lane" + lane); route.localPosition = new Vector3(lane == 0 ? 0 : lane == 1 ? -3 : 3, 0, 0); var spawn = Group(spawns, "Lane" + lane); spawn.localPosition = route.localPosition + new Vector3(0, 0, 11); var goal = Group(goals, "Lane" + lane); goal.localPosition = route.localPosition + new Vector3(0, 0, -4); }
var decorations = Group(root.transform, "Decorations");
foreach (var item in data.components)
{
if (!item.placeInScene) continue;
if (item.placements != null && item.placements.Length > 0)
{
for (int i = 0; i < item.placements.Length; i++)
{
var place = item.placements[i];
if (place.position == null || place.rotation == null || place.scale == null)
throw new Exception("Component " + item.type + " placement " + i + " is missing a layout transform.");
AddPrefab(item.prefab, decorations, string.IsNullOrEmpty(place.name) ? item.type + i : place.name, place.position, place.rotation, place.scale);
}
}
else
{
if (item.position == null || item.rotation == null || item.scale == null)
throw new Exception("Component " + item.type + " is missing a layout transform.");
AddPrefab(item.prefab, decorations, item.type, item.position, item.rotation, item.scale);
}
}
var lighting = Group(root.transform, "Lighting");
var sunObject = new GameObject("Sun");
sunObject.transform.SetParent(lighting, false);
sunObject.transform.localRotation = Quaternion.Euler(48f, -30f, 0f);
var sun = sunObject.AddComponent<Light>();
sun.type = LightType.Directional;
sun.color = new Color(1f, 0.96f, 0.88f);
sun.intensity = 1.1f;
sun.shadows = LightShadows.Soft;
SetLayer(root, 30);
PrefabUtility.SaveAsPrefabAsset(root, output);
}
finally { UnityEngine.Object.DestroyImmediate(root); }
AssetDatabase.SaveAssets(); AssetDatabase.Refresh();
return "Built battlefield prefab " + output + " from design " + data.selectedDesign + " with " + data.components.Length + " component types.";
}
public static string VerifyBuilt()
{
const string output = "Assets/Bundles/Scenes/Prototype/Battlefield/Battlefield_Greenfield.prefab";
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(output);
if (prefab == null) throw new Exception("Missing " + output);
var scene = EditorSceneManager.NewPreviewScene();
try
{
var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab, scene);
string[] names = { "Ground/GroundMesh", "Routes/Lane0", "Routes/Lane1", "Routes/Lane2", "Gameplay/SpawnPoints/Lane0", "Gameplay/SpawnPoints/Lane1", "Gameplay/SpawnPoints/Lane2", "Gameplay/GoalPoints/Lane0", "Gameplay/GoalPoints/Lane1", "Gameplay/GoalPoints/Lane2", "Decorations/tree", "Decorations/rock", "Decorations/sign", "Decorations/shrub", "Lighting/Sun" };
foreach (var name in names)
if (instance.transform.Find(name) == null) throw new Exception("Missing node " + name);
var ground = instance.transform.Find("Ground/GroundMesh");
var collider = ground.GetComponentInChildren<MeshCollider>(true);
if (collider == null || collider.sharedMesh == null || collider.convex || !collider.sharedMesh.isReadable)
throw new Exception("Ground collider must be a readable non-convex MeshCollider.");
var groundBounds = RendererBounds(ground.gameObject);
if (groundBounds.max.y > 0.05f || groundBounds.max.y < -0.05f) throw new Exception("Ground top is not on y=0: " + groundBounds.max.y);
if (groundBounds.min.x > -3f || groundBounds.max.x < 3f || groundBounds.min.z > -4f || groundBounds.max.z < 11f)
throw new Exception("Ground footprint does not cover the three lanes: " + groundBounds);
var report = new StringBuilder();
var decorations = instance.transform.Find("Decorations");
foreach (Transform child in decorations)
if (child.name.IndexOf("bridge", StringComparison.OrdinalIgnoreCase) >= 0) throw new Exception("Bridge is still placed: " + child.name);
var badLayer = instance.GetComponentsInChildren<Renderer>(true).FirstOrDefault(r => r.gameObject.layer != 30);
if (badLayer != null) throw new Exception("Renderer not on battle layer 30: " + badLayer.name + " layer=" + badLayer.gameObject.layer);
foreach (var decoName in new[] { "tree", "rock", "sign", "shrub" })
{
var deco = instance.transform.Find("Decorations/" + decoName);
var renderer = deco.GetComponentInChildren<Renderer>(true);
if (renderer == null || renderer.sharedMaterial == null || renderer.sharedMaterial.shader == null)
throw new Exception(decoName + " has no material.");
if (renderer.sharedMaterial.shader.name != "Test/XPbr") throw new Exception(decoName + " shader is " + renderer.sharedMaterial.shader.name);
report.Append(decoName).Append('@').Append(deco.localPosition.ToString("F1")).Append(' ');
}
return "PASS ground=" + groundBounds.size + " topY=" + groundBounds.max.y.ToString("F3") + " " + report;
}
finally { EditorSceneManager.ClosePreviewScene(scene); }
}
public static string FramePrefabStage()
{
const string output = "Assets/Bundles/Scenes/Prototype/Battlefield/Battlefield_Greenfield.prefab";
var stage = UnityEditor.SceneManagement.PrefabStageUtility.OpenPrefab(output);
var sv = SceneView.lastActiveSceneView;
if (sv == null) throw new Exception("No Scene View is open.");
sv.orthographic = false;
sv.LookAt(new Vector3(0f, 0f, 3.5f), Quaternion.Euler(38f, -35f, 0f), 22f);
sv.Repaint();
return "Framed " + stage.assetPath;
}
public static string ClosePrefabStage()
{
StageUtility.GoToMainStage();
return "Closed prefab stage";
}
}