822 lines
38 KiB
C#
822 lines
38 KiB
C#
using System.Collections.Generic;
|
||
using System.Reflection;
|
||
using BaseGames.Camera;
|
||
using BaseGames.Combat;
|
||
using BaseGames.Dialogue;
|
||
using BaseGames.Enemies;
|
||
using BaseGames.Player;
|
||
using BaseGames.Player.States;
|
||
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
|
||
{
|
||
// ══ 菜单入口 ══════════════════════════════════════════════════════════
|
||
|
||
[MenuItem("BaseGames/Scene/Place/Player", priority = 100)]
|
||
public static void PlacePlayer()
|
||
{
|
||
var report = new List<string>();
|
||
|
||
GameObject go = new GameObject("Player");
|
||
Undo.RegisterCreatedObjectUndo(go, "Place Player");
|
||
go.transform.position = GetDropPosition();
|
||
go.tag = "Player";
|
||
SetLayer(go, "Player", report);
|
||
|
||
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
|
||
rb.bodyType = RigidbodyType2D.Dynamic;
|
||
rb.gravityScale = 2f;
|
||
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||
|
||
GetOrAddComponent<CapsuleCollider2D>(go);
|
||
GetOrAddComponent<Animator>(go);
|
||
SetupSpriteRenderer(go);
|
||
|
||
PlayerStats playerStats = GetOrAddComponent<PlayerStats>(go);
|
||
PlayerMovement playerMovement = GetOrAddComponent<PlayerMovement>(go);
|
||
PlayerController playerController = GetOrAddComponent<PlayerController>(go);
|
||
PlayerCombat playerCombat = GetOrAddComponent<PlayerCombat>(go);
|
||
|
||
// Ground check pivot
|
||
Transform groundCheckGo = GetOrCreateChild(go.transform, "GroundCheck");
|
||
groundCheckGo.localPosition = new Vector3(0f, -0.75f, 0f);
|
||
AssignReference(playerMovement, "_groundCheck", groundCheckGo, report);
|
||
AssignLayerMask(playerMovement, "_groundLayer", "Ground", report);
|
||
|
||
// Weapon socket (WeaponManager instantiates weapons here at runtime)
|
||
GetOrCreateChild(go.transform, "WeaponSocket");
|
||
|
||
// Camera follow target — CinemachineCamera.Follow 使用此子节点而非 Player 根节点
|
||
GetOrCreateChild(go.transform, "CameraFollowTarget");
|
||
|
||
// HurtBox child
|
||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||
SetLayer(hurtBoxT.gameObject, "PlayerHurtBox", report);
|
||
CapsuleCollider2D hurtCollider = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
|
||
hurtCollider.isTrigger = true;
|
||
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
|
||
|
||
// Assign controller references
|
||
AssignReference(playerController, "_stats", playerStats, report);
|
||
AssignReference(playerController, "_hurtBox", hurtBox, report);
|
||
AssignReference(playerController, "_movement", playerMovement, report);
|
||
AssignReference(playerController, "_combat", playerCombat, report);
|
||
|
||
// Event channels (all optional — will be skipped silently if assets missing)
|
||
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");
|
||
|
||
// Config ScriptableObjects (optional — link manually after placing)
|
||
Object statsConfig = FindFirstAsset("PLY_PlayerStats", "PlayerStats");
|
||
Object movConfig = FindFirstAsset("PLY_PlayerMovementConfig", "PlayerMovementConfig");
|
||
if (movConfig != null) AssignReference(playerController, "_movementConfig", movConfig, report);
|
||
if (statsConfig != null) AssignReference(playerStats, "_config", statsConfig, report);
|
||
if (movConfig != null) AssignReference(playerMovement, "_config", movConfig, report);
|
||
|
||
report.Add("PlayerMovement._config、PlayerController._animConfig、_inputReader 等需后续手动绑定。");
|
||
|
||
Selection.activeGameObject = go;
|
||
MarkDirtyAndLog("Player", go, 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()
|
||
{
|
||
var report = new List<string>();
|
||
|
||
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;
|
||
|
||
GetOrAddComponent<CapsuleCollider2D>(go);
|
||
GetOrAddComponent<Animator>(go);
|
||
SetupSpriteRenderer(go);
|
||
|
||
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
|
||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(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);
|
||
|
||
// References
|
||
AssignReference(enemyBase, "_stats", enemyStats, report);
|
||
|
||
// DamageSourceSO for body contact (optional — create manually if missing)
|
||
Object dmgSrc = FindFirstAsset("DS_EnemyBody", "DS_TestEnemyBody");
|
||
if (dmgSrc != null)
|
||
AssignReference(hitBox, "_defaultSource", dmgSrc, report);
|
||
else
|
||
report.Add("未找到 DamageSourceSO (DS_EnemyBody),HitBox_Body._defaultSource 未绑定。请创建后手动指定。");
|
||
|
||
// Event channels
|
||
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
|
||
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
|
||
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
|
||
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
||
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
||
|
||
// EnemyStatsSO (optional)
|
||
Object enemyStatsSO = FindFirstAsset("BasicEnemyStats", "EnemyStatsSO");
|
||
if (enemyStatsSO != null)
|
||
AssignReference(enemyBase, "_statsSO", enemyStatsSO, report);
|
||
else
|
||
report.Add("未找到 EnemyStatsSO,EnemyBase._statsSO 未绑定。请在 Data/Enemies/ 创建后手动指定。");
|
||
|
||
report.Add("行为树、导航参数(NavAgent)、动画片段需后续手工挂载。");
|
||
|
||
Selection.activeGameObject = go;
|
||
MarkDirtyAndLog("Enemy (Basic)", go, report);
|
||
}
|
||
|
||
[MenuItem("BaseGames/Scene/Place/Boss Enemy", priority = 115)]
|
||
public static void PlaceBossEnemy()
|
||
{
|
||
var report = new List<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;
|
||
|
||
GetOrAddComponent<CapsuleCollider2D>(go);
|
||
GetOrAddComponent<Animator>(go);
|
||
SetupSpriteRenderer(go);
|
||
|
||
BossBase bossBase = GetOrAddComponent<BossBase>(go);
|
||
EnemyStats bossStats = GetOrAddComponent<EnemyStats>(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("DS_BossBody", "DS_EnemyBody");
|
||
if (dmgSrc != null)
|
||
AssignReference(hitBox, "_defaultSource", dmgSrc, report);
|
||
else
|
||
report.Add("未找到 DamageSourceSO,HitBox_Body._defaultSource 未绑定。");
|
||
|
||
// Event channels
|
||
AssignAsset(bossBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
|
||
AssignAsset(bossBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
|
||
AssignAsset(bossBase, "_onBossFightEnded", report, false, "EVT_BossFightEnded");
|
||
AssignAsset(bossBase, "_onBossPhaseChanged", report, false, "EVT_BossPhaseChanged");
|
||
AssignAsset(bossStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
|
||
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
||
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
||
|
||
report.Add("填写 _bossId。");
|
||
report.Add("挂载 BossSkillSequencer 组件并指定技能序列 SO;行为树、NavAgent 需手工添加。");
|
||
report.Add("多阶段 Boss 可在此 GameObject 上继续 AddComponent 阶段切换控制器。");
|
||
|
||
Selection.activeGameObject = go;
|
||
MarkDirtyAndLog("Boss Enemy", go, report);
|
||
}
|
||
|
||
[MenuItem("BaseGames/Scene/Place/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");
|
||
|
||
report.Add("_canPogo=true:子 HurtBox 供玩家下劈弹起;设为 false 可改为纯死亡区(无需子 HurtBox)。");
|
||
|
||
Selection.activeGameObject = go;
|
||
MarkDirtyAndLog("Hazard (LethalTrap)", go, report);
|
||
}
|
||
|
||
[MenuItem("BaseGames/Scene/Place/Collectible (LingZhu)", priority = 125)]
|
||
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);
|
||
|
||
AssignAsset(savePoint, "_onSavePointActivated", report, false, "EVT_SavePointActivated");
|
||
AssignAsset(savePoint, "_onFastTravelOpen", report, false, "EVT_FastTravelOpen");
|
||
|
||
Selection.activeGameObject = go;
|
||
MarkDirtyAndLog("Save Point", go, report);
|
||
}
|
||
|
||
[MenuItem("BaseGames/Scene/Place/Room Transition", priority = 135)]
|
||
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);
|
||
}
|
||
|
||
[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 — 提供 CinemachineConfiner2D 所需的限位多边形(isTrigger = true,仅作为相机约束边界)
|
||
Transform boundaryT = GetOrCreateChild(go.transform, $"{areaName}_AreaBoundary");
|
||
PolygonCollider2D boundaryCollider = GetOrAddComponent<PolygonCollider2D>(boundaryT.gameObject);
|
||
boundaryCollider.isTrigger = true;
|
||
boundaryCollider.pathCount = 1;
|
||
// 顶点必须逆时针(CCW)排列:Cinemachine 底层 Clipper 库对 CW 多边形(area<0)会取反 delta,
|
||
// 导致向外膨胀而非向内收缩,相机将不受限制地跑出边界。
|
||
boundaryCollider.SetPath(0, new Vector2[]
|
||
{
|
||
new Vector2(-12f, -6f), // BL
|
||
new Vector2( 12f, -6f), // BR
|
||
new Vector2( 12f, 6f), // TR
|
||
new Vector2(-12f, 6f), // TL
|
||
});
|
||
|
||
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);
|
||
|
||
PolygonCollider2D col = GetOrAddComponent<PolygonCollider2D>(zoneGo);
|
||
col.isTrigger = true;
|
||
// 默认矩形轮廓(CCW),与 AreaBoundary 默认尺寸一致(可在 Inspector 中编辑顶点调整为任意多边形)
|
||
col.SetPath(0, new Vector2[]
|
||
{
|
||
new Vector2(-12f, -6f), // BL
|
||
new Vector2( 12f, -6f), // BR
|
||
new Vector2( 12f, 6f), // TR
|
||
new Vector2(-12f, 6f), // TL
|
||
});
|
||
|
||
CameraTriggerZone zone = GetOrAddComponent<CameraTriggerZone>(zoneGo);
|
||
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($"调整 {areaName}_AreaBoundary PolygonCollider2D 顶点以匹配区域边界。");
|
||
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, "Ground", 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 go = new GameObject("MovingPlatform");
|
||
Undo.RegisterCreatedObjectUndo(go, "Place Moving Platform");
|
||
go.transform.position = GetDropPosition();
|
||
SetLayer(go, "Ground", 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);
|
||
|
||
// Waypoint markers (LinearAB mode end points)
|
||
Transform wpA = GetOrCreateChild(go.transform, "WaypointA");
|
||
Transform wpB = GetOrCreateChild(go.transform, "WaypointB");
|
||
wpA.localPosition = new Vector3(-3f, 0f, 0f);
|
||
wpB.localPosition = new Vector3(3f, 0f, 0f);
|
||
|
||
MovingPlatform platform = GetOrAddComponent<MovingPlatform>(go);
|
||
AssignReference(platform, "_passengerSensor", sensorCol, report);
|
||
AssignObjectArray(platform, "_wayPoints", new Object[] { wpA, wpB }, report);
|
||
|
||
report.Add("WaypointA / WaypointB 为移动端点,可将其拖出平台并在场景中调整位置。");
|
||
report.Add("如需触发激活,改 _moveType = TriggeredLinear 并将 VoidEventChannelSO 拖入 _activationChannel。");
|
||
|
||
Selection.activeGameObject = go;
|
||
MarkDirtyAndLog("Moving Platform", go, 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, "Ground", 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, "Ground", 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 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();
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
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} 已放置。");
|
||
}
|
||
}
|
||
}
|