Files
zeling_v2/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs
Joywayer a1f54b68e6 Refactor interaction prompt system to use world space prompts
- Removed the InteractPromptWidget from HUD and its references in HUDController.
- Introduced IInteractPromptView interface for world space interaction prompts.
- Implemented WorldInteractPrompt class to manage display of interaction prompts in world space.
- Updated InteractableDetector to handle showing/hiding of world space prompts based on player proximity to interactable objects.
- Created a new prefab for UI_WorldInteractPrompt to facilitate the new interaction prompt system.
2026-06-10 14:14:08 +08:00

2399 lines
134 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using System.Reflection;
using Animancer;
using BaseGames.Boss;
using BaseGames.Camera;
using BaseGames.Combat;
using BaseGames.Combat.StatusEffects;
using BaseGames.Core.Assets;
using BaseGames.Core.Pool;
using BaseGames.Dialogue;
using BaseGames.Enemies;
using BaseGames.Enemies.Abilities;
using BaseGames.Enemies.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 PathBerserker2d;
using Unity.Cinemachine;
using UnityEditor;
using UnityEngine;
using UnityEngine.Tilemaps;
namespace BaseGames.Editor
{
/// <summary>
/// 场景对象快速放置工具。
/// 在当前活动场景中生成常用游戏对象(玩家、敌人、机关、存档点、相机等),
/// 并自动挂载基础组件、设置正确的物理层、绑定已有的事件频道资产。
///
/// 菜单BaseGames → Scene → Place → …
///
/// 所有操作支持 UndoCtrl+Z。生成后选中对象便于立即调整位置。
/// </summary>
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<string>();
// ── Player 根节点(行为+物理+标签三合一)──────────────────────────────
// Rigidbody2D / 所有 MonoBehaviour 集中于此节点。
// HurtBox 作为其子节点GetComponentInParent<IDamageable>() 向上即可找到
// 本节点上的 PlayerControllerIDamageable 实现者)。
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<Rigidbody2D>(root);
rb.bodyType = RigidbodyType2D.Dynamic;
rb.gravityScale = 2f;
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
GetOrAddComponent<BoxCollider2D>(root);
// 动画组件AnimancerComponent 需要 Animator 存在PlayerController
// [RequireComponent(typeof(AnimancerComponent))] 保证其存在)
GetOrAddComponent<Animator>(root);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(root);
SetupSpriteRenderer(root);
// 核心行为组件
PlayerStats playerStats = GetOrAddComponent<PlayerStats>(root);
PlayerMovement playerMovement = GetOrAddComponent<PlayerMovement>(root);
PlayerCombat playerCombat = GetOrAddComponent<PlayerCombat>(root);
FormController formController = GetOrAddComponent<FormController>(root);
WeaponManager weaponManager = GetOrAddComponent<WeaponManager>(root);
SkillManager skillManager = GetOrAddComponent<SkillManager>(root);
SpringSystem springSystem = GetOrAddComponent<SpringSystem>(root);
ParrySystem parrySystem = GetOrAddComponent<ParrySystem>(root);
ShieldComponent shield = GetOrAddComponent<ShieldComponent>(root);
PlayerWallDetector wallDetector = GetOrAddComponent<PlayerWallDetector>(root);
EquipmentManager equipmentManager = GetOrAddComponent<EquipmentManager>(root);
// PlayerFeedbackEquipmentManager.Awake 经 GetComponent<PlayerFeedback>() 取本节点引用(护符效果反馈),
// 缺失会触发断言。须与 EquipmentManager 同节点。Feel 反馈链(MMF_Player)留待 Inspector 配置。
GetOrAddComponent<PlayerFeedback>(root);
GetOrAddComponent<SkillModifierRegistry>(root);
StatusEffectManager statusEffectManager = GetOrAddComponent<StatusEffectManager>(root);
// PlayerController 最后添加RequireComponent 会拉取上方已加好的组件
PlayerController playerController = GetOrAddComponent<PlayerController>(root);
// ── HurtBox 子节点 ───────────────────────────────────────────────────
Transform hurtBoxT = GetOrCreateChild(root.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "PlayerHurtBox", report);
BoxCollider2D hurtCollider = GetOrAddComponent<BoxCollider2D>(hurtBoxT.gameObject);
hurtCollider.isTrigger = true;
HurtBox hurtBox = GetOrAddComponent<HurtBox>(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<SkillModifierRegistry>(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<InteractableDetector>(root);
AssignLayerMask(interactDetector, "_interactableLayer", new[] { "TriggerZone" }, report);
if (inputReader != null)
AssignReference(interactDetector, "_inputReader", inputReader, report);
if (animConfig == null) report.Add("★ 需创建并绑定PlayerController._animConfigPLY_PlayerAnimationConfig");
if (statsConfig == null) report.Add("★ 需创建并绑定PlayerStats._configPlayerStatsSO");
if (inputReader == null) report.Add("★ 需手动绑定PlayerController._inputReader / FormController._input / SkillManager._inputInputReaderSO");
if (equipmentConfig == null) report.Add("★ 需创建并绑定EquipmentManager._configEquipmentConfigSO");
if (charmCatalog == null) report.Add("★ 需创建并绑定EquipmentManager._charmCatalogCharmCatalogSO");
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<string>();
GameObject go = new GameObject("SpawnPoint");
Undo.RegisterCreatedObjectUndo(go, "Place Player Spawn Point");
go.transform.position = GetDropPosition();
PlayerSpawnPoint spawnPoint = GetOrAddComponent<PlayerSpawnPoint>(go);
AssignString(spawnPoint, "_transitionId", "default", report);
AssignInt(spawnPoint, "_facingDirection", 1);
report.Add("修改 _transitionId使其与对应 RoomTransition._targetTransitionId 匹配。");
report.Add("+1 = 朝右出生,-1 = 朝左出生_facingDirection。");
Selection.activeGameObject = go;
MarkDirtyAndLog("Player Spawn Point", go, report);
}
[MenuItem("BaseGames/Scene/Place/Enemy (Basic)", priority = 110)]
public static void PlaceEnemy() => PlaceEnemy(EnemyBodyColliderType.Box);
public static void PlaceEnemy(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
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<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Dynamic;
rb.gravityScale = 2f;
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.7f, 0.9f));
Transform visual = GetOrCreateChild(go.transform, "Visual");
visual.localPosition = (Vector3)(Vector2)body.offset;
GetOrAddComponent<Animator>(visual.gameObject);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
SpriteRenderer sr = SetupSpriteRenderer(visual.gameObject);
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
// HurtBox child
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
CapsuleCollider2D hurtCollider = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
hurtCollider.isTrigger = true;
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
// Contact-damage HitBox child
Transform hitBodyT = GetOrCreateChild(go.transform, "HitBox_Body");
SetLayer(hitBodyT.gameObject, "EnemyHitBox", report);
CircleCollider2D hitCollider = GetOrAddComponent<CircleCollider2D>(hitBodyT.gameObject);
hitCollider.isTrigger = true;
hitCollider.radius = 0.55f;
HitBox hitBox = GetOrAddComponent<HitBox>(hitBodyT.gameObject);
GetOrAddComponent<BodyContactDamage>(hitBodyT.gameObject);
// Wire EnemyBase
AssignReference(enemyBase, "_stats", enemyStats, report);
AssignReference(enemyBase, "_movement", movement, report);
AssignReference(enemyBase, "_animancer", animancer, report);
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
// Wire EnemyMovement
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr, report);
AssignLayerMask(movement, "_groundMask",
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
report);
// DamageSourceSO for body contact
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody", "DS_EnemyBody");
if (dmgSrc != null)
AssignReference(hitBox, "_defaultSource", dmgSrc, report);
else
report.Add("未找到 DamageSourceSOHitBox_Body._defaultSource 未绑定。请创建 CMB_DS_EnemyBody.asset。");
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report);
report.Add("★ 指定 EnemyBase._statsSO、_animConfig 资产(按所创建的敌人类型命名)。");
report.Add("★ 挂载行为树 BehaviorTree 组件,指定对应 .asset。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
EnemyBase.SuppressValidationWarnings = false;
MarkDirtyAndLog("Enemy (Basic)", go, report);
}
[MenuItem("BaseGames/Scene/Place/Boss Enemy", priority = 115)]
public static void PlaceBossEnemy() => PlaceBossEnemy(EnemyBodyColliderType.Box);
public static void PlaceBossEnemy(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
GameObject go = new GameObject("BossEnemy");
Undo.RegisterCreatedObjectUndo(go, "Place Boss Enemy");
go.transform.position = GetDropPosition();
SetLayer(go, "Enemy", report);
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Dynamic;
rb.gravityScale = 2f;
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
CreateBodyCollider(go, bodyCollider, new Vector2(1.5f, 2.5f));
GetOrAddComponent<Animator>(go);
SetupSpriteRenderer(go);
BossBase bossBase = GetOrAddComponent<BossBase>(go);
EnemyStats bossStats = GetOrAddComponent<EnemyStats>(go);
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
// HurtBox child
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
CapsuleCollider2D hurtCollider = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
hurtCollider.isTrigger = true;
hurtCollider.size = new Vector2(1.5f, 2.5f);
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
// Contact-damage HitBox child
Transform hitBodyT = GetOrCreateChild(go.transform, "HitBox_Body");
SetLayer(hitBodyT.gameObject, "EnemyHitBox", report);
CircleCollider2D hitCollider = GetOrAddComponent<CircleCollider2D>(hitBodyT.gameObject);
hitCollider.isTrigger = true;
hitCollider.radius = 0.9f;
HitBox hitBox = GetOrAddComponent<HitBox>(hitBodyT.gameObject);
GetOrAddComponent<BodyContactDamage>(hitBodyT.gameObject);
// References
AssignReference(bossBase, "_stats", bossStats, report);
// DamageSourceSO
Object dmgSrc = FindFirstAsset("CMB_DS_BossBody", "CMB_DS_EnemyBody", "DS_BossBody");
if (dmgSrc != null)
AssignReference(hitBox, "_defaultSource", dmgSrc, report);
else
report.Add("未找到 DamageSourceSOHitBox_Body._defaultSource 未绑定。请按规范创建 CMB_DS_BossBody.asset。");
// Event channels
AssignAsset(bossBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(bossBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
AssignAsset(bossBase, "_onBossFightEnded", report, false, "EVT_BossFightEnded");
AssignAsset(bossBase, "_onBossPhaseChanged", report, false, "EVT_BossPhaseChanged");
AssignAsset(bossStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
report.Add("填写 _bossId。");
report.Add("挂载 BossSkillSequencer 组件并指定技能序列 SO行为树、NavAgent 需手工添加。");
report.Add("多阶段 Boss 可在此 GameObject 上继续 AddComponent 阶段切换控制器。");
Selection.activeGameObject = go;
MarkDirtyAndLog("Boss Enemy", go, report);
}
// ══ 具体敌人快速放置 ════════════════════════════════════════════════════
[MenuItem("BaseGames/Scene/Place/Enemy E001 (草蛭)", priority = 111)]
public static void PlaceE001_CaoZhi() => PlaceE001_CaoZhi(EnemyBodyColliderType.Box);
public static void PlaceE001_CaoZhi(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
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<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Dynamic;
rb.gravityScale = 2f;
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.6f, 0.8f));
Transform visual = GetOrCreateChild(go.transform, "Visual");
visual.localPosition = (Vector3)(Vector2)body.offset;
GetOrAddComponent<Animator>(visual.gameObject);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
SpriteRenderer sr1 = SetupSpriteRenderer(visual.gameObject);
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
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.55f, 0.75f);
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
Transform contactT = GetOrCreateChild(go.transform, "ContactDamageZone");
SetLayer(contactT.gameObject, "EnemyHitBox", report);
CircleCollider2D contactCol = GetOrAddComponent<CircleCollider2D>(contactT.gameObject);
contactCol.isTrigger = true;
contactCol.radius = 0.4f;
HitBox contactHitBox = GetOrAddComponent<HitBox>(contactT.gameObject);
BodyContactDamage bodyContact = GetOrAddComponent<BodyContactDamage>(contactT.gameObject);
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
Transform alertT = GetOrCreateChild(abilitiesT, "PlayClipAbility_Alert");
PlayClipAbility alertAbility = GetOrAddComponent<PlayClipAbility>(alertT.gameObject);
Transform chaseT = GetOrCreateChild(abilitiesT, "ContactChaseAbility_Chase");
ContactChaseAbility chaseAbility = GetOrAddComponent<ContactChaseAbility>(chaseT.gameObject);
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E001_Stats");
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E001_AnimConfig");
// Component wiring
AssignReference(enemyBase, "_stats", enemyStats, report);
AssignReference(enemyBase, "_movement", movement, report);
AssignReference(enemyBase, "_animancer", animancer, report);
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignAsset(movement, "_config", report, false, "ENM_E001_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E001_AnimConfig");
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr1, report);
AssignLayerMask(movement, "_groundMask",
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
report);
AssignAsset(alertAbility, "_config", report, false, "ABL_E001_Alert");
AssignAsset(chaseAbility, "_config", report, false, "ABL_E001_Chase");
AssignReference(chaseAbility, "_contactDamage", bodyContact, report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report);
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E001_CaoZhi.asset。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
EnemyBase.SuppressValidationWarnings = false;
MarkDirtyAndLog("E001 草蛭", go, report);
}
[MenuItem("BaseGames/Scene/Place/Enemy E002 (簧蛭)", priority = 112)]
public static void PlaceE002_HuangZhi() => PlaceE002_HuangZhi(EnemyBodyColliderType.Box);
public static void PlaceE002_HuangZhi(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
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<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Kinematic;
rb.gravityScale = 0f;
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.5f, 0.7f));
// Visual 子节点:挂载精灵 / 动画EnemyMovement 翻转时操作此节点)
Transform visual = GetOrCreateChild(go.transform, "Visual");
visual.localPosition = (Vector3)(Vector2)body.offset;
GetOrAddComponent<Animator>(visual.gameObject);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
SpriteRenderer sr = SetupSpriteRenderer(visual.gameObject);
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
// HurtBoxcomponent.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);
hurtBox.enabled = false; // 悬挂阶段外不可被攻击CeilingHangStrikeAbility 在 _loopClip 期间开启
// AttackHitBox钻出啃咬瞬间判定由 CeilingHangStrikeAbility 激活)
Transform attackHitBoxT = GetOrCreateChild(go.transform, "AttackHitBox");
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 strikeT = GetOrCreateChild(abilitiesT, "CeilingHangStrikeAbility");
CeilingHangStrikeAbility strikeAbility = GetOrAddComponent<CeilingHangStrikeAbility>(strikeT.gameObject);
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E002_Stats");
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E002_AnimConfig");
AssignReference(enemyBase, "_stats", enemyStats, report);
AssignReference(enemyBase, "_movement", movement, report);
AssignReference(enemyBase, "_animancer", animancer, report);
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignAsset(movement, "_config", report, false, "ENM_E002_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E002_AnimConfig");
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr, report);
AssignLayerMask(movement, "_groundMask",
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
report);
AssignAsset(strikeAbility, "_config", report, false, "ABL_E002_CeilingStrike");
AssignReference(strikeAbility, "_attackHitBox", attackHitBox, report);
AssignReference(strikeAbility, "_hurtBox", hurtBox, report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null)
AssignReference(attackHitBox, "_defaultSource", dmgSrc, report);
SetupPerceptionSystemSlots(sensorHub, new[] { "attack_range" }, report);
report.Add("★ 将此对象放置于天花板,调整位置使 CapsuleCollider 正好贴合天花板底面。");
report.Add("★ HurtBox.enabled 初始为 false钻出后由 CeilingHangStrikeAbility 在悬挂窗口开启。");
report.Add("★ attack_range 槽位为正下方 BoxCast玩家经过检测区按需在 Inspector 调整 offset/size。");
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E002_HuangZhi.asset。");
report.Add("★ BD 树逻辑建议Idle悬挂→ IsSensorDetecting(aggro) → UseAbility(CeilingDrop) → IsGrounded → Patrol(Pace)。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
EnemyBase.SuppressValidationWarnings = false;
MarkDirtyAndLog("E002 簧蛭", go, report);
}
[MenuItem("BaseGames/Scene/Place/Enemy E003 (幼蛭)", priority = 113)]
public static void PlaceE003_YouZhi_Enemy() => PlaceE003_YouZhi_Enemy(EnemyBodyColliderType.Box);
public static void PlaceE003_YouZhi_Enemy(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
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<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Kinematic;
rb.gravityScale = 0f;
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.5f, 0.6f));
Transform visual = GetOrCreateChild(go.transform, "Visual");
visual.localPosition = (Vector3)(Vector2)body.offset;
GetOrAddComponent<Animator>(visual.gameObject);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
SpriteRenderer sr3 = SetupSpriteRenderer(visual.gameObject);
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
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.55f);
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
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);
BodyContactDamage bodyContact = GetOrAddComponent<BodyContactDamage>(contactT.gameObject);
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
Transform fallT = GetOrCreateChild(abilitiesT, "AnimatedCeilingDropAbility");
AnimatedCeilingDropAbility fallAbility = GetOrAddComponent<AnimatedCeilingDropAbility>(fallT.gameObject);
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E003_Stats");
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E003_AnimConfig");
AssignReference(enemyBase, "_stats", enemyStats, report);
AssignReference(enemyBase, "_movement", movement, report);
AssignReference(enemyBase, "_animancer", animancer, report);
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignAsset(movement, "_config", report, false, "ENM_E003_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E003_AnimConfig");
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr3, report);
AssignLayerMask(movement, "_groundMask",
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
report);
AssignAsset(fallAbility, "_config", report, false, "ABL_E003_Fall");
AssignReference(fallAbility, "_contactDamage", bodyContact, report);
// 出生 / 外部触发执行下坠能力(零代码:替代过去的 E003_YouZhi 专属脚本)
EnemyAbilityTrigger fallTrigger = GetOrAddComponent<EnemyAbilityTrigger>(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("★ 挂载行为树 BehaviorTree 组件,指定对应外部行为树资产。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
EnemyBase.SuppressValidationWarnings = false;
MarkDirtyAndLog("E003 幼蛭", go, report);
}
[MenuItem("BaseGames/Scene/Place/Enemy E004 (蛭母)", priority = 114)]
public static void PlaceE004_ZhiMu_Enemy() => PlaceE004_ZhiMu_Enemy(EnemyBodyColliderType.Box);
public static void PlaceE004_ZhiMu_Enemy(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
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<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Dynamic;
rb.gravityScale = 2f;
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.8f, 1.2f));
Transform visual = GetOrCreateChild(go.transform, "Visual");
visual.localPosition = (Vector3)(Vector2)body.offset;
GetOrAddComponent<Animator>(visual.gameObject);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
SpriteRenderer sr4 = SetupSpriteRenderer(visual.gameObject);
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
EnemyFeedback feedback = GetOrAddComponent<EnemyFeedback>(go);
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
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.75f, 1.1f);
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
Transform biteT = GetOrCreateChild(go.transform, "BiteHitBox");
SetLayer(biteT.gameObject, "EnemyHitBox", report);
BoxCollider2D biteCol = GetOrAddComponent<BoxCollider2D>(biteT.gameObject);
biteCol.isTrigger = true;
biteCol.size = new Vector2(0.6f, 0.4f);
HitBox biteHitBox = GetOrAddComponent<HitBox>(biteT.gameObject);
biteT.gameObject.SetActive(false);
Transform slamT = GetOrCreateChild(go.transform, "SlamHitBox");
SetLayer(slamT.gameObject, "EnemyHitBox", report);
CircleCollider2D slamCol = GetOrAddComponent<CircleCollider2D>(slamT.gameObject);
slamCol.isTrigger = true;
slamCol.radius = 0.7f;
HitBox slamHitBox = GetOrAddComponent<HitBox>(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<AppearAbility>(appearAblT.gameObject);
Transform biteAblT = GetOrCreateChild(abilitiesT, "MeleeAttackAbility_Bite");
MeleeAttackAbility biteAbl = GetOrAddComponent<MeleeAttackAbility>(biteAblT.gameObject);
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 flipAblT = GetOrCreateChild(abilitiesT, "FacePlayerAbility_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");
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E004_AnimConfig");
AssignReference(enemyBase, "_stats", enemyStats, report);
AssignReference(enemyBase, "_movement", movement, report);
AssignReference(enemyBase, "_animancer", animancer, report);
AssignReference(enemyBase, "_feedback", feedback, report);
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignAsset(movement, "_config", report, false, "ENM_E004_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E004_AnimConfig");
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr4, report);
AssignLayerMask(movement, "_groundMask",
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
report);
AssignAsset(appearAbl, "_config", report, false, "ABL_E004_Appear");
AssignAsset(biteAbl, "_config", report, false, "ABL_E004_Bite");
AssignAsset(slamAbl, "_config", report, false, "ABL_E004_HeadSlam");
AssignAsset(acidAbl, "_config", report, false, "ABL_E004_Acid");
AssignAsset(flipAbl, "_config", report, false, "ABL_E004_Flip");
AssignMeleeHitBoxSlots(biteAbl, new[] { ("bite", biteHitBox) }, report);
AssignReference(slamAbl, "_hitBox", slamHitBox, report);
AssignReference(acidAbl, "_muzzle", acidMuzzleT, report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null)
{
AssignReference(biteHitBox, "_defaultSource", dmgSrc, report);
AssignReference(slamHitBox, "_defaultSource", dmgSrc, report);
}
// 死亡前摇无敌演出(零代码:替代过去的 E004_ZhiMu 专属 Die() 重写)
EnemyDeathSequence deathSeq = GetOrAddComponent<EnemyDeathSequence>(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("★ 挂载行为树 BehaviorTree 组件,指定对应外部行为树资产。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
EnemyBase.SuppressValidationWarnings = false;
MarkDirtyAndLog("E004 蛭母", go, report);
}
[MenuItem("BaseGames/Scene/Place/Enemy E005 (肥蛭)", priority = 115)]
public static void PlaceE005_FeiZhi_Enemy() => PlaceE005_FeiZhi_Enemy(EnemyBodyColliderType.Box);
public static void PlaceE005_FeiZhi_Enemy(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
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<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Dynamic;
rb.gravityScale = 2f;
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.9f, 1.0f));
Transform visual = GetOrCreateChild(go.transform, "Visual");
visual.localPosition = (Vector3)(Vector2)body.offset;
GetOrAddComponent<Animator>(visual.gameObject);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
SpriteRenderer sr5 = SetupSpriteRenderer(visual.gameObject);
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
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.85f, 0.95f);
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
Transform biteT = GetOrCreateChild(go.transform, "BiteHitBox");
SetLayer(biteT.gameObject, "EnemyHitBox", report);
BoxCollider2D biteCol = GetOrAddComponent<BoxCollider2D>(biteT.gameObject);
biteCol.isTrigger = true;
biteCol.size = new Vector2(0.7f, 0.45f);
HitBox biteHitBox = GetOrAddComponent<HitBox>(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<MeleeAttackAbility>(biteAblT.gameObject);
Transform acidAblT = GetOrCreateChild(abilitiesT, "ProjectileAttackAbility_Acid");
ProjectileAttackAbility acidAbl = GetOrAddComponent<ProjectileAttackAbility>(acidAblT.gameObject);
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E005_Stats");
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E005_AnimConfig");
AssignReference(enemyBase, "_stats", enemyStats, report);
AssignReference(enemyBase, "_movement", movement, report);
AssignReference(enemyBase, "_animancer", animancer, report);
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignAsset(movement, "_config", report, false, "ENM_E005_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E005_AnimConfig");
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr5, report);
AssignLayerMask(movement, "_groundMask",
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
report);
AssignAsset(biteAbl, "_config", report, false, "ABL_E005_Bite");
AssignAsset(acidAbl, "_config", report, false, "ABL_E005_Acid");
AssignMeleeHitBoxSlots(biteAbl, new[] { ("bite", biteHitBox) }, report);
AssignReference(acidAbl, "_muzzle", acidMuzzleT, report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null) AssignReference(biteHitBox, "_defaultSource", dmgSrc, report);
// 死亡前摇无敌演出 + 动画事件池生成幼蛭(零代码:替代过去的 E005_FeiZhi 专属 Die()/SpawnProjectile 重写)
EnemyDeathSequence deathSeq = GetOrAddComponent<EnemyDeathSequence>(go);
AssignObjectArray(deathSeq, "_hurtBoxesToDisable", new Object[] { hurtBox }, report);
EnemySpawnerOnEvent spawner = GetOrAddComponent<EnemySpawnerOnEvent>(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("★ 挂载行为树 BehaviorTree 组件,指定对应外部行为树资产。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
EnemyBase.SuppressValidationWarnings = false;
MarkDirtyAndLog("E005 肥蛭", go, report);
}
[MenuItem("BaseGames/Scene/Place/Enemy E006 (讙)", priority = 116)]
public static void PlaceE006_Huan() => PlaceE006_Huan(EnemyBodyColliderType.Box);
public static void PlaceE006_Huan(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
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<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Dynamic;
rb.gravityScale = 2f;
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.7f, 1.0f));
Transform visual = GetOrCreateChild(go.transform, "Visual");
visual.localPosition = (Vector3)(Vector2)body.offset;
GetOrAddComponent<Animator>(visual.gameObject);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
SpriteRenderer sr6 = SetupSpriteRenderer(visual.gameObject);
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
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.65f, 0.95f);
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
Transform contactT = GetOrCreateChild(go.transform, "ContactDamageZone");
SetLayer(contactT.gameObject, "EnemyHitBox", report);
CircleCollider2D contactCol = GetOrAddComponent<CircleCollider2D>(contactT.gameObject);
contactCol.isTrigger = true;
contactCol.radius = 0.4f;
HitBox contactHitBox = GetOrAddComponent<HitBox>(contactT.gameObject);
BodyContactDamage bodyContact = GetOrAddComponent<BodyContactDamage>(contactT.gameObject);
Transform landT = GetOrCreateChild(go.transform, "LandingHitBox");
SetLayer(landT.gameObject, "EnemyHitBox", report);
CircleCollider2D landCol = GetOrAddComponent<CircleCollider2D>(landT.gameObject);
landCol.isTrigger = true;
landCol.radius = 0.8f;
HitBox landHitBox = GetOrAddComponent<HitBox>(landT.gameObject);
landT.gameObject.SetActive(false);
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
Transform leapT = GetOrCreateChild(abilitiesT, "LeapAttackAbility");
LeapAttackAbility leapAbl = GetOrAddComponent<LeapAttackAbility>(leapT.gameObject);
Transform chaseT = GetOrCreateChild(abilitiesT, "ContactChaseAbility");
ContactChaseAbility chaseAbl = GetOrAddComponent<ContactChaseAbility>(chaseT.gameObject);
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E006_Stats");
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E006_AnimConfig");
AssignReference(enemyBase, "_stats", enemyStats, report);
AssignReference(enemyBase, "_movement", movement, report);
AssignReference(enemyBase, "_animancer", animancer, report);
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignAsset(movement, "_config", report, false, "ENM_E006_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E006_AnimConfig");
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr6, report);
AssignLayerMask(movement, "_groundMask",
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
report);
AssignAsset(leapAbl,"_config", report, false, "ABL_E006_Leap");
AssignAsset(chaseAbl, "_config", report, false, "ABL_E006_Chase");
AssignReference(leapAbl, "_landingHitBox", landHitBox, report);
AssignReference(chaseAbl, "_contactDamage", bodyContact, report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null)
{
AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
AssignReference(landHitBox, "_defaultSource", dmgSrc, report);
}
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report);
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E006_Huan.asset。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
EnemyBase.SuppressValidationWarnings = false;
MarkDirtyAndLog("E006 讙", go, report);
}
[MenuItem("BaseGames/Scene/Place/Boss 嘲风 (ChaoFeng)", priority = 117)]
public static void PlaceChaoFeng() => PlaceChaoFeng(EnemyBodyColliderType.Box);
public static void PlaceChaoFeng(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
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<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Dynamic;
rb.gravityScale = 2f;
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(1.2f, 2.0f));
Transform visual = GetOrCreateChild(go.transform, "Visual");
visual.localPosition = (Vector3)(Vector2)body.offset;
GetOrAddComponent<Animator>(visual.gameObject);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
SpriteRenderer srBoss = SetupSpriteRenderer(visual.gameObject);
ChaoFengBoss bossBase = GetOrAddComponent<ChaoFengBoss>(go);
EnemyStats bossStats = GetOrAddComponent<EnemyStats>(go);
EnemyFeedback feedback = GetOrAddComponent<EnemyFeedback>(go);
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
BossSkillExecutor skillExec = GetOrAddComponent<BossSkillExecutor>(go);
ChaoFengFloatController floatCtrl = GetOrAddComponent<ChaoFengFloatController>(go);
ChaoFengKnockdownCounter knockdown = GetOrAddComponent<ChaoFengKnockdownCounter>(go);
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
// HurtBox
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
hurtCap.isTrigger = true;
hurtCap.size = new Vector2(1.1f, 1.9f);
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
// Phase1 攻击 HitBox默认禁用技能执行时由 BossSkillExecutor 开关)。
// 计划:挥扇三连 FanCombo ×3 + 龙卷接触 Tornado。
HitBox fan1 = CreateDisabledHitBox(go.transform, "Phase1_FanCombo_HitBox_1", "EnemyHitBox",
true, report, size: new Vector2(1.0f, 0.5f));
HitBox fan2 = CreateDisabledHitBox(go.transform, "Phase1_FanCombo_HitBox_2", "EnemyHitBox",
true, report, size: new Vector2(1.0f, 0.5f));
HitBox fan3 = CreateDisabledHitBox(go.transform, "Phase1_FanCombo_HitBox_3", "EnemyHitBox",
true, report, size: new Vector2(1.2f, 0.6f));
HitBox tornadoHB = CreateDisabledHitBox(go.transform, "Phase1_Tornado_HitBox", "EnemyHitBox",
true, report, size: new Vector2(0.6f, 1.2f));
// 弹体发射点Phase1 回旋扇 / 龙卷Phase2 风石)
Transform boomerangMuzzleT = GetOrCreateChild(go.transform, "BoomerangMuzzle");
Transform tornadoMuzzleT = GetOrCreateChild(go.transform, "TornadoMuzzle");
Transform windStoneMuzzleT = GetOrCreateChild(go.transform, "WindStoneMuzzle");
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset(bossBase, "_statsSO", report, false, "ENM_ChaoFeng_Stats");
AssignAsset(bossBase, "_animConfig", report, false, "ENM_ChaoFeng_AnimConfig");
// Component wiring
AssignReference(bossBase, "_stats", bossStats, report);
AssignReference(bossBase, "_movement", movement, report);
AssignReference(bossBase, "_animancer", animancer, report);
AssignReference(bossBase, "_feedback", feedback, report);
AssignReference(bossBase, "_hurtBox", hurtBox, report);
AssignReference(skillExec, "_animancer", animancer, report);
// 浮空 / 击落 / 弹体发射点接线(计划)
AssignReference(bossBase, "_floatController", floatCtrl, report);
AssignReference(bossBase, "_knockdownCounter", knockdown, report);
AssignReference(bossBase, "_boomerangMuzzle", boomerangMuzzleT, report);
AssignReference(bossBase, "_tornadoMuzzle", tornadoMuzzleT, report);
AssignReference(bossBase, "_windStoneMuzzle", windStoneMuzzleT, report);
AssignReference(floatCtrl, "_rb", rb, report);
AssignReference(knockdown, "_boss", bossBase, report);
AssignReference(knockdown, "_floatCtrl", floatCtrl, report);
// 弹体配置接线ProjectileConfigSO存在时自动绑定
AssignAsset(bossBase, "_boomerangConfig", report, false, "PROJ_Boomerang_Config");
AssignAsset(bossBase, "_tornadoSmallConfig", report, false, "PROJ_TornadoSmall_Config");
AssignAsset(bossBase, "_tornadoLargeConfig", report, false, "PROJ_TornadoLarge_Config");
AssignAsset(bossBase, "_windStoneConfig", report, false, "PROJ_WindStone_Config");
AssignAsset(bossBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(bossBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
AssignAsset(bossBase, "_onBossFightEnded", report, false, "EVT_BossFightEnded");
AssignAsset(bossBase, "_onBossPhaseChanged", report, false, "EVT_BossPhaseChanged");
AssignAsset(bossStats, "_onDifficultyChanged",report, false, "EVT_DifficultyChanged");
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignAsset(movement, "_config", report, false, "ENM_ChaoFeng_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_ChaoFeng_AnimConfig");
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", srBoss, report);
AssignLayerMask(movement, "_groundMask",
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
report);
// 收集 BossSkillSO 并赋给执行器(计划技能集)
var skillAssets = new System.Collections.Generic.List<Object>();
foreach (var n in new[] { "ABL_ChaoFeng_Boomerang", "ABL_ChaoFeng_FanCombo",
"ABL_ChaoFeng_TornadoSmall", "ABL_ChaoFeng_TornadoLarge",
"ABL_ChaoFeng_WindStone" })
{
Object sk = FindFirstAsset(n);
if (sk != null) skillAssets.Add(sk);
else report.Add($"未找到 BossSkillSO{n},请先一键创建 ChaoFeng SO 后再重新运行此放置操作。");
}
if (skillAssets.Count > 0)
AssignObjectArray(skillExec, "_skills", skillAssets.ToArray(), report);
AssignString(skillExec, "_bossId", "ChaoFeng", report);
AssignObjectArray(skillExec, "_hitBoxes", new Object[] { fan1, fan2, fan3, tornadoHB }, report);
Object dmgSrc = FindFirstAsset("CMB_DS_BossBody", "CMB_DS_EnemyBody");
if (dmgSrc != null)
{
foreach (var hb in new[] { fan1, fan2, fan3, tornadoHB })
if (hb != null) AssignReference(hb, "_defaultSource", dmgSrc, report);
}
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "sight" }, report);
report.Add("★ FanCombo 三段 HitBox 与 Tornado HitBox 已挂入 BossSkillExecutor._hitBoxes。");
report.Add("★ 将 BoomerangMuzzle / TornadoMuzzle / WindStoneMuzzle 拖入对应 BossSkillSO 的发射点字段(如有)。");
report.Add("★ 回旋扇收招/阶段过渡/击败演出等动画 Clip 待美术接入后在 ChaoFengBoss Inspector 指定。");
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 Boss_ChaoFeng.asset。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
EnemyBase.SuppressValidationWarnings = false;
MarkDirtyAndLog("Boss 嘲风 (ChaoFeng)", go, report);
}
// ══ 敌人放置辅助方法 ═══════════════════════════════════════════════════
/// <summary>
/// 创建禁用的 HitBox 子节点Box 或 Circle 碰撞体)。
/// </summary>
private static HitBox CreateDisabledHitBox(Transform parent, string childName, string layer,
bool isBox, List<string> report,
Vector2 size = default, float radius = 0.5f)
{
Transform t = GetOrCreateChild(parent, childName);
SetLayer(t.gameObject, layer, report);
if (isBox)
{
BoxCollider2D col = GetOrAddComponent<BoxCollider2D>(t.gameObject);
col.isTrigger = true;
col.size = size == default ? new Vector2(0.5f, 0.5f) : size;
}
else
{
CircleCollider2D col = GetOrAddComponent<CircleCollider2D>(t.gameObject);
col.isTrigger = true;
col.radius = radius;
}
HitBox hb = GetOrAddComponent<HitBox>(t.gameObject);
t.gameObject.SetActive(false);
return hb;
}
/// <summary>
/// 在 <see cref="PhysicsPerceptionSystem"/> 上预填充 <c>_slots</c> 数组,
/// 根据 slotName 自动选择类型、半径、检测层及 GizmoColor。
/// </summary>
private static void SetupPerceptionSystemSlots(PhysicsPerceptionSystem system, string[] slotNames, List<string> report)
{
var so = new SerializedObject(system);
var slots = so.FindProperty("_slots");
if (slots == null || !slots.isArray)
{
report?.Add("PhysicsPerceptionSystem._slots 字段未找到,请检查脚本序列化。");
return;
}
int playerLayer = LayerMask.GetMask("Player");
slots.arraySize = slotNames.Length;
for (int i = 0; i < slotNames.Length; i++)
{
var elem = slots.GetArrayElementAtIndex(i);
string name = slotNames[i];
elem.FindPropertyRelative("slotName").stringValue = name;
int enumIdx = 0; // RangeCircle
float radius = 3f;
int layer = playerLayer;
switch (name)
{
case "aggro": enumIdx = 0; radius = 5f; layer = playerLayer; break;
case "los": enumIdx = 1; radius = 0f; layer = 0; break;
case "attack_melee":enumIdx = 0; radius = 1.5f; layer = playerLayer; break;
case "attack_range":enumIdx = 0; radius = 8f; layer = playerLayer; break;
case "patrol": enumIdx = 0; radius = 5f; layer = 0; break;
case "alert": enumIdx = 0; radius = 3f; layer = playerLayer; break;
case "sight": enumIdx = 4; radius = 6f; layer = playerLayer; break;
}
elem.FindPropertyRelative("type").enumValueIndex = enumIdx;
elem.FindPropertyRelative("radius").floatValue = radius;
elem.FindPropertyRelative("detectLayer").intValue = layer;
// sight 槽位默认设置推荐的 LOS 采样点数3中心+上+下)
if (name == "sight")
{
var losRayCountProp = elem.FindPropertyRelative("losRayCount");
if (losRayCountProp != null) losRayCountProp.intValue = 3;
}
// 各 slot 分配语义化默认颜色,可在 Inspector 中按需覆盖
Color defaultColor = name switch
{
"aggro" => new Color(1.00f, 0.60f, 0.10f, 1f), // 橙
"los" => new Color(0.00f, 0.80f, 1.00f, 1f), // 青
"attack_melee" => new Color(1.00f, 0.20f, 0.20f, 1f), // 红
"attack_range" => new Color(1.00f, 0.40f, 0.60f, 1f), // 粉红
"patrol" => new Color(0.20f, 0.90f, 0.20f, 1f), // 绿
"alert" => new Color(1.00f, 0.90f, 0.10f, 1f), // 黄
"sight" => new Color(0.30f, 0.85f, 1.00f, 1f), // 浅蓝LOS 传感器)
_ => Color.clear, // 未知 slot 回退为紫色
};
elem.FindPropertyRelative("gizmoColor").colorValue = defaultColor;
}
so.ApplyModifiedPropertiesWithoutUndo();
}
// ══════════════════════════════════════════════════════════════════════
// 放置 + 存盘为 Prefab + 注册 Addressable
//
// 规范:敌人/弹体 Prefab 须落到 Prefabs/ 下并注册 Addressable
// (地址 = 文件名;分组/标签由 AddressableRules 推导,与校验器一致)。
// 复用上方各 PlaceE00X / PlaceChaoFeng 搭建逻辑,确保场景搭建与 Prefab 产出同源。
// 菜单BaseGames → Scene → Save Prefab → …
// ══════════════════════════════════════════════════════════════════════
private const string EnemyPrefabRoot = "Assets/_Game/Prefabs/Enemies";
private const string ProjectilePrefabFolder = "Assets/_Game/Prefabs/Combat/Projectiles";
/// <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>
/// 搭建一个弹体 GameObjectRigidbody2D + 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_EnemyBodyHitBox._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()
{
var report = new List<string>();
GameObject go = new GameObject("LethalTrap");
Undo.RegisterCreatedObjectUndo(go, "Place LethalTrap");
go.transform.position = GetDropPosition();
SetLayer(go, "EnemyHitBox", report);
BoxCollider2D col = GetOrAddComponent<BoxCollider2D>(go);
col.isTrigger = true;
col.size = new Vector2(2f, 0.5f);
SetupSpriteRenderer(go);
LethalTrap trap = GetOrAddComponent<LethalTrap>(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<BoxCollider2D>(hurtBoxT.gameObject);
hurtCol.isTrigger = true;
hurtCol.size = new Vector2(2f, 0.3f);
GetOrAddComponent<HurtBox>(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<string>();
GameObject go = new GameObject("CheckpointMarker");
Undo.RegisterCreatedObjectUndo(go, "Place CheckpointMarker");
go.transform.position = GetDropPosition();
SetLayer(go, "TriggerZone", report);
BoxCollider2D col = GetOrAddComponent<BoxCollider2D>(go);
col.isTrigger = true;
col.size = new Vector2(1f, 2f);
CheckpointMarker marker = GetOrAddComponent<CheckpointMarker>(go);
AssignLayerMask(marker, "_playerLayers", "Player", report);
AssignAsset(marker, "_onCheckpointReached", report, false, "EVT_CheckpointReached");
report.Add("放置于跳跳乐段落的关键节点处;玩家经过后成为该房间最近检查点。");
report.Add("同一房间可放置多个,以最近经过的为准。");
Selection.activeGameObject = go;
MarkDirtyAndLog("Checkpoint Marker", go, report);
}
[MenuItem("BaseGames/Scene/Place/Collectible (LingZhu)", priority = 130)]
public static void PlaceCollectible()
{
var report = new List<string>();
GameObject go = new GameObject("Collectible_LingZhu");
Undo.RegisterCreatedObjectUndo(go, "Place Collectible");
go.transform.position = GetDropPosition();
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
rb.gravityScale = 1f;
rb.freezeRotation = true;
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
CircleCollider2D col = GetOrAddComponent<CircleCollider2D>(go);
col.isTrigger = true;
col.radius = 0.3f;
SetupSpriteRenderer(go);
Collectible collectible = GetOrAddComponent<Collectible>(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<string>();
GameObject go = new GameObject("SavePoint");
Undo.RegisterCreatedObjectUndo(go, "Place Save Point");
go.transform.position = GetDropPosition();
SetLayer(go, "TriggerZone", report);
BoxCollider2D col = GetOrAddComponent<BoxCollider2D>(go);
col.isTrigger = true;
col.size = new Vector2(1f, 1.5f);
SetupSpriteRenderer(go);
SavePoint savePoint = GetOrAddComponent<SavePoint>(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<string>();
GameObject go = new GameObject("TeleportStation");
Undo.RegisterCreatedObjectUndo(go, "Place TeleportStation");
go.transform.position = GetDropPosition();
SetLayer(go, "TriggerZone", report);
BoxCollider2D col = GetOrAddComponent<BoxCollider2D>(go);
col.isTrigger = true;
col.size = new Vector2(1.5f, 2f);
SetupSpriteRenderer(go);
TeleportStation station = GetOrAddComponent<TeleportStation>(go);
AssignAsset(station, "_onFastTravelOpen", report, false, "EVT_FastTravelOpen");
report.Add("填写 _stationId传送站唯一 ID用于地图 UI 标注)。");
report.Add("传送站不存档、不复活、不恢复 HP与存档点是独立对象。");
Selection.activeGameObject = go;
MarkDirtyAndLog("Teleport Station", go, report);
}
[MenuItem("BaseGames/Scene/Place/Room Transition", priority = 140)]
public static void PlaceRoomTransition()
{
var report = new List<string>();
GameObject go = new GameObject("RoomTransition");
Undo.RegisterCreatedObjectUndo(go, "Place Room Transition");
go.transform.position = GetDropPosition();
SetLayer(go, "TriggerZone", report);
BoxCollider2D col = GetOrAddComponent<BoxCollider2D>(go);
col.isTrigger = true;
col.size = new Vector2(1f, 2.5f);
RoomTransition transition = GetOrAddComponent<RoomTransition>(go);
AssignString(transition, "_transitionId", "exit_default", report);
AssignBool(transition, "_autoTrigger", true);
AssignBool(transition, "_requiresKeyItem", false);
AssignAsset(transition, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
report.Add("填写 _transitionId本出口唯一 ID、_targetSceneAddress目标场景 Addressable Key、_targetTransitionId目标出生点 ID。");
report.Add("若需锁门,设 _requiresKeyItem = true 并填写 _requiredItemId。");
report.Add("_worldState 字段需拖入 WorldStateRegistry SO可选。");
Selection.activeGameObject = go;
MarkDirtyAndLog("Room Transition", go, report);
}
/// <summary>
/// 在交互物上挂载世界空间交互提示UI_WorldInteractPrompt 预制体),作为子节点跟随物体显示。
/// 提示显隐由玩家身上的 InteractableDetector 在进入/离开最近可交互物时驱动(按 IInteractPromptView 接口)。
/// 幂等已存在同名子节点则跳过。localY 为气泡相对物体的高度,可后续在场景中拖动该子节点单独微调。
/// </summary>
private static void AttachInteractPrompt(GameObject host, float localY, List<string> report)
{
if (host.transform.Find("InteractPrompt") != null) return;
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(
"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<string>();
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<BoxCollider2D>(go);
col.isTrigger = true;
col.size = new Vector2(1.5f, 2.5f);
// 精灵渲染器 + 动画(门对象通常有外观与开关动画)
SetupSpriteRenderer(go);
GetOrAddComponent<Animator>(go);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
// DoorTransition 组件
DoorTransition door = GetOrAddComponent<DoorTransition>(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<string>();
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<BoxCollider2D>(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<LinkedDoorTransition>(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<BoxCollider2D>(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<LinkedDoorTransition>(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("_facingDirectionOnArriveA→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");
/// <param name="areaName">
/// 生成的 CameraArea GameObject 名称。
/// 子节点 AreaBoundary 和 TriggerZone 将以此为前缀命名(如 MyZone_AreaBoundary
/// </param>
/// <param name="parent">生成的 GameObject 所挂载的父节点(为 null 时放置于场景根节点)。</param>
public static void PlaceCameraArea(string areaName, Transform parent = null)
{
var report = new List<string>();
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<CameraArea>(go);
// AreaBoundary child — 提供 CinemachineConfiner3D 所需的限位体积
Transform boundaryT = GetOrCreateChild(go.transform, $"{areaName}_AreaBoundary");
BoxCollider boundaryCollider = GetOrAddComponent<BoxCollider>(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<CameraTriggerZone>(zoneGo);
PolygonCollider2D col = GetOrAddComponent<PolygonCollider2D>(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<RoomController>(FindObjectsSortMode.None);
#else
var roomControllers = Object.FindObjectsOfType<RoomController>();
#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<string>();
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<BoxCollider2D>(go);
SetupSpriteRenderer(go);
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(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<string>();
// 根节点:平台实体 + 路径点都挂在此节点下,路径点不随平台本体移动
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<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Kinematic;
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
rb.freezeRotation = true;
BoxCollider2D col = GetOrAddComponent<BoxCollider2D>(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<BoxCollider2D>(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<MovingPlatform>(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<string>();
GameObject gridGo = new GameObject("GroundGrid");
Undo.RegisterCreatedObjectUndo(gridGo, "Place Tilemap Ground");
gridGo.transform.position = GetDropPosition();
GetOrAddComponent<Grid>(gridGo);
GameObject groundGo = GetOrCreateChild(gridGo.transform, "Ground").gameObject;
SetLayer(groundGo, "Platform", report);
GetOrAddComponent<Tilemap>(groundGo);
GetOrAddComponent<TilemapRenderer>(groundGo);
TilemapCollider2D tilemapCollider = GetOrAddComponent<TilemapCollider2D>(groundGo);
tilemapCollider.usedByComposite = true;
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(groundGo);
rb.bodyType = RigidbodyType2D.Static;
GetOrAddComponent<CompositeCollider2D>(groundGo);
report.Add("在 Tilemap 组件中使用 Tile Palette 绘制地形。");
Selection.activeGameObject = gridGo;
MarkDirtyAndLog("Tilemap Ground", gridGo, report);
}
[MenuItem("BaseGames/Scene/Place/Nav Surface", priority = 170)]
public static void PlaceNavSurface()
{
var report = new List<string>();
GameObject go = new GameObject("NavSurface");
Undo.RegisterCreatedObjectUndo(go, "Place Nav Surface");
go.transform.position = GetDropPosition();
GetOrAddComponent<NavSurface>(go);
report.Add("NavSurface 已添加。在 Inspector 中点击 Bake 生成导航网格。");
Selection.activeGameObject = go;
MarkDirtyAndLog("Nav Surface", go, report);
}
[MenuItem("BaseGames/Scene/Place/Obstacle (Static)", priority = 190)]
public static void PlaceObstacle()
{
var report = new List<string>();
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<BoxCollider2D>(go);
SetupSpriteRenderer(go);
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(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<string>();
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<CircleCollider2D>(go);
rangeTrigger.isTrigger = true;
rangeTrigger.radius = 1.5f;
GetOrAddComponent<InteractableNPC>(go);
GetOrAddComponent<Animator>(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);
}
// ══ 私有辅助方法 ══════════════════════════════════════════════════════
/// <summary>
/// 返回用于放置新对象的世界坐标:优先使用 SceneView 视口中心,否则原点。
/// </summary>
private static Vector3 GetDropPosition()
{
SceneView sv = SceneView.lastActiveSceneView;
if (sv != null)
{
Vector3 pos = sv.pivot;
pos.z = 0f; // 2D 游戏固定 z=0
return pos;
}
return Vector3.zero;
}
private static Collider2D CreateBodyCollider(GameObject go, EnemyBodyColliderType type, Vector2 size)
{
switch (type)
{
case EnemyBodyColliderType.Capsule:
var cap = GetOrAddComponent<CapsuleCollider2D>(go);
cap.size = size;
return cap;
case EnemyBodyColliderType.Circle:
var cir = GetOrAddComponent<CircleCollider2D>(go);
cir.radius = Mathf.Min(size.x, size.y) * 0.5f;
return cir;
default: // Box
var box = GetOrAddComponent<BoxCollider2D>(go);
box.size = size;
return box;
}
}
private static T GetOrAddComponent<T>(GameObject go) where T : Component
{
T comp = go.GetComponent<T>();
return comp != null ? comp : Undo.AddComponent<T>(go);
}
/// <summary>
/// SpriteRenderer 添加并赋值 Unity 内置默认 Sprite白色圆角方块
/// 若已有 Sprite 则不覆盖(防止覆盖手动赋値)。
/// </summary>
private static SpriteRenderer SetupSpriteRenderer(GameObject go)
{
var sr = GetOrAddComponent<SpriteRenderer>(go);
if (sr.sprite == null)
sr.sprite = AssetDatabase.LoadAssetAtPath<Sprite>(
"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<string> report)
{
int layer = LayerMask.NameToLayer(layerName);
if (layer == -1)
report.Add($"Layer '{layerName}' 不存在,请在 Tags and Layers 中创建。");
else
go.layer = layer;
}
private static void AssignReference(Object target, string propName, Object value, List<string> report = null)
{
var so = new SerializedObject(target);
var sp = so.FindProperty(propName);
if (sp == null)
{
report?.Add($"{target.GetType().Name}.{propName} 字段不存在,跳过引用赋值。");
return;
}
sp.objectReferenceValue = value;
so.ApplyModifiedPropertiesWithoutUndo();
}
private static void AssignAsset(Object target, string propName, List<string> report, bool required, params string[] candidates)
{
Object asset = FindFirstAsset(candidates);
if (asset == null && required)
report.Add($"未找到 {target.GetType().Name}.{propName} 需要的资产: {string.Join(" / ", candidates)}");
if (asset != null)
AssignReference(target, propName, asset, report);
}
private static void AssignLayerMask(Object target, string propName, string layerName, List<string> report)
{
int layer = LayerMask.NameToLayer(layerName);
if (layer == -1)
{
report.Add($"Layer '{layerName}' 不存在,{target.GetType().Name}.{propName} 未能赋值 LayerMask。");
return;
}
var so = new SerializedObject(target);
var sp = so.FindProperty(propName);
if (sp == null)
{
report.Add($"{target.GetType().Name}.{propName} 字段不存在,跳过 LayerMask 赋值。");
return;
}
sp.intValue = 1 << layer;
so.ApplyModifiedPropertiesWithoutUndo();
}
/// <summary>将多个 Layer 名称合并为一个 LayerMask 并写入 SerializedProperty。</summary>
private static void AssignLayerMask(Object target, string propName, string[] layerNames, List<string> report)
{
int mask = 0;
foreach (var name in layerNames)
{
int layer = LayerMask.NameToLayer(name);
if (layer == -1)
report.Add($"Layer '{name}' 不存在,已跳过({target.GetType().Name}.{propName})。");
else
mask |= 1 << layer;
}
if (mask == 0) return;
var so = new SerializedObject(target);
var sp = so.FindProperty(propName);
if (sp == null)
{
report.Add($"{target.GetType().Name}.{propName} 字段不存在,跳过 LayerMask 赋值。");
return;
}
sp.intValue = mask;
so.ApplyModifiedPropertiesWithoutUndo();
}
private static void AssignInt(Object target, string propName, int value)
{
var so = new SerializedObject(target);
var sp = so.FindProperty(propName);
if (sp != null)
{
sp.intValue = value;
so.ApplyModifiedPropertiesWithoutUndo();
}
}
private static void AssignBool(Object target, string propName, bool value)
{
var so = new SerializedObject(target);
var sp = so.FindProperty(propName);
if (sp != null)
{
sp.boolValue = value;
so.ApplyModifiedPropertiesWithoutUndo();
}
}
private static void AssignString(Object target, string propName, string value, List<string> report = null)
{
var so = new SerializedObject(target);
var sp = so.FindProperty(propName);
if (sp == null)
{
report?.Add($"{target.GetType().Name}.{propName} 字段不存在,跳过字符串赋值。");
return;
}
sp.stringValue = value;
so.ApplyModifiedPropertiesWithoutUndo();
}
private static void AssignObjectArray(Object target, string propName, Object[] values, List<string> report = null)
{
var so = new SerializedObject(target);
var sp = so.FindProperty(propName);
if (sp == null || !sp.isArray)
{
report?.Add($"{target.GetType().Name}.{propName} 不是可写数组字段,跳过数组赋值。");
return;
}
sp.arraySize = values.Length;
for (int i = 0; i < values.Length; i++)
sp.GetArrayElementAtIndex(i).objectReferenceValue = values[i];
so.ApplyModifiedPropertiesWithoutUndo();
}
/// <summary>
/// 为 MeleeAttackAbility._hitBoxSlots 赋值struct 数组 {slotName, hitBox})。
/// </summary>
private static void AssignMeleeHitBoxSlots(MeleeAttackAbility ability, (string slot, HitBox hb)[] slots, List<string> report)
{
if (ability == null) return;
var so = new SerializedObject(ability);
var prop = so.FindProperty("_hitBoxSlots");
if (prop == null || !prop.isArray)
{
report?.Add($"[WARN] MeleeAttackAbility._hitBoxSlots 属性未找到,请检查字段名。");
return;
}
prop.arraySize = slots.Length;
for (int i = 0; i < slots.Length; i++)
{
var elem = prop.GetArrayElementAtIndex(i);
elem.FindPropertyRelative("slotName").stringValue = slots[i].slot;
elem.FindPropertyRelative("hitBox").objectReferenceValue = slots[i].hb;
}
so.ApplyModifiedPropertiesWithoutUndo();
report?.Add($"[OK] MeleeAttackAbility._hitBoxSlots 已配置 {slots.Length} 个槽位。");
}
private static Object FindFirstAsset(params string[] candidates)
{
foreach (string candidate in candidates)
{
if (string.IsNullOrWhiteSpace(candidate))
continue;
string[] guids = AssetDatabase.FindAssets(candidate);
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
Object asset = AssetDatabase.LoadMainAssetAtPath(path);
if (asset != null && asset.name == candidate)
return asset;
}
}
return null;
}
private static void MarkDirtyAndLog(string label, GameObject root, List<string> report)
{
EditorUtility.SetDirty(root);
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(root.scene);
if (report != null && report.Count > 0)
Debug.Log($"[SceneObjectPlacer] {label} 已放置。\n " + string.Join("\n ", report));
else
Debug.Log($"[SceneObjectPlacer] {label} 已放置。");
}
}
}