using System.Collections.Generic; using System.Reflection; using BaseGames.Camera; using BaseGames.Combat; using BaseGames.Dialogue; using BaseGames.Enemies; using BaseGames.Player; using BaseGames.Player.States; using BaseGames.World; using PathBerserker2d; using Unity.Cinemachine; using UnityEditor; using UnityEngine; using UnityEngine.Tilemaps; namespace BaseGames.Editor { /// /// 场景对象快速放置工具。 /// 在当前活动场景中生成常用游戏对象(玩家、敌人、机关、存档点、相机等), /// 并自动挂载基础组件、设置正确的物理层、绑定已有的事件频道资产。 /// /// 菜单:BaseGames → Scene → Place → … /// /// 所有操作支持 Undo(Ctrl+Z)。生成后选中对象便于立即调整位置。 /// public static class SceneObjectPlacerTool { // ══ 菜单入口 ══════════════════════════════════════════════════════════ [MenuItem("BaseGames/Scene/Place/Player", priority = 100)] public static void PlacePlayer() { var report = new List(); GameObject go = new GameObject("Player"); Undo.RegisterCreatedObjectUndo(go, "Place Player"); go.transform.position = GetDropPosition(); go.tag = "Player"; SetLayer(go, "Player", report); Rigidbody2D rb = GetOrAddComponent(go); rb.bodyType = RigidbodyType2D.Dynamic; rb.gravityScale = 2f; rb.constraints = RigidbodyConstraints2D.FreezeRotation; rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous; GetOrAddComponent(go); GetOrAddComponent(go); SetupSpriteRenderer(go); PlayerStats playerStats = GetOrAddComponent(go); PlayerMovement playerMovement = GetOrAddComponent(go); PlayerController playerController = GetOrAddComponent(go); PlayerCombat playerCombat = GetOrAddComponent(go); // Ground check pivot Transform groundCheckGo = GetOrCreateChild(go.transform, "GroundCheck"); groundCheckGo.localPosition = new Vector3(0f, -0.75f, 0f); AssignReference(playerMovement, "_groundCheck", groundCheckGo, report); AssignLayerMask(playerMovement, "_groundLayer", "Ground", report); // Weapon socket (WeaponManager instantiates weapons here at runtime) GetOrCreateChild(go.transform, "WeaponSocket"); // Camera follow target — CinemachineCamera.Follow 使用此子节点而非 Player 根节点 GetOrCreateChild(go.transform, "CameraFollowTarget"); // HurtBox child Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox"); SetLayer(hurtBoxT.gameObject, "PlayerHurtBox", report); CapsuleCollider2D hurtCollider = GetOrAddComponent(hurtBoxT.gameObject); hurtCollider.isTrigger = true; HurtBox hurtBox = GetOrAddComponent(hurtBoxT.gameObject); // Assign controller references AssignReference(playerController, "_stats", playerStats, report); AssignReference(playerController, "_hurtBox", hurtBox, report); AssignReference(playerController, "_movement", playerMovement, report); AssignReference(playerController, "_combat", playerCombat, report); // Event channels (all optional — will be skipped silently if assets missing) AssignAsset(playerStats, "_onHPChanged", report, false, "EVT_HPChanged"); AssignAsset(playerStats, "_onMaxHPChanged", report, false, "EVT_MaxHPChanged"); AssignAsset(playerStats, "_onSoulPowerChanged", report, false, "EVT_SoulPowerChanged"); AssignAsset(playerStats, "_onSpiritPowerChanged", report, false, "EVT_SpiritPowerChanged"); AssignAsset(playerStats, "_onSpringChargesChanged", report, false, "EVT_SpringChargesChanged"); AssignAsset(playerStats, "_onLingZhuChanged", report, false, "EVT_LingZhuChanged"); AssignAsset(playerStats, "_onAbilityUnlocked", report, false, "EVT_AbilityUnlocked"); AssignAsset(playerStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged"); AssignAsset(playerController, "_onPlayerDied", report, false, "EVT_PlayerDied"); AssignAsset(playerController, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned"); AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt"); AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed"); // Config ScriptableObjects (optional — link manually after placing) Object statsConfig = FindFirstAsset("PLY_PlayerStats", "PlayerStats"); Object movConfig = FindFirstAsset("PLY_PlayerMovementConfig", "PlayerMovementConfig"); if (movConfig != null) AssignReference(playerController, "_movementConfig", movConfig, report); if (statsConfig != null) AssignReference(playerStats, "_config", statsConfig, report); if (movConfig != null) AssignReference(playerMovement, "_config", movConfig, report); report.Add("PlayerMovement._config、PlayerController._animConfig、_inputReader 等需后续手动绑定。"); Selection.activeGameObject = go; MarkDirtyAndLog("Player", go, report); } [MenuItem("BaseGames/Scene/Place/Player Spawn Point", priority = 105)] public static void PlacePlayerSpawnPoint() { var report = new List(); GameObject go = new GameObject("SpawnPoint"); Undo.RegisterCreatedObjectUndo(go, "Place Player Spawn Point"); go.transform.position = GetDropPosition(); PlayerSpawnPoint spawnPoint = GetOrAddComponent(go); AssignString(spawnPoint, "_transitionId", "default", report); AssignInt(spawnPoint, "_facingDirection", 1); report.Add("修改 _transitionId,使其与对应 RoomTransition._targetTransitionId 匹配。"); report.Add("+1 = 朝右出生,-1 = 朝左出生(_facingDirection)。"); Selection.activeGameObject = go; MarkDirtyAndLog("Player Spawn Point", go, report); } [MenuItem("BaseGames/Scene/Place/Enemy (Basic)", priority = 110)] public static void PlaceEnemy() { var report = new List(); GameObject go = new GameObject("BasicEnemy"); Undo.RegisterCreatedObjectUndo(go, "Place Enemy"); go.transform.position = GetDropPosition(); SetLayer(go, "Enemy", report); Rigidbody2D rb = GetOrAddComponent(go); rb.bodyType = RigidbodyType2D.Dynamic; rb.gravityScale = 2f; rb.constraints = RigidbodyConstraints2D.FreezeRotation; GetOrAddComponent(go); GetOrAddComponent(go); SetupSpriteRenderer(go); EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); // HurtBox child Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox"); SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report); CapsuleCollider2D hurtCollider = GetOrAddComponent(hurtBoxT.gameObject); hurtCollider.isTrigger = true; HurtBox hurtBox = GetOrAddComponent(hurtBoxT.gameObject); // Contact-damage HitBox child Transform hitBodyT = GetOrCreateChild(go.transform, "HitBox_Body"); SetLayer(hitBodyT.gameObject, "EnemyHitBox", report); CircleCollider2D hitCollider = GetOrAddComponent(hitBodyT.gameObject); hitCollider.isTrigger = true; hitCollider.radius = 0.55f; HitBox hitBox = GetOrAddComponent(hitBodyT.gameObject); GetOrAddComponent(hitBodyT.gameObject); // References AssignReference(enemyBase, "_stats", enemyStats, report); // DamageSourceSO for body contact (optional — create manually if missing) Object dmgSrc = FindFirstAsset("DS_EnemyBody", "DS_TestEnemyBody"); if (dmgSrc != null) AssignReference(hitBox, "_defaultSource", dmgSrc, report); else report.Add("未找到 DamageSourceSO (DS_EnemyBody),HitBox_Body._defaultSource 未绑定。请创建后手动指定。"); // Event channels AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied"); AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned"); AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged"); AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt"); AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed"); // EnemyStatsSO (optional) Object enemyStatsSO = FindFirstAsset("BasicEnemyStats", "EnemyStatsSO"); if (enemyStatsSO != null) AssignReference(enemyBase, "_statsSO", enemyStatsSO, report); else report.Add("未找到 EnemyStatsSO,EnemyBase._statsSO 未绑定。请在 Data/Enemies/ 创建后手动指定。"); report.Add("行为树、导航参数(NavAgent)、动画片段需后续手工挂载。"); Selection.activeGameObject = go; MarkDirtyAndLog("Enemy (Basic)", go, report); } [MenuItem("BaseGames/Scene/Place/Boss Enemy", priority = 115)] public static void PlaceBossEnemy() { var report = new List(); GameObject go = new GameObject("BossEnemy"); Undo.RegisterCreatedObjectUndo(go, "Place Boss Enemy"); go.transform.position = GetDropPosition(); SetLayer(go, "Enemy", report); Rigidbody2D rb = GetOrAddComponent(go); rb.bodyType = RigidbodyType2D.Dynamic; rb.gravityScale = 2f; rb.constraints = RigidbodyConstraints2D.FreezeRotation; rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous; rb.interpolation = RigidbodyInterpolation2D.Interpolate; GetOrAddComponent(go); GetOrAddComponent(go); SetupSpriteRenderer(go); BossBase bossBase = GetOrAddComponent(go); EnemyStats bossStats = GetOrAddComponent(go); // HurtBox child Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox"); SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report); CapsuleCollider2D hurtCollider = GetOrAddComponent(hurtBoxT.gameObject); hurtCollider.isTrigger = true; hurtCollider.size = new Vector2(1.5f, 2.5f); HurtBox hurtBox = GetOrAddComponent(hurtBoxT.gameObject); // Contact-damage HitBox child Transform hitBodyT = GetOrCreateChild(go.transform, "HitBox_Body"); SetLayer(hitBodyT.gameObject, "EnemyHitBox", report); CircleCollider2D hitCollider = GetOrAddComponent(hitBodyT.gameObject); hitCollider.isTrigger = true; hitCollider.radius = 0.9f; HitBox hitBox = GetOrAddComponent(hitBodyT.gameObject); GetOrAddComponent(hitBodyT.gameObject); // References AssignReference(bossBase, "_stats", bossStats, report); // DamageSourceSO Object dmgSrc = FindFirstAsset("DS_BossBody", "DS_EnemyBody"); if (dmgSrc != null) AssignReference(hitBox, "_defaultSource", dmgSrc, report); else report.Add("未找到 DamageSourceSO,HitBox_Body._defaultSource 未绑定。"); // Event channels AssignAsset(bossBase, "_onEnemyDied", report, false, "EVT_EnemyDied"); AssignAsset(bossBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned"); AssignAsset(bossBase, "_onBossFightEnded", report, false, "EVT_BossFightEnded"); AssignAsset(bossBase, "_onBossPhaseChanged", report, false, "EVT_BossPhaseChanged"); AssignAsset(bossStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged"); AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt"); AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed"); report.Add("填写 _bossId。"); report.Add("挂载 BossSkillSequencer 组件并指定技能序列 SO;行为树、NavAgent 需手工添加。"); report.Add("多阶段 Boss 可在此 GameObject 上继续 AddComponent 阶段切换控制器。"); Selection.activeGameObject = go; MarkDirtyAndLog("Boss Enemy", go, report); } [MenuItem("BaseGames/Scene/Place/Hazard (LethalTrap)", priority = 120)] public static void PlaceLethalTrap() { var report = new List(); GameObject go = new GameObject("LethalTrap"); Undo.RegisterCreatedObjectUndo(go, "Place LethalTrap"); go.transform.position = GetDropPosition(); SetLayer(go, "EnemyHitBox", report); BoxCollider2D col = GetOrAddComponent(go); col.isTrigger = true; col.size = new Vector2(2f, 0.5f); SetupSpriteRenderer(go); LethalTrap trap = GetOrAddComponent(go); AssignLayerMask(trap, "_playerLayers", "PlayerHurtBox", report); AssignInt(trap, "_damage", 1); AssignBool(trap, "_canPogo", true); // Child HurtBox (EnemyHurtBox layer) to allow pogo when _canPogo = true Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox"); SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report); BoxCollider2D hurtCol = GetOrAddComponent(hurtBoxT.gameObject); hurtCol.isTrigger = true; hurtCol.size = new Vector2(2f, 0.3f); GetOrAddComponent(hurtBoxT.gameObject); AssignAsset(trap, "_onPlayerDied", report, false, "EVT_PlayerDied"); report.Add("_canPogo=true:子 HurtBox 供玩家下劈弹起;设为 false 可改为纯死亡区(无需子 HurtBox)。"); Selection.activeGameObject = go; MarkDirtyAndLog("Hazard (LethalTrap)", go, report); } [MenuItem("BaseGames/Scene/Place/Collectible (LingZhu)", priority = 125)] public static void PlaceCollectible() { var report = new List(); GameObject go = new GameObject("Collectible_LingZhu"); Undo.RegisterCreatedObjectUndo(go, "Place Collectible"); go.transform.position = GetDropPosition(); Rigidbody2D rb = GetOrAddComponent(go); rb.gravityScale = 1f; rb.freezeRotation = true; rb.interpolation = RigidbodyInterpolation2D.Interpolate; CircleCollider2D col = GetOrAddComponent(go); col.isTrigger = true; col.radius = 0.3f; SetupSpriteRenderer(go); Collectible collectible = GetOrAddComponent(go); // CollectibleType.LingZhu = 0 AssignInt(collectible, "_type", 0); AssignInt(collectible, "_lingZhuAmount", 1); AssignBool(collectible, "_isPersistent", false); AssignAsset(collectible, "_onCollectiblePickup", report, false, "EVT_ItemPickup", "EVT_CollectiblePickup"); AssignAsset(collectible, "_onCollectibleSaved", report, false, "EVT_CollectibleSaved"); report.Add("若为场景固定摆放道具,设 _isPersistent = true 并填写唯一 _collectibleId。"); Selection.activeGameObject = go; MarkDirtyAndLog("Collectible (LingZhu)", go, report); } [MenuItem("BaseGames/Scene/Place/Save Point", priority = 130)] public static void PlaceSavePoint() { var report = new List(); GameObject go = new GameObject("SavePoint"); Undo.RegisterCreatedObjectUndo(go, "Place Save Point"); go.transform.position = GetDropPosition(); SetLayer(go, "TriggerZone", report); BoxCollider2D col = GetOrAddComponent(go); col.isTrigger = true; col.size = new Vector2(1f, 1.5f); SetupSpriteRenderer(go); SavePoint savePoint = GetOrAddComponent(go); AssignAsset(savePoint, "_onSavePointActivated", report, false, "EVT_SavePointActivated"); AssignAsset(savePoint, "_onFastTravelOpen", report, false, "EVT_FastTravelOpen"); Selection.activeGameObject = go; MarkDirtyAndLog("Save Point", go, report); } [MenuItem("BaseGames/Scene/Place/Room Transition", priority = 135)] public static void PlaceRoomTransition() { var report = new List(); GameObject go = new GameObject("RoomTransition"); Undo.RegisterCreatedObjectUndo(go, "Place Room Transition"); go.transform.position = GetDropPosition(); SetLayer(go, "TriggerZone", report); BoxCollider2D col = GetOrAddComponent(go); col.isTrigger = true; col.size = new Vector2(1f, 2.5f); RoomTransition transition = GetOrAddComponent(go); AssignString(transition, "_transitionId", "exit_default", report); AssignBool(transition, "_autoTrigger", true); AssignBool(transition, "_requiresKeyItem", false); AssignAsset(transition, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest"); report.Add("填写 _transitionId(本出口唯一 ID)、_targetSceneAddress(目标场景 Addressable Key)、_targetTransitionId(目标出生点 ID)。"); report.Add("若需锁门,设 _requiresKeyItem = true 并填写 _requiredItemId。"); report.Add("_worldState 字段需拖入 WorldStateRegistry SO(可选)。"); Selection.activeGameObject = go; MarkDirtyAndLog("Room Transition", go, report); } [MenuItem("BaseGames/Scene/Place/Room Camera", priority = 140)] public static void PlaceRoomCamera() { var report = new List(); GameObject go = new GameObject("RoomCamera"); Undo.RegisterCreatedObjectUndo(go, "Place Room Camera"); go.transform.position = GetDropPosition(); CinemachineCamera cinemachine = GetOrAddComponent(go); RoomCamera roomCamera = GetOrAddComponent(go); CinemachineConfiner2D confiner = GetOrAddComponent(go); // RoomBoundary child — defines the camera confinement area Transform boundaryT = GetOrCreateChild(go.transform, "RoomBoundary"); PolygonCollider2D boundaryCollider = GetOrAddComponent(boundaryT.gameObject); boundaryCollider.pathCount = 1; boundaryCollider.SetPath(0, new Vector2[] { new Vector2(-12f, -6f), new Vector2(-12f, 6f), new Vector2( 12f, 6f), new Vector2( 12f, -6f), }); RoomVisibleArea visibleArea = GetOrAddComponent(boundaryT.gameObject); AssignReference(roomCamera, "_visibleArea", visibleArea, report); AssignReference(confiner, "m_BoundingShape2D", boundaryCollider, report); // Disable any Camera and AudioListener added by Cinemachine UnityEngine.Camera cam = go.GetComponent(); if (cam != null) cam.enabled = false; AudioListener al = go.GetComponent(); if (al != null) { Undo.DestroyObjectImmediate(al); } report.Add("将 Player/CameraFollowTarget Transform 拖入 CinemachineCamera.Follow 字段以跟随玩家(或使用 Room Camera Setup 工具批量赋值)。"); report.Add("调整 RoomBoundary PolygonCollider2D 顶点以匹配房间边界。"); Selection.activeGameObject = go; MarkDirtyAndLog("Room Camera", go, report); } [MenuItem("BaseGames/Scene/Place/Ground Platform", priority = 150)] public static void PlaceGroundPlatform() { var report = new List(); GameObject go = new GameObject("GroundPlatform"); Undo.RegisterCreatedObjectUndo(go, "Place Ground Platform"); go.transform.position = GetDropPosition(); SetLayer(go, "Ground", report); // 2D Sprite:用 localScale 设定尺寸,让 SpriteRenderer 和 BoxCollider2D 同步缩放 go.transform.localScale = new Vector3(8f, 0.5f, 1f); GetOrAddComponent(go); SetupSpriteRenderer(go); Rigidbody2D rb = GetOrAddComponent(go); rb.bodyType = RigidbodyType2D.Static; Selection.activeGameObject = go; MarkDirtyAndLog("Ground Platform", go, report); } [MenuItem("BaseGames/Scene/Place/Moving Platform", priority = 155)] public static void PlaceMovingPlatform() { var report = new List(); GameObject go = new GameObject("MovingPlatform"); Undo.RegisterCreatedObjectUndo(go, "Place Moving Platform"); go.transform.position = GetDropPosition(); SetLayer(go, "Ground", report); Rigidbody2D rb = GetOrAddComponent(go); rb.bodyType = RigidbodyType2D.Kinematic; rb.interpolation = RigidbodyInterpolation2D.Interpolate; rb.freezeRotation = true; BoxCollider2D col = GetOrAddComponent(go); col.size = new Vector2(4f, 0.4f); SetupSpriteRenderer(go); // Passenger sensor — trigger collider just above the platform surface Transform sensorT = GetOrCreateChild(go.transform, "PassengerSensor"); BoxCollider2D sensorCol = GetOrAddComponent(sensorT.gameObject); sensorCol.isTrigger = true; sensorCol.size = new Vector2(3.8f, 0.25f); sensorCol.offset = new Vector2(0f, 0.33f); // Waypoint markers (LinearAB mode end points) Transform wpA = GetOrCreateChild(go.transform, "WaypointA"); Transform wpB = GetOrCreateChild(go.transform, "WaypointB"); wpA.localPosition = new Vector3(-3f, 0f, 0f); wpB.localPosition = new Vector3(3f, 0f, 0f); MovingPlatform platform = GetOrAddComponent(go); AssignReference(platform, "_passengerSensor", sensorCol, report); AssignObjectArray(platform, "_wayPoints", new Object[] { wpA, wpB }, report); report.Add("WaypointA / WaypointB 为移动端点,可将其拖出平台并在场景中调整位置。"); report.Add("如需触发激活,改 _moveType = TriggeredLinear 并将 VoidEventChannelSO 拖入 _activationChannel。"); Selection.activeGameObject = go; MarkDirtyAndLog("Moving Platform", go, report); } [MenuItem("BaseGames/Scene/Place/Tilemap Ground", priority = 160)] public static void PlaceTilemapGround() { var report = new List(); GameObject gridGo = new GameObject("GroundGrid"); Undo.RegisterCreatedObjectUndo(gridGo, "Place Tilemap Ground"); gridGo.transform.position = GetDropPosition(); GetOrAddComponent(gridGo); GameObject groundGo = GetOrCreateChild(gridGo.transform, "Ground").gameObject; SetLayer(groundGo, "Ground", report); GetOrAddComponent(groundGo); GetOrAddComponent(groundGo); TilemapCollider2D tilemapCollider = GetOrAddComponent(groundGo); tilemapCollider.usedByComposite = true; Rigidbody2D rb = GetOrAddComponent(groundGo); rb.bodyType = RigidbodyType2D.Static; GetOrAddComponent(groundGo); report.Add("在 Tilemap 组件中使用 Tile Palette 绘制地形。"); Selection.activeGameObject = gridGo; MarkDirtyAndLog("Tilemap Ground", gridGo, report); } [MenuItem("BaseGames/Scene/Place/Nav Surface", priority = 170)] public static void PlaceNavSurface() { var report = new List(); GameObject go = new GameObject("NavSurface"); Undo.RegisterCreatedObjectUndo(go, "Place Nav Surface"); go.transform.position = GetDropPosition(); GetOrAddComponent(go); report.Add("NavSurface 已添加。在 Inspector 中点击 Bake 生成导航网格。"); Selection.activeGameObject = go; MarkDirtyAndLog("Nav Surface", go, report); } [MenuItem("BaseGames/Scene/Place/Camera Trigger Zone", priority = 180)] public static void PlaceCameraTriggerZone() { var report = new List(); GameObject go = new GameObject("CameraTriggerZone"); Undo.RegisterCreatedObjectUndo(go, "Place Camera Trigger Zone"); go.transform.position = GetDropPosition(); SetLayer(go, "TriggerZone", report); BoxCollider2D col = GetOrAddComponent(go); col.isTrigger = true; col.size = new Vector2(2f, 2f); GetOrAddComponent(go); report.Add("将目标 RoomCamera 拖入 CameraTriggerZone._targetCamera 字段。"); Selection.activeGameObject = go; MarkDirtyAndLog("Camera Trigger Zone", go, report); } [MenuItem("BaseGames/Scene/Place/Obstacle (Static)", priority = 190)] public static void PlaceObstacle() { var report = new List(); GameObject go = new GameObject("Obstacle"); Undo.RegisterCreatedObjectUndo(go, "Place Obstacle"); go.transform.position = GetDropPosition(); SetLayer(go, "Ground", report); // 2D Sprite:用 localScale 设定尺寸,让 SpriteRenderer 和 BoxCollider2D 同步缩放 go.transform.localScale = new Vector3(1f, 1f, 1f); GetOrAddComponent(go); SetupSpriteRenderer(go); Rigidbody2D rb = GetOrAddComponent(go); rb.bodyType = RigidbodyType2D.Static; Selection.activeGameObject = go; MarkDirtyAndLog("Obstacle (Static)", go, report); } [MenuItem("BaseGames/Scene/Place/Interactable NPC", priority = 195)] public static void PlaceInteractableNPC() { var report = new List(); GameObject go = new GameObject("NPC"); Undo.RegisterCreatedObjectUndo(go, "Place Interactable NPC"); go.transform.position = GetDropPosition(); // Interaction range trigger (matches InteractableNPC._interactRadius default) CircleCollider2D rangeTrigger = GetOrAddComponent(go); rangeTrigger.isTrigger = true; rangeTrigger.radius = 1.5f; GetOrAddComponent(go); GetOrAddComponent(go); SetupSpriteRenderer(go); report.Add("填写 _npcId(全局唯一)。"); report.Add("将 DialogueSequenceSO 拖入 _defaultDialogue 字段。"); report.Add("若为任务 NPC,将 InteractableNPC 替换为 QuestGiver 组件。"); report.Add("NPC 动画控制器需手工指定。"); Selection.activeGameObject = go; MarkDirtyAndLog("Interactable NPC", go, report); } // ══ 私有辅助方法 ══════════════════════════════════════════════════════ /// /// 返回用于放置新对象的世界坐标:优先使用 SceneView 视口中心,否则原点。 /// private static Vector3 GetDropPosition() { SceneView sv = SceneView.lastActiveSceneView; if (sv != null) { Vector3 pos = sv.pivot; pos.z = 0f; // 2D 游戏固定 z=0 return pos; } return Vector3.zero; } private static T GetOrAddComponent(GameObject go) where T : Component { T comp = go.GetComponent(); return comp != null ? comp : Undo.AddComponent(go); } /// /// SpriteRenderer 添加并赋值 Unity 内置默认 Sprite(白色圆角方块)。 /// 若已有 Sprite 则不覆盖(防止覆盖手动赋値)。 /// private static SpriteRenderer SetupSpriteRenderer(GameObject go) { var sr = GetOrAddComponent(go); if (sr.sprite == null) sr.sprite = AssetDatabase.LoadAssetAtPath( "Packages/com.unity.2d.sprite/Editor/ObjectMenuCreation/DefaultAssets/Textures/v2/Square.png"); return sr; } private static Transform GetOrCreateChild(Transform parent, string name) { Transform child = parent.Find(name); if (child != null) return child; GameObject go = new GameObject(name); Undo.RegisterCreatedObjectUndo(go, $"Create {name}"); go.transform.SetParent(parent, false); return go.transform; } private static void SetLayer(GameObject go, string layerName, List report) { int layer = LayerMask.NameToLayer(layerName); if (layer == -1) report.Add($"Layer '{layerName}' 不存在,请在 Tags and Layers 中创建。"); else go.layer = layer; } private static void AssignReference(Object target, string propName, Object value, List report = null) { var so = new SerializedObject(target); var sp = so.FindProperty(propName); if (sp == null) { report?.Add($"{target.GetType().Name}.{propName} 字段不存在,跳过引用赋值。"); return; } sp.objectReferenceValue = value; so.ApplyModifiedPropertiesWithoutUndo(); } private static void AssignAsset(Object target, string propName, List report, bool required, params string[] candidates) { Object asset = FindFirstAsset(candidates); if (asset == null && required) report.Add($"未找到 {target.GetType().Name}.{propName} 需要的资产: {string.Join(" / ", candidates)}"); if (asset != null) AssignReference(target, propName, asset, report); } private static void AssignLayerMask(Object target, string propName, string layerName, List report) { int layer = LayerMask.NameToLayer(layerName); if (layer == -1) { report.Add($"Layer '{layerName}' 不存在,{target.GetType().Name}.{propName} 未能赋值 LayerMask。"); return; } var so = new SerializedObject(target); var sp = so.FindProperty(propName); if (sp == null) { report.Add($"{target.GetType().Name}.{propName} 字段不存在,跳过 LayerMask 赋值。"); return; } sp.intValue = 1 << layer; so.ApplyModifiedPropertiesWithoutUndo(); } private static void AssignInt(Object target, string propName, int value) { var so = new SerializedObject(target); var sp = so.FindProperty(propName); if (sp != null) { sp.intValue = value; so.ApplyModifiedPropertiesWithoutUndo(); } } private static void AssignBool(Object target, string propName, bool value) { var so = new SerializedObject(target); var sp = so.FindProperty(propName); if (sp != null) { sp.boolValue = value; so.ApplyModifiedPropertiesWithoutUndo(); } } private static void AssignString(Object target, string propName, string value, List report = null) { var so = new SerializedObject(target); var sp = so.FindProperty(propName); if (sp == null) { report?.Add($"{target.GetType().Name}.{propName} 字段不存在,跳过字符串赋值。"); return; } sp.stringValue = value; so.ApplyModifiedPropertiesWithoutUndo(); } private static void AssignObjectArray(Object target, string propName, Object[] values, List report = null) { var so = new SerializedObject(target); var sp = so.FindProperty(propName); if (sp == null || !sp.isArray) { report?.Add($"{target.GetType().Name}.{propName} 不是可写数组字段,跳过数组赋值。"); return; } sp.arraySize = values.Length; for (int i = 0; i < values.Length; i++) sp.GetArrayElementAtIndex(i).objectReferenceValue = values[i]; so.ApplyModifiedPropertiesWithoutUndo(); } private static Object FindFirstAsset(params string[] candidates) { foreach (string candidate in candidates) { if (string.IsNullOrWhiteSpace(candidate)) continue; string[] guids = AssetDatabase.FindAssets(candidate); foreach (string guid in guids) { string path = AssetDatabase.GUIDToAssetPath(guid); Object asset = AssetDatabase.LoadMainAssetAtPath(path); if (asset != null && asset.name == candidate) return asset; } } return null; } private static void MarkDirtyAndLog(string label, GameObject root, List report) { EditorUtility.SetDirty(root); UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(root.scene); if (report != null && report.Count > 0) Debug.Log($"[SceneObjectPlacer] {label} 已放置。\n " + string.Join("\n ", report)); else Debug.Log($"[SceneObjectPlacer] {label} 已放置。"); } } }