Files
zeling_v2/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs

789 lines
36 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/Room Camera", priority = 140)]
public static void PlaceRoomCamera()
{
var report = new List<string>();
GameObject go = new GameObject("RoomCamera");
Undo.RegisterCreatedObjectUndo(go, "Place Room Camera");
go.transform.position = GetDropPosition();
CinemachineCamera cinemachine = GetOrAddComponent<CinemachineCamera>(go);
RoomCamera roomCamera = GetOrAddComponent<RoomCamera>(go);
CinemachineConfiner2D confiner = GetOrAddComponent<CinemachineConfiner2D>(go);
// RoomBoundary child — defines the camera confinement area
Transform boundaryT = GetOrCreateChild(go.transform, "RoomBoundary");
PolygonCollider2D boundaryCollider = GetOrAddComponent<PolygonCollider2D>(boundaryT.gameObject);
boundaryCollider.pathCount = 1;
boundaryCollider.SetPath(0, new Vector2[]
{
new Vector2(-12f, -6f),
new Vector2(-12f, 6f),
new Vector2( 12f, 6f),
new Vector2( 12f, -6f),
});
RoomVisibleArea visibleArea = GetOrAddComponent<RoomVisibleArea>(boundaryT.gameObject);
AssignReference(roomCamera, "_visibleArea", visibleArea, report);
AssignReference(confiner, "m_BoundingShape2D", boundaryCollider, report);
// Disable any Camera and AudioListener added by Cinemachine
UnityEngine.Camera cam = go.GetComponent<UnityEngine.Camera>();
if (cam != null) cam.enabled = false;
AudioListener al = go.GetComponent<AudioListener>();
if (al != null) { Undo.DestroyObjectImmediate(al); }
report.Add("将 Player/CameraFollowTarget Transform 拖入 CinemachineCamera.Follow 字段以跟随玩家(或使用 Room Camera Setup 工具批量赋值)。");
report.Add("调整 RoomBoundary PolygonCollider2D 顶点以匹配房间边界。");
Selection.activeGameObject = go;
MarkDirtyAndLog("Room Camera", 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/Camera Trigger Zone", priority = 180)]
public static void PlaceCameraTriggerZone()
{
var report = new List<string>();
GameObject go = new GameObject("CameraTriggerZone");
Undo.RegisterCreatedObjectUndo(go, "Place Camera Trigger Zone");
go.transform.position = GetDropPosition();
SetLayer(go, "TriggerZone", report);
BoxCollider2D col = GetOrAddComponent<BoxCollider2D>(go);
col.isTrigger = true;
col.size = new Vector2(2f, 2f);
GetOrAddComponent<CameraTriggerZone>(go);
report.Add("将目标 RoomCamera 拖入 CameraTriggerZone._targetCamera 字段。");
Selection.activeGameObject = go;
MarkDirtyAndLog("Camera Trigger Zone", 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} 已放置。");
}
}
}