using System.Collections.Generic; using System.Reflection; using Animancer; using BaseGames.Boss; using BaseGames.Camera; using BaseGames.Combat; using BaseGames.Combat.StatusEffects; using BaseGames.Core.Assets; using BaseGames.Core.Pool; using BaseGames.Dialogue; using BaseGames.Enemies; using BaseGames.Enemies.Abilities; using BaseGames.Enemies.Boss; using BaseGames.Enemies.Navigation; using BaseGames.Enemies.Perception; using BaseGames.Equipment; using BaseGames.Feedback; 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 { // ── 碰撞器类型 ──────────────────────────────────────────────────────── public enum EnemyBodyColliderType { Box, Capsule, Circle } // ══ 菜单入口 ══════════════════════════════════════════════════════════ [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); AnimancerComponent animancer = 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); // PlayerFeedback:EquipmentManager.Awake 经 GetComponent() 取本节点引用(护符效果反馈), // 缺失会触发断言。须与 EquipmentManager 同节点。Feel 反馈链(MMF_Player)留待 Inspector 配置。 GetOrAddComponent(root); GetOrAddComponent(root); StatusEffectManager statusEffectManager = 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 的挂点) Transform weaponSocketT = 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", new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" }, report); // ── SkillHitBox_Slot 子节点(技能 HitBox 实例化挂点)──────────────── Transform skillSocketT = 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); // ── 其他组件内部引用 ──────────────────────────────────────────────── AssignReference(playerCombat, "_weaponManager", weaponManager, report); AssignReference(springSystem, "_stats", playerStats, report); // WeaponManager 内部引用 AssignReference(weaponManager, "_formController", formController, report); AssignReference(weaponManager, "_weaponSocket", weaponSocketT, report); // SkillManager 内部引用(技能系统核心依赖) AssignReference(skillManager, "_stats", playerStats, report); AssignReference(skillManager, "_animancer", animancer, report); AssignReference(skillManager, "_formController", formController, report); AssignReference(skillManager, "_modifiers", GetOrAddComponent(root), report); AssignReference(skillManager, "_skillSocket", skillSocketT, report); // PlayerWallDetector 墙壁检测层(Wall + Platform 组合) { int wallMask = 0; int wallL = LayerMask.NameToLayer("Wall"); int groundL = LayerMask.NameToLayer("Platform"); if (wallL != -1) wallMask |= 1 << wallL; if (groundL != -1) wallMask |= 1 << groundL; if (wallMask != 0) { var wso = new SerializedObject(wallDetector); var wsp = wso.FindProperty("_wallLayer"); if (wsp != null) { wsp.intValue = wallMask; wso.ApplyModifiedPropertiesWithoutUndo(); } } else report.Add("★ Layer 'Wall'/'Platform' 不存在,PlayerWallDetector._wallLayer 未赋值。"); } // ── 事件频道(可选,缺失时跳过) ─────────────────────────────────── 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"); AssignAsset(springSystem, "_onEnemyDied", report, false, "EVT_EnemyDied"); AssignAsset(parrySystem, "_onParrySuccess", report, false, "EVT_ParrySuccess"); AssignAsset(formController, "_onFormChanged", report, false, "EVT_FormChanged"); AssignAsset(formController, "_onSkillSetChanged", report, false, "EVT_SkillSetChanged"); AssignAsset(equipmentManager, "_onCharmEquipped", report, false, "EVT_CharmEquipped"); AssignAsset(equipmentManager, "_onCharmUnequipped", report, false, "EVT_CharmUnequipped"); AssignAsset(equipmentManager, "_onEquipmentChanged", report, false, "EVT_EquipmentChanged"); AssignAsset(equipmentManager, "_onAchievementNotchGranted", report, false, "EVT_AchievementNotchGranted"); AssignAsset(statusEffectManager, "_onStatusEffectApplied", report, false, "EVT_StatusEffectApplied"); AssignAsset(statusEffectManager, "_onStatusEffectExpired", report, false, "EVT_StatusEffectExpired"); AssignAsset(shield, "_onShieldBrokenChannel", report, false, "EVT_ShieldBroken"); AssignAsset(shield, "_onShieldRestoredChannel", report, false, "EVT_ShieldRestored"); // ── 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("CHM_Catalog"); Object animConfig = FindFirstAsset("PLY_PlayerAnimationConfig"); 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 (animConfig != null) AssignReference(playerController, "_animConfig", animConfig, report); if (inputReader != null) { AssignReference(playerController, "_inputReader", inputReader, report); AssignReference(formController, "_input", inputReader, report); AssignReference(skillManager, "_input", inputReader, report); } if (equipmentConfig != null) AssignReference(equipmentManager, "_config", equipmentConfig, report); if (charmCatalog != null) AssignReference(equipmentManager, "_charmCatalog", charmCatalog, report); if (animConfig == null) report.Add("★ 需创建并绑定:PlayerController._animConfig(PLY_PlayerAnimationConfig)"); if (statsConfig == null) report.Add("★ 需创建并绑定:PlayerStats._config(PlayerStatsSO)"); if (inputReader == null) report.Add("★ 需手动绑定:PlayerController._inputReader / FormController._input / SkillManager._input(InputReaderSO)"); if (equipmentConfig == null) report.Add("★ 需创建并绑定:EquipmentManager._config(EquipmentConfigSO)"); if (charmCatalog == null) report.Add("★ 需创建并绑定:EquipmentManager._charmCatalog(CharmCatalogSO)"); report.Add("SkillManager._formSkillSets 技能槽 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() => PlaceEnemy(EnemyBodyColliderType.Box); public static void PlaceEnemy(EnemyBodyColliderType bodyCollider) { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place Basic Enemy"); EnemyBase.SuppressValidationWarnings = true; 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; rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous; Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.7f, 0.9f)); Transform visual = GetOrCreateChild(go.transform, "Visual"); visual.localPosition = (Vector3)(Vector2)body.offset; GetOrAddComponent(visual.gameObject); AnimancerComponent animancer = GetOrAddComponent(visual.gameObject); SpriteRenderer sr = SetupSpriteRenderer(visual.gameObject); EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); // required by EnemyNavAgent [RequireComponent] PhysicsPerceptionSystem sensorHub = 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); // Wire EnemyBase AssignReference(enemyBase, "_stats", enemyStats, report); AssignReference(enemyBase, "_movement", movement, report); AssignReference(enemyBase, "_animancer", animancer, report); AssignReference(enemyBase, "_hurtBox", hurtBox, report); 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"); // Wire EnemyMovement AssignReference(movement, "_visualRoot", visual, report); AssignReference(movement, "_animancer", animancer, report); AssignReference(movement, "_spriteRenderer", sr, report); AssignLayerMask(movement, "_groundMask", new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" }, report); // DamageSourceSO for body contact 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。"); SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report); report.Add("★ 指定 EnemyBase._statsSO、_animConfig 资产(按所创建的敌人类型命名)。"); report.Add("★ 挂载行为树 BehaviorTree 组件,指定对应 .asset。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; EnemyBase.SuppressValidationWarnings = false; MarkDirtyAndLog("Enemy (Basic)", go, report); } [MenuItem("BaseGames/Scene/Place/Boss Enemy", priority = 115)] public static void PlaceBossEnemy() => PlaceBossEnemy(EnemyBodyColliderType.Box); public static void PlaceBossEnemy(EnemyBodyColliderType bodyCollider) { 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; CreateBodyCollider(go, bodyCollider, new Vector2(1.5f, 2.5f)); GetOrAddComponent(go); SetupSpriteRenderer(go); BossBase bossBase = GetOrAddComponent(go); EnemyStats bossStats = GetOrAddComponent(go); PhysicsPerceptionSystem sensorHub = 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"); SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report); 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/Enemy E001 (草蛭)", priority = 111)] public static void PlaceE001_CaoZhi() => PlaceE001_CaoZhi(EnemyBodyColliderType.Box); public static void PlaceE001_CaoZhi(EnemyBodyColliderType bodyCollider) { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place E001 草蛭"); EnemyBase.SuppressValidationWarnings = true; GameObject go = new GameObject("ENM_CaoZhi"); Undo.RegisterCreatedObjectUndo(go, "Place E001"); 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; Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.6f, 0.8f)); Transform visual = GetOrCreateChild(go.transform, "Visual"); visual.localPosition = (Vector3)(Vector2)body.offset; GetOrAddComponent(visual.gameObject); AnimancerComponent animancer = GetOrAddComponent(visual.gameObject); SpriteRenderer sr1 = SetupSpriteRenderer(visual.gameObject); EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); // required by EnemyNavAgent [RequireComponent] PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go); Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox"); SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report); CapsuleCollider2D hurtCap = GetOrAddComponent(hurtBoxT.gameObject); hurtCap.isTrigger = true; hurtCap.size = new Vector2(0.55f, 0.75f); HurtBox hurtBox = GetOrAddComponent(hurtBoxT.gameObject); Transform contactT = GetOrCreateChild(go.transform, "ContactDamageZone"); SetLayer(contactT.gameObject, "EnemyHitBox", report); CircleCollider2D contactCol = GetOrAddComponent(contactT.gameObject); contactCol.isTrigger = true; contactCol.radius = 0.4f; HitBox contactHitBox = GetOrAddComponent(contactT.gameObject); BodyContactDamage bodyContact = GetOrAddComponent(contactT.gameObject); Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities"); Transform alertT = GetOrCreateChild(abilitiesT, "PlayClipAbility_Alert"); PlayClipAbility alertAbility = GetOrAddComponent(alertT.gameObject); Transform chaseT = GetOrCreateChild(abilitiesT, "ContactChaseAbility_Chase"); ContactChaseAbility chaseAbility = GetOrAddComponent(chaseT.gameObject); // SOs — assign first so OnValidate doesn't warn during wiring AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E001_Stats"); AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E001_AnimConfig"); // Component wiring AssignReference(enemyBase, "_stats", enemyStats, report); AssignReference(enemyBase, "_movement", movement, report); AssignReference(enemyBase, "_animancer", animancer, report); AssignReference(enemyBase, "_hurtBox", hurtBox, report); 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"); AssignAsset(movement, "_config", report, false, "ENM_E001_Stats"); AssignAsset(movement, "_animConfig", report, false, "ENM_E001_AnimConfig"); AssignReference(movement, "_visualRoot", visual, report); AssignReference(movement, "_animancer", animancer, report); AssignReference(movement, "_spriteRenderer", sr1, report); AssignLayerMask(movement, "_groundMask", new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" }, report); AssignAsset(alertAbility, "_config", report, false, "ABL_E001_Alert"); AssignAsset(chaseAbility, "_config", report, false, "ABL_E001_Chase"); AssignReference(chaseAbility, "_contactDamage", bodyContact, report); Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody"); if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report); SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report); report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E001_CaoZhi.asset。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; EnemyBase.SuppressValidationWarnings = false; MarkDirtyAndLog("E001 草蛭", go, report); } [MenuItem("BaseGames/Scene/Place/Enemy E002 (簧蛭)", priority = 112)] public static void PlaceE002_HuangZhi() => PlaceE002_HuangZhi(EnemyBodyColliderType.Box); public static void PlaceE002_HuangZhi(EnemyBodyColliderType bodyCollider) { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place E002 簧蛭"); EnemyBase.SuppressValidationWarnings = true; GameObject go = new GameObject("ENM_HuangZhi"); Undo.RegisterCreatedObjectUndo(go, "Place E002"); go.transform.position = GetDropPosition(); SetLayer(go, "Enemy", report); Rigidbody2D rb = GetOrAddComponent(go); rb.bodyType = RigidbodyType2D.Kinematic; rb.gravityScale = 0f; Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.5f, 0.7f)); // Visual 子节点:挂载精灵 / 动画(EnemyMovement 翻转时操作此节点) Transform visual = GetOrCreateChild(go.transform, "Visual"); visual.localPosition = (Vector3)(Vector2)body.offset; GetOrAddComponent(visual.gameObject); AnimancerComponent animancer = GetOrAddComponent(visual.gameObject); SpriteRenderer sr = SetupSpriteRenderer(visual.gameObject); EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); // required by EnemyNavAgent [RequireComponent] PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go); // HurtBox(component.enabled 初始为 false:仅悬挂脆弱窗口期间由能力开启) Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox"); SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report); CapsuleCollider2D hurtCap = GetOrAddComponent(hurtBoxT.gameObject); hurtCap.isTrigger = true; hurtCap.size = new Vector2(0.45f, 0.65f); HurtBox hurtBox = GetOrAddComponent(hurtBoxT.gameObject); hurtBox.enabled = false; // 悬挂阶段外不可被攻击;CeilingHangStrikeAbility 在 _loopClip 期间开启 // AttackHitBox(钻出啃咬瞬间判定,由 CeilingHangStrikeAbility 激活) Transform attackHitBoxT = GetOrCreateChild(go.transform, "AttackHitBox"); SetLayer(attackHitBoxT.gameObject, "EnemyHitBox", report); BoxCollider2D attackCol = GetOrAddComponent(attackHitBoxT.gameObject); attackCol.isTrigger = true; attackCol.size = new Vector2(0.6f, 0.8f); // 正下方钻出范围 HitBox attackHitBox = GetOrAddComponent(attackHitBoxT.gameObject); Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities"); Transform strikeT = GetOrCreateChild(abilitiesT, "CeilingHangStrikeAbility"); CeilingHangStrikeAbility strikeAbility = GetOrAddComponent(strikeT.gameObject); // SOs — assign first so OnValidate doesn't warn during wiring AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E002_Stats"); AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E002_AnimConfig"); AssignReference(enemyBase, "_stats", enemyStats, report); AssignReference(enemyBase, "_movement", movement, report); AssignReference(enemyBase, "_animancer", animancer, report); AssignReference(enemyBase, "_hurtBox", hurtBox, report); 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"); AssignAsset(movement, "_config", report, false, "ENM_E002_Stats"); AssignAsset(movement, "_animConfig", report, false, "ENM_E002_AnimConfig"); AssignReference(movement, "_visualRoot", visual, report); AssignReference(movement, "_animancer", animancer, report); AssignReference(movement, "_spriteRenderer", sr, report); AssignLayerMask(movement, "_groundMask", new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" }, report); AssignAsset(strikeAbility, "_config", report, false, "ABL_E002_CeilingStrike"); AssignReference(strikeAbility, "_attackHitBox", attackHitBox, report); AssignReference(strikeAbility, "_hurtBox", hurtBox, report); Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody"); if (dmgSrc != null) AssignReference(attackHitBox, "_defaultSource", dmgSrc, report); SetupPerceptionSystemSlots(sensorHub, new[] { "attack_range" }, report); report.Add("★ 将此对象放置于天花板,调整位置使 CapsuleCollider 正好贴合天花板底面。"); report.Add("★ HurtBox.enabled 初始为 false;钻出后由 CeilingHangStrikeAbility 在悬挂窗口开启。"); report.Add("★ attack_range 槽位为正下方 BoxCast(玩家经过检测区);按需在 Inspector 调整 offset/size。"); report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E002_HuangZhi.asset。"); report.Add("★ BD 树逻辑建议:Idle(悬挂)→ IsSensorDetecting(aggro) → UseAbility(CeilingDrop) → IsGrounded → Patrol(Pace)。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; EnemyBase.SuppressValidationWarnings = false; MarkDirtyAndLog("E002 簧蛭", go, report); } [MenuItem("BaseGames/Scene/Place/Enemy E003 (幼蛭)", priority = 113)] public static void PlaceE003_YouZhi_Enemy() => PlaceE003_YouZhi_Enemy(EnemyBodyColliderType.Box); public static void PlaceE003_YouZhi_Enemy(EnemyBodyColliderType bodyCollider) { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place E003 幼蛭"); EnemyBase.SuppressValidationWarnings = true; GameObject go = new GameObject("ENM_YouZhi"); Undo.RegisterCreatedObjectUndo(go, "Place E003"); go.transform.position = GetDropPosition(); SetLayer(go, "Enemy", report); Rigidbody2D rb = GetOrAddComponent(go); rb.bodyType = RigidbodyType2D.Kinematic; rb.gravityScale = 0f; Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.5f, 0.6f)); Transform visual = GetOrCreateChild(go.transform, "Visual"); visual.localPosition = (Vector3)(Vector2)body.offset; GetOrAddComponent(visual.gameObject); AnimancerComponent animancer = GetOrAddComponent(visual.gameObject); SpriteRenderer sr3 = SetupSpriteRenderer(visual.gameObject); E003_YouZhi enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); // required by EnemyNavAgent [RequireComponent] PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go); Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox"); SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report); CapsuleCollider2D hurtCap = GetOrAddComponent(hurtBoxT.gameObject); hurtCap.isTrigger = true; hurtCap.size = new Vector2(0.45f, 0.55f); HurtBox hurtBox = GetOrAddComponent(hurtBoxT.gameObject); Transform contactT = GetOrCreateChild(go.transform, "ContactDamageZone"); SetLayer(contactT.gameObject, "EnemyHitBox", report); CircleCollider2D contactCol = GetOrAddComponent(contactT.gameObject); contactCol.isTrigger = true; contactCol.radius = 0.35f; HitBox contactHitBox = GetOrAddComponent(contactT.gameObject); BodyContactDamage bodyContact = GetOrAddComponent(contactT.gameObject); Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities"); Transform fallT = GetOrCreateChild(abilitiesT, "AnimatedCeilingDropAbility"); AnimatedCeilingDropAbility fallAbility = GetOrAddComponent(fallT.gameObject); // SOs — assign first so OnValidate doesn't warn during wiring AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E003_Stats"); AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E003_AnimConfig"); AssignReference(enemyBase, "_stats", enemyStats, report); AssignReference(enemyBase, "_movement", movement, report); AssignReference(enemyBase, "_animancer", animancer, report); AssignReference(enemyBase, "_hurtBox", hurtBox, report); 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"); AssignAsset(movement, "_config", report, false, "ENM_E003_Stats"); AssignAsset(movement, "_animConfig", report, false, "ENM_E003_AnimConfig"); AssignReference(movement, "_visualRoot", visual, report); AssignReference(movement, "_animancer", animancer, report); AssignReference(movement, "_spriteRenderer", sr3, report); AssignLayerMask(movement, "_groundMask", new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" }, report); AssignAsset(fallAbility, "_config", report, false, "ABL_E003_Fall"); AssignReference(fallAbility, "_contactDamage", bodyContact, report); Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody"); if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report); SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report); report.Add("★ 将此对象放置于天花板下方,E003_YouZhi 会在 OnSpawn/ActivateFromCeiling 时执行下坠。"); report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E003_YouZhi.asset。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; EnemyBase.SuppressValidationWarnings = false; MarkDirtyAndLog("E003 幼蛭", go, report); } [MenuItem("BaseGames/Scene/Place/Enemy E004 (蛭母)", priority = 114)] public static void PlaceE004_ZhiMu_Enemy() => PlaceE004_ZhiMu_Enemy(EnemyBodyColliderType.Box); public static void PlaceE004_ZhiMu_Enemy(EnemyBodyColliderType bodyCollider) { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place E004 蛭母"); EnemyBase.SuppressValidationWarnings = true; GameObject go = new GameObject("ENM_ZhiMu"); Undo.RegisterCreatedObjectUndo(go, "Place E004"); 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; Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.8f, 1.2f)); Transform visual = GetOrCreateChild(go.transform, "Visual"); visual.localPosition = (Vector3)(Vector2)body.offset; GetOrAddComponent(visual.gameObject); AnimancerComponent animancer = GetOrAddComponent(visual.gameObject); SpriteRenderer sr4 = SetupSpriteRenderer(visual.gameObject); E004_ZhiMu enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyFeedback feedback = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); // required by EnemyNavAgent [RequireComponent] PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go); Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox"); SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report); CapsuleCollider2D hurtCap = GetOrAddComponent(hurtBoxT.gameObject); hurtCap.isTrigger = true; hurtCap.size = new Vector2(0.75f, 1.1f); HurtBox hurtBox = GetOrAddComponent(hurtBoxT.gameObject); Transform biteT = GetOrCreateChild(go.transform, "BiteHitBox"); SetLayer(biteT.gameObject, "EnemyHitBox", report); BoxCollider2D biteCol = GetOrAddComponent(biteT.gameObject); biteCol.isTrigger = true; biteCol.size = new Vector2(0.6f, 0.4f); HitBox biteHitBox = GetOrAddComponent(biteT.gameObject); biteT.gameObject.SetActive(false); Transform slamT = GetOrCreateChild(go.transform, "SlamHitBox"); SetLayer(slamT.gameObject, "EnemyHitBox", report); CircleCollider2D slamCol = GetOrAddComponent(slamT.gameObject); slamCol.isTrigger = true; slamCol.radius = 0.7f; HitBox slamHitBox = GetOrAddComponent(slamT.gameObject); slamT.gameObject.SetActive(false); Transform acidMuzzleT = GetOrCreateChild(go.transform, "AcidMuzzle"); // 能力集(计划 E004):出场 / 撕咬 / 头槌 / 酸液 / 转身 Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities"); Transform appearAblT = GetOrCreateChild(abilitiesT, "AppearAbility"); AppearAbility appearAbl = GetOrAddComponent(appearAblT.gameObject); Transform biteAblT = GetOrCreateChild(abilitiesT, "MeleeAttackAbility_Bite"); MeleeAttackAbility biteAbl = GetOrAddComponent(biteAblT.gameObject); Transform slamAblT = GetOrCreateChild(abilitiesT, "RepeatSlamAbility_HeadSlam"); RepeatSlamAbility slamAbl = GetOrAddComponent(slamAblT.gameObject); Transform acidAblT = GetOrCreateChild(abilitiesT, "ProjectileAttackAbility_Acid"); ProjectileAttackAbility acidAbl = GetOrAddComponent(acidAblT.gameObject); Transform flipAblT = GetOrCreateChild(abilitiesT, "FacePlayerAbility_Flip"); FacePlayerAbility flipAbl = GetOrAddComponent(flipAblT.gameObject); // SOs — assign first so OnValidate doesn't warn during wiring AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E004_Stats"); AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E004_AnimConfig"); AssignReference(enemyBase, "_stats", enemyStats, report); AssignReference(enemyBase, "_movement", movement, report); AssignReference(enemyBase, "_animancer", animancer, report); AssignReference(enemyBase, "_feedback", feedback, report); AssignReference(enemyBase, "_hurtBox", hurtBox, report); 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"); AssignAsset(movement, "_config", report, false, "ENM_E004_Stats"); AssignAsset(movement, "_animConfig", report, false, "ENM_E004_AnimConfig"); AssignReference(movement, "_visualRoot", visual, report); AssignReference(movement, "_animancer", animancer, report); AssignReference(movement, "_spriteRenderer", sr4, report); AssignLayerMask(movement, "_groundMask", new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" }, report); AssignAsset(appearAbl, "_config", report, false, "ABL_E004_Appear"); AssignAsset(biteAbl, "_config", report, false, "ABL_E004_Bite"); AssignAsset(slamAbl, "_config", report, false, "ABL_E004_HeadSlam"); AssignAsset(acidAbl, "_config", report, false, "ABL_E004_Acid"); AssignAsset(flipAbl, "_config", report, false, "ABL_E004_Flip"); AssignMeleeHitBoxSlots(biteAbl, new[] { ("bite", biteHitBox) }, report); AssignReference(slamAbl, "_hitBox", slamHitBox, report); AssignReference(acidAbl, "_muzzle", acidMuzzleT, report); Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody"); if (dmgSrc != null) { AssignReference(biteHitBox, "_defaultSource", dmgSrc, report); AssignReference(slamHitBox, "_defaultSource", dmgSrc, report); } SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "sight" }, report); report.Add("★ AppearAbility._appearClip / FacePlayerAbility._faceClip 等动画 Clip 待美术接入后在 Inspector 指定。"); report.Add("★ 在 E004_ZhiMu._deathPreClip 配置死亡前摇动画(两阶段死亡 Death_Pre 无敌)。"); report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E004_ZhiMu.asset。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; EnemyBase.SuppressValidationWarnings = false; MarkDirtyAndLog("E004 蛭母", go, report); } [MenuItem("BaseGames/Scene/Place/Enemy E005 (肥蛭)", priority = 115)] public static void PlaceE005_FeiZhi_Enemy() => PlaceE005_FeiZhi_Enemy(EnemyBodyColliderType.Box); public static void PlaceE005_FeiZhi_Enemy(EnemyBodyColliderType bodyCollider) { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place E005 肥蛭"); EnemyBase.SuppressValidationWarnings = true; GameObject go = new GameObject("ENM_FeiZhi"); Undo.RegisterCreatedObjectUndo(go, "Place E005"); 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; Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.9f, 1.0f)); Transform visual = GetOrCreateChild(go.transform, "Visual"); visual.localPosition = (Vector3)(Vector2)body.offset; GetOrAddComponent(visual.gameObject); AnimancerComponent animancer = GetOrAddComponent(visual.gameObject); SpriteRenderer sr5 = SetupSpriteRenderer(visual.gameObject); E005_FeiZhi enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); // required by EnemyNavAgent [RequireComponent] PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go); Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox"); SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report); CapsuleCollider2D hurtCap = GetOrAddComponent(hurtBoxT.gameObject); hurtCap.isTrigger = true; hurtCap.size = new Vector2(0.85f, 0.95f); HurtBox hurtBox = GetOrAddComponent(hurtBoxT.gameObject); Transform biteT = GetOrCreateChild(go.transform, "BiteHitBox"); SetLayer(biteT.gameObject, "EnemyHitBox", report); BoxCollider2D biteCol = GetOrAddComponent(biteT.gameObject); biteCol.isTrigger = true; biteCol.size = new Vector2(0.7f, 0.45f); HitBox biteHitBox = GetOrAddComponent(biteT.gameObject); biteT.gameObject.SetActive(false); Transform acidMuzzleT = GetOrCreateChild(go.transform, "AcidMuzzle"); Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities"); Transform biteAblT = GetOrCreateChild(abilitiesT, "MeleeAttackAbility_Bite"); MeleeAttackAbility biteAbl = GetOrAddComponent(biteAblT.gameObject); Transform acidAblT = GetOrCreateChild(abilitiesT, "ProjectileAttackAbility_Acid"); ProjectileAttackAbility acidAbl = GetOrAddComponent(acidAblT.gameObject); // SOs — assign first so OnValidate doesn't warn during wiring AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E005_Stats"); AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E005_AnimConfig"); AssignReference(enemyBase, "_stats", enemyStats, report); AssignReference(enemyBase, "_movement", movement, report); AssignReference(enemyBase, "_animancer", animancer, report); AssignReference(enemyBase, "_hurtBox", hurtBox, report); 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"); AssignAsset(movement, "_config", report, false, "ENM_E005_Stats"); AssignAsset(movement, "_animConfig", report, false, "ENM_E005_AnimConfig"); AssignReference(movement, "_visualRoot", visual, report); AssignReference(movement, "_animancer", animancer, report); AssignReference(movement, "_spriteRenderer", sr5, report); AssignLayerMask(movement, "_groundMask", new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" }, report); AssignAsset(biteAbl, "_config", report, false, "ABL_E005_Bite"); AssignAsset(acidAbl, "_config", report, false, "ABL_E005_Acid"); AssignMeleeHitBoxSlots(biteAbl, new[] { ("bite", biteHitBox) }, report); AssignReference(acidAbl, "_muzzle", acidMuzzleT, report); Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody"); if (dmgSrc != null) AssignReference(biteHitBox, "_defaultSource", dmgSrc, report); SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report); report.Add("★ 在 E005_FeiZhi._deathPreClip 上添加 AnimationEvent 调用 SpawnProjectile(\"spawn_e003\")。"); report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E005_FeiZhi.asset。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; EnemyBase.SuppressValidationWarnings = false; MarkDirtyAndLog("E005 肥蛭", go, report); } [MenuItem("BaseGames/Scene/Place/Enemy E006 (讙)", priority = 116)] public static void PlaceE006_Huan() => PlaceE006_Huan(EnemyBodyColliderType.Box); public static void PlaceE006_Huan(EnemyBodyColliderType bodyCollider) { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place E006 讙"); EnemyBase.SuppressValidationWarnings = true; GameObject go = new GameObject("ENM_Huan"); Undo.RegisterCreatedObjectUndo(go, "Place E006"); 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; Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.7f, 1.0f)); Transform visual = GetOrCreateChild(go.transform, "Visual"); visual.localPosition = (Vector3)(Vector2)body.offset; GetOrAddComponent(visual.gameObject); AnimancerComponent animancer = GetOrAddComponent(visual.gameObject); SpriteRenderer sr6 = SetupSpriteRenderer(visual.gameObject); EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); // required by EnemyNavAgent [RequireComponent] PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go); Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox"); SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report); CapsuleCollider2D hurtCap = GetOrAddComponent(hurtBoxT.gameObject); hurtCap.isTrigger = true; hurtCap.size = new Vector2(0.65f, 0.95f); HurtBox hurtBox = GetOrAddComponent(hurtBoxT.gameObject); Transform contactT = GetOrCreateChild(go.transform, "ContactDamageZone"); SetLayer(contactT.gameObject, "EnemyHitBox", report); CircleCollider2D contactCol = GetOrAddComponent(contactT.gameObject); contactCol.isTrigger = true; contactCol.radius = 0.4f; HitBox contactHitBox = GetOrAddComponent(contactT.gameObject); BodyContactDamage bodyContact = GetOrAddComponent(contactT.gameObject); Transform landT = GetOrCreateChild(go.transform, "LandingHitBox"); SetLayer(landT.gameObject, "EnemyHitBox", report); CircleCollider2D landCol = GetOrAddComponent(landT.gameObject); landCol.isTrigger = true; landCol.radius = 0.8f; HitBox landHitBox = GetOrAddComponent(landT.gameObject); landT.gameObject.SetActive(false); Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities"); Transform leapT = GetOrCreateChild(abilitiesT, "LeapAttackAbility"); LeapAttackAbility leapAbl = GetOrAddComponent(leapT.gameObject); Transform chaseT = GetOrCreateChild(abilitiesT, "ContactChaseAbility"); ContactChaseAbility chaseAbl = GetOrAddComponent(chaseT.gameObject); // SOs — assign first so OnValidate doesn't warn during wiring AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E006_Stats"); AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E006_AnimConfig"); AssignReference(enemyBase, "_stats", enemyStats, report); AssignReference(enemyBase, "_movement", movement, report); AssignReference(enemyBase, "_animancer", animancer, report); AssignReference(enemyBase, "_hurtBox", hurtBox, report); 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"); AssignAsset(movement, "_config", report, false, "ENM_E006_Stats"); AssignAsset(movement, "_animConfig", report, false, "ENM_E006_AnimConfig"); AssignReference(movement, "_visualRoot", visual, report); AssignReference(movement, "_animancer", animancer, report); AssignReference(movement, "_spriteRenderer", sr6, report); AssignLayerMask(movement, "_groundMask", new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" }, report); AssignAsset(leapAbl,"_config", report, false, "ABL_E006_Leap"); AssignAsset(chaseAbl, "_config", report, false, "ABL_E006_Chase"); AssignReference(leapAbl, "_landingHitBox", landHitBox, report); AssignReference(chaseAbl, "_contactDamage", bodyContact, report); Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody"); if (dmgSrc != null) { AssignReference(contactHitBox, "_defaultSource", dmgSrc, report); AssignReference(landHitBox, "_defaultSource", dmgSrc, report); } SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report); report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E006_Huan.asset。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; EnemyBase.SuppressValidationWarnings = false; MarkDirtyAndLog("E006 讙", go, report); } [MenuItem("BaseGames/Scene/Place/Boss 嘲风 (ChaoFeng)", priority = 117)] public static void PlaceChaoFeng() => PlaceChaoFeng(EnemyBodyColliderType.Box); public static void PlaceChaoFeng(EnemyBodyColliderType bodyCollider) { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place Boss 嘲风"); EnemyBase.SuppressValidationWarnings = true; GameObject go = new GameObject("ENM_ChaoFeng"); Undo.RegisterCreatedObjectUndo(go, "Place ChaoFeng"); 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; Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(1.2f, 2.0f)); Transform visual = GetOrCreateChild(go.transform, "Visual"); visual.localPosition = (Vector3)(Vector2)body.offset; GetOrAddComponent(visual.gameObject); AnimancerComponent animancer = GetOrAddComponent(visual.gameObject); SpriteRenderer srBoss = SetupSpriteRenderer(visual.gameObject); ChaoFengBoss bossBase = GetOrAddComponent(go); EnemyStats bossStats = GetOrAddComponent(go); EnemyFeedback feedback = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); // required by EnemyNavAgent [RequireComponent] BossSkillExecutor skillExec = GetOrAddComponent(go); ChaoFengFloatController floatCtrl = GetOrAddComponent(go); ChaoFengKnockdownCounter knockdown = GetOrAddComponent(go); PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go); // HurtBox Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox"); SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report); CapsuleCollider2D hurtCap = GetOrAddComponent(hurtBoxT.gameObject); hurtCap.isTrigger = true; hurtCap.size = new Vector2(1.1f, 1.9f); HurtBox hurtBox = GetOrAddComponent(hurtBoxT.gameObject); // Phase1 攻击 HitBox(默认禁用;技能执行时由 BossSkillExecutor 开关)。 // 计划:挥扇三连 FanCombo ×3 + 龙卷接触 Tornado。 HitBox fan1 = CreateDisabledHitBox(go.transform, "Phase1_FanCombo_HitBox_1", "EnemyHitBox", true, report, size: new Vector2(1.0f, 0.5f)); HitBox fan2 = CreateDisabledHitBox(go.transform, "Phase1_FanCombo_HitBox_2", "EnemyHitBox", true, report, size: new Vector2(1.0f, 0.5f)); HitBox fan3 = CreateDisabledHitBox(go.transform, "Phase1_FanCombo_HitBox_3", "EnemyHitBox", true, report, size: new Vector2(1.2f, 0.6f)); HitBox tornadoHB = CreateDisabledHitBox(go.transform, "Phase1_Tornado_HitBox", "EnemyHitBox", true, report, size: new Vector2(0.6f, 1.2f)); // 弹体发射点(Phase1 回旋扇 / 龙卷;Phase2 风石) Transform boomerangMuzzleT = GetOrCreateChild(go.transform, "BoomerangMuzzle"); Transform tornadoMuzzleT = GetOrCreateChild(go.transform, "TornadoMuzzle"); Transform windStoneMuzzleT = GetOrCreateChild(go.transform, "WindStoneMuzzle"); // SOs — assign first so OnValidate doesn't warn during wiring AssignAsset(bossBase, "_statsSO", report, false, "ENM_ChaoFeng_Stats"); AssignAsset(bossBase, "_animConfig", report, false, "ENM_ChaoFeng_AnimConfig"); // Component wiring AssignReference(bossBase, "_stats", bossStats, report); AssignReference(bossBase, "_movement", movement, report); AssignReference(bossBase, "_animancer", animancer, report); AssignReference(bossBase, "_feedback", feedback, report); AssignReference(bossBase, "_hurtBox", hurtBox, report); AssignReference(skillExec, "_animancer", animancer, report); // 浮空 / 击落 / 弹体发射点接线(计划) AssignReference(bossBase, "_floatController", floatCtrl, report); AssignReference(bossBase, "_knockdownCounter", knockdown, report); AssignReference(bossBase, "_boomerangMuzzle", boomerangMuzzleT, report); AssignReference(bossBase, "_tornadoMuzzle", tornadoMuzzleT, report); AssignReference(bossBase, "_windStoneMuzzle", windStoneMuzzleT, report); AssignReference(floatCtrl, "_rb", rb, report); AssignReference(knockdown, "_boss", bossBase, report); AssignReference(knockdown, "_floatCtrl", floatCtrl, report); // 弹体配置接线(ProjectileConfigSO,存在时自动绑定) AssignAsset(bossBase, "_boomerangConfig", report, false, "PROJ_Boomerang_Config"); AssignAsset(bossBase, "_tornadoSmallConfig", report, false, "PROJ_TornadoSmall_Config"); AssignAsset(bossBase, "_tornadoLargeConfig", report, false, "PROJ_TornadoLarge_Config"); AssignAsset(bossBase, "_windStoneConfig", report, false, "PROJ_WindStone_Config"); 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"); AssignAsset(movement, "_config", report, false, "ENM_ChaoFeng_Stats"); AssignAsset(movement, "_animConfig", report, false, "ENM_ChaoFeng_AnimConfig"); AssignReference(movement, "_visualRoot", visual, report); AssignReference(movement, "_animancer", animancer, report); AssignReference(movement, "_spriteRenderer", srBoss, report); AssignLayerMask(movement, "_groundMask", new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" }, report); // 收集 BossSkillSO 并赋给执行器(计划技能集) var skillAssets = new System.Collections.Generic.List(); foreach (var n in new[] { "ABL_ChaoFeng_Boomerang", "ABL_ChaoFeng_FanCombo", "ABL_ChaoFeng_TornadoSmall", "ABL_ChaoFeng_TornadoLarge", "ABL_ChaoFeng_WindStone" }) { Object sk = FindFirstAsset(n); if (sk != null) skillAssets.Add(sk); else report.Add($"未找到 BossSkillSO:{n},请先一键创建 ChaoFeng SO 后再重新运行此放置操作。"); } if (skillAssets.Count > 0) AssignObjectArray(skillExec, "_skills", skillAssets.ToArray(), report); AssignString(skillExec, "_bossId", "ChaoFeng", report); AssignObjectArray(skillExec, "_hitBoxes", new Object[] { fan1, fan2, fan3, tornadoHB }, report); Object dmgSrc = FindFirstAsset("CMB_DS_BossBody", "CMB_DS_EnemyBody"); if (dmgSrc != null) { foreach (var hb in new[] { fan1, fan2, fan3, tornadoHB }) if (hb != null) AssignReference(hb, "_defaultSource", dmgSrc, report); } SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "sight" }, report); report.Add("★ FanCombo 三段 HitBox 与 Tornado HitBox 已挂入 BossSkillExecutor._hitBoxes。"); report.Add("★ 将 BoomerangMuzzle / TornadoMuzzle / WindStoneMuzzle 拖入对应 BossSkillSO 的发射点字段(如有)。"); report.Add("★ 回旋扇收招/阶段过渡/击败演出等动画 Clip 待美术接入后在 ChaoFengBoss Inspector 指定。"); report.Add("★ 挂载行为树 BehaviorTree 组件,指定 Boss_ChaoFeng.asset。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; EnemyBase.SuppressValidationWarnings = false; MarkDirtyAndLog("Boss 嘲风 (ChaoFeng)", go, report); } // ══ 敌人放置辅助方法 ═══════════════════════════════════════════════════ /// /// 创建禁用的 HitBox 子节点(Box 或 Circle 碰撞体)。 /// private static HitBox CreateDisabledHitBox(Transform parent, string childName, string layer, bool isBox, List report, Vector2 size = default, float radius = 0.5f) { Transform t = GetOrCreateChild(parent, childName); SetLayer(t.gameObject, layer, report); if (isBox) { BoxCollider2D col = GetOrAddComponent(t.gameObject); col.isTrigger = true; col.size = size == default ? new Vector2(0.5f, 0.5f) : size; } else { CircleCollider2D col = GetOrAddComponent(t.gameObject); col.isTrigger = true; col.radius = radius; } HitBox hb = GetOrAddComponent(t.gameObject); t.gameObject.SetActive(false); return hb; } /// /// 在 上预填充 _slots 数组, /// 根据 slotName 自动选择类型、半径、检测层及 GizmoColor。 /// private static void SetupPerceptionSystemSlots(PhysicsPerceptionSystem system, string[] slotNames, List report) { var so = new SerializedObject(system); var slots = so.FindProperty("_slots"); if (slots == null || !slots.isArray) { report?.Add("PhysicsPerceptionSystem._slots 字段未找到,请检查脚本序列化。"); return; } int playerLayer = LayerMask.GetMask("Player"); slots.arraySize = slotNames.Length; for (int i = 0; i < slotNames.Length; i++) { var elem = slots.GetArrayElementAtIndex(i); string name = slotNames[i]; elem.FindPropertyRelative("slotName").stringValue = name; int enumIdx = 0; // RangeCircle float radius = 3f; int layer = playerLayer; switch (name) { case "aggro": enumIdx = 0; radius = 5f; layer = playerLayer; break; case "los": enumIdx = 1; radius = 0f; layer = 0; break; case "attack_melee":enumIdx = 0; radius = 1.5f; layer = playerLayer; break; case "attack_range":enumIdx = 0; radius = 8f; layer = playerLayer; break; case "patrol": enumIdx = 0; radius = 5f; layer = 0; break; case "alert": enumIdx = 0; radius = 3f; layer = playerLayer; break; case "sight": enumIdx = 4; radius = 6f; layer = playerLayer; break; } elem.FindPropertyRelative("type").enumValueIndex = enumIdx; elem.FindPropertyRelative("radius").floatValue = radius; elem.FindPropertyRelative("detectLayer").intValue = layer; // sight 槽位默认设置推荐的 LOS 采样点数(3:中心+上+下) if (name == "sight") { var losRayCountProp = elem.FindPropertyRelative("losRayCount"); if (losRayCountProp != null) losRayCountProp.intValue = 3; } // 各 slot 分配语义化默认颜色,可在 Inspector 中按需覆盖 Color defaultColor = name switch { "aggro" => new Color(1.00f, 0.60f, 0.10f, 1f), // 橙 "los" => new Color(0.00f, 0.80f, 1.00f, 1f), // 青 "attack_melee" => new Color(1.00f, 0.20f, 0.20f, 1f), // 红 "attack_range" => new Color(1.00f, 0.40f, 0.60f, 1f), // 粉红 "patrol" => new Color(0.20f, 0.90f, 0.20f, 1f), // 绿 "alert" => new Color(1.00f, 0.90f, 0.10f, 1f), // 黄 "sight" => new Color(0.30f, 0.85f, 1.00f, 1f), // 浅蓝(LOS 传感器) _ => Color.clear, // 未知 slot 回退为紫色 }; elem.FindPropertyRelative("gizmoColor").colorValue = defaultColor; } so.ApplyModifiedPropertiesWithoutUndo(); } // ══════════════════════════════════════════════════════════════════════ // 放置 + 存盘为 Prefab + 注册 Addressable // // 规范:敌人/弹体 Prefab 须落到 Prefabs/ 下并注册 Addressable // (地址 = 文件名;分组/标签由 AddressableRules 推导,与校验器一致)。 // 复用上方各 PlaceE00X / PlaceChaoFeng 搭建逻辑,确保场景搭建与 Prefab 产出同源。 // 菜单:BaseGames → Scene → Save Prefab → … // ══════════════════════════════════════════════════════════════════════ private const string EnemyPrefabRoot = "Assets/_Game/Prefabs/Enemies"; private const string ProjectilePrefabFolder = "Assets/_Game/Prefabs/Combat/Projectiles"; /// 敌人 Prefab 存盘目标:键 = 根对象名(= 地址),值 = (目标文件夹, 规范标签之外的额外标签)。 private static readonly Dictionary EnemyPrefabTargets = new() { { "ENM_CaoZhi", (EnemyPrefabRoot + "/E001", null) }, { "ENM_HuangZhi", (EnemyPrefabRoot + "/E002", null) }, // E003 幼蛭可被 E005 死亡时对象池生成 → 规范 Enemy 标签之外额外加 Poolable + Preload { "ENM_YouZhi", (EnemyPrefabRoot + "/E003", new[] { AddressKeys.Labels.Poolable, AddressKeys.Labels.Preload }) }, { "ENM_ZhiMu", (EnemyPrefabRoot + "/E004", null) }, { "ENM_FeiZhi", (EnemyPrefabRoot + "/E005", null) }, { "ENM_Huan", (EnemyPrefabRoot + "/E006", null) }, { "ENM_ChaoFeng", (EnemyPrefabRoot + "/ChaoFeng", null) }, }; /// 弹体 Prefab 存盘目标:键 = 地址(= PoolKey),值 = Projectile 子类类型。 private static readonly Dictionary ProjectileTypes = new() { { "PROJ_Boomerang", typeof(ReturnProjectile) }, // 回旋扇(往返) { "PROJ_ZhiMu_Acid", typeof(ArcProjectile) }, // E004 酸液(抛物线) { "PROJ_FeiZhi_Acid", typeof(ArcProjectile) }, // E005 酸液(抛物线) { "PROJ_WindStone", typeof(ArcProjectile) }, // 嘲风风石(抛物/落体) { "PROJ_TornadoSmall", typeof(LinearProjectile) }, // 小龙卷(左右直线) { "PROJ_TornadoLarge", typeof(LinearProjectile) }, // 大龙卷(定点,速度由 config 控) }; // ── 各敌人「放置 + 存盘」菜单 ───────────────────────────────────────── [MenuItem("BaseGames/Scene/Save Prefab/Enemy E001 (草蛭)", priority = 200)] public static void SaveE001Prefab() => PlaceAndSaveEnemyPrefab("ENM_CaoZhi", PlaceE001_CaoZhi); [MenuItem("BaseGames/Scene/Save Prefab/Enemy E002 (簧蛭)", priority = 201)] public static void SaveE002Prefab() => PlaceAndSaveEnemyPrefab("ENM_HuangZhi", PlaceE002_HuangZhi); [MenuItem("BaseGames/Scene/Save Prefab/Enemy E003 (幼蛭)", priority = 202)] public static void SaveE003Prefab() => PlaceAndSaveEnemyPrefab("ENM_YouZhi", PlaceE003_YouZhi_Enemy); [MenuItem("BaseGames/Scene/Save Prefab/Enemy E004 (蛭母)", priority = 203)] public static void SaveE004Prefab() => PlaceAndSaveEnemyPrefab("ENM_ZhiMu", PlaceE004_ZhiMu_Enemy); [MenuItem("BaseGames/Scene/Save Prefab/Enemy E005 (肥蛭)", priority = 204)] public static void SaveE005Prefab() => PlaceAndSaveEnemyPrefab("ENM_FeiZhi", PlaceE005_FeiZhi_Enemy); [MenuItem("BaseGames/Scene/Save Prefab/Enemy E006 (讙)", priority = 205)] public static void SaveE006Prefab() => PlaceAndSaveEnemyPrefab("ENM_Huan", PlaceE006_Huan); [MenuItem("BaseGames/Scene/Save Prefab/Boss 嘲风 (ChaoFeng)", priority = 206)] public static void SaveChaoFengPrefab() => PlaceAndSaveEnemyPrefab("ENM_ChaoFeng", PlaceChaoFeng); [MenuItem("BaseGames/Scene/Save Prefab/★ All Enemies + Boss", priority = 210)] public static void SaveAllEnemyPrefabs() { PlaceAndSaveEnemyPrefab("ENM_CaoZhi", PlaceE001_CaoZhi, removeSceneInstance: true); PlaceAndSaveEnemyPrefab("ENM_HuangZhi", PlaceE002_HuangZhi, removeSceneInstance: true); PlaceAndSaveEnemyPrefab("ENM_YouZhi", PlaceE003_YouZhi_Enemy, removeSceneInstance: true); PlaceAndSaveEnemyPrefab("ENM_ZhiMu", PlaceE004_ZhiMu_Enemy, removeSceneInstance: true); PlaceAndSaveEnemyPrefab("ENM_FeiZhi", PlaceE005_FeiZhi_Enemy, removeSceneInstance: true); PlaceAndSaveEnemyPrefab("ENM_Huan", PlaceE006_Huan, removeSceneInstance: true); PlaceAndSaveEnemyPrefab("ENM_ChaoFeng", PlaceChaoFeng, removeSceneInstance: true); AssetDatabase.SaveAssets(); Debug.Log("[SceneObjectPlacer] 已批量生成全部敌人/Boss Prefab 并注册 Addressable。"); } // ── 各弹体「放置 + 存盘」菜单 ───────────────────────────────────────── [MenuItem("BaseGames/Scene/Save Prefab/Projectile PROJ_Boomerang", priority = 220)] public static void SaveBoomerangPrefab() => PlaceAndSaveProjectile("PROJ_Boomerang"); [MenuItem("BaseGames/Scene/Save Prefab/Projectile PROJ_ZhiMu_Acid", priority = 221)] public static void SaveZhiMuAcidPrefab() => PlaceAndSaveProjectile("PROJ_ZhiMu_Acid"); [MenuItem("BaseGames/Scene/Save Prefab/Projectile PROJ_FeiZhi_Acid", priority = 222)] public static void SaveFeiZhiAcidPrefab() => PlaceAndSaveProjectile("PROJ_FeiZhi_Acid"); [MenuItem("BaseGames/Scene/Save Prefab/Projectile PROJ_WindStone", priority = 223)] public static void SaveWindStonePrefab() => PlaceAndSaveProjectile("PROJ_WindStone"); [MenuItem("BaseGames/Scene/Save Prefab/Projectile PROJ_TornadoSmall", priority = 224)] public static void SaveTornadoSmallPrefab() => PlaceAndSaveProjectile("PROJ_TornadoSmall"); [MenuItem("BaseGames/Scene/Save Prefab/Projectile PROJ_TornadoLarge", priority = 225)] public static void SaveTornadoLargePrefab() => PlaceAndSaveProjectile("PROJ_TornadoLarge"); [MenuItem("BaseGames/Scene/Save Prefab/★ All Projectiles", priority = 230)] public static void SaveAllProjectilePrefabs() { foreach (var key in ProjectileTypes.Keys) PlaceAndSaveProjectile(key, removeSceneInstance: true); AssetDatabase.SaveAssets(); Debug.Log("[SceneObjectPlacer] 已批量生成全部弹体 Prefab 并注册 Addressable。"); } private const string ProjectileConfigFolder = "Assets/_Game/Data/Combat/Projectiles"; /// /// 弹体配置默认值:键 = PoolKey(= Prefab 地址),值 = (Speed, Lifetime, LaunchAngleDeg, GravityScale)。 /// ArcProjectile 用 LaunchAngleDeg + GravityScale 形成抛物线;LinearProjectile 用 Speed 直线; /// ReturnProjectile(回旋扇)无重力、速度由弹体脚本控制。数值为占位,策划可在 Inspector 调整。 /// private static readonly Dictionary ProjectileConfigDefs = new() { { "PROJ_Boomerang", (10f, 6f, 0f, 0f) }, // 回旋扇:直线飞出,脚本控制往返 { "PROJ_ZhiMu_Acid", ( 9f, 4f, 45f, 1f) }, // E004 酸液:抛物线 { "PROJ_FeiZhi_Acid", ( 9f, 4f, 45f, 1f) }, // E005 酸液:抛物线 { "PROJ_WindStone", ( 6f, 4f, -90f, 2.5f) }, // 风石:向下落体 { "PROJ_TornadoSmall", ( 7f, 4f, 0f, 0f) }, // 小龙卷:水平直线 { "PROJ_TornadoLarge", ( 0f, 5f, 0f, 0f) }, // 大龙卷:定点驻留 }; /// /// 创建 6 个 (PoolKey 与弹体 Prefab 地址一致), /// 绑定默认 DamageSource。配置 SO 经 Inspector 引用,不注册 Addressable。 /// [MenuItem("BaseGames/Scene/Save Prefab/★ Projectile Configs (SO)", priority = 231)] public static void CreateProjectileConfigs() { var report = new List(); var enemyDmg = FindFirstAsset("CMB_DS_EnemyBody") as DamageSourceSO; var bossDmg = FindFirstAsset("CMB_DS_BossBody", "CMB_DS_EnemyBody") as DamageSourceSO; foreach (var kv in ProjectileConfigDefs) { string poolKey = kv.Key; var cfg = EditorScaffoldUtils.CreateSOAsset(ProjectileConfigFolder, $"{poolKey}_Config"); if (cfg == null) cfg = AssetDatabase.LoadAssetAtPath($"{ProjectileConfigFolder}/{poolKey}_Config.asset"); if (cfg == null) { report.Add($"✗ 创建失败:{poolKey}_Config"); continue; } cfg.PoolKey = poolKey; cfg.Speed = kv.Value.speed; cfg.Lifetime = kv.Value.lifetime; cfg.LaunchAngleDeg = kv.Value.launchAngle; cfg.GravityScale = kv.Value.gravity; cfg.DamageSource = poolKey.Contains("Acid") ? enemyDmg : bossDmg; EditorUtility.SetDirty(cfg); report.Add($"✅ {poolKey}_Config (PoolKey={poolKey}, spd={kv.Value.speed}, grav={kv.Value.gravity})"); } AssetDatabase.SaveAssets(); Debug.Log("[SceneObjectPlacer] ProjectileConfigSO 创建完成。\n " + string.Join("\n ", report)); } /// /// 调用现有 PlaceE00X / PlaceChaoFeng 在场景中搭建敌人,随后存盘为 Prefab 并注册 Addressable。 /// /// 根对象名(同时作为 Prefab 文件名与 Addressable 地址)。 /// 现有的放置委托。 /// true 时存盘后删除场景实例(批量生成时避免堆叠)。 public static void PlaceAndSaveEnemyPrefab(string key, System.Action placer, bool removeSceneInstance = false) { if (!EnemyPrefabTargets.TryGetValue(key, out var target)) { Debug.LogError($"[SceneObjectPlacer] 未登记的敌人 Prefab 键:{key}"); return; } placer(); GameObject root = Selection.activeGameObject; if (root == null) { Debug.LogError($"[SceneObjectPlacer] {key}:放置后未取得根对象,已跳过 Prefab 存盘。"); return; } var report = new List(); string prefabPath = SaveRootAsPrefab(root, key, target.folder, target.extraLabels, report); if (removeSceneInstance && !string.IsNullOrEmpty(prefabPath)) { var sc = root.scene; Undo.DestroyObjectImmediate(root); if (sc.IsValid()) UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(sc); } else if (root != null) { EditorUtility.SetDirty(root); UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(root.scene); } Debug.Log($"[SceneObjectPlacer] {key} Prefab 流程完成。\n " + string.Join("\n ", report)); } /// /// 搭建一个弹体 GameObject(Rigidbody2D + Collider(trigger) + HitBox + PooledObject + 指定 Projectile 子类), /// 存盘为 Prefab 并注册 Addressable。运行时由发射方 Initialize(ProjectileConfigSO,...) 注入速度/重力/伤害源。 /// public static void PlaceAndSaveProjectile(string key, bool removeSceneInstance = false) { if (!ProjectileTypes.TryGetValue(key, out var projType)) { Debug.LogError($"[SceneObjectPlacer] 未登记的弹体键:{key}"); return; } var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName($"Place {key}"); GameObject go = new GameObject(key); Undo.RegisterCreatedObjectUndo(go, $"Place {key}"); go.transform.position = GetDropPosition(); SetLayer(go, "EnemyProjectile", report); Rigidbody2D rb = GetOrAddComponent(go); rb.bodyType = RigidbodyType2D.Dynamic; // ArcProjectile 运行时按 config 设置 gravityScale rb.gravityScale = 0f; // 默认无重力(直线/回旋扇);抛物线弹由 config 注入 rb.constraints = RigidbodyConstraints2D.FreezeRotation; rb.interpolation = RigidbodyInterpolation2D.Interpolate; CircleCollider2D col = GetOrAddComponent(go); col.isTrigger = true; col.radius = 0.25f; HitBox hitBox = GetOrAddComponent(go); // Projectile [RequireComponent(HitBox)] GetOrAddComponent(go); // 对象池归还所需 Undo.AddComponent(go, projType); // Projectile 子类 SetupSpriteRenderer(go); Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody"); if (dmgSrc != null) AssignReference(hitBox, "_defaultSource", dmgSrc, report); else report.Add("未找到 CMB_DS_EnemyBody;HitBox._defaultSource 未绑定(运行时也会用 ProjectileConfigSO.DamageSource)。"); report.Add($"弹体类型:{projType.Name};Layer=EnemyProjectile;已挂 HitBox + PooledObject。"); report.Add($"★ 创建对应 ProjectileConfigSO,其 PoolKey 必须 = \"{key}\"(与地址一致)。"); report.Add("★ 速度/重力/伤害源由发射方在 Initialize(ProjectileConfigSO,...) 时注入,无需序列化到 Prefab。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; string prefabPath = SaveRootAsPrefab(go, key, ProjectilePrefabFolder, null, report); if (removeSceneInstance && !string.IsNullOrEmpty(prefabPath)) Undo.DestroyObjectImmediate(go); Debug.Log($"[SceneObjectPlacer] {key} 弹体 Prefab 流程完成。\n " + string.Join("\n ", report)); } /// /// 将场景根对象存盘为 Prefab(已存在则弹窗确认覆盖)并注册 Addressable,返回 Prefab 路径(失败/取消返回 null)。 /// private static string SaveRootAsPrefab(GameObject root, string fileName, string folder, string[] extraLabels, List report) { EditorScaffoldUtils.EnsureFolder(folder); AssetDatabase.Refresh(); string prefabPath = $"{folder}/{fileName}.prefab"; if (System.IO.File.Exists(prefabPath) && !EditorUtility.DisplayDialog("Prefab 已存在", $"{prefabPath}\n已存在,覆盖?", "覆盖", "取消")) { report.Add($"用户取消覆盖:{prefabPath}"); return null; } GameObject prefab = PrefabUtility.SaveAsPrefabAssetAndConnect(root, prefabPath, InteractionMode.UserAction); if (prefab == null) { report.Add($"✗ Prefab 存盘失败:{prefabPath}"); return null; } report.Add($"✅ 已存盘 Prefab:{prefabPath}"); AddressableRegistrar.Register(prefabPath, fileName, extraLabels, report); return prefabPath; } [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); // 自动生成唯一 _savePointId(场景名 + 短 GUID),避免手动填写遗漏导致存档点无法定位/复活 string sceneName = go.scene.IsValid() ? go.scene.name : "Scene"; string uid = System.Guid.NewGuid().ToString("N").Substring(0, 8); AssignString(savePoint, "_savePointId", $"SP_{sceneName}_{uid}", report); AssignAsset(savePoint, "_onSceneLoaded", report, false, "EVT_SceneLoaded"); AssignAsset(savePoint, "_onSavePointActivated", report, false, "EVT_SavePointActivated"); report.Add("已自动生成唯一 _savePointId(可按需改为语义化 ID,如 SP_Forest_Entrance)。"); 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/Door Transition", priority = 141)] public static void PlaceDoorTransition() { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place Door Transition"); GameObject go = new GameObject("DoorTransition"); Undo.RegisterCreatedObjectUndo(go, "Place Door Transition"); go.transform.position = GetDropPosition(); SetLayer(go, "TriggerZone", report); // 触发碰撞体(门宽×门高) BoxCollider2D col = GetOrAddComponent(go); col.isTrigger = true; col.size = new Vector2(1.5f, 2.5f); // 精灵渲染器 + 动画(门对象通常有外观与开关动画) SetupSpriteRenderer(go); GetOrAddComponent(go); AnimancerComponent animancer = GetOrAddComponent(go); // DoorTransition 组件 DoorTransition door = GetOrAddComponent(go); AssignBool(door, "_autoTrigger", false); // 默认需玩家按交互键 AssignReference(door, "_animancer", animancer, report); AssignAsset(door, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest"); report.Add("填写 _targetSceneAddress(目标场景 Addressable Key)与 _targetTransitionId(目标 PlayerSpawnPoint 的 _transitionId)。"); report.Add("将开门动画片段拖入 _openClip;若目标场景有玩家从门中走出的动画,拖入 _enterClip 并在目标场景 PlayerSpawnPoint._exitDoor 引用该侧的 DoorTransition。"); report.Add("过渡类型默认 Room(极短淡出);若跨大区域,将 _transitionType 改为 Scene。"); report.Add("若需钥匙解锁,设 _requiresKeyItem = true 并填写 _requiredItemId。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; MarkDirtyAndLog("Door Transition", go, report); } [MenuItem("BaseGames/Scene/Place/Linked Door Pair (Same-Scene)", priority = 142)] public static void PlaceLinkedDoorPair() { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place Linked Door Pair"); Vector3 basePos = GetDropPosition(); // ── 共同父节点 ──────────────────────────────────────────────────── GameObject parent = new GameObject("LinkedDoorPair"); Undo.RegisterCreatedObjectUndo(parent, "Place LinkedDoorPair Root"); parent.transform.position = basePos; // ── 门 A ───────────────────────────────────────────────────────── GameObject goA = new GameObject("LinkedDoor_A"); Undo.RegisterCreatedObjectUndo(goA, "Place LinkedDoor_A"); Undo.SetTransformParent(goA.transform, parent.transform, "Parent LinkedDoor_A"); goA.transform.position = basePos + new Vector3(-3f, 0f, 0f); SetLayer(goA, "TriggerZone", report); BoxCollider2D colA = GetOrAddComponent(goA); colA.isTrigger = true; colA.size = new Vector2(1.5f, 2.5f); Transform spawnA = GetOrCreateChild(goA.transform, "SpawnPoint"); spawnA.localPosition = new Vector3(1.2f, 0f, 0f); // 门右侧走出 LinkedDoorTransition doorA = GetOrAddComponent(goA); AssignBool(doorA, "_autoTrigger", true); AssignReference(doorA, "_spawnPoint", spawnA, report); AssignInt(doorA, "_facingDirectionOnArrive", 1); // 朝右走出 // ── 门 B ───────────────────────────────────────────────────────── GameObject goB = new GameObject("LinkedDoor_B"); Undo.RegisterCreatedObjectUndo(goB, "Place LinkedDoor_B"); Undo.SetTransformParent(goB.transform, parent.transform, "Parent LinkedDoor_B"); goB.transform.position = basePos + new Vector3(3f, 0f, 0f); SetLayer(goB, "TriggerZone", report); BoxCollider2D colB = GetOrAddComponent(goB); colB.isTrigger = true; colB.size = new Vector2(1.5f, 2.5f); Transform spawnB = GetOrCreateChild(goB.transform, "SpawnPoint"); spawnB.localPosition = new Vector3(-1.2f, 0f, 0f); // 门左侧走出 LinkedDoorTransition doorB = GetOrAddComponent(goB); AssignBool(doorB, "_autoTrigger", true); AssignReference(doorB, "_spawnPoint", spawnB, report); AssignInt(doorB, "_facingDirectionOnArrive", -1); // 朝左走出 // ── 互相绑定 ───────────────────────────────────────────────────── AssignReference(doorA, "_linkedDoor", doorB, report); AssignReference(doorB, "_linkedDoor", doorA, report); report.Add("LinkedDoor_A ↔ LinkedDoor_B 已互相绑定,统一挂在 LinkedDoorPair 父节点下。"); report.Add("将两扇门移到场景中正确位置后,拖动各自的子节点 SpawnPoint 调整玩家传送到达位置。"); report.Add("转场效果:在各门 GameObject 上添加 SceneFeedback 组件并绑定 MMF_Player(如淡入淡出),再将其拖入 _transitionOut(淡出)和 _transitionIn(淡入)字段。"); report.Add("_facingDirectionOnArrive:A→B 时玩家朝向由 B 的该值决定,B→A 反之。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = parent; MarkDirtyAndLog("Linked Door Pair (Same-Scene)", parent, 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, "Platform", 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 root = new GameObject("MovingPlatform_Root"); Undo.RegisterCreatedObjectUndo(root, "Place Moving Platform"); root.transform.position = GetDropPosition(); // 平台实体:作为 root 子节点 GameObject go = GetOrCreateChild(root.transform, "MovingPlatform").gameObject; SetLayer(go, "Platform", 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); // 路径点:挂在 root 下而非平台下,平台移动时路径点位置不变 Transform wpA = GetOrCreateChild(root.transform, "WaypointA"); Transform wpB = GetOrCreateChild(root.transform, "WaypointB"); wpA.position = root.transform.position + new Vector3(-3f, 0f, 0f); wpB.position = root.transform.position + new Vector3( 3f, 0f, 0f); MovingPlatform platform = GetOrAddComponent(go); AssignReference(platform, "_passengerSensor", sensorCol, report); AssignLayerMask(platform, "_passengerLayer", new[] { "Player", "Enemy" }, report); AssignObjectArray(platform, "_wayPoints", new Object[] { wpA, wpB }, report); report.Add("WaypointA / WaypointB 已挂在 MovingPlatform_Root 下(非平台子节点),平台移动时路径点保持原位。"); report.Add("在场景中调整 WaypointA / WaypointB 的世界位置即可设置移动端点。"); report.Add("如需触发激活,改 _moveType = TriggeredLinear 并将 VoidEventChannelSO 拖入 _activationChannel。"); Selection.activeGameObject = root; MarkDirtyAndLog("Moving Platform", root, 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, "Platform", 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, "Platform", 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 Collider2D CreateBodyCollider(GameObject go, EnemyBodyColliderType type, Vector2 size) { switch (type) { case EnemyBodyColliderType.Capsule: var cap = GetOrAddComponent(go); cap.size = size; return cap; case EnemyBodyColliderType.Circle: var cir = GetOrAddComponent(go); cir.radius = Mathf.Min(size.x, size.y) * 0.5f; return cir; default: // Box var box = GetOrAddComponent(go); box.size = size; return box; } } 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(); } /// 将多个 Layer 名称合并为一个 LayerMask 并写入 SerializedProperty。 private static void AssignLayerMask(Object target, string propName, string[] layerNames, List report) { int mask = 0; foreach (var name in layerNames) { int layer = LayerMask.NameToLayer(name); if (layer == -1) report.Add($"Layer '{name}' 不存在,已跳过({target.GetType().Name}.{propName})。"); else mask |= 1 << layer; } if (mask == 0) return; var so = new SerializedObject(target); var sp = so.FindProperty(propName); if (sp == null) { report.Add($"{target.GetType().Name}.{propName} 字段不存在,跳过 LayerMask 赋值。"); return; } sp.intValue = mask; 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(); } /// /// 为 MeleeAttackAbility._hitBoxSlots 赋值(struct 数组 {slotName, hitBox})。 /// private static void AssignMeleeHitBoxSlots(MeleeAttackAbility ability, (string slot, HitBox hb)[] slots, List report) { if (ability == null) return; var so = new SerializedObject(ability); var prop = so.FindProperty("_hitBoxSlots"); if (prop == null || !prop.isArray) { report?.Add($"[WARN] MeleeAttackAbility._hitBoxSlots 属性未找到,请检查字段名。"); return; } prop.arraySize = slots.Length; for (int i = 0; i < slots.Length; i++) { var elem = prop.GetArrayElementAtIndex(i); elem.FindPropertyRelative("slotName").stringValue = slots[i].slot; elem.FindPropertyRelative("hitBox").objectReferenceValue = slots[i].hb; } so.ApplyModifiedPropertiesWithoutUndo(); report?.Add($"[OK] MeleeAttackAbility._hitBoxSlots 已配置 {slots.Length} 个槽位。"); } 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} 已放置。"); } } }