@@ -5,6 +5,8 @@ using BaseGames.Boss;
using BaseGames.Camera ;
using BaseGames.Combat ;
using BaseGames.Combat.StatusEffects ;
using BaseGames.Core.Assets ;
using BaseGames.Core.Pool ;
using BaseGames.Dialogue ;
using BaseGames.Enemies ;
using BaseGames.Enemies.Abilities ;
@@ -546,37 +548,26 @@ namespace BaseGames.Editor
GetOrAddComponent < TransformBasedMovement > ( go ) ; // required by EnemyNavAgent [RequireComponent]
PhysicsPerceptionSystem sensorHub = GetOrAddComponent < PhysicsPerceptionSystem > ( go ) ;
// HurtBox( 初始禁用,悬挂阶段无法被攻击 )
// HurtBox( component.enabled 初始为 false:仅悬挂脆弱窗口期间由能力开启 )
Transform hurtBoxT = GetOrCreateChild ( go . transform , "HurtBox" ) ;
SetLayer ( hurtBoxT . gameObject , "EnemyHurtBox" , report ) ;
CapsuleCollider2D hurtCap = GetOrAddComponent < CapsuleCollider2D > ( hurtBoxT . gameObject ) ;
hurtCap . isTrigger = true ;
hurtCap . size = new Vector2 ( 0.45f , 0.65f ) ;
HurtBox hurtBox = GetOrAddComponent < HurtBox > ( hurtBoxT . gameObject ) ;
hurtBoxT . gameObject . SetActive ( false ) ;
hurtBox . enabled = false ; // 悬挂阶段外不可被攻击;CeilingHangStrikeAbility 在 _loopClip 期间开启
// LandingHitBox(落地瞬间 AoE,由 CeilingDrop Ability 激活)
Transform landingHitBoxT = GetOrCreateChild ( go . transform , "Landing HitBox" ) ;
SetLayer ( landingHitBoxT . gameObject , "EnemyHitBox" , report ) ;
BoxCollider2D landingCol = GetOrAddComponent < BoxCollider2D > ( landingHitBoxT . gameObject ) ;
landingCol . isTrigger = true ;
landingCol . size = new Vector2 ( 0.8f , 0.3f ) ;
HitBox landingHitBox = GetOrAddComponent < HitBox > ( landingHitBoxT . gameObject ) ;
landingHitBoxT . gameObject . SetActive ( false ) ;
// ContactDamageZone(地面巡逻时造成接触伤害,落地后由行为树启用)
Transform contactT = GetOrCreateChild ( go . transform , "ContactDamageZone" ) ;
SetLayer ( contactT . gameObject , "EnemyHitBox" , report ) ;
CircleCollider2D contactCol = GetOrAddComponent < CircleCollider2D > ( contactT . gameObject ) ;
contactCol . isTrigger = true ;
contactCol . radius = 0.35f ;
HitBox contactHitBox = GetOrAddComponent < HitBox > ( contactT . gameObject ) ;
GetOrAddComponent < BodyContactDamage > ( contactT . gameObject ) ;
contactT . gameObject . SetActive ( false ) ;
// AttackHitBox(钻出啃咬瞬间判定,由 CeilingHangStrike Ability 激活)
Transform attackHitBoxT = GetOrCreateChild ( go . transform , "Attack HitBox" ) ;
SetLayer ( attackHitBoxT . gameObject , "EnemyHitBox" , report ) ;
BoxCollider2D attackCol = GetOrAddComponent < BoxCollider2D > ( attackHitBoxT . gameObject ) ;
attackCol . isTrigger = true ;
attackCol . size = new Vector2 ( 0.6f , 0.8f ) ; // 正下方钻出范围
HitBox attackHitBox = GetOrAddComponent < HitBox > ( attackHitBoxT . gameObject ) ;
Transform abilitiesT = GetOrCreateChild ( go . transform , "Abilities" ) ;
Transform dropT = GetOrCreateChild ( abilitiesT , "CeilingDrop Ability" ) ;
CeilingDropAbility dropAbility = GetOrAddComponent < CeilingDropAbility > ( dropT . gameObject ) ;
Transform strikeT = GetOrCreateChild ( abilitiesT , "CeilingHangStrike Ability" ) ;
CeilingHangStrikeAbility strikeAbility = GetOrAddComponent < CeilingHangStrikeAbility > ( strikeT . gameObject ) ;
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset ( enemyBase , "_statsSO" , report , false , "ENM_E002_Stats" ) ;
@@ -601,21 +592,18 @@ namespace BaseGames.Editor
new [ ] { "Platform" , "OneWayPlatform" , "MovingOneWayPlatform" , "MidHeightOneWayPlatform" } ,
report ) ;
AssignReference ( dropAbility , "_landingHitBox" , landingHitBox , report ) ;
AssignLayerMask ( dropAbility , "_groundMask" ,
new [ ] { "Platform" , "OneWayPlatform" , "MovingOneWayPlatform" , "MidHeightOneWayPlatform" } ,
report ) ;
AssignAsset ( strikeAbility , "_config" , report , false , "ABL_E002_CeilingStrike" ) ;
AssignReference ( strikeAbility , "_attackHitBox" , attackHitBox , report ) ;
AssignReference ( strikeAbility , "_hurtBox" , hurtBox , report ) ;
Object dmgSrc = FindFirstAsset ( "CMB_DS_EnemyBody" ) ;
if ( dmgSrc ! = null )
{
AssignReference ( landingHitBox , "_defaultSource" , dmgSrc , report ) ;
AssignReference ( contactHitBox , "_defaultSource" , dmgSrc , report ) ;
}
AssignReference ( attackHitBox , "_defaultSource" , dmgSrc , report ) ;
SetupPerceptionSystemSlots ( sensorHub , new [ ] { "aggro" , " attack_range" } , report ) ;
SetupPerceptionSystemSlots ( sensorHub , new [ ] { "attack_range" } , report ) ;
report . Add ( "★ 将此对象放置于天花板,调整位置使 CapsuleCollider 正好贴合天花板底面。" ) ;
report . Add ( "★ HurtBox / ContactDamageZone 初始禁用;落地后由行为树激活 。" ) ;
report . Add ( "★ HurtBox.enabled 初始为 false;钻出后由 CeilingHangStrikeAbility 在悬挂窗口开启 。" ) ;
report . Add ( "★ attack_range 槽位为正下方 BoxCast(玩家经过检测区);按需在 Inspector 调整 offset/size。" ) ;
report . Add ( "★ 挂载行为树 BehaviorTree 组件,指定 E002_HuangZhi.asset。" ) ;
report . Add ( "★ BD 树逻辑建议:Idle(悬挂)→ IsSensorDetecting(aggro) → UseAbility(CeilingDrop) → IsGrounded → Patrol(Pace)。" ) ;
@@ -775,27 +763,20 @@ namespace BaseGames.Editor
HitBox slamHitBox = GetOrAddComponent < HitBox > ( slamT . gameObject ) ;
slamT . gameObject . SetActive ( false ) ;
Transform chargeHitBoxT = GetOrCreateChild ( go . transform , "ChargeHitBox" ) ;
SetLayer ( chargeHitBoxT . gameObject , "EnemyHitBox" , report ) ;
BoxCollider2D chargeHitCol = GetOrAddComponent < BoxCollider2D > ( chargeHitBoxT . gameObject ) ;
chargeHitCol . isTrigger = true ;
chargeHitCol . size = new Vector2 ( 0.9f , 0.8f ) ;
HitBox chargeHitBox = GetOrAddComponent < HitBox > ( chargeHitBoxT . gameObject ) ;
chargeHitBoxT . gameObject . SetActive ( false ) ;
Transform acidMuzzleT = GetOrCreateChild ( go . transform , "AcidMuzzle" ) ;
// 能力集(计划 E004):出场 / 撕咬 / 头槌 / 酸液 / 转身
Transform abilitiesT = GetOrCreateChild ( go . transform , "Abilities" ) ;
Transform appearAblT = GetOrCreateChild ( abilitiesT , "AppearAbility" ) ;
AppearAbility appearAbl = GetOrAddComponent < AppearAbility > ( appearAblT . gameObject ) ;
Transform biteAblT = GetOrCreateChild ( abilitiesT , "MeleeAttackAbility_Bite" ) ;
MeleeAttackAbility biteAbl = GetOrAddComponent < MeleeAttackAbility > ( biteAblT . gameObject ) ;
Transform slamAblT = GetOrCreateChild ( abilitiesT , "RepeatSlamAbility" ) ;
Transform slamAblT = GetOrCreateChild ( abilitiesT , "RepeatSlamAbility_HeadSlam " ) ;
RepeatSlamAbility slamAbl = GetOrAddComponent < RepeatSlamAbility > ( slamAblT . gameObject ) ;
Transform acidAblT = GetOrCreateChild ( abilitiesT , "ProjectileAttackAbility_Acid" ) ;
ProjectileAttackAbility acidAbl = GetOrAddComponent < ProjectileAttackAbility > ( acidAblT . gameObject ) ;
Transform chargeAblT = GetOrCreateChild ( abilitiesT , "Charge Ability" ) ;
ChargeAbility chargeAbl = GetOrAddComponent < ChargeAbility > ( chargeAblT . gameObject ) ;
Transform chaseAblT = GetOrCreateChild ( abilitiesT , "ContactChaseAbility" ) ;
ContactChaseAbility chaseAbl = GetOrAddComponent < ContactChaseAbility > ( chaseAblT . gameObject ) ;
Transform flipAblT = GetOrCreateChild ( abilitiesT , "FacePlayer Ability_Flip " ) ;
FacePlayerAbility flipAbl = GetOrAddComponent < FacePlayerAbility > ( flipAblT . gameObject ) ;
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset ( enemyBase , "_statsSO" , report , false , "ENM_E004_Stats" ) ;
@@ -821,26 +802,26 @@ namespace BaseGames.Editor
new [ ] { "Platform" , "OneWayPlatform" , "MovingOneWayPlatform" , "MidHeightOneWayPlatform" } ,
report ) ;
AssignAsset ( biteAbl , "_config" , report , false , "ABL_E004_Bite " ) ;
AssignAsset ( slamAbl , "_config" , report , false , "ABL_E004_Slam " ) ;
AssignAsset ( acidAbl , "_config" , report , false , "ABL_E004_Acid " ) ;
AssignAsset ( chargeAbl , "_config" , report , false , "ABL_E004_Charge " ) ;
AssignAsset ( chaseAbl , "_config" , report , false , "ABL_E004_Chase " ) ;
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 ) ;
AssignReference ( chargeAbl , "_chargeHitBox" , chargeHitBox , 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 ( chargeHitBox , "_defaultSource" , dmgSrc , report ) ;
AssignReference ( biteHitBox , "_defaultSource" , dmgSrc , report ) ;
AssignReference ( slamHitBox , "_defaultSource" , dmgSrc , report ) ;
}
SetupPerceptionSystemSlots ( sensorHub , new [ ] { "aggro" , "attack_melee" , "attack_range" , "los " } , report ) ;
SetupPerceptionSystemSlots ( sensorHub , new [ ] { "aggro" , "attack_melee" , "attack_range" , "sight " } , report ) ;
report . Add ( "★ AppearAbility._appearClip / FacePlayerAbility._faceClip 等动画 Clip 待美术接入后在 Inspector 指定。" ) ;
report . Add ( "★ 在 E004_ZhiMu._deathPreClip 配置死亡前摇动画(两阶段死亡 Death_Pre 无敌)。" ) ;
report . Add ( "★ 挂载行为树 BehaviorTree 组件,指定 E004_ZhiMu.asset。" ) ;
Undo . CollapseUndoOperations ( undoGroup ) ;
@@ -1106,20 +1087,21 @@ namespace BaseGames.Editor
hurtCap . size = new Vector2 ( 1.1f , 1.9f ) ;
HurtBox hurtBox = GetOrAddComponent < HurtBox > ( hurtBoxT . gameObject ) ;
// Phase1 attack h itb oxes (disabled by default; abilities enable/disable as needed)
HitBox biteHB = CreateDisabledHitBox ( go . transform , "Phase1_BiteHitBox" , "EnemyHitBox" ,
true , report , size : new Vector2 ( 0.8f , 0.5f ) ) ;
HitBox swipeR = CreateDisabledHitBox ( go . transform , "Phase1_SwipeHitBox_R" , "EnemyHitBox" ,
true , report , size : new Vector2 ( 1.2f , 0.4f ) ) ;
HitBox swipeL = CreateDisabledHitBox ( go . transform , "Phase1_SwipeHitBox_L" , "EnemyHitBox" ,
true , report , size : new Vector2 ( 1.2f , 0.4f ) ) ;
HitBox stompHB = CreateDisabledHitBox ( go . transform , "Phase1_StompHitBox" , "EnemyHitBox" ,
false , report , radius : 1.0f ) ;
// Phase1 攻击 H itB ox(默认禁用;技能执行时由 BossSkillExecutor 开关)。
// 计划:挥扇三连 FanCombo × 3 + 龙卷接触 Tornado。
HitBox fan1 = CreateDisabledHitBox ( go . transform , "Phase1_FanCombo_HitBox_1" , "EnemyHitBox" ,
true , report , size : new Vector2 ( 1.0f , 0.5f ) ) ;
HitBox fan2 = CreateDisabledHitBox ( go . transform , "Phase1_FanCombo_HitBox_2" , "EnemyHitBox" ,
true , report , size : new Vector2 ( 1.0f , 0.5f ) ) ;
HitBox fan3 = CreateDisabledHitBox ( go . transform , "Phase1_FanCombo_HitBox_3" , "EnemyHitBox" ,
true , report , size : new Vector2 ( 1.2f , 0.6f ) ) ;
HitBox tornadoHB = CreateDisabledHitBox ( go . transform , "Phase1_Tornado_HitBox" , "EnemyHitBox" ,
true , report , size : new Vector2 ( 0.6f , 1.2f ) ) ;
// Muzzle transforms for Phase 2 skills
GetOrCreateChild ( go . transform , "WindBlade Muzzle" ) ;
GetOrCreateChild ( go . transform , "TornadoMuzzle" ) ;
GetOrCreateChild ( go . transform , "SummonSpawnPoint " ) ;
// 弹体发射点(Phase1 回旋扇 / 龙卷; Phase2 风石)
Transform boomerangMuzzleT = GetOrCreateChild ( go . transform , "Boomerang Muzzle" ) ;
Transform tornadoMuzzleT = GetOrCreateChild ( go . transform , "TornadoMuzzle" ) ;
Transform windStoneMuzzleT = GetOrCreateChild ( go . transform , "WindStoneMuzzle " ) ;
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset ( bossBase , "_statsSO" , report , false , "ENM_ChaoFeng_Stats" ) ;
@@ -1133,6 +1115,22 @@ namespace BaseGames.Editor
AssignReference ( bossBase , "_hurtBox" , hurtBox , report ) ;
AssignReference ( skillExec , "_animancer" , animancer , report ) ;
// 浮空 / 击落 / 弹体发射点接线(计划)
AssignReference ( bossBase , "_floatController" , floatCtrl , report ) ;
AssignReference ( bossBase , "_knockdownCounter" , knockdown , report ) ;
AssignReference ( bossBase , "_boomerangMuzzle" , boomerangMuzzleT , report ) ;
AssignReference ( bossBase , "_tornadoMuzzle" , tornadoMuzzleT , report ) ;
AssignReference ( bossBase , "_windStoneMuzzle" , windStoneMuzzleT , report ) ;
AssignReference ( floatCtrl , "_rb" , rb , report ) ;
AssignReference ( knockdown , "_boss" , bossBase , report ) ;
AssignReference ( knockdown , "_floatCtrl" , floatCtrl , report ) ;
// 弹体配置接线(ProjectileConfigSO,存在时自动绑定)
AssignAsset ( bossBase , "_boomerangConfig" , report , false , "PROJ_Boomerang_Config" ) ;
AssignAsset ( bossBase , "_tornadoSmallConfig" , report , false , "PROJ_TornadoSmall_Config" ) ;
AssignAsset ( bossBase , "_tornadoLargeConfig" , report , false , "PROJ_TornadoLarge_Config" ) ;
AssignAsset ( bossBase , "_windStoneConfig" , report , false , "PROJ_WindStone_Config" ) ;
AssignAsset ( bossBase , "_onEnemyDied" , report , false , "EVT_EnemyDied" ) ;
AssignAsset ( bossBase , "_onPlayerSpawned" , report , false , "EVT_PlayerSpawned" ) ;
AssignAsset ( bossBase , "_onBossFightEnded" , report , false , "EVT_BossFightEnded" ) ;
@@ -1150,30 +1148,34 @@ namespace BaseGames.Editor
new [ ] { "Platform" , "OneWayPlatform" , "MovingOneWayPlatform" , "MidHeightOneWayPlatform" } ,
report ) ;
// Collect BossSkillSOs and assign to executor
// 收集 BossSkillSO 并赋给执行器(计划技能集)
var skillAssets = new System . Collections . Generic . List < Object > ( ) ;
foreach ( var n in new [ ] { "ABL_ChaoFeng_Idle" , "ABL_ChaoFeng_Slam ", "ABL_ChaoFeng_Sweep " ,
"ABL_ChaoFeng_WindBlade " , "ABL_ChaoFeng_Summon" } )
foreach ( var n in new [ ] { "ABL_ChaoFeng_Boomerang " , "ABL_ChaoFeng_FanCombo " ,
"ABL_ChaoFeng_TornadoSmall " , "ABL_ChaoFeng_TornadoLarge" ,
"ABL_ChaoFeng_WindStone" } )
{
Object sk = FindFirstAsset ( n ) ;
if ( sk ! = null ) skillAssets . Add ( sk ) ;
else report . Add ( $"未找到 BossSkillSO: {n},请先一键创建 SO 后再重新运行此放置操作。" ) ;
else report . Add ( $"未找到 BossSkillSO: {n},请先一键创建 ChaoFeng SO 后再重新运行此放置操作。" ) ;
}
if ( skillAssets . Count > 0 )
AssignObjectArray ( skillExec , "_skills" , skillAssets . ToArray ( ) , report ) ;
AssignString ( skillExec , "_bossId" , "ChaoFeng" , report ) ;
AssignObjectArray ( skillExec , "_hitBoxes" , new Object [ ] { fan1 , fan2 , fan3 , tornadoHB } , report ) ;
Object dmgSrc = FindFirstAsset ( "CMB_DS_BossBody" , "CMB_DS_EnemyBody" ) ;
if ( dmgSrc ! = null )
{
foreach ( var hb in new [ ] { biteHB , swipeR , swipeL , stompHB } )
foreach ( var hb in new [ ] { fan1 , fan2 , fan3 , tornadoHB } )
if ( hb ! = null ) AssignReference ( hb , "_defaultSource" , dmgSrc , report ) ;
}
SetupPerceptionSystemSlots ( sensorHub , new [ ] { "aggro" , "attack_melee" , "attack_range" , "los " } , report ) ;
SetupPerceptionSystemSlots ( sensorHub , new [ ] { "aggro" , "attack_melee" , "attack_range" , "sight " } , report ) ;
report . Add ( "★ 设置 BossSkillExecutor._bossId = \"ChaoFeng\" 。" ) ;
report . Add ( "★ 将各 Phase1 HitBox 引用拖入 BossSkillExecutor._hitBoxes 数组 。" ) ;
report . Add ( "★ 将 WindBladeMuzzle / TornadoMuzzle / SummonSpawnPoint 拖入对应 BossSkillSO 字段 。" ) ;
report . Add ( "★ FanCombo 三段 HitBox 与 Tornado HitBox 已挂入 BossSkillExecutor._hitBoxes 。" ) ;
report . Add ( "★ 将 BoomerangMuzzle / TornadoMuzzle / WindStoneMuzzle 拖入对应 BossSkillSO 的发射点字段(如有) 。" ) ;
report . Add ( "★ 回旋扇收招/阶段过渡/击败演出等动画 Clip 待美术接入后在 ChaoFengBoss Inspector 指定 。" ) ;
report . Add ( "★ 挂载行为树 BehaviorTree 组件,指定 Boss_ChaoFeng.asset。" ) ;
Undo . CollapseUndoOperations ( undoGroup ) ;
@@ -1277,6 +1279,276 @@ namespace BaseGames.Editor
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" ;
/// <summary>敌人 Prefab 存盘目标:键 = 根对象名(= 地址),值 = (目标文件夹, 规范标签之外的额外标签)。</summary>
private static readonly Dictionary < string , ( string folder , string [ ] extraLabels ) > 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 ) } ,
} ;
/// <summary>弹体 Prefab 存盘目标:键 = 地址(= PoolKey),值 = Projectile 子类类型。</summary>
private static readonly Dictionary < string , System . Type > 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" ;
/// <summary>
/// 弹体配置默认值:键 = PoolKey(= Prefab 地址),值 = (Speed, Lifetime, LaunchAngleDeg, GravityScale)。
/// ArcProjectile 用 LaunchAngleDeg + GravityScale 形成抛物线;LinearProjectile 用 Speed 直线;
/// ReturnProjectile(回旋扇)无重力、速度由弹体脚本控制。数值为占位,策划可在 Inspector 调整。
/// </summary>
private static readonly Dictionary < string , ( float speed , float lifetime , float launchAngle , float gravity ) > 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 ) } , // 大龙卷:定点驻留
} ;
/// <summary>
/// 创建 6 个 <see cref="ProjectileConfigSO"/>( PoolKey 与弹体 Prefab 地址一致),
/// 绑定默认 DamageSource。配置 SO 经 Inspector 引用,不注册 Addressable。
/// </summary>
[MenuItem("BaseGames/Scene/Save Prefab/★ Projectile Configs (SO)", priority = 231)]
public static void CreateProjectileConfigs ( )
{
var report = new List < string > ( ) ;
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 < ProjectileConfigSO > ( ProjectileConfigFolder , $"{poolKey}_Config" ) ;
if ( cfg = = null )
cfg = AssetDatabase . LoadAssetAtPath < ProjectileConfigSO > ( $"{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 ) ) ;
}
/// <summary>
/// 调用现有 PlaceE00X / PlaceChaoFeng 在场景中搭建敌人,随后存盘为 Prefab 并注册 Addressable。
/// </summary>
/// <param name="key">根对象名(同时作为 Prefab 文件名与 Addressable 地址)。</param>
/// <param name="placer">现有的放置委托。</param>
/// <param name="removeSceneInstance">true 时存盘后删除场景实例(批量生成时避免堆叠)。</param>
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 > ( ) ;
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 ) ) ;
}
/// <summary>
/// 搭建一个弹体 GameObject( Rigidbody2D + Collider(trigger) + HitBox + PooledObject + 指定 Projectile 子类),
/// 存盘为 Prefab 并注册 Addressable。运行时由发射方 Initialize(ProjectileConfigSO,...) 注入速度/重力/伤害源。
/// </summary>
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 < string > ( ) ;
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 < Rigidbody2D > ( go ) ;
rb . bodyType = RigidbodyType2D . Dynamic ; // ArcProjectile 运行时按 config 设置 gravityScale
rb . gravityScale = 0f ; // 默认无重力(直线/回旋扇);抛物线弹由 config 注入
rb . constraints = RigidbodyConstraints2D . FreezeRotation ;
rb . interpolation = RigidbodyInterpolation2D . Interpolate ;
CircleCollider2D col = GetOrAddComponent < CircleCollider2D > ( go ) ;
col . isTrigger = true ;
col . radius = 0.25f ;
HitBox hitBox = GetOrAddComponent < HitBox > ( go ) ; // Projectile [RequireComponent(HitBox)]
GetOrAddComponent < PooledObject > ( 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 ) ) ;
}
/// <summary>
/// 将场景根对象存盘为 Prefab(已存在则弹窗确认覆盖)并注册 Addressable,返回 Prefab 路径(失败/取消返回 null)。
/// </summary>
private static string SaveRootAsPrefab ( GameObject root , string fileName , string folder ,
string [ ] extraLabels , List < string > 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 ( )
{