using System.Collections.Generic; using System.Reflection; using Animancer; using BaseGames.Boss; using BaseGames.Camera; using BaseGames.Combat; using BaseGames.Combat.StatusEffects; 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.Parry; using BaseGames.Player; using BaseGames.Player.States; using BaseGames.Skills; using BaseGames.World; using PathBerserker2d; using Unity.Cinemachine; using UnityEditor; using UnityEngine; using UnityEngine.Tilemaps; namespace BaseGames.Editor { /// /// 场景对象快速放置工具。 /// 在当前活动场景中生成常用游戏对象(玩家、敌人、机关、存档点、相机等), /// 并自动挂载基础组件、设置正确的物理层、绑定已有的事件频道资产。 /// /// 菜单:BaseGames → Scene → Place → … /// /// 所有操作支持 Undo(Ctrl+Z)。生成后选中对象便于立即调整位置。 /// public static class SceneObjectPlacerTool { // ══ 菜单入口 ══════════════════════════════════════════════════════════ [MenuItem("BaseGames/Scene/Place/Player", priority = 100)] public static void PlacePlayer() { var report = new List(); // ── Player 根节点(行为+物理+标签三合一)────────────────────────────── // Rigidbody2D / 所有 MonoBehaviour 集中于此节点。 // HurtBox 作为其子节点,GetComponentInParent() 向上即可找到 // 本节点上的 PlayerController(IDamageable 实现者)。 GameObject root = new GameObject("Player"); Undo.RegisterCreatedObjectUndo(root, "Place Player"); root.transform.position = GetDropPosition(); root.tag = "Player"; SetLayer(root, "Player", report); // 物理组件(PlayerMovement RequireComponent(Rigidbody2D),必须同节点) Rigidbody2D rb = GetOrAddComponent(root); rb.bodyType = RigidbodyType2D.Dynamic; rb.gravityScale = 2f; rb.constraints = RigidbodyConstraints2D.FreezeRotation; rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous; rb.interpolation = RigidbodyInterpolation2D.Interpolate; GetOrAddComponent(root); // 动画组件(AnimancerComponent 需要 Animator 存在;PlayerController // [RequireComponent(typeof(AnimancerComponent))] 保证其存在) GetOrAddComponent(root); 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); 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() { var report = new List(); GameObject go = new GameObject("BasicEnemy"); Undo.RegisterCreatedObjectUndo(go, "Place Enemy"); go.transform.position = GetDropPosition(); SetLayer(go, "Enemy", report); Rigidbody2D rb = GetOrAddComponent(go); rb.bodyType = RigidbodyType2D.Dynamic; rb.gravityScale = 2f; rb.constraints = RigidbodyConstraints2D.FreezeRotation; GetOrAddComponent(go); GetOrAddComponent(go); SetupSpriteRenderer(go); EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); // HurtBox child Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox"); SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report); CapsuleCollider2D hurtCollider = GetOrAddComponent(hurtBoxT.gameObject); hurtCollider.isTrigger = true; HurtBox hurtBox = GetOrAddComponent(hurtBoxT.gameObject); // Contact-damage HitBox child Transform hitBodyT = GetOrCreateChild(go.transform, "HitBox_Body"); SetLayer(hitBodyT.gameObject, "EnemyHitBox", report); CircleCollider2D hitCollider = GetOrAddComponent(hitBodyT.gameObject); hitCollider.isTrigger = true; hitCollider.radius = 0.55f; HitBox hitBox = GetOrAddComponent(hitBodyT.gameObject); GetOrAddComponent(hitBodyT.gameObject); // References AssignReference(enemyBase, "_stats", enemyStats, report); // DamageSourceSO for body contact (optional — create manually if missing) Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody", "DS_EnemyBody"); if (dmgSrc != null) AssignReference(hitBox, "_defaultSource", dmgSrc, report); else report.Add("未找到 DamageSourceSO,HitBox_Body._defaultSource 未绑定。请按规范创建 CMB_DS_EnemyBody.asset。"); // Event channels AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied"); AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned"); AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged"); AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt"); AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed"); // EnemyStatsSO (optional) Object enemyStatsSO = FindFirstAsset("BasicEnemyStats", "EnemyStatsSO"); if (enemyStatsSO != null) AssignReference(enemyBase, "_statsSO", enemyStatsSO, report); else report.Add("未找到 EnemyStatsSO,EnemyBase._statsSO 未绑定。请在 Data/Enemies/ 创建 ENM_{id}_Stats.asset 后手动指定。"); report.Add("行为树、导航参数(NavAgent)、动画片段需后续手工挂载。"); Selection.activeGameObject = go; MarkDirtyAndLog("Enemy (Basic)", go, report); } [MenuItem("BaseGames/Scene/Place/Boss Enemy", priority = 115)] public static void PlaceBossEnemy() { var report = new List(); GameObject go = new GameObject("BossEnemy"); Undo.RegisterCreatedObjectUndo(go, "Place Boss Enemy"); go.transform.position = GetDropPosition(); SetLayer(go, "Enemy", report); Rigidbody2D rb = GetOrAddComponent(go); rb.bodyType = RigidbodyType2D.Dynamic; rb.gravityScale = 2f; rb.constraints = RigidbodyConstraints2D.FreezeRotation; rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous; rb.interpolation = RigidbodyInterpolation2D.Interpolate; GetOrAddComponent(go); GetOrAddComponent(go); SetupSpriteRenderer(go); BossBase bossBase = GetOrAddComponent(go); EnemyStats bossStats = GetOrAddComponent(go); // HurtBox child Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox"); SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report); CapsuleCollider2D hurtCollider = GetOrAddComponent(hurtBoxT.gameObject); hurtCollider.isTrigger = true; hurtCollider.size = new Vector2(1.5f, 2.5f); HurtBox hurtBox = GetOrAddComponent(hurtBoxT.gameObject); // Contact-damage HitBox child Transform hitBodyT = GetOrCreateChild(go.transform, "HitBox_Body"); SetLayer(hitBodyT.gameObject, "EnemyHitBox", report); CircleCollider2D hitCollider = GetOrAddComponent(hitBodyT.gameObject); hitCollider.isTrigger = true; hitCollider.radius = 0.9f; HitBox hitBox = GetOrAddComponent(hitBodyT.gameObject); GetOrAddComponent(hitBodyT.gameObject); // References AssignReference(bossBase, "_stats", bossStats, report); // DamageSourceSO Object dmgSrc = FindFirstAsset("CMB_DS_BossBody", "CMB_DS_EnemyBody", "DS_BossBody"); if (dmgSrc != null) AssignReference(hitBox, "_defaultSource", dmgSrc, report); else report.Add("未找到 DamageSourceSO,HitBox_Body._defaultSource 未绑定。请按规范创建 CMB_DS_BossBody.asset。"); // Event channels AssignAsset(bossBase, "_onEnemyDied", report, false, "EVT_EnemyDied"); AssignAsset(bossBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned"); AssignAsset(bossBase, "_onBossFightEnded", report, false, "EVT_BossFightEnded"); AssignAsset(bossBase, "_onBossPhaseChanged", report, false, "EVT_BossPhaseChanged"); AssignAsset(bossStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged"); AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt"); AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed"); report.Add("填写 _bossId。"); report.Add("挂载 BossSkillSequencer 组件并指定技能序列 SO;行为树、NavAgent 需手工添加。"); report.Add("多阶段 Boss 可在此 GameObject 上继续 AddComponent 阶段切换控制器。"); Selection.activeGameObject = go; MarkDirtyAndLog("Boss Enemy", go, report); } // ══ 具体敌人快速放置 ════════════════════════════════════════════════════ [MenuItem("BaseGames/Scene/Place/Enemy E001 (草蛭)", priority = 111)] public static void PlaceE001_CaoZhi() { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place E001 草蛭"); 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; CapsuleCollider2D body = GetOrAddComponent(go); body.size = new Vector2(0.6f, 0.8f); GetOrAddComponent(go); AnimancerComponent animancer = GetOrAddComponent(go); SpriteRenderer sr1 = SetupSpriteRenderer(go); EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); EnemySensorHub 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, "_animancer", animancer, report); AssignReference(movement, "_spriteRenderer", sr1, report); AssignAsset(alertAbility, "_config", report, false, "ABL_E001_Alert"); AssignAsset(chaseAbility, "_config", report, false, "ABL_E001_Chase"); AssignReference(chaseAbility, "_contactDamage", bodyContact, report); AssignReference(chaseAbility, "_sensorHub", sensorHub, report); Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody"); if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report); SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "wall_ahead", "ledge" }, report); report.Add("★ 在 EnemySensorHub Inspector 中绑定 Sensor 子节点(aggro/wall_ahead/ledge)。"); report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E001_CaoZhi.asset。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; MarkDirtyAndLog("E001 草蛭", go, report); } [MenuItem("BaseGames/Scene/Place/Enemy E002 (簧蛭)", priority = 112)] public static void PlaceE002_HuangZhi() { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place E002 簧蛭"); 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; CapsuleCollider2D body = GetOrAddComponent(go); body.size = new Vector2(0.5f, 0.7f); GetOrAddComponent(go); AnimancerComponent animancer = GetOrAddComponent(go); SetupSpriteRenderer(go); EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemySensorHub 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(0.45f, 0.65f); HurtBox hurtBox = GetOrAddComponent(hurtBoxT.gameObject); hurtBoxT.gameObject.SetActive(false); // AttackHitBox(下坠发动时由能力启用) Transform atkT = GetOrCreateChild(go.transform, "AttackHitBox"); SetLayer(atkT.gameObject, "EnemyHitBox", report); BoxCollider2D atkCol = GetOrAddComponent(atkT.gameObject); atkCol.isTrigger = true; atkCol.size = new Vector2(0.5f, 0.5f); HitBox atkHitBox = GetOrAddComponent(atkT.gameObject); atkT.gameObject.SetActive(false); 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, "_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(strikeAbility, "_config", report, false, "ABL_E002_Strike"); AssignReference(strikeAbility, "_attackHitBox", atkHitBox, report); AssignReference(strikeAbility, "_hurtBox", hurtBox, report); Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody"); if (dmgSrc != null) AssignReference(atkHitBox, "_defaultSource", dmgSrc, report); SetupSensorHubSlotNames(sensorHub, new[] { "attack_range" }, report); report.Add("★ 将此对象放置于天花板,调整位置使 CapsuleCollider 正好贴合天花板底面。"); report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E002_HuangZhi.asset。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; MarkDirtyAndLog("E002 簧蛭", go, report); } [MenuItem("BaseGames/Scene/Place/Enemy E003 (幼蛭)", priority = 113)] public static void PlaceE003_YouZhi_Enemy() { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place E003 幼蛭"); 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; CapsuleCollider2D body = GetOrAddComponent(go); body.size = new Vector2(0.5f, 0.6f); GetOrAddComponent(go); AnimancerComponent animancer = GetOrAddComponent(go); SpriteRenderer sr3 = SetupSpriteRenderer(go); E003_YouZhi enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); EnemySensorHub 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, "_animancer", animancer, report); AssignReference(movement, "_spriteRenderer", sr3, 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); SetupSensorHubSlotNames(sensorHub, new[] { "aggro" }, report); report.Add("★ 将此对象放置于天花板下方,E003_YouZhi 会在 OnSpawn/ActivateFromCeiling 时执行下坠。"); report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E003_YouZhi.asset。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; MarkDirtyAndLog("E003 幼蛭", go, report); } [MenuItem("BaseGames/Scene/Place/Enemy E004 (蛭母)", priority = 114)] public static void PlaceE004_ZhiMu_Enemy() { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place E004 蛭母"); 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; CapsuleCollider2D body = GetOrAddComponent(go); body.size = new Vector2(0.8f, 1.2f); GetOrAddComponent(go); AnimancerComponent animancer = GetOrAddComponent(go); SpriteRenderer sr4 = SetupSpriteRenderer(go); E004_ZhiMu enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyFeedback feedback = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); EnemySensorHub 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 chargeHitBoxT = GetOrCreateChild(go.transform, "ChargeHitBox"); SetLayer(chargeHitBoxT.gameObject, "EnemyHitBox", report); BoxCollider2D chargeHitCol = GetOrAddComponent(chargeHitBoxT.gameObject); chargeHitCol.isTrigger = true; chargeHitCol.size = new Vector2(0.9f, 0.8f); HitBox chargeHitBox = GetOrAddComponent(chargeHitBoxT.gameObject); chargeHitBoxT.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 slamAblT = GetOrCreateChild(abilitiesT, "RepeatSlamAbility"); RepeatSlamAbility slamAbl = GetOrAddComponent(slamAblT.gameObject); Transform acidAblT = GetOrCreateChild(abilitiesT, "ProjectileAttackAbility_Acid"); ProjectileAttackAbility acidAbl = GetOrAddComponent(acidAblT.gameObject); Transform chargeAblT = GetOrCreateChild(abilitiesT, "ChargeAbility"); ChargeAbility chargeAbl = GetOrAddComponent(chargeAblT.gameObject); Transform chaseAblT = GetOrCreateChild(abilitiesT, "ContactChaseAbility"); ContactChaseAbility chaseAbl = GetOrAddComponent(chaseAblT.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, "_animancer", animancer, report); AssignReference(movement, "_spriteRenderer", sr4, report); AssignAsset(biteAbl, "_config", report, false, "ABL_E004_Bite"); AssignAsset(slamAbl, "_config", report, false, "ABL_E004_Slam"); AssignAsset(acidAbl, "_config", report, false, "ABL_E004_Acid"); AssignAsset(chargeAbl, "_config", report, false, "ABL_E004_Charge"); AssignAsset(chaseAbl, "_config", report, false, "ABL_E004_Chase"); AssignMeleeHitBoxSlots(biteAbl, new[] { ("bite", biteHitBox) }, report); AssignReference(slamAbl, "_hitBox", slamHitBox, report); AssignReference(acidAbl, "_muzzle", acidMuzzleT, report); AssignReference(chargeAbl, "_chargeHitBox", chargeHitBox, report); AssignReference(chaseAbl, "_sensorHub", sensorHub, report); Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody"); if (dmgSrc != null) { AssignReference(biteHitBox, "_defaultSource", dmgSrc, report); AssignReference(slamHitBox, "_defaultSource", dmgSrc, report); AssignReference(chargeHitBox, "_defaultSource", dmgSrc, report); } SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "los" }, report); report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E004_ZhiMu.asset。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; MarkDirtyAndLog("E004 蛭母", go, report); } [MenuItem("BaseGames/Scene/Place/Enemy E005 (肥蛭)", priority = 115)] public static void PlaceE005_FeiZhi_Enemy() { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place E005 肥蛭"); 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; CapsuleCollider2D body = GetOrAddComponent(go); body.size = new Vector2(0.9f, 1.0f); GetOrAddComponent(go); AnimancerComponent animancer = GetOrAddComponent(go); SpriteRenderer sr5 = SetupSpriteRenderer(go); E005_FeiZhi enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); EnemySensorHub 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, "_animancer", animancer, report); AssignReference(movement, "_spriteRenderer", sr5, 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); SetupSensorHubSlotNames(sensorHub, new[] { "aggro" }, report); report.Add("★ 在 E005_FeiZhi._deathPreClip 上添加 AnimationEvent 调用 SpawnProjectile(\"spawn_e003\")。"); report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E005_FeiZhi.asset。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; MarkDirtyAndLog("E005 肥蛭", go, report); } [MenuItem("BaseGames/Scene/Place/Enemy E006 (讙)", priority = 116)] public static void PlaceE006_Huan() { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place E006 讙"); 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; CapsuleCollider2D body = GetOrAddComponent(go); body.size = new Vector2(0.7f, 1.0f); GetOrAddComponent(go); AnimancerComponent animancer = GetOrAddComponent(go); SpriteRenderer sr6 = SetupSpriteRenderer(go); EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); EnemySensorHub 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, "_animancer", animancer, report); AssignReference(movement, "_spriteRenderer", sr6, 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); AssignReference(chaseAbl, "_sensorHub", sensorHub, report); Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody"); if (dmgSrc != null) { AssignReference(contactHitBox, "_defaultSource", dmgSrc, report); AssignReference(landHitBox, "_defaultSource", dmgSrc, report); } SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "wall_ahead", "ledge" }, report); report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E006_Huan.asset。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; MarkDirtyAndLog("E006 讙", go, report); } [MenuItem("BaseGames/Scene/Place/Boss 嘲风 (ChaoFeng)", priority = 117)] public static void PlaceChaoFeng() { var report = new List(); int undoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName("Place Boss 嘲风"); 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; CapsuleCollider2D body = GetOrAddComponent(go); body.size = new Vector2(1.2f, 2.0f); GetOrAddComponent(go); AnimancerComponent animancer = GetOrAddComponent(go); SpriteRenderer srBoss = SetupSpriteRenderer(go); ChaoFengBoss bossBase = GetOrAddComponent(go); EnemyStats bossStats = GetOrAddComponent(go); EnemyFeedback feedback = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); GetOrAddComponent(go); BossSkillExecutor skillExec = GetOrAddComponent(go); ChaoFengFloatController floatCtrl = GetOrAddComponent(go); ChaoFengKnockdownCounter knockdown = GetOrAddComponent(go); EnemySensorHub 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 attack hitboxes (disabled by default; abilities enable/disable as needed) HitBox biteHB = CreateDisabledHitBox(go.transform, "Phase1_BiteHitBox", "EnemyHitBox", true, report, size: new Vector2(0.8f, 0.5f)); HitBox swipeR = CreateDisabledHitBox(go.transform, "Phase1_SwipeHitBox_R","EnemyHitBox", true, report, size: new Vector2(1.2f, 0.4f)); HitBox swipeL = CreateDisabledHitBox(go.transform, "Phase1_SwipeHitBox_L","EnemyHitBox", true, report, size: new Vector2(1.2f, 0.4f)); HitBox stompHB = CreateDisabledHitBox(go.transform, "Phase1_StompHitBox", "EnemyHitBox", false, report, radius: 1.0f); // Muzzle transforms for Phase 2 skills GetOrCreateChild(go.transform, "WindBladeMuzzle"); GetOrCreateChild(go.transform, "TornadoMuzzle"); GetOrCreateChild(go.transform, "SummonSpawnPoint"); // 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); 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, "_animancer", animancer, report); AssignReference(movement, "_spriteRenderer", srBoss, report); // Collect BossSkillSOs and assign to executor var skillAssets = new System.Collections.Generic.List(); foreach (var n in new[] { "ABL_ChaoFeng_Idle", "ABL_ChaoFeng_Slam", "ABL_ChaoFeng_Sweep", "ABL_ChaoFeng_WindBlade", "ABL_ChaoFeng_Summon" }) { Object sk = FindFirstAsset(n); if (sk != null) skillAssets.Add(sk); else report.Add($"未找到 BossSkillSO:{n},请先一键创建 SO 后再重新运行此放置操作。"); } if (skillAssets.Count > 0) AssignObjectArray(skillExec, "_skills", skillAssets.ToArray(), report); Object dmgSrc = FindFirstAsset("CMB_DS_BossBody", "CMB_DS_EnemyBody"); if (dmgSrc != null) { foreach (var hb in new[] { biteHB, swipeR, swipeL, stompHB }) if (hb != null) AssignReference(hb, "_defaultSource", dmgSrc, report); } SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report); report.Add("★ 设置 BossSkillExecutor._bossId = \"ChaoFeng\"。"); report.Add("★ 将各 Phase1 HitBox 引用拖入 BossSkillExecutor._hitBoxes 数组。"); report.Add("★ 将 WindBladeMuzzle / TornadoMuzzle / SummonSpawnPoint 拖入对应 BossSkillSO 字段。"); report.Add("★ 挂载行为树 BehaviorTree 组件,指定 Boss_ChaoFeng.asset。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; 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; } /// /// 设置 EnemySensorHub._slots 的 slotName 字段(Sensor 引用需在 Inspector 中手工绑定)。 /// private static void SetupSensorHubSlotNames(EnemySensorHub hub, string[] slotNames, List report) { var so = new SerializedObject(hub); var slots = so.FindProperty("_slots"); if (slots == null || !slots.isArray) { report?.Add("EnemySensorHub._slots 字段未找到,传感器槽位需手工配置。"); return; } slots.arraySize = slotNames.Length; for (int i = 0; i < slotNames.Length; i++) slots.GetArrayElementAtIndex(i).FindPropertyRelative("slotName").stringValue = slotNames[i]; so.ApplyModifiedPropertiesWithoutUndo(); } [MenuItem("BaseGames/Scene/Place/Hazard (LethalTrap)", priority = 120)] public static void PlaceLethalTrap() { var report = new List(); GameObject go = new GameObject("LethalTrap"); Undo.RegisterCreatedObjectUndo(go, "Place LethalTrap"); go.transform.position = GetDropPosition(); SetLayer(go, "EnemyHitBox", report); BoxCollider2D col = GetOrAddComponent(go); col.isTrigger = true; col.size = new Vector2(2f, 0.5f); SetupSpriteRenderer(go); LethalTrap trap = GetOrAddComponent(go); AssignLayerMask(trap, "_playerLayers", "PlayerHurtBox", report); AssignInt(trap, "_damage", 1); AssignBool(trap, "_canPogo", true); // Child HurtBox (EnemyHurtBox layer) to allow pogo when _canPogo = true Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox"); SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report); BoxCollider2D hurtCol = GetOrAddComponent(hurtBoxT.gameObject); hurtCol.isTrigger = true; hurtCol.size = new Vector2(2f, 0.3f); GetOrAddComponent(hurtBoxT.gameObject); AssignAsset(trap, "_onPlayerDied", report, false, "EVT_PlayerDied"); AssignAsset(trap, "_onCheckpointRespawn", report, false, "EVT_CheckpointRespawn"); report.Add("_canPogo=true:子 HurtBox 供玩家下劈弹起;设为 false 可改为纯死亡区(无需子 HurtBox)。"); Selection.activeGameObject = go; MarkDirtyAndLog("Hazard (LethalTrap)", go, report); } [MenuItem("BaseGames/Scene/Place/Checkpoint Marker", priority = 125)] public static void PlaceCheckpointMarker() { var report = new List(); GameObject go = new GameObject("CheckpointMarker"); Undo.RegisterCreatedObjectUndo(go, "Place CheckpointMarker"); go.transform.position = GetDropPosition(); SetLayer(go, "TriggerZone", report); BoxCollider2D col = GetOrAddComponent(go); col.isTrigger = true; col.size = new Vector2(1f, 2f); CheckpointMarker marker = GetOrAddComponent(go); AssignLayerMask(marker, "_playerLayers", "Player", report); AssignAsset(marker, "_onCheckpointReached", report, false, "EVT_CheckpointReached"); report.Add("放置于跳跳乐段落的关键节点处;玩家经过后成为该房间最近检查点。"); report.Add("同一房间可放置多个,以最近经过的为准。"); Selection.activeGameObject = go; MarkDirtyAndLog("Checkpoint Marker", go, report); } [MenuItem("BaseGames/Scene/Place/Collectible (LingZhu)", priority = 130)] public static void PlaceCollectible() { var report = new List(); GameObject go = new GameObject("Collectible_LingZhu"); Undo.RegisterCreatedObjectUndo(go, "Place Collectible"); go.transform.position = GetDropPosition(); Rigidbody2D rb = GetOrAddComponent(go); rb.gravityScale = 1f; rb.freezeRotation = true; rb.interpolation = RigidbodyInterpolation2D.Interpolate; CircleCollider2D col = GetOrAddComponent(go); col.isTrigger = true; col.radius = 0.3f; SetupSpriteRenderer(go); Collectible collectible = GetOrAddComponent(go); // CollectibleType.LingZhu = 0 AssignInt(collectible, "_type", 0); AssignInt(collectible, "_lingZhuAmount", 1); AssignBool(collectible, "_isPersistent", false); AssignAsset(collectible, "_onCollectiblePickup", report, false, "EVT_ItemPickup", "EVT_CollectiblePickup"); AssignAsset(collectible, "_onCollectibleSaved", report, false, "EVT_CollectibleSaved"); report.Add("若为场景固定摆放道具,设 _isPersistent = true 并填写唯一 _collectibleId。"); Selection.activeGameObject = go; MarkDirtyAndLog("Collectible (LingZhu)", go, report); } [MenuItem("BaseGames/Scene/Place/Save Point", priority = 130)] public static void PlaceSavePoint() { var report = new List(); GameObject go = new GameObject("SavePoint"); Undo.RegisterCreatedObjectUndo(go, "Place Save Point"); go.transform.position = GetDropPosition(); SetLayer(go, "TriggerZone", report); BoxCollider2D col = GetOrAddComponent(go); col.isTrigger = true; col.size = new Vector2(1f, 1.5f); SetupSpriteRenderer(go); SavePoint savePoint = GetOrAddComponent(go); AssignAsset(savePoint, "_onSceneLoaded", report, false, "EVT_SceneLoaded"); AssignAsset(savePoint, "_onSavePointActivated", report, false, "EVT_SavePointActivated"); report.Add("填写 _savePointId(全局唯一字符串,用于存档点激活记录与复活定位)。"); Selection.activeGameObject = go; MarkDirtyAndLog("Save Point", go, report); } [MenuItem("BaseGames/Scene/Place/Teleport Station", priority = 135)] public static void PlaceTeleportStation() { var report = new List(); GameObject go = new GameObject("TeleportStation"); Undo.RegisterCreatedObjectUndo(go, "Place TeleportStation"); go.transform.position = GetDropPosition(); SetLayer(go, "TriggerZone", report); BoxCollider2D col = GetOrAddComponent(go); col.isTrigger = true; col.size = new Vector2(1.5f, 2f); SetupSpriteRenderer(go); TeleportStation station = GetOrAddComponent(go); AssignAsset(station, "_onFastTravelOpen", report, false, "EVT_FastTravelOpen"); report.Add("填写 _stationId(传送站唯一 ID,用于地图 UI 标注)。"); report.Add("传送站不存档、不复活、不恢复 HP;与存档点是独立对象。"); Selection.activeGameObject = go; MarkDirtyAndLog("Teleport Station", go, report); } [MenuItem("BaseGames/Scene/Place/Room Transition", priority = 140)] public static void PlaceRoomTransition() { var report = new List(); GameObject go = new GameObject("RoomTransition"); Undo.RegisterCreatedObjectUndo(go, "Place Room Transition"); go.transform.position = GetDropPosition(); SetLayer(go, "TriggerZone", report); BoxCollider2D col = GetOrAddComponent(go); col.isTrigger = true; col.size = new Vector2(1f, 2.5f); RoomTransition transition = GetOrAddComponent(go); AssignString(transition, "_transitionId", "exit_default", report); AssignBool(transition, "_autoTrigger", true); AssignBool(transition, "_requiresKeyItem", false); AssignAsset(transition, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest"); report.Add("填写 _transitionId(本出口唯一 ID)、_targetSceneAddress(目标场景 Addressable Key)、_targetTransitionId(目标出生点 ID)。"); report.Add("若需锁门,设 _requiresKeyItem = true 并填写 _requiredItemId。"); report.Add("_worldState 字段需拖入 WorldStateRegistry SO(可选)。"); Selection.activeGameObject = go; MarkDirtyAndLog("Room Transition", go, report); } [MenuItem("BaseGames/Scene/Place/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 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} 已放置。"); } } }