#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