Files
zeling_v2/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs
2026-05-17 07:56:12 +08:00

822 lines
38 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using System.Reflection;
using 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 → …
///
/// 所有操作支持 UndoCtrl+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("未找到 EnemyStatsSOEnemyBase._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("未找到 DamageSourceSOHitBox_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} 已放置。");
}
}
}