#if UNITY_EDITOR
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;
using YooAsset.Editor;
///
/// 把 TownSquare 8 类分件做成 Prefab,拼成可加载场景,并登记 Art 收集。
/// 由 Unity Pipeline: unity command run_script --file AgentScripts/BuildTownSquare.cs --entry BuildTownSquare.Run
///
public static class BuildTownSquare
{
const string SceneRoot = "Assets/Bundles/Scenes/Town/TownSquare";
const string ScenePath = SceneRoot + "/TownSquare.unity";
const string ScenePrefabPath = SceneRoot + "/TownSquare.prefab";
const string XPbrShaderName = "Test/XPbr";
const string ArtPackage = "Art";
static readonly string[] Parts =
{
"ground", "shop", "tree", "lamp", "board", "fountain", "bench", "bush"
};
public static string Run()
{
AssetDatabase.Refresh();
Directory.CreateDirectory(SceneRoot.Replace("Assets/", Application.dataPath + "/../Assets/").Replace('/', Path.DirectorySeparatorChar));
var reports = new List();
foreach (var name in Parts)
{
reports.Add(EnsurePartPrefab(name));
}
reports.Add(AssembleScenePrefab());
reports.Add(EnsurePlayableScene());
reports.Add(EnsureArtCollector());
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
return string.Join(" | ", reports);
}
static string EnsurePartPrefab(string name)
{
var dir = $"{SceneRoot}/{name}";
var fbxPath = $"{dir}/{name}.fbx";
var matPath = $"{dir}/{name}.mat";
var basePath = $"{dir}/{name}_base.png";
var mixPath = $"{dir}/{name}_mix.png";
var prefabPath = $"{dir}/{name}.prefab";
if (!File.Exists(fbxPath))
return $"{name}: missing fbx";
ConfigureTextures(basePath, mixPath);
ConfigureFbx(fbxPath, File.Exists(matPath), name);
var xpbr = Shader.Find(XPbrShaderName);
var urpLit = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard");
var baseMap = AssetDatabase.LoadAssetAtPath(basePath);
var mixMap = AssetDatabase.LoadAssetAtPath(mixPath);
var useXpbr = xpbr != null && mixMap != null && baseMap != null;
var shader = useXpbr ? xpbr : urpLit;
var mat = AssetDatabase.LoadAssetAtPath(matPath);
if (mat == null)
{
mat = new Material(shader);
AssetDatabase.CreateAsset(mat, matPath);
}
else
{
mat.shader = shader;
}
if (useXpbr)
{
if (mat.HasProperty("_MainTex")) mat.SetTexture("_MainTex", baseMap);
if (mat.HasProperty("_MixTex")) mat.SetTexture("_MixTex", mixMap);
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", Color.white);
}
else if (baseMap != null && mat.HasProperty("_BaseMap"))
{
mat.SetTexture("_BaseMap", baseMap);
}
EditorUtility.SetDirty(mat);
var modelRoot = AssetDatabase.LoadAssetAtPath(fbxPath);
if (modelRoot == null)
return $"{name}: fbx not loaded";
var instance = (GameObject)PrefabUtility.InstantiatePrefab(modelRoot);
instance.name = name;
foreach (var renderer in instance.GetComponentsInChildren())
renderer.sharedMaterial = mat;
UprightAndSit(instance, name);
if (name == "ground")
EnsureGroundCollider(instance);
PrefabUtility.SaveAsPrefabAsset(instance, prefabPath);
Object.DestroyImmediate(instance);
return name == "ground" ? $"{name}.prefab+MeshCollider" : $"{name}.prefab";
}
static void ConfigureTextures(string basePath, string mixPath)
{
ConfigureOneTexture(basePath, srgb: true, alpha: true);
ConfigureOneTexture(mixPath, srgb: false, alpha: false);
}
static void ConfigureOneTexture(string path, bool srgb, bool alpha)
{
if (!File.Exists(path)) return;
var importer = AssetImporter.GetAtPath(path) as TextureImporter;
if (importer == null) return;
importer.textureType = TextureImporterType.Default;
importer.sRGBTexture = srgb;
importer.mipmapEnabled = true;
importer.alphaIsTransparency = alpha;
importer.SaveAndReimport();
}
static void ConfigureFbx(string fbxPath, bool hasMat, string name)
{
var model = AssetImporter.GetAtPath(fbxPath) as ModelImporter;
if (model == null) return;
model.globalScale = 1f;
model.useFileScale = false;
model.bakeAxisConversion = true;
model.materialImportMode = hasMat
? ModelImporterMaterialImportMode.ImportViaMaterialDescription
: ModelImporterMaterialImportMode.None;
model.meshCompression = ModelImporterMeshCompression.Medium;
// MeshCollider 烹饪需要可读网格;其它分件保持不可读。
model.isReadable = name == "ground";
model.addCollider = false;
model.SaveAndReimport();
}
static void EnsureGroundCollider(GameObject instance)
{
var added = 0;
foreach (var filter in instance.GetComponentsInChildren())
{
if (filter.sharedMesh == null) continue;
var col = filter.GetComponent();
if (col == null)
col = filter.gameObject.AddComponent();
col.sharedMesh = filter.sharedMesh;
col.convex = false;
added++;
}
if (added == 0)
throw new System.InvalidOperationException("ground has no mesh for MeshCollider");
}
static void UprightAndSit(GameObject instance, string name)
{
instance.transform.localPosition = Vector3.zero;
instance.transform.localScale = Vector3.one;
instance.transform.localRotation = Quaternion.identity;
var bounds = MeshBounds(instance);
var target = TargetSize(name, bounds);
var current = CurrentSize(name, bounds);
if (target > 0.01f && current > 0.01f)
instance.transform.localScale = Vector3.one * (target / current);
// 游戏 FBX 原点已在底面中点,缩放后仍贴地;position 保持 0。
}
static float TargetSize(string name, Bounds bounds)
{
switch (name)
{
case "ground": return 18f;
case "shop": return 5.5f;
case "tree": return 5.5f;
case "lamp": return 3.6f;
case "board": return 2.4f;
case "fountain": return 3.2f;
case "bench": return 1.8f;
case "bush": return 0.9f;
default: return 1f;
}
}
static float CurrentSize(string name, Bounds bounds)
{
if (name == "ground" || name == "bench")
return Mathf.Max(bounds.size.x, bounds.size.z);
return bounds.size.y;
}
static Bounds MeshBounds(GameObject root)
{
Bounds bounds = default;
var has = false;
foreach (var r in root.GetComponentsInChildren())
{
if (!has)
{
bounds = r.bounds;
has = true;
}
else bounds.Encapsulate(r.bounds);
}
return has ? bounds : new Bounds(root.transform.position, Vector3.one * 0.01f);
}
static string AssembleScenePrefab()
{
var root = new GameObject("TownSquare");
var ground = Place(root.transform, "ground", Vector3.zero, 0f);
Physics.SyncTransforms();
// 总览 L 形;游戏网格正面是局部 -Z,用 YawFrontToward 对准广场中心。
// 水平位置按总览,Y 取地表 MeshCollider 碰撞高度。
var sits = new List();
void Sit(string part, Vector3 xz, float yaw, string instanceName = null)
{
var y = SampleGroundY(ground, xz.x, xz.z);
sits.Add($"{instanceName ?? part}={y:F3}");
Place(root.transform, part, new Vector3(xz.x, y, xz.z), yaw, instanceName);
}
var shopLeft = new Vector3(-6.3f, 0f, 3.4f);
var shopBack = new Vector3(0.6f, 0f, 6.7f);
Sit("shop", shopLeft, YawFrontToward(shopLeft, Vector3.zero), "shop_left");
Sit("shop", shopBack, YawFrontToward(shopBack, Vector3.zero), "shop_back");
Sit("tree", new Vector3(-8.0f, 0f, 5.2f), 15f, "tree_left");
Sit("tree", new Vector3(7.5f, 0f, 6.2f), -15f, "tree_right");
var boardPos = new Vector3(5.0f, 0f, 6.5f);
Sit("board", boardPos, YawFrontToward(boardPos, Vector3.zero));
Sit("fountain", new Vector3(6.1f, 0f, 0.6f), 0f);
Sit("lamp", new Vector3(-6.4f, 0f, -2.8f), 0f, "lamp_front_left");
Sit("lamp", new Vector3(2.7f, 0f, 6.6f), 10f, "lamp_back");
Sit("lamp", new Vector3(8.1f, 0f, 1.3f), -15f, "lamp_right");
var benchLeft = new Vector3(-5.2f, 0f, -5.2f);
var benchRight = new Vector3(5.6f, 0f, -4.2f);
Sit("bench", benchLeft, YawFrontToward(benchLeft, Vector3.zero), "bench_left");
Sit("bench", benchRight, YawFrontToward(benchRight, Vector3.zero), "bench_right");
Sit("bush", new Vector3(-6.2f, 0f, -5.8f), 0f, "bush_bench_l1");
Sit("bush", new Vector3(-4.2f, 0f, -6.0f), 40f, "bush_bench_l2");
Sit("bush", new Vector3(6.6f, 0f, -4.8f), 10f, "bush_bench_r");
Sit("bush", new Vector3(7.4f, 0f, 2.0f), 25f, "bush_fountain_1");
Sit("bush", new Vector3(4.8f, 0f, 1.4f), -10f, "bush_fountain_2");
Sit("bush", new Vector3(7.0f, 0f, 0.2f), 20f, "bush_fountain_3");
Sit("bush", new Vector3(6.6f, 0f, 6.8f), 0f, "bush_board");
PrefabUtility.SaveAsPrefabAsset(root, ScenePrefabPath);
Object.DestroyImmediate(root);
return "TownSquare.prefab sit=[" + string.Join(",", sits) + "]";
}
static GameObject Place(Transform parent, string part, Vector3 pos, float yaw, string instanceName = null)
{
var prefab = AssetDatabase.LoadAssetAtPath($"{SceneRoot}/{part}/{part}.prefab");
if (prefab == null)
throw new System.InvalidOperationException($"missing part prefab {part}");
var go = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
go.name = instanceName ?? part;
go.transform.position = pos;
go.transform.rotation = Quaternion.Euler(0f, yaw, 0f) * go.transform.rotation;
return go;
}
static float SampleGroundY(GameObject ground, float x, float z)
{
if (ground == null)
return 0f;
Physics.SyncTransforms();
var ray = new Ray(new Vector3(x, 80f, z), Vector3.down);
var bestDist = float.MaxValue;
var y = 0f;
var hitAny = false;
foreach (var col in ground.GetComponentsInChildren())
{
if (col == null || !col.enabled) continue;
if (!col.Raycast(ray, out var hit, 160f)) continue;
if (hit.distance >= bestDist) continue;
bestDist = hit.distance;
y = hit.point.y;
hitAny = true;
}
if (!hitAny)
Debug.LogWarning($"[BuildTownSquare] ground miss at ({x:F2}, {z:F2}), y=0");
return y;
}
static float YawFrontToward(Vector3 from, Vector3 to)
{
// 导出网格正面是局部 -Z(不是 Unity 默认 +Z)。让 +Z 背对目标,则 -Z 朝向目标。
var away = from - to;
away.y = 0f;
if (away.sqrMagnitude < 0.0001f) return 0f;
return Quaternion.LookRotation(away).eulerAngles.y;
}
static string EnsurePlayableScene()
{
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
var lightGo = new GameObject("Directional Light");
var light = lightGo.AddComponent();
light.type = LightType.Directional;
light.color = new Color(1f, 0.96f, 0.88f);
light.intensity = 1.15f;
light.shadows = LightShadows.Soft;
light.shadowStrength = 1f;
lightGo.transform.rotation = Quaternion.Euler(50f, -30f, 0f);
var camGo = new GameObject("Main Camera");
camGo.tag = "MainCamera";
var cam = camGo.AddComponent();
cam.clearFlags = CameraClearFlags.SolidColor;
cam.backgroundColor = new Color(0.78f, 0.88f, 0.94f);
cam.fieldOfView = 40f;
cam.nearClipPlane = 0.1f;
cam.farClipPlane = 80f;
camGo.AddComponent();
camGo.transform.position = new Vector3(0f, 12f, -22f);
camGo.transform.rotation = Quaternion.Euler(28f, 0f, 0f);
var plaza = AssetDatabase.LoadAssetAtPath(ScenePrefabPath);
PrefabUtility.InstantiatePrefab(plaza);
RenderSettings.ambientMode = AmbientMode.Trilight;
RenderSettings.ambientSkyColor = new Color(0.32f, 0.40f, 0.50f);
RenderSettings.ambientEquatorColor = new Color(0.24f, 0.26f, 0.24f);
RenderSettings.ambientGroundColor = new Color(0.16f, 0.15f, 0.13f);
EditorSceneManager.SaveScene(scene, ScenePath);
return ScenePath;
}
static string EnsureArtCollector()
{
var setting = BundleCollectorSettingData.Setting;
setting.ShowPackageView = true;
var package = setting.Packages.Find(p => p.PackageName == ArtPackage);
if (package == null)
package = BundleCollectorSettingData.CreatePackage(ArtPackage);
package.PackageDesc = "角色/场景美术";
package.EnableAddressable = true;
package.SupportExtensionless = true;
package.AutoCollectShaders = true;
package.IgnoreRuleName = nameof(NormalIgnoreRule);
BundleCollectorSettingData.ModifyPackage(package);
var group = package.Groups.Find(g => g.GroupName == "Scenes");
if (group == null)
group = BundleCollectorSettingData.CreateGroup(package, "Scenes");
group.ActiveRuleName = nameof(EnableGroup);
group.AssetTags = "art";
BundleCollectorSettingData.ModifyGroup(package, group);
const string collectPath = "Assets/Bundles/Scenes";
var collector = group.Collectors.Find(c => c.CollectPath == collectPath);
if (collector == null)
{
collector = new BundleCollector
{
CollectPath = collectPath,
CollectorGUID = AssetDatabase.AssetPathToGUID(collectPath),
CollectorType = ECollectorType.MainAssetCollector,
AddressRuleName = nameof(AddressByFileName),
PackRuleName = nameof(PackDirectory),
FilterRuleName = nameof(CollectAll),
AssetTags = "art",
};
BundleCollectorSettingData.CreateCollector(group, collector);
}
else
{
collector.CollectorGUID = AssetDatabase.AssetPathToGUID(collectPath);
collector.AddressRuleName = nameof(AddressByFileName);
collector.PackRuleName = nameof(PackDirectory);
collector.FilterRuleName = nameof(CollectAll);
BundleCollectorSettingData.ModifyCollector(group, collector);
}
package.CheckConfigError();
BundleCollectorSettingData.SaveFile();
return $"art collect={collectPath}";
}
}
#endif