- 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.
2399 lines
134 KiB
C#
2399 lines
134 KiB
C#
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 → …
|
||
///
|
||
/// 所有操作支持 Undo(Ctrl+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>() 向上即可找到
|
||
// 本节点上的 PlayerController(IDamageable 实现者)。
|
||
GameObject root = new GameObject("Player");
|
||
Undo.RegisterCreatedObjectUndo(root, "Place Player");
|
||
root.transform.position = GetDropPosition();
|
||
root.tag = "Player";
|
||
SetLayer(root, "Player", report);
|
||
|
||
// 物理组件(PlayerMovement RequireComponent(Rigidbody2D),必须同节点)
|
||
Rigidbody2D rb = GetOrAddComponent<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);
|
||
// PlayerFeedback:EquipmentManager.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._animConfig(PLY_PlayerAnimationConfig)");
|
||
if (statsConfig == null) report.Add("★ 需创建并绑定:PlayerStats._config(PlayerStatsSO)");
|
||
if (inputReader == null) report.Add("★ 需手动绑定:PlayerController._inputReader / FormController._input / SkillManager._input(InputReaderSO)");
|
||
if (equipmentConfig == null) report.Add("★ 需创建并绑定:EquipmentManager._config(EquipmentConfigSO)");
|
||
if (charmCatalog == null) report.Add("★ 需创建并绑定:EquipmentManager._charmCatalog(CharmCatalogSO)");
|
||
report.Add("SkillManager._formSkillSets 技能槽 SO 需手动填入。");
|
||
|
||
Selection.activeGameObject = root;
|
||
MarkDirtyAndLog("Player", root, report);
|
||
}
|
||
|
||
[MenuItem("BaseGames/Scene/Place/Player Spawn Point", priority = 105)]
|
||
public static void PlacePlayerSpawnPoint()
|
||
{
|
||
var report = new List<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("未找到 DamageSourceSO,HitBox_Body._defaultSource 未绑定。请创建 CMB_DS_EnemyBody.asset。");
|
||
|
||
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report);
|
||
report.Add("★ 指定 EnemyBase._statsSO、_animConfig 资产(按所创建的敌人类型命名)。");
|
||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定对应 .asset。");
|
||
|
||
Undo.CollapseUndoOperations(undoGroup);
|
||
Selection.activeGameObject = go;
|
||
EnemyBase.SuppressValidationWarnings = false;
|
||
MarkDirtyAndLog("Enemy (Basic)", go, report);
|
||
}
|
||
|
||
[MenuItem("BaseGames/Scene/Place/Boss Enemy", priority = 115)]
|
||
public static void PlaceBossEnemy() => PlaceBossEnemy(EnemyBodyColliderType.Box);
|
||
|
||
public static void PlaceBossEnemy(EnemyBodyColliderType bodyCollider)
|
||
{
|
||
var report = new List<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("未找到 DamageSourceSO,HitBox_Body._defaultSource 未绑定。请按规范创建 CMB_DS_BossBody.asset。");
|
||
|
||
// Event channels
|
||
AssignAsset(bossBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
|
||
AssignAsset(bossBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
|
||
AssignAsset(bossBase, "_onBossFightEnded", report, false, "EVT_BossFightEnded");
|
||
AssignAsset(bossBase, "_onBossPhaseChanged", report, false, "EVT_BossPhaseChanged");
|
||
AssignAsset(bossStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
|
||
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
||
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
||
|
||
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
|
||
report.Add("填写 _bossId。");
|
||
report.Add("挂载 BossSkillSequencer 组件并指定技能序列 SO;行为树、NavAgent 需手工添加。");
|
||
report.Add("多阶段 Boss 可在此 GameObject 上继续 AddComponent 阶段切换控制器。");
|
||
|
||
Selection.activeGameObject = go;
|
||
MarkDirtyAndLog("Boss Enemy", go, report);
|
||
}
|
||
|
||
// ══ 具体敌人快速放置 ════════════════════════════════════════════════════
|
||
|
||
[MenuItem("BaseGames/Scene/Place/Enemy E001 (草蛭)", priority = 111)]
|
||
public static void PlaceE001_CaoZhi() => PlaceE001_CaoZhi(EnemyBodyColliderType.Box);
|
||
|
||
public static void PlaceE001_CaoZhi(EnemyBodyColliderType bodyCollider)
|
||
{
|
||
var report = new List<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);
|
||
|
||
// HurtBox(component.enabled 初始为 false:仅悬挂脆弱窗口期间由能力开启)
|
||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
||
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
|
||
hurtCap.isTrigger = true;
|
||
hurtCap.size = new Vector2(0.45f, 0.65f);
|
||
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
|
||
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>
|
||
/// 搭建一个弹体 GameObject(Rigidbody2D + Collider(trigger) + HitBox + PooledObject + 指定 Projectile 子类),
|
||
/// 存盘为 Prefab 并注册 Addressable。运行时由发射方 Initialize(ProjectileConfigSO,...) 注入速度/重力/伤害源。
|
||
/// </summary>
|
||
public static void PlaceAndSaveProjectile(string key, bool removeSceneInstance = false)
|
||
{
|
||
if (!ProjectileTypes.TryGetValue(key, out var projType))
|
||
{
|
||
Debug.LogError($"[SceneObjectPlacer] 未登记的弹体键:{key}");
|
||
return;
|
||
}
|
||
|
||
var report = new List<string>();
|
||
int undoGroup = Undo.GetCurrentGroup();
|
||
Undo.SetCurrentGroupName($"Place {key}");
|
||
|
||
GameObject go = new GameObject(key);
|
||
Undo.RegisterCreatedObjectUndo(go, $"Place {key}");
|
||
go.transform.position = GetDropPosition();
|
||
SetLayer(go, "EnemyProjectile", report);
|
||
|
||
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
|
||
rb.bodyType = RigidbodyType2D.Dynamic; // ArcProjectile 运行时按 config 设置 gravityScale
|
||
rb.gravityScale = 0f; // 默认无重力(直线/回旋扇);抛物线弹由 config 注入
|
||
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
|
||
|
||
CircleCollider2D col = GetOrAddComponent<CircleCollider2D>(go);
|
||
col.isTrigger = true;
|
||
col.radius = 0.25f;
|
||
|
||
HitBox hitBox = GetOrAddComponent<HitBox>(go); // Projectile [RequireComponent(HitBox)]
|
||
GetOrAddComponent<PooledObject>(go); // 对象池归还所需
|
||
Undo.AddComponent(go, projType); // Projectile 子类
|
||
SetupSpriteRenderer(go);
|
||
|
||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
||
if (dmgSrc != null)
|
||
AssignReference(hitBox, "_defaultSource", dmgSrc, report);
|
||
else
|
||
report.Add("未找到 CMB_DS_EnemyBody;HitBox._defaultSource 未绑定(运行时也会用 ProjectileConfigSO.DamageSource)。");
|
||
|
||
report.Add($"弹体类型:{projType.Name};Layer=EnemyProjectile;已挂 HitBox + PooledObject。");
|
||
report.Add($"★ 创建对应 ProjectileConfigSO,其 PoolKey 必须 = \"{key}\"(与地址一致)。");
|
||
report.Add("★ 速度/重力/伤害源由发射方在 Initialize(ProjectileConfigSO,...) 时注入,无需序列化到 Prefab。");
|
||
|
||
Undo.CollapseUndoOperations(undoGroup);
|
||
Selection.activeGameObject = go;
|
||
|
||
string prefabPath = SaveRootAsPrefab(go, key, ProjectilePrefabFolder, null, report);
|
||
if (removeSceneInstance && !string.IsNullOrEmpty(prefabPath))
|
||
Undo.DestroyObjectImmediate(go);
|
||
|
||
Debug.Log($"[SceneObjectPlacer] {key} 弹体 Prefab 流程完成。\n " + string.Join("\n ", report));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将场景根对象存盘为 Prefab(已存在则弹窗确认覆盖)并注册 Addressable,返回 Prefab 路径(失败/取消返回 null)。
|
||
/// </summary>
|
||
private static string SaveRootAsPrefab(GameObject root, string fileName, string folder,
|
||
string[] extraLabels, List<string> report)
|
||
{
|
||
EditorScaffoldUtils.EnsureFolder(folder);
|
||
AssetDatabase.Refresh();
|
||
string prefabPath = $"{folder}/{fileName}.prefab";
|
||
|
||
if (System.IO.File.Exists(prefabPath)
|
||
&& !EditorUtility.DisplayDialog("Prefab 已存在",
|
||
$"{prefabPath}\n已存在,覆盖?", "覆盖", "取消"))
|
||
{
|
||
report.Add($"用户取消覆盖:{prefabPath}");
|
||
return null;
|
||
}
|
||
|
||
GameObject prefab = PrefabUtility.SaveAsPrefabAssetAndConnect(root, prefabPath, InteractionMode.UserAction);
|
||
if (prefab == null)
|
||
{
|
||
report.Add($"✗ Prefab 存盘失败:{prefabPath}");
|
||
return null;
|
||
}
|
||
|
||
report.Add($"✅ 已存盘 Prefab:{prefabPath}");
|
||
AddressableRegistrar.Register(prefabPath, fileName, extraLabels, report);
|
||
return prefabPath;
|
||
}
|
||
|
||
[MenuItem("BaseGames/Scene/Place/Hazard (LethalTrap)", priority = 120)]
|
||
public static void PlaceLethalTrap()
|
||
{
|
||
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("_facingDirectionOnArrive:A→B 时玩家朝向由 B 的该值决定,B→A 反之。");
|
||
|
||
Undo.CollapseUndoOperations(undoGroup);
|
||
|
||
Selection.activeGameObject = parent;
|
||
MarkDirtyAndLog("Linked Door Pair (Same-Scene)", parent, report);
|
||
}
|
||
|
||
[MenuItem("BaseGames/Scene/Place/Camera Area", priority = 140)]
|
||
public static void PlaceCameraArea() => PlaceCameraArea("CameraArea");
|
||
|
||
/// <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} 已放置。");
|
||
}
|
||
}
|
||
}
|