using System.Collections.Generic;
using System.Reflection;
using Animancer;
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.Behaviors;
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 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);
// ── 交互探测器(检测最近可交互物 + 交互键调用 IInteractable.Interact + 驱动其世界空间提示)──
// 扫描 TriggerZone 层上的可交互物(门/存档点/商店/传送点…)。
// 提示表现由各交互物自带的世界空间子节点(WorldInteractPrompt)负责,本组件不持有 UI。
InteractableDetector interactDetector = GetOrAddComponent(root);
AssignLayerMask(interactDetector, "_interactableLayer", new[] { "TriggerZone" }, report);
if (inputReader != null)
AssignReference(interactDetector, "_inputReader", inputReader, report);
// ── 检查点回溯(LethalTrap 存活路径:淡出 → 传送至最近检查点 → 淡入)──
// 须与 Rigidbody2D 同节点(传送时直接写 rb.position)。
CheckpointRespawnHandler checkpointRespawn = GetOrAddComponent(root);
AssignAsset(checkpointRespawn, "_onCheckpointRespawn", report, false, "EVT_CheckpointRespawn");
AssignAsset(checkpointRespawn, "_onFadeOutRequest", report, false, "EVT_FadeOutRequest");
AssignAsset(checkpointRespawn, "_onFadeInRequest", report, false, "EVT_FadeInRequest");
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, null);
public static void PlaceEnemy(EnemyBodyColliderType bodyCollider, Sprite defaultSprite = null)
{
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;
Vector2 size = SpriteSizeOr(defaultSprite, new Vector2(0.7f, 0.9f));
Collider2D body = CreateBodyCollider(go, bodyCollider, size);
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);
if (defaultSprite != null) sr.sprite = defaultSprite;
EnemyBase enemyBase = GetOrAddComponent(go);
EnemyStats enemyStats = GetOrAddComponent(go);
EnemyMovement movement = GetOrAddComponent(go);
GetOrAddComponent(go); // 地面导航(直接移动,无寻路图)
GetOrAddComponent(go); // 移动执行器:EnemyBase.Awake 必须找得到,否则 AI 图的移动意图无处落地
PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go);
var (hurtBox, bodyContact, contactHitBox) =
SetupHurtAndContactBoxes(go, size, report);
// Wire EnemyBase
AssignReference(enemyBase, "_stats", enemyStats, report);
AssignReference(enemyBase, "_movement", movement, report);
AssignReference(enemyBase, "_animancer", animancer, report);
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
AssignReference(enemyBase, "_bodyCollider", body, report); // 身体几何唯一权威(EnemyBase.Body)
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(contactHitBox, "_defaultSource", dmgSrc, report);
else
report.Add("未找到 DamageSourceSO,ContactDamageZone._defaultSource 未绑定。请创建 CMB_DS_EnemyBody.asset。");
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report);
report.Add("★ 指定 EnemyBase._statsSO、_animConfig 资产(按所创建的敌人类型命名)。");
report.Add("★ AI(BrainGraph):用菜单 BaseGames/AI/Enemy AI Recipe Wizard 建一个配方资产," +
"在 Inspector 选两层模块(未发现/交战),再挂 EnemyAiBrain 组件并把 _recipe 指向该资产。" +
"有独门机制的敌人才写 [AiDefinition] 的 AiScript 子类并改用 _definitionId。");
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, null);
public static void PlaceBossEnemy(EnemyBodyColliderType bodyCollider, Sprite defaultSprite = null)
{
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;
Vector2 size = SpriteSizeOr(defaultSprite, new Vector2(1.5f, 2.5f));
Collider2D body = CreateBodyCollider(go, bodyCollider, size);
GetOrAddComponent(go);
SpriteRenderer sr = SetupSpriteRenderer(go);
if (defaultSprite != null) sr.sprite = defaultSprite;
BossBase bossBase = GetOrAddComponent(go);
EnemyStats bossStats = GetOrAddComponent(go);
PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go);
var (hurtBox, bodyContact, contactHitBox) =
SetupHurtAndContactBoxes(go, size, report);
// References
AssignReference(bossBase, "_stats", bossStats, report);
AssignReference(bossBase, "_bodyCollider", body, report); // 身体几何唯一权威(EnemyBase.Body)
// DamageSourceSO
Object dmgSrc = FindFirstAsset("CMB_DS_BossBody", "CMB_DS_EnemyBody", "DS_BossBody");
if (dmgSrc != null)
AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
else
report.Add("未找到 DamageSourceSO,ContactDamageZone._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("★ AI(BrainGraph):用菜单 BaseGames/AI/Enemy AI Recipe Wizard 建一个配方资产," +
"在 Inspector 选两层模块(未发现/交战),再挂 EnemyAiBrain 组件并把 _recipe 指向该资产。" +
"有独门机制的敌人才写 [AiDefinition] 的 AiScript 子类并改用 _definitionId。");
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, null);
public static void PlaceE001_CaoZhi(EnemyBodyColliderType bodyCollider, Sprite defaultSprite = null)
{
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;
Vector2 size = SpriteSizeOr(defaultSprite, new Vector2(0.6f, 0.8f));
Collider2D body = CreateBodyCollider(go, bodyCollider, size);
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);
if (defaultSprite != null) sr1.sprite = defaultSprite;
EnemyBase enemyBase = GetOrAddComponent(go);
EnemyStats enemyStats = GetOrAddComponent(go);
EnemyMovement movement = GetOrAddComponent(go);
GetOrAddComponent(go); // 地面导航(直接移动,无寻路图)
GetOrAddComponent(go); // 移动执行器:EnemyBase.Awake 必须找得到,否则 AI 图的移动意图无处落地
PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go);
var (hurtBox, bodyContact, contactHitBox) =
SetupHurtAndContactBoxes(go, size, report); // 身体接触伤害常驻开启(与追击能力解耦)
// Model A:AI 只决策触发哪个能力(Idle/Patrol/Alert 已改为纯移动意图,由 EnemyLocomotion 执行,无需挂能力组件)
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
Transform chaseT = GetOrCreateChild(abilitiesT, "RushAbility_Chase");
RushAbility 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);
AssignReference(enemyBase, "_bodyCollider", body, report); // 身体几何唯一权威(EnemyBase.Body)
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");
AssignBool(movement, "_enableTurnAnimation", true); // 转向播 Flip(巡逻翻边/追击起手前转身)
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr1, report);
AssignLayerMask(movement, "_groundMask",
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
report);
AssignLayerMask(movement, "_wallMask", new[] { "Platform", "Wall" }, report); // 撞墙判定层(策划可调)
AssignAsset(chaseAbility, "_config", report, false, "ABL_E001_Chase");
// 追击动画:起手/循环/收招(设计文档 Skill_Start/Loop/End)
AssignClipTransition(chaseAbility, "_startClip", "Skill_Start", "Enemies/E001", report);
AssignClipTransition(chaseAbility, "_loopClip", "Skill_Loop", "Enemies/E001", report);
AssignClipTransition(chaseAbility, "_endClip", "Skill_End", "Enemies/E001", report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report);
// BrainGraph AI:挂载决策组件并绑定 AI 配方资产
var brain = GetOrAddComponent(go);
var recipe = AssetDatabase.LoadAssetAtPath(
"Assets/_Game/Data/Enemies/E001/ENM_E001_Ai.asset");
if (recipe == null)
{
// 根因暴露:配方缺失就报出来,不退回旧的字符串 id 路径掩盖。
report.Add("✘ 未找到 AI 配方 ENM_E001_Ai —— 请先用菜单 BaseGames/AI/Enemy AI Recipe Wizard 创建。");
}
else
{
AssignReference(brain, "_recipe", recipe, report);
AssignString(brain, "_definitionId", "", report);
report.Add("✔ 已挂载 EnemyAiBrain 并绑定 AI 配方 ENM_E001_Ai(BrainGraph)。");
}
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, null);
public static void PlaceE002_HuangZhi(EnemyBodyColliderType bodyCollider, Sprite defaultSprite = null)
{
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;
Vector2 size = SpriteSizeOr(defaultSprite, new Vector2(0.5f, 0.7f));
Collider2D body = CreateBodyCollider(go, bodyCollider, size);
// 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);
if (defaultSprite != null) sr.sprite = defaultSprite;
EnemyBase enemyBase = GetOrAddComponent(go);
EnemyStats enemyStats = GetOrAddComponent(go);
EnemyMovement movement = GetOrAddComponent(go);
GetOrAddComponent(go); // 地面导航(直接移动,无寻路图)
GetOrAddComponent(go); // 移动执行器:EnemyBase.Awake 必须找得到,否则 AI 图的移动意图无处落地
PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go);
// HurtBox + ContactDamageZone(身体接触伤害常驻开启;HurtBox 悬挂期外关闭见下)
var (hurtBox, bodyContact, contactHitBox) =
SetupHurtAndContactBoxes(go, size, report);
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);
AssignReference(enemyBase, "_bodyCollider", body, report); // 身体几何唯一权威(EnemyBase.Body)
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);
AssignLayerMask(movement, "_wallMask", new[] { "Platform", "Wall" }, 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);
AssignReference(contactHitBox, "_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("★ AI(BrainGraph):用菜单 BaseGames/AI/Enemy AI Recipe Wizard 建一个配方资产," +
"在 Inspector 选两层模块(未发现/交战),再挂 EnemyAiBrain 组件并把 _recipe 指向该资产。" +
"有独门机制的敌人才写 [AiDefinition] 的 AiScript 子类并改用 _definitionId。");
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, null);
public static void PlaceE003_YouZhi_Enemy(EnemyBodyColliderType bodyCollider, Sprite defaultSprite = null)
{
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;
Vector2 size = SpriteSizeOr(defaultSprite, new Vector2(0.5f, 0.6f));
Collider2D body = CreateBodyCollider(go, bodyCollider, size);
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);
if (defaultSprite != null) sr3.sprite = defaultSprite;
EnemyBase enemyBase = GetOrAddComponent(go);
EnemyStats enemyStats = GetOrAddComponent(go);
EnemyMovement movement = GetOrAddComponent(go);
GetOrAddComponent(go); // 地面导航(直接移动,无寻路图)
GetOrAddComponent(go); // 移动执行器:EnemyBase.Awake 必须找得到,否则 AI 图的移动意图无处落地
PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go);
var (hurtBox, bodyContact, contactHitBox) =
SetupHurtAndContactBoxes(go, size, report);
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);
AssignReference(enemyBase, "_bodyCollider", body, report); // 身体几何唯一权威(EnemyBase.Body)
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);
AssignLayerMask(movement, "_wallMask", new[] { "Platform", "Wall" }, report); // 撞墙判定层(策划可调)
AssignAsset(fallAbility, "_config", report, false, "ABL_E003_Fall");
// 出生 / 外部触发执行下坠能力(零代码:替代过去的 E003_YouZhi 专属脚本)
EnemyAbilityTrigger fallTrigger = GetOrAddComponent(go);
AssignString(fallTrigger, "_abilityId", "e003_fall", report);
AssignBool(fallTrigger, "_executeOnSpawn", true);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report);
report.Add("★ 将此对象放置于天花板下方;EnemyAbilityTrigger 在出生时(executeOnSpawn)自动、或被场景触发器调用 Trigger() 时执行下坠能力(e003_fall)。");
report.Add("★ AI(BrainGraph):用菜单 BaseGames/AI/Enemy AI Recipe Wizard 建一个配方资产," +
"在 Inspector 选两层模块(未发现/交战),再挂 EnemyAiBrain 组件并把 _recipe 指向该资产。" +
"有独门机制的敌人才写 [AiDefinition] 的 AiScript 子类并改用 _definitionId。");
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, null);
public static void PlaceE004_ZhiMu_Enemy(EnemyBodyColliderType bodyCollider, Sprite defaultSprite = null)
{
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;
Vector2 size = SpriteSizeOr(defaultSprite, new Vector2(0.8f, 1.2f));
Collider2D body = CreateBodyCollider(go, bodyCollider, size);
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);
if (defaultSprite != null) sr4.sprite = defaultSprite;
EnemyBase enemyBase = GetOrAddComponent(go);
EnemyStats enemyStats = GetOrAddComponent(go);
EnemyFeedback feedback = GetOrAddComponent(go);
EnemyMovement movement = GetOrAddComponent(go);
GetOrAddComponent(go); // 地面导航(直接移动,无寻路图)
GetOrAddComponent(go); // 移动执行器:EnemyBase.Awake 必须找得到,否则 AI 图的移动意图无处落地
PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go);
var (hurtBox, bodyContact, contactHitBox) =
SetupHurtAndContactBoxes(go, size, report);
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);
AssignReference(enemyBase, "_bodyCollider", body, report); // 身体几何唯一权威(EnemyBase.Body)
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);
AssignLayerMask(movement, "_wallMask", new[] { "Platform", "Wall" }, 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);
AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
}
// 死亡前摇无敌演出(零代码:替代过去的 E004_ZhiMu 专属 Die() 重写)
EnemyDeathSequence deathSeq = GetOrAddComponent(go);
AssignObjectArray(deathSeq, "_hurtBoxesToDisable", new Object[] { hurtBox }, report);
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "sight" }, report);
report.Add("★ AppearAbility._appearClip / FacePlayerAbility._faceClip 等动画 Clip 待美术接入后在 Inspector 指定。");
report.Add("★ 在 EnemyDeathSequence._deathPreClip 配置死亡前摇动画(两阶段死亡 Death_Pre 无敌)。");
report.Add("★ AI(BrainGraph):用菜单 BaseGames/AI/Enemy AI Recipe Wizard 建一个配方资产," +
"在 Inspector 选两层模块(未发现/交战),再挂 EnemyAiBrain 组件并把 _recipe 指向该资产。" +
"有独门机制的敌人才写 [AiDefinition] 的 AiScript 子类并改用 _definitionId。");
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, null);
public static void PlaceE005_FeiZhi_Enemy(EnemyBodyColliderType bodyCollider, Sprite defaultSprite = null)
{
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;
Vector2 size = SpriteSizeOr(defaultSprite, new Vector2(0.9f, 1.0f));
Collider2D body = CreateBodyCollider(go, bodyCollider, size);
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);
if (defaultSprite != null) sr5.sprite = defaultSprite;
EnemyBase enemyBase = GetOrAddComponent(go);
EnemyStats enemyStats = GetOrAddComponent(go);
EnemyMovement movement = GetOrAddComponent(go);
GetOrAddComponent(go); // 地面导航(直接移动,无寻路图)
GetOrAddComponent(go); // 移动执行器:EnemyBase.Awake 必须找得到,否则 AI 图的移动意图无处落地
PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go);
var (hurtBox, bodyContact, contactHitBox) =
SetupHurtAndContactBoxes(go, size, report);
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);
AssignReference(enemyBase, "_bodyCollider", body, report); // 身体几何唯一权威(EnemyBase.Body)
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);
AssignLayerMask(movement, "_wallMask", new[] { "Platform", "Wall" }, 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);
AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
}
// 死亡前摇无敌演出 + 动画事件池生成幼蛭(零代码:替代过去的 E005_FeiZhi 专属 Die()/SpawnProjectile 重写)
EnemyDeathSequence deathSeq = GetOrAddComponent(go);
AssignObjectArray(deathSeq, "_hurtBoxesToDisable", new Object[] { hurtBox }, report);
EnemySpawnerOnEvent spawner = GetOrAddComponent(go);
AssignString(spawner, "_payloadKey", "spawn_e003", report);
AssignString(spawner, "_poolKey", "ENM_YouZhi", report);
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
report.Add("★ 在 EnemyDeathSequence._deathPreClip 上添加 AnimationEvent 调用 SpawnProjectile(\"spawn_e003\"),由 EnemySpawnerOnEvent 从对象池生成幼蛭(ENM_YouZhi)。");
report.Add("★ AI(BrainGraph):用菜单 BaseGames/AI/Enemy AI Recipe Wizard 建一个配方资产," +
"在 Inspector 选两层模块(未发现/交战),再挂 EnemyAiBrain 组件并把 _recipe 指向该资产。" +
"有独门机制的敌人才写 [AiDefinition] 的 AiScript 子类并改用 _definitionId。");
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, null);
public static void PlaceE006_Huan(EnemyBodyColliderType bodyCollider, Sprite defaultSprite = null)
{
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;
Vector2 size = SpriteSizeOr(defaultSprite, new Vector2(0.7f, 1.0f));
Collider2D body = CreateBodyCollider(go, bodyCollider, size);
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);
if (defaultSprite != null) sr6.sprite = defaultSprite;
EnemyBase enemyBase = GetOrAddComponent(go);
EnemyStats enemyStats = GetOrAddComponent(go);
EnemyMovement movement = GetOrAddComponent(go);
GetOrAddComponent(go); // 地面导航(直接移动,无寻路图)
GetOrAddComponent(go); // 移动执行器:EnemyBase.Awake 必须找得到,否则 AI 图的移动意图无处落地
PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go);
var (hurtBox, bodyContact, contactHitBox) =
SetupHurtAndContactBoxes(go, size, report);
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, "RushAbility");
RushAbility 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);
AssignReference(enemyBase, "_bodyCollider", body, report); // 身体几何唯一权威(EnemyBase.Body)
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);
AssignLayerMask(movement, "_wallMask", new[] { "Platform", "Wall" }, report); // 撞墙判定层(策划可调)
AssignAsset(leapAbl,"_config", report, false, "ABL_E006_Leap");
AssignAsset(chaseAbl, "_config", report, false, "ABL_E006_Chase");
AssignReference(leapAbl, "_landingHitBox", landHitBox, 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("★ AI(BrainGraph):用菜单 BaseGames/AI/Enemy AI Recipe Wizard 建一个配方资产," +
"在 Inspector 选两层模块(未发现/交战),再挂 EnemyAiBrain 组件并把 _recipe 指向该资产。" +
"有独门机制的敌人才写 [AiDefinition] 的 AiScript 子类并改用 _definitionId。");
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, null);
public static void PlaceChaoFeng(EnemyBodyColliderType bodyCollider, Sprite defaultSprite = null)
{
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;
Vector2 size = SpriteSizeOr(defaultSprite, new Vector2(1.2f, 2.0f));
Collider2D body = CreateBodyCollider(go, bodyCollider, size);
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);
if (defaultSprite != null) srBoss.sprite = defaultSprite;
ChaoFengBoss bossBase = GetOrAddComponent(go);
EnemyStats bossStats = GetOrAddComponent(go);
EnemyFeedback feedback = GetOrAddComponent(go);
EnemyMovement movement = GetOrAddComponent(go);
GetOrAddComponent(go); // 地面导航(直接移动,无寻路图)
// 移动执行器:AI 图的移动意图(Idle/Face/Approach)全靠它落地。
// 少了它 EnemyBase.Awake 就报「未找到 EnemyLocomotion」,且 Locomotion 为 null,
// 建图时 AiStateFragments.ApplyLocomotion 会 NRE,AiRuntime 构造失败 → Boss 完全不动。
GetOrAddComponent(go);
EnemyAiBrain brain = GetOrAddComponent(go);
BossPhaseAbilityGate phaseGate = GetOrAddComponent(go);
ChaoFengFloatController floatCtrl = GetOrAddComponent(go);
ChaoFengKnockdownCounter knockdown = GetOrAddComponent(go);
PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go);
// HurtBox + ContactDamageZone(Boss 身体接触伤害区,默认启用;策划可在 Inspector 关闭该子节点)
var (hurtBox, bodyContact, contactHitBox) =
SetupHurtAndContactBoxes(go, size, report);
// 近战攻击 HitBox(默认禁用;由 MeleeAttackAbility 按 EnemyAttackSO.hitBoxSlot 归一化时机开关)。
// 只有挥扇三连需要 Boss 自身的判定;回旋扇 / 龙卷 / 风石都是弹体招,
// 判定随 SpawnProjectile 动画事件生成的弹体走,Boss 身上不留对应 HitBox
// (留一个无人引用的 HitBox 会让作者误以为该招判定挂在 Boss 身上)。
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));
// 弹体发射点(Phase1 回旋扇 / 龙卷;Phase2 风石)
Transform boomerangMuzzleT = GetOrCreateChild(go.transform, "BoomerangMuzzle");
Transform tornadoMuzzleT = GetOrCreateChild(go.transform, "TornadoMuzzle");
Transform windStoneMuzzleT = GetOrCreateChild(go.transform, "WindStoneMuzzle");
// 能力集:入场演出 + 五个战斗招,一招一个子物体。
// EnemyAbilityBase 带 [DisallowMultipleComponent],同一物体放不下四个 PlayClipAbility;
// EnemyAbilityRegistry.CollectFrom 用 GetComponentsInChildren 递归收集,子物体照常注册。
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
var abIntro = GetOrAddComponent(
GetOrCreateChild(abilitiesT, "PlayClipAbility_Intro").gameObject);
var abFan = GetOrAddComponent(
GetOrCreateChild(abilitiesT, "MeleeAttackAbility_FanCombo").gameObject);
var abBoomer = GetOrAddComponent(
GetOrCreateChild(abilitiesT, "PlayClipAbility_Boomerang").gameObject);
var abTornadoS = GetOrAddComponent(
GetOrCreateChild(abilitiesT, "PlayClipAbility_TornadoSmall").gameObject);
var abTornadoL = GetOrAddComponent(
GetOrCreateChild(abilitiesT, "PlayClipAbility_TornadoLarge").gameObject);
var abWind = GetOrAddComponent(
GetOrCreateChild(abilitiesT, "PlayClipAbility_WindStone").gameObject);
// 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(bossBase, "_bodyCollider", body, report); // 身体几何唯一权威(EnemyBase.Body)
// 浮空 / 击落 / 弹体发射点接线(计划)
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(bossBase, "_phaseGate", phaseGate, 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);
AssignLayerMask(movement, "_wallMask", new[] { "Platform", "Wall" }, report); // 撞墙判定层(策划可调)
// 决策层:Boss 走定制路径(AiScript),不用配方
AssignString(brain, "_definitionId", "ChaoFeng", report);
// 能力 SO 绑定(一招一组件)
var abilityWiring = new (Object comp, string assetName)[]
{
(abIntro, "ABL_ChaoFeng_Intro"),
(abFan, "ABL_ChaoFeng_FanCombo"),
(abBoomer, "ABL_ChaoFeng_Boomerang"),
(abTornadoS, "ABL_ChaoFeng_TornadoSmall"),
(abTornadoL, "ABL_ChaoFeng_TornadoLarge"),
(abWind, "ABL_ChaoFeng_WindStone"),
};
foreach (var (comp, assetName) in abilityWiring)
{
Object so = FindFirstAsset(assetName);
if (so != null) AssignReference(comp, "_config", so, report);
else report.Add($"未找到 EnemyAbilitySO:{assetName}," +
"请先在角色向导 Boss 页「一键创建全部 ChaoFeng SO」后重新运行本放置操作。");
}
// 近战三段 HitBox 走命名槽位(EnemyAttackSO.hitBoxSlot 按名索引)
AssignMeleeHitBoxSlots(abFan, new[] { ("fan_1", fan1), ("fan_2", fan2), ("fan_3", fan3) }, report);
// 阶段招池:地面四招 = 阶段 0,风石 = 阶段 1;
// 入场演出不登记(category=None,本就不进选招池,且需全阶段可用)
AssignPhaseGate(phaseGate, new (EnemyAbilityBase ability, int[] phases)[]
{
(abFan, new[] { 0 }),
(abBoomer, new[] { 0 }),
(abTornadoS, new[] { 0 }),
(abTornadoL, new[] { 0 }),
(abWind, new[] { 1 }),
}, report);
Object dmgSrc = FindFirstAsset("CMB_DS_BossBody", "CMB_DS_EnemyBody");
if (dmgSrc != null)
{
foreach (var hb in new[] { fan1, fan2, fan3, contactHitBox })
if (hb != null) AssignReference(hb, "_defaultSource", dmgSrc, report);
}
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "sight" }, report);
report.Add("★ FanCombo 三段 HitBox 已绑为命名槽位 fan_1 / fan_2 / fan_3," +
"在各 EATK_ 资产的 hitBoxSlot 里填对应名字。");
report.Add("★ 回旋扇 / 龙卷 / 风石的生成由技能 clip 上的 SpawnProjectile 动画事件驱动," +
"payload 填 boomerang / tornado_small / tornado_large / wind_stone;" +
"炮口引用留在 ChaoFengBoss 上,无需再拖到 SO。");
report.Add("★ 回旋扇收招 / 阶段过渡 / 击败演出等动画 Clip 待美术接入后在 ChaoFengBoss Inspector 指定。");
report.Add("★ AI:Boss 走定制路径,EnemyAiBrain._definitionId 已设为 ChaoFeng(对应 ChaoFengAi)。" +
"小怪才用配方(_recipe)。");
report.Add("★ 开战触发:Boss 房间入口需另放一个带 BossFightTrigger 的触发区," +
"指向本 Boss 的 EnemyAiBrain 并填 bossId=ChaoFeng,否则 Boss 不会开战。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
EnemyBase.SuppressValidationWarnings = false;
MarkDirtyAndLog("Boss 嘲风 (ChaoFeng)", go, report);
}
[MenuItem("BaseGames/Scene/Place/Boss 开战触发区 (嘲风)", priority = 118)]
public static void PlaceChaoFengFightTrigger() => PlaceBossFightTrigger("ChaoFeng");
///
/// 放置 Boss 开战触发区:玩家进入即广播开战事件并让 Boss 决策层进战。
/// 没有它,Boss 会一直停在静置态——开战事件频道此前全项目零发送方。
///
///
/// Boss id。既写进触发器的 _bossId(BGM / 血条按它定位 Boss),
/// 也用来在活动场景里定位目标 Boss——按 EnemyAiBrain._definitionId 匹配。
///
public static void PlaceBossFightTrigger(string bossId)
{
var report = new List();
int undoGroup = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Place Boss Fight Trigger");
GameObject go = new GameObject($"BossFightTrigger_{bossId}");
Undo.RegisterCreatedObjectUndo(go, "Place Boss Fight Trigger");
go.transform.position = GetDropPosition();
SetLayer(go, "TriggerZone", report);
// 碰撞体必须先于 BossFightTrigger 挂上:该组件标了 [RequireComponent(typeof(Collider2D))],
// 而 Collider2D 是抽象类,Unity 补不出来,AddComponent 会返回真正的 null。
BoxCollider2D col = GetOrAddComponent(go);
col.isTrigger = true;
col.size = new Vector2(4f, 6f); // 门口尺寸占位,按竞技场入口在 Inspector 调整
BossFightTrigger trigger = GetOrAddComponent(go);
AssignString(trigger, "_bossId", bossId, report);
AssignLayerMask(trigger, "_playerLayers", "Player", report);
AssignAsset(trigger, "_onBossFightStarted", report, true, "EVT_BossFightStarted");
// 开关频道:资产名叫 EVT_BossFightEnded,但实际是「战斗中 true/false」的开关,
// BGMController 与 BossHPBar 绑的也是它。名字与用途不符是既存问题,此处沿用同一资产。
AssignAsset(trigger, "_onBossFightToggled", report, true, "EVT_BossFightEnded");
// 目标 Boss:按 EnemyAiBrain._definitionId 在活动场景里精确匹配,不做名字模糊猜测。
// 匹配不到(或匹配到多个)一律如实报告,不塞一个凑数的引用——
// 漏绑时 BossFightTrigger.Awake 也会再报一次并停用自己。
var matches = new List();
foreach (var root in UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().GetRootGameObjects())
foreach (var b in root.GetComponentsInChildren(true))
if (new SerializedObject(b).FindProperty("_definitionId")?.stringValue == bossId)
matches.Add(b);
if (matches.Count == 1)
{
AssignReference(trigger, "_bossBrain", matches[0], report);
report.Add($"[OK] _bossBrain → {matches[0].name}(_definitionId={bossId})");
}
else if (matches.Count == 0)
{
report.Add($"✗ 活动场景里找不到 _definitionId={bossId} 的 EnemyAiBrain," +
"_bossBrain 未绑定。请先放置该 Boss,再重跑本操作。");
}
else
{
report.Add($"✗ 活动场景里有 {matches.Count} 个 _definitionId={bossId} 的 EnemyAiBrain," +
"无法确定目标,_bossBrain 未绑定。请先清理重复的 Boss。");
}
report.Add("★ 把触发区拖到竞技场入口并按门口尺寸调整 BoxCollider2D。只触发一次。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
MarkDirtyAndLog($"Boss 开战触发区 ({bossId})", 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 = 8f; layer = playerLayer; 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");
// RespawnPoint 子节点:玩家回溯时的出生位置(触发区与出生点解耦)
Transform respawnT = GetOrCreateChild(go.transform, "RespawnPoint");
respawnT.localPosition = Vector3.zero;
AssignReference(marker, "_respawnPoint", respawnT, report);
report.Add("放置于跳跳乐段落的关键节点处;玩家经过后成为该房间最近检查点。");
report.Add("同一房间可放置多个,以最近经过的为准。");
report.Add("调整 RespawnPoint 子节点位置 = 玩家回溯出生位置(须落在安全平台上)。");
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");
// 世界空间交互提示
AttachInteractPrompt(go, 1.3f, report);
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");
AttachInteractPrompt(go, 1.6f, report);
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");
AttachInteractPrompt(go, 1.6f, report);
report.Add("填写 _transitionId(本出口唯一 ID)、_targetSceneAddress(目标场景 Addressable Key)、_targetTransitionId(目标出生点 ID)。");
report.Add("若需锁门,设 _requiresKeyItem = true 并填写 _requiredItemId。");
report.Add("_worldState 字段需拖入 WorldStateRegistry SO(可选)。");
report.Add("交互提示仅在 _autoTrigger = false(按键进入)时显示;自动触发的门不显示提示属正常。");
Selection.activeGameObject = go;
MarkDirtyAndLog("Room Transition", go, report);
}
///
/// 在交互物上挂载世界空间交互提示(UI_WorldInteractPrompt 预制体),作为子节点跟随物体显示。
/// 提示显隐由玩家身上的 InteractableDetector 在进入/离开最近可交互物时驱动(按 IInteractPromptView 接口)。
/// 幂等:已存在同名子节点则跳过。localY 为气泡相对物体的高度,可后续在场景中拖动该子节点单独微调。
///
private static void AttachInteractPrompt(GameObject host, float localY, List report)
{
if (host.transform.Find("InteractPrompt") != null) return;
var prefab = AssetDatabase.LoadAssetAtPath(
"Assets/_Game/Prefabs/UI/UI_WorldInteractPrompt.prefab");
if (prefab == null)
{
report.Add("★ 未找到 UI_WorldInteractPrompt 预制体,已跳过交互提示挂载(检查 Assets/_Game/Prefabs/UI/)。");
return;
}
var go = (GameObject)PrefabUtility.InstantiatePrefab(prefab, host.transform);
go.name = "InteractPrompt";
go.transform.localPosition = new Vector3(0f, localY, 0f);
Undo.RegisterCreatedObjectUndo(go, "Attach InteractPrompt");
report.Add("已挂载世界空间交互提示(InteractPrompt 子节点)。拖动它可单独微调气泡位置/样式。");
}
[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);
// 世界空间交互提示(按交互键模式默认显示)
AttachInteractPrompt(go, 1.6f, 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);
// 世界空间交互提示(仅在该门切到按交互键模式 _autoTrigger=false 时才会显示)
AttachInteractPrompt(goA, 1.6f, report);
AttachInteractPrompt(goB, 1.6f, 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/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)
{
Collider2D col;
switch (type)
{
case EnemyBodyColliderType.Capsule:
var cap = GetOrAddComponent(go);
cap.size = size;
col = cap;
break;
case EnemyBodyColliderType.Circle:
var cir = GetOrAddComponent(go);
cir.radius = Mathf.Min(size.x, size.y) * 0.5f;
col = cir;
break;
default: // Box
var box = GetOrAddComponent(go);
box.size = size;
col = box;
break;
}
AlignColliderBottomToPivot(col); // 底部对齐 y=0:原点=脚底
return col;
}
///
/// 把碰撞体底部对齐到 transform 原点(y=0)——原点即脚底。
/// 所有创建物的碰撞体底部对齐 y=0(原点=脚底),使地面探测与站位判定以脚底为基准,
/// 否则站到地面时原点悬在地面上方,导致地面检测/落点判定出现偏差。
///
private static void AlignColliderBottomToPivot(Collider2D col)
{
switch (col)
{
case CapsuleCollider2D cap:
cap.offset = new Vector2(cap.offset.x, cap.size.y * 0.5f);
break;
case BoxCollider2D box:
box.offset = new Vector2(box.offset.x, box.size.y * 0.5f);
break;
case CircleCollider2D cir:
cir.offset = new Vector2(cir.offset.x, cir.radius);
break;
}
}
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;
}
///
/// 把节点上所有 Collider2D 设为 trigger。HurtBox/HitBox/接触伤害语义要求碰撞体为 trigger;
/// 只对脚手架创建的具体类型(Capsule/Circle)设 isTrigger 不够——若碰撞体被手动换成别的类型
/// (如 BoxCollider2D,默认 isTrigger=false),那条就漏掉。此助手对该节点全部碰撞体兜底设 trigger。
///
private static void EnsureCollidersAreTriggers(GameObject go)
{
foreach (var c in go.GetComponents())
c.isTrigger = true;
}
/// Sprite 有则用其世界包围盒尺寸(sprite.bounds.size),否则回退。
private static Vector2 SpriteSizeOr(Sprite s, Vector2 fallback)
=> s != null ? (Vector2)s.bounds.size : fallback;
///
/// 建/更新 HurtBox 与 ContactDamageZone 两个子节点:均为 BoxCollider2D、同尺寸 size、
/// 底部对齐 y=0、isTrigger=true。ContactDamageZone 带 HitBox + BodyContactDamage。
/// 身体接触伤害默认常驻开启:所有敌人存活期间身体接触即持续造成伤害,与能力解耦、不由能力开关,
/// 死亡时经 EnemyBase.PerformDeath 统一禁用碰撞体而停止。
/// 返回 (hurtBox, bodyContact, contactHitBox) 供上层按需接线(如伤害源)。
///
private static (HurtBox hurt, BodyContactDamage contact, HitBox contactHitBox)
SetupHurtAndContactBoxes(GameObject root, Vector2 size, List report)
{
// HurtBox 子节点
var hurtT = GetOrCreateChild(root.transform, "HurtBox");
SetLayer(hurtT.gameObject, "EnemyHurtBox", report);
var hurtCol = GetOrAddComponent(hurtT.gameObject);
hurtCol.size = size;
hurtCol.isTrigger = true;
AlignColliderBottomToPivot(hurtCol);
var hurtBox = GetOrAddComponent(hurtT.gameObject);
EnsureCollidersAreTriggers(hurtT.gameObject);
// ContactDamageZone 子节点
var contactT = GetOrCreateChild(root.transform, "ContactDamageZone");
SetLayer(contactT.gameObject, "EnemyHitBox", report);
var contactCol = GetOrAddComponent(contactT.gameObject);
contactCol.size = size;
contactCol.isTrigger = true;
AlignColliderBottomToPivot(contactCol);
var contactHitBox = GetOrAddComponent