using System.Collections.Generic; using System.Reflection; using Animancer; using BaseGames.Camera; using BaseGames.Combat; using BaseGames.Combat.StatusEffects; using BaseGames.Dialogue; using BaseGames.Enemies; using BaseGames.Equipment; using BaseGames.Parry; using BaseGames.Player; using BaseGames.Player.States; using BaseGames.Skills; 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(); // ── Player 根节点(行为+物理+标签三合一)────────────────────────────── // Rigidbody2D / 所有 MonoBehaviour 集中于此节点。 // HurtBox 作为其子节点,GetComponentInParent() 向上即可找到 // 本节点上的 PlayerController(IDamageable 实现者)。 GameObject root = new GameObject("Player"); Undo.RegisterCreatedObjectUndo(root, "Place Player"); root.transform.position = GetDropPosition(); root.tag = "Player"; SetLayer(root, "Player", report); // 物理组件(PlayerMovement RequireComponent(Rigidbody2D),必须同节点) Rigidbody2D rb = GetOrAddComponent(root); rb.bodyType = RigidbodyType2D.Dynamic; rb.gravityScale = 2f; rb.constraints = RigidbodyConstraints2D.FreezeRotation; rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous; rb.interpolation = RigidbodyInterpolation2D.Interpolate; GetOrAddComponent(root); // 动画组件(AnimancerComponent 需要 Animator 存在;PlayerController // [RequireComponent(typeof(AnimancerComponent))] 保证其存在) GetOrAddComponent(root); GetOrAddComponent(root); SetupSpriteRenderer(root); // 核心行为组件 PlayerStats playerStats = GetOrAddComponent(root); PlayerMovement playerMovement = GetOrAddComponent(root); PlayerCombat playerCombat = GetOrAddComponent(root); FormController formController = GetOrAddComponent(root); WeaponManager weaponManager = GetOrAddComponent(root); SkillManager skillManager = GetOrAddComponent(root); SpringSystem springSystem = GetOrAddComponent(root); ParrySystem parrySystem = GetOrAddComponent(root); ShieldComponent shield = GetOrAddComponent(root); PlayerWallDetector wallDetector = GetOrAddComponent(root); EquipmentManager equipmentManager = GetOrAddComponent(root); GetOrAddComponent(root); GetOrAddComponent(root); // PlayerController 最后添加:RequireComponent 会拉取上方已加好的组件 PlayerController playerController = GetOrAddComponent(root); // ── HurtBox 子节点 ─────────────────────────────────────────────────── Transform hurtBoxT = GetOrCreateChild(root.transform, "HurtBox"); SetLayer(hurtBoxT.gameObject, "PlayerHurtBox", report); BoxCollider2D hurtCollider = GetOrAddComponent(hurtBoxT.gameObject); hurtCollider.isTrigger = true; HurtBox hurtBox = GetOrAddComponent(hurtBoxT.gameObject); // ── [WeaponSocket] 子节点(WeaponManager 动态实例化武器 HitBox 的挂点) GetOrCreateChild(root.transform, "[WeaponSocket]"); // ── GroundCheck 子节点(地面检测 Transform)──────────────────────── Transform groundCheckT = GetOrCreateChild(root.transform, "GroundCheck"); groundCheckT.localPosition = new Vector3(0f, -0.75f, 0f); AssignReference(playerMovement, "_groundCheck", groundCheckT, report); AssignLayerMask(playerMovement, "_groundLayer", "Ground", report); // ── SkillHitBox_Slot 子节点(技能 HitBox 实例化挂点)──────────────── GetOrCreateChild(root.transform, "SkillHitBox_Slot"); // ── CameraFollowTarget 子节点(CinemachineCamera.Follow 目标)──────── GetOrCreateChild(root.transform, "CameraFollowTarget"); // ── PlayerController SerializeField 引用赋值 ────────────────────── AssignReference(playerController, "_combat", playerCombat, report); AssignReference(playerController, "_formController", formController, report); AssignReference(playerController, "_weaponManager", weaponManager, report); AssignReference(playerController, "_skillManager", skillManager, report); AssignReference(playerController, "_springSystem", springSystem, report); AssignReference(playerController, "_parrySystem", parrySystem, report); AssignReference(playerController, "_hurtBox", hurtBox, report); AssignReference(playerController, "_shield", shield, report); AssignReference(playerController, "_wallDetector", wallDetector, report); // ── 事件频道(可选,缺失时跳过) ─────────────────────────────────── 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 SO 自动查找(资产存在时自动绑定)────────────────────── Object statsConfig = FindFirstAsset("PLY_PlayerStats"); Object movConfig = FindFirstAsset("PLY_PlayerMovementConfig"); Object formConfig = FindFirstAsset("PLY_FormConfig"); Object parryConfig = FindFirstAsset("PLY_ParryConfig"); Object shieldConfig = FindFirstAsset("PLY_ShieldConfig"); Object inputReader = FindFirstAsset("InputReader"); Object equipmentConfig = FindFirstAsset("PLY_EquipmentConfig"); Object charmCatalog = FindFirstAsset("PLY_CharmCatalog"); if (statsConfig != null) AssignReference(playerStats, "_config", statsConfig, report); if (movConfig != null) { AssignReference(playerController, "_movementConfig", movConfig, report); AssignReference(playerMovement, "_config", movConfig, report); AssignReference(wallDetector, "_config", movConfig, report); } if (formConfig != null) { AssignReference(playerController, "_formConfig", formConfig, report); AssignReference(formController, "_config", formConfig, report); } if (parryConfig != null) AssignReference(parrySystem, "_config", parryConfig, report); if (shieldConfig != null) AssignReference(shield, "_config", shieldConfig, report); if (inputReader != null) AssignReference(playerController, "_inputReader", inputReader, report); if (equipmentConfig != null) AssignReference(equipmentManager, "_config", equipmentConfig, report); if (charmCatalog != null) AssignReference(equipmentManager, "_charmCatalog", charmCatalog, report); report.Add("★ 需手动绑定:PlayerController._animConfig(PLY_PlayerAnimationConfig)"); if (statsConfig == null) report.Add("★ 需创建并绑定:PlayerStats._config(PlayerStatsSO)"); if (inputReader == null) report.Add("★ 需手动绑定:PlayerController._inputReader(InputReaderSO)"); if (equipmentConfig == null) report.Add("★ 需创建并绑定:EquipmentManager._config(EquipmentConfigSO)"); if (charmCatalog == null) report.Add("★ 需创建并绑定:EquipmentManager._charmCatalog(CharmCatalogSO)"); report.Add("SkillManager 技能槽 SO 需手动填入。"); Selection.activeGameObject = root; MarkDirtyAndLog("Player", root, 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("CMB_DS_EnemyBody", "DS_EnemyBody"); if (dmgSrc != null) AssignReference(hitBox, "_defaultSource", dmgSrc, report); else report.Add("未找到 DamageSourceSO,HitBox_Body._defaultSource 未绑定。请按规范创建 CMB_DS_EnemyBody.asset。"); // 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/ 创建 ENM_{id}_Stats.asset 后手动指定。"); 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("CMB_DS_BossBody", "CMB_DS_EnemyBody", "DS_BossBody"); if (dmgSrc != null) AssignReference(hitBox, "_defaultSource", dmgSrc, report); else report.Add("未找到 DamageSourceSO,HitBox_Body._defaultSource 未绑定。请按规范创建 CMB_DS_BossBody.asset。"); // 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"); AssignAsset(trap, "_onCheckpointRespawn", report, false, "EVT_CheckpointRespawn"); report.Add("_canPogo=true:子 HurtBox 供玩家下劈弹起;设为 false 可改为纯死亡区(无需子 HurtBox)。"); Selection.activeGameObject = go; MarkDirtyAndLog("Hazard (LethalTrap)", go, report); } [MenuItem("BaseGames/Scene/Place/Checkpoint Marker", priority = 125)] public static void PlaceCheckpointMarker() { var report = new List(); GameObject go = new GameObject("CheckpointMarker"); Undo.RegisterCreatedObjectUndo(go, "Place CheckpointMarker"); go.transform.position = GetDropPosition(); SetLayer(go, "TriggerZone", report); BoxCollider2D col = GetOrAddComponent(go); col.isTrigger = true; col.size = new Vector2(1f, 2f); CheckpointMarker marker = GetOrAddComponent(go); AssignLayerMask(marker, "_playerLayers", "Player", report); AssignAsset(marker, "_onCheckpointReached", report, false, "EVT_CheckpointReached"); report.Add("放置于跳跳乐段落的关键节点处;玩家经过后成为该房间最近检查点。"); report.Add("同一房间可放置多个,以最近经过的为准。"); Selection.activeGameObject = go; MarkDirtyAndLog("Checkpoint Marker", go, report); } [MenuItem("BaseGames/Scene/Place/Collectible (LingZhu)", priority = 130)] 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, "_onSceneLoaded", report, false, "EVT_SceneLoaded"); AssignAsset(savePoint, "_onSavePointActivated", report, false, "EVT_SavePointActivated"); report.Add("填写 _savePointId(全局唯一字符串,用于存档点激活记录与复活定位)。"); Selection.activeGameObject = go; MarkDirtyAndLog("Save Point", go, report); } [MenuItem("BaseGames/Scene/Place/Teleport Station", priority = 135)] public static void PlaceTeleportStation() { var report = new List(); GameObject go = new GameObject("TeleportStation"); Undo.RegisterCreatedObjectUndo(go, "Place TeleportStation"); go.transform.position = GetDropPosition(); SetLayer(go, "TriggerZone", report); BoxCollider2D col = GetOrAddComponent(go); col.isTrigger = true; col.size = new Vector2(1.5f, 2f); SetupSpriteRenderer(go); TeleportStation station = GetOrAddComponent(go); AssignAsset(station, "_onFastTravelOpen", report, false, "EVT_FastTravelOpen"); report.Add("填写 _stationId(传送站唯一 ID,用于地图 UI 标注)。"); report.Add("传送站不存档、不复活、不恢复 HP;与存档点是独立对象。"); Selection.activeGameObject = go; MarkDirtyAndLog("Teleport Station", go, report); } [MenuItem("BaseGames/Scene/Place/Room Transition", priority = 140)] 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/Camera Area", priority = 140)] public static void PlaceCameraArea() => PlaceCameraArea("CameraArea"); /// /// 生成的 CameraArea GameObject 名称。 /// 子节点 AreaBoundary 和 TriggerZone 将以此为前缀命名(如 MyZone_AreaBoundary)。 /// /// 生成的 GameObject 所挂载的父节点(为 null 时放置于场景根节点)。 public static void PlaceCameraArea(string areaName, Transform parent = null) { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place Camera Area (+ TriggerZone)"); Vector3 pos = GetDropPosition(); // ── CameraArea ───────────────────────────────────────────────────── GameObject go = new GameObject(areaName); Undo.RegisterCreatedObjectUndo(go, "Place Camera Area"); go.transform.position = pos; if (parent != null) Undo.SetTransformParent(go.transform, parent, "Parent Camera Area"); CameraArea cameraArea = GetOrAddComponent(go); // AreaBoundary child — 提供 CinemachineConfiner3D 所需的限位体积 Transform boundaryT = GetOrCreateChild(go.transform, $"{areaName}_AreaBoundary"); BoxCollider boundaryCollider = GetOrAddComponent(boundaryT.gameObject); boundaryCollider.isTrigger = true; boundaryCollider.center = new Vector3(0f, 0f, -10f); // Z 占位符,实际深度由 SyncConfiner 按 LensConfig 计算 boundaryCollider.size = new Vector3(24f, 12f, 1f); // 默认房间尺寸占位符 AssignReference(cameraArea, "_confinerCollider", boundaryCollider, report); // ── CameraTriggerZone(配对)───────────────────────────────────────── GameObject zoneGo = new GameObject($"{areaName}_TriggerZone"); Undo.RegisterCreatedObjectUndo(zoneGo, "Place Camera Trigger Zone"); zoneGo.transform.position = pos; SetLayer(zoneGo, "TriggerZone", report); CameraTriggerZone zone = GetOrAddComponent(zoneGo); PolygonCollider2D col = GetOrAddComponent(zoneGo); col.isTrigger = true; // 默认矩形多边形(24×12),可在 Inspector 中编辑顶点 col.SetPath(0, new Vector2[] { new Vector2(-12f, -6f), new Vector2(-12f, 6f), new Vector2( 12f, 6f), new Vector2( 12f, -6f), }); AssignReference(zone, "_targetArea", cameraArea, report); // TriggerZone 归入 CameraArea 节点,方便统一调整与查找 Undo.SetTransformParent(zoneGo.transform, go.transform, "Parent TriggerZone to CameraArea"); zoneGo.transform.localPosition = Vector3.zero; Undo.CollapseUndoOperations(undoGroup); report.Add($"绑定 LensConfig SO 后单击 Inspector 中「从可视区域更新限位区域」计算 {areaName}_AreaBoundary BoxCollider。"); report.Add($"编辑 {areaName}_TriggerZone PolygonCollider2D 的顶点以匹配入口多边形区域。"); // ── 自动关联到同场景 RoomController(若其 _cameraArea 为空)──────── #if UNITY_6000_0_OR_NEWER var roomControllers = Object.FindObjectsByType(FindObjectsSortMode.None); #else var roomControllers = Object.FindObjectsOfType(); #endif bool autoAssigned = false; foreach (var rc in roomControllers) { // 仅使用反射检查,避免每次都覆盖已绑定的引用 var fi = typeof(RoomController).GetField("_cameraArea", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); if (fi == null) continue; if (fi.GetValue(rc) != null) continue; Undo.RecordObject(rc, "Auto-assign CameraArea to RoomController"); fi.SetValue(rc, cameraArea); EditorUtility.SetDirty(rc); report.Add($"✅ 已自动将 {areaName} 关联到 {rc.gameObject.name}.RoomController._cameraArea。"); autoAssigned = true; } if (!autoAssigned) report.Add("将此 CameraArea 拖入 RoomController._cameraArea 字段(未找到空 _cameraArea 的 RoomController)。"); Selection.activeGameObject = go; MarkDirtyAndLog($"Camera Area (+ TriggerZone): {areaName}", 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/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} 已放置。"); } } }