摄像机区域的架构改动
This commit is contained in:
788
Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs
Normal file
788
Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs
Normal file
@@ -0,0 +1,788 @@
|
||||
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/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} 已放置。");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f3e7994893f6c2942acb4d724e879460
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
566
Assets/_Game/Scripts/Editor/Scene/SceneScaffoldTools.cs
Normal file
566
Assets/_Game/Scripts/Editor/Scene/SceneScaffoldTools.cs
Normal file
@@ -0,0 +1,566 @@
|
||||
using System.Collections.Generic;
|
||||
using BaseGames.Audio;
|
||||
using BaseGames.Camera;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Core.Save;
|
||||
using BaseGames.Core.Pool;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.UI;
|
||||
using BaseGames.UI.HUD;
|
||||
using BaseGames.UI.Menus;
|
||||
using BaseGames.World;
|
||||
using PathBerserker2d;
|
||||
using Unity.Cinemachine;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.Tilemaps;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
public static class SceneScaffoldTools
|
||||
{
|
||||
|
||||
[MenuItem("BaseGames/Tools/Scaffold Persistent Scene")]
|
||||
public static void ScaffoldPersistentScene()
|
||||
{
|
||||
var report = new List<string>();
|
||||
|
||||
EnsureEventChannelAssets(report);
|
||||
|
||||
GameObject root = GetOrCreateRoot("[Persistent]");
|
||||
Transform services = GetOrCreateChild(root.transform, "[Services]");
|
||||
Transform input = GetOrCreateChild(root.transform, "[Input]");
|
||||
Transform camera = GetOrCreateChild(root.transform, "[Camera]");
|
||||
Transform ui = GetOrCreateChild(root.transform, "[UI]");
|
||||
|
||||
GameObject registrarGo = GetOrCreateChild(services, "GameServiceRegistrar").gameObject;
|
||||
GameObject deathRespawnGo = GetOrCreateChild(services, "DeathRespawnService").gameObject;
|
||||
GameObject sceneServiceGo = GetOrCreateChild(services, "SceneService").gameObject;
|
||||
GameObject sceneLoaderGo = GetOrCreateChild(services, "SceneLoader").gameObject;
|
||||
GameObject registryGo = GetOrCreateChild(services, "EventChannelRegistry").gameObject;
|
||||
GameObject settingsGo = GetOrCreateChild(services, "SettingsManager").gameObject;
|
||||
GameObject poolGo = GetOrCreateChild(services, "GlobalObjectPool").gameObject;
|
||||
GameObject gameManagerGo = GetOrCreateChild(services, "GameManager").gameObject;
|
||||
GameObject audioManagerGo = GetOrCreateChild(services, "AudioManager").gameObject;
|
||||
GameObject saveManagerGo = GetOrCreateChild(services, "GameSaveManager").gameObject;
|
||||
|
||||
GameServiceRegistrar registrar = GetOrAddComponent<GameServiceRegistrar>(registrarGo);
|
||||
DeathRespawnService deathRespawnService = GetOrAddComponent<DeathRespawnService>(deathRespawnGo);
|
||||
SceneService sceneService = GetOrAddComponent<SceneService>(sceneServiceGo);
|
||||
SceneLoader sceneLoader = GetOrAddComponent<SceneLoader>(sceneLoaderGo);
|
||||
EventChannelRegistry registry = GetOrAddComponent<EventChannelRegistry>(registryGo);
|
||||
SettingsManager settingsManager = GetOrAddComponent<SettingsManager>(settingsGo);
|
||||
GetOrAddComponent<GlobalObjectPool>(poolGo);
|
||||
GameManager gameManager = GetOrAddComponent<GameManager>(gameManagerGo);
|
||||
AudioManager audioManager = GetOrAddComponent<AudioManager>(audioManagerGo);
|
||||
GameSaveManager gameSaveManager = GetOrAddComponent<GameSaveManager>(saveManagerGo);
|
||||
|
||||
GameObject inputHolderGo = GetOrCreateChild(input, "InputReaderHolder").gameObject;
|
||||
Object inputReaderAsset = FindFirstAssetByType<InputReaderSO>("InputReader", "InputReaderSO");
|
||||
if (inputReaderAsset == null)
|
||||
inputReaderAsset = EnsureInputReaderAsset(report);
|
||||
|
||||
InputReaderBootstrap inputBootstrap = GetOrAddComponent<InputReaderBootstrap>(inputHolderGo);
|
||||
AssignReference(inputBootstrap, "_inputReader", inputReaderAsset, report);
|
||||
if (inputReaderAsset != null)
|
||||
{
|
||||
AssignReference(inputReaderAsset, "_onPauseRequested", FindFirstAssetByType<VoidEventChannelSO>("EVT_PauseRequested"), report);
|
||||
AssignReference(inputReaderAsset, "_inputActions", FindFirstAssetWithExtension(".inputactions", "PlayerInputActions", "InputActions"), report);
|
||||
}
|
||||
if (inputReaderAsset == null)
|
||||
report.Add("未找到 InputReaderSO 资产,InputReaderBootstrap 将保持空引用。请补齐 Assets/_Game/Data/Player/Input/InputReader.asset。");
|
||||
|
||||
GameObject mainCameraGo = GetOrCreateChild(camera, "Main Camera").gameObject;
|
||||
UnityEngine.Camera mainCamera = GetOrAddComponent<UnityEngine.Camera>(mainCameraGo);
|
||||
mainCamera.orthographic = false;
|
||||
mainCamera.fieldOfView = 60f;
|
||||
mainCameraGo.tag = "MainCamera";
|
||||
GetOrAddComponent<AudioListener>(mainCameraGo);
|
||||
CinemachineBrain brain = GetOrAddComponent<CinemachineBrain>(mainCameraGo);
|
||||
|
||||
GameObject cameraStateGo = GetOrCreateChild(camera, "CameraStateController").gameObject;
|
||||
CameraStateController cameraStateController = GetOrAddComponent<CameraStateController>(cameraStateGo);
|
||||
CinemachineImpulseSource impulseSource = GetOrAddComponent<CinemachineImpulseSource>(cameraStateGo);
|
||||
|
||||
GameObject uiRootGo = GetOrCreateChild(ui, "UIRoot").gameObject;
|
||||
UIManager uiManager = GetOrAddComponent<UIManager>(uiRootGo);
|
||||
|
||||
GameObject hudCanvasGo = GetOrCreateCanvas(uiRootGo.transform, "HUD Canvas", 0);
|
||||
GameObject hudRootGo = GetOrCreateChild(hudCanvasGo.transform, "HUDRoot").gameObject;
|
||||
HUDController hudController = GetOrAddComponent<HUDController>(hudRootGo);
|
||||
|
||||
GameObject pauseRootGo = GetOrCreateChild(uiRootGo.transform, "PauseMenuRoot").gameObject;
|
||||
GameObject settingsRootGo = GetOrCreateChild(uiRootGo.transform, "SettingsRoot").gameObject;
|
||||
GameObject mapRootGo = GetOrCreateChild(uiRootGo.transform, "MapRoot").gameObject;
|
||||
GameObject shopRootGo = GetOrCreateChild(uiRootGo.transform, "ShopRoot").gameObject;
|
||||
pauseRootGo.SetActive(false);
|
||||
settingsRootGo.SetActive(false);
|
||||
mapRootGo.SetActive(false);
|
||||
shopRootGo.SetActive(false);
|
||||
|
||||
GameObject deathCanvasGo = GetOrCreateCanvas(uiRootGo.transform, "DeathScreen Canvas", 10);
|
||||
GameObject deathRootGo = GetOrCreateChild(deathCanvasGo.transform, "DeathScreenRoot").gameObject;
|
||||
DeathScreenController deathScreenController = GetOrAddComponent<DeathScreenController>(deathRootGo);
|
||||
deathRootGo.SetActive(false);
|
||||
GameObject respawnButtonGo = GetOrCreateChild(deathRootGo.transform, "RespawnButton").gameObject;
|
||||
GetOrAddComponent<Image>(respawnButtonGo);
|
||||
Button respawnButton = GetOrAddComponent<Button>(respawnButtonGo);
|
||||
|
||||
EnsureAudioSources(audioManagerGo, audioManager, report);
|
||||
|
||||
AssignReference(registrar, "_deathRespawnService", deathRespawnService);
|
||||
AssignReference(registrar, "_sceneService", sceneService);
|
||||
AssignReference(registrar, "_eventChannelRegistry", registry);
|
||||
AssignReference(registrar, "_saveManager", gameSaveManager);
|
||||
|
||||
AssignReference(gameManager, "_settingsManager", settingsManager);
|
||||
AssignReference(gameManager, "_deathRespawnService", deathRespawnService);
|
||||
AssignReference(gameManager, "_sceneService", sceneService);
|
||||
AssignAsset(gameManager, "_onPlayerDied", report, true, "EVT_PlayerDied");
|
||||
AssignAsset(gameManager, "_onPauseRequested", report, false, "EVT_PauseRequested");
|
||||
AssignAsset(gameManager, "_onResumeRequested", report, false, "EVT_ResumeRequested", "EVT_PauseResumed");
|
||||
AssignAsset(gameManager, "_onBossFightStarted", report, false, "EVT_BossFightStarted", "EVT_BossFight");
|
||||
AssignAsset(gameManager, "_onBossFightEnded", report, false, "EVT_BossFightEnded");
|
||||
AssignAsset(gameManager, "_onDeathScreenConfirmed", report, true, "EVT_DeathScreenConfirmed");
|
||||
AssignAsset(gameManager, "_onGameStateChanged", report, true, "EVT_GameStateChanged", "EVT_GameState");
|
||||
AssignAsset(gameManager, "_onPlayerRespawned", report, false, "EVT_PlayerRespawned", "EVT_PlayerRespawn");
|
||||
|
||||
AssignAsset(sceneService, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
|
||||
AssignAsset(sceneService, "_onFadeInRequest", report, false, "EVT_FadeInRequest");
|
||||
AssignAsset(sceneService, "_onFadeOutRequest", report, false, "EVT_FadeOutRequest");
|
||||
AssignReference(sceneService, "_sceneLoader", sceneLoader);
|
||||
|
||||
AssignAsset(sceneLoader, "_onSceneLoaded", report, false, "EVT_SceneLoaded");
|
||||
|
||||
AssignAsset(deathRespawnService, "_onRespawnStarted", report, false, "EVT_RespawnStarted");
|
||||
AssignAsset(deathRespawnService, "_onRespawnCompleted", report, false, "EVT_RespawnCompleted");
|
||||
AssignAsset(deathRespawnService, "_onDeathScreenConfirmed", report, true, "EVT_DeathScreenConfirmed");
|
||||
|
||||
AssignAsset(settingsManager, "_defaultSettings", report, false, "SET_GlobalSettings");
|
||||
|
||||
AssignAsset(audioManager, "_onPlayerDied", report, false, "EVT_PlayerDied");
|
||||
|
||||
AssignReference(cameraStateController, "_brain", brain);
|
||||
AssignReference(cameraStateController, "_impulseSource", impulseSource);
|
||||
|
||||
AssignReference(uiManager, "_hudRoot", hudRootGo);
|
||||
AssignReference(uiManager, "_pauseMenuRoot", pauseRootGo);
|
||||
AssignReference(uiManager, "_deathScreenRoot", deathRootGo);
|
||||
AssignReference(uiManager, "_settingsRoot", settingsRootGo);
|
||||
AssignReference(uiManager, "_mapRoot", mapRootGo);
|
||||
AssignReference(uiManager, "_shopRoot", shopRootGo);
|
||||
AssignAsset(uiManager, "_onGameStateChanged", report, true, "EVT_GameStateChanged", "EVT_GameState");
|
||||
AssignAsset(uiManager, "_onPauseRequested", report, false, "EVT_PauseRequested");
|
||||
AssignAsset(uiManager, "_onFastTravelOpen", report, false, "EVT_FastTravelOpen");
|
||||
AssignAsset(uiManager, "_onShopOpen", report, false, "EVT_ShopOpen");
|
||||
AssignAsset(uiManager, "_onMapOpen", report, false, "EVT_MapOpen");
|
||||
|
||||
AssignReference(deathScreenController, "_btnRespawn", respawnButton);
|
||||
AssignAsset(deathScreenController, "_onPlayerDied", report, true, "EVT_PlayerDied");
|
||||
AssignAsset(deathScreenController, "_onDeathScreenConfirmed", report, true, "EVT_DeathScreenConfirmed");
|
||||
|
||||
AddScaffoldNote(hudRootGo, "HUDController 已挂载。其内部图片/文本/图标 Prefab 依赖较多,需后续手工补 UI 资源与事件频道。", report);
|
||||
|
||||
MarkDirtyAndLog("Persistent 场景脚手架", root, report);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Scaffold Game Room
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 在当前活动场景中生成标准游戏关卡房间的完整层级结构:
|
||||
/// [RoomRoot] → [Camera] / [SpawnPoints] / [Environment] / [Transitions]
|
||||
/// 可配合 SceneObjectPlacerTool 在层级内快速追加更多对象。
|
||||
/// </summary>
|
||||
[MenuItem("BaseGames/Tools/Scaffold Game Room", priority = 201)]
|
||||
public static void ScaffoldGameRoom()
|
||||
{
|
||||
var report = new List<string>();
|
||||
|
||||
// ── [RoomRoot] ─────────────────────────────────────────────────
|
||||
GameObject root = GetOrCreateRoot("[RoomRoot]");
|
||||
RoomController roomController = GetOrAddComponent<RoomController>(root);
|
||||
|
||||
// ── [Camera] ───────────────────────────────────────────────────
|
||||
Transform cameraGroup = GetOrCreateChild(root.transform, "[Camera]");
|
||||
|
||||
GameObject roomCameraGo = GetOrCreateChild(cameraGroup, "RoomCamera").gameObject;
|
||||
CinemachineCamera cinemachineCamera = GetOrAddComponent<CinemachineCamera>(roomCameraGo);
|
||||
RoomCamera roomCamera = GetOrAddComponent<RoomCamera>(roomCameraGo);
|
||||
CinemachineConfiner2D confiner = GetOrAddComponent<CinemachineConfiner2D>(roomCameraGo);
|
||||
|
||||
// RoomBoundary — defines visible area and confiner polygon
|
||||
Transform boundaryT = GetOrCreateChild(roomCameraGo.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);
|
||||
AssignReference(confiner, "m_BoundingShape2D", boundaryCollider);
|
||||
|
||||
// Disable stray Camera / AudioListener components sometimes added by Cinemachine
|
||||
UnityEngine.Camera staleCam = roomCameraGo.GetComponent<UnityEngine.Camera>();
|
||||
if (staleCam != null) staleCam.enabled = false;
|
||||
AudioListener staleAl = roomCameraGo.GetComponent<AudioListener>();
|
||||
if (staleAl != null) { Undo.DestroyObjectImmediate(staleAl); }
|
||||
|
||||
// ── [SpawnPoints] ──────────────────────────────────────────────
|
||||
Transform spawnGroup = GetOrCreateChild(root.transform, "[SpawnPoints]");
|
||||
|
||||
GameObject defaultSpawnGo = GetOrCreateChild(spawnGroup, "SpawnPoint_Default").gameObject;
|
||||
PlayerSpawnPoint defaultSpawn = GetOrAddComponent<PlayerSpawnPoint>(defaultSpawnGo);
|
||||
AssignString(defaultSpawn, "_transitionId", "default");
|
||||
AssignInt(defaultSpawn, "_facingDirection", 1);
|
||||
|
||||
// ── [Environment] ──────────────────────────────────────────────
|
||||
Transform envGroup = GetOrCreateChild(root.transform, "[Environment]");
|
||||
|
||||
// Ground Tilemap
|
||||
GameObject gridGo = GetOrCreateChild(envGroup, "GroundGrid").gameObject;
|
||||
GetOrAddComponent<Grid>(gridGo);
|
||||
|
||||
GameObject groundTileGo = GetOrCreateChild(gridGo.transform, "Ground").gameObject;
|
||||
int groundLayer = LayerMask.NameToLayer("Ground");
|
||||
if (groundLayer >= 0) groundTileGo.layer = groundLayer;
|
||||
else report.Add("Layer 'Ground' 不存在,请在 Tags and Layers 中创建。");
|
||||
|
||||
GetOrAddComponent<Tilemap>(groundTileGo);
|
||||
GetOrAddComponent<TilemapRenderer>(groundTileGo);
|
||||
TilemapCollider2D tilemapCol = GetOrAddComponent<TilemapCollider2D>(groundTileGo);
|
||||
tilemapCol.usedByComposite = true;
|
||||
Rigidbody2D groundRb = GetOrAddComponent<Rigidbody2D>(groundTileGo);
|
||||
groundRb.bodyType = RigidbodyType2D.Static;
|
||||
GetOrAddComponent<CompositeCollider2D>(groundTileGo);
|
||||
|
||||
// NavSurface for PathBerserker2d
|
||||
GameObject navGo = GetOrCreateChild(envGroup, "NavSurface").gameObject;
|
||||
GetOrAddComponent<NavSurface>(navGo);
|
||||
|
||||
// ── [Transitions] ──────────────────────────────────────────────
|
||||
GetOrCreateChild(root.transform, "[Transitions]");
|
||||
|
||||
// ── Wire RoomController ────────────────────────────────────────
|
||||
AssignReference(roomController, "_roomCamera", roomCamera);
|
||||
|
||||
SerializedObject roomSO = new SerializedObject(roomController);
|
||||
SerializedProperty spawnArrayProp = roomSO.FindProperty("_spawnPoints");
|
||||
if (spawnArrayProp != null && spawnArrayProp.isArray)
|
||||
{
|
||||
spawnArrayProp.arraySize = 1;
|
||||
spawnArrayProp.GetArrayElementAtIndex(0).objectReferenceValue = defaultSpawn;
|
||||
roomSO.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
// ── Report ─────────────────────────────────────────────────────
|
||||
report.Add("在 RoomController._roomId 填写唯一房间 ID(如 \"Room_Forest_01\")。");
|
||||
report.Add("将 Player/CameraFollowTarget Transform 拖入 CinemachineCamera.Follow 字段以跟随玩家(或使用 BaseGames → Camera → Room Camera Setup 工具批量赋值)。");
|
||||
report.Add("调整 RoomBoundary PolygonCollider2D 顶点以匹配实际房间大小。");
|
||||
report.Add("使用 Tile Palette 在 Ground Tilemap 上绘制地形,然后在 NavSurface Inspector 中点击 Bake。");
|
||||
report.Add("[Transitions] 子节点下使用 BaseGames/Scene/Place/Room Transition 添加过渡点。");
|
||||
|
||||
MarkDirtyAndLog("Game Room 脚手架", root, report);
|
||||
}
|
||||
|
||||
private static void AssignString(Object target, string propertyName, string value, List<string> report = null)
|
||||
{
|
||||
SerializedObject serializedObject = new SerializedObject(target);
|
||||
SerializedProperty property = serializedObject.FindProperty(propertyName);
|
||||
if (property == null)
|
||||
{
|
||||
report?.Add($"{target.GetType().Name}.{propertyName} 字段不存在,未写入字符串值。");
|
||||
return;
|
||||
}
|
||||
property.stringValue = value;
|
||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static void AssignInt(Object target, string propertyName, int value, List<string> report = null)
|
||||
{
|
||||
SerializedObject serializedObject = new SerializedObject(target);
|
||||
SerializedProperty property = serializedObject.FindProperty(propertyName);
|
||||
if (property == null)
|
||||
{
|
||||
report?.Add($"{target.GetType().Name}.{propertyName} 字段不存在,未写入整型值。");
|
||||
return;
|
||||
}
|
||||
property.intValue = value;
|
||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static void EnsureEventChannelAssets(List<string> report)
|
||||
{
|
||||
bool hasCoreSet =
|
||||
FindFirstAsset("EVT_PlayerDied") != null &&
|
||||
FindFirstAsset("EVT_DeathScreenConfirmed") != null &&
|
||||
(FindFirstAsset("EVT_GameStateChanged") != null || FindFirstAsset("EVT_GameState") != null) &&
|
||||
FindFirstAsset("EVT_PauseRequested") != null &&
|
||||
FindFirstAsset("EVT_SceneLoadRequest") != null;
|
||||
|
||||
if (hasCoreSet)
|
||||
return;
|
||||
|
||||
CreateEventChannelAssets.CreateAll();
|
||||
report?.Add("检测到关键事件频道缺失,已自动执行 Create Event Channel Assets。");
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static void EnsureAudioSources(GameObject audioManagerGo, AudioManager audioManager, List<string> report)
|
||||
{
|
||||
GameObject bgmAGo = GetOrCreateChild(audioManagerGo.transform, "BGM Source A").gameObject;
|
||||
GameObject bgmBGo = GetOrCreateChild(audioManagerGo.transform, "BGM Source B").gameObject;
|
||||
GameObject sfxRootGo = GetOrCreateChild(audioManagerGo.transform, "SFX Sources").gameObject;
|
||||
|
||||
AudioSource bgmA = GetOrAddComponent<AudioSource>(bgmAGo);
|
||||
AudioSource bgmB = GetOrAddComponent<AudioSource>(bgmBGo);
|
||||
bgmA.playOnAwake = false;
|
||||
bgmB.playOnAwake = false;
|
||||
bgmA.loop = true;
|
||||
bgmB.loop = true;
|
||||
|
||||
var sfxSources = new AudioSource[6];
|
||||
for (int i = 0; i < sfxSources.Length; i++)
|
||||
{
|
||||
GameObject sfxGo = GetOrCreateChild(sfxRootGo.transform, $"SFX Source {i + 1}").gameObject;
|
||||
AudioSource sfxSource = GetOrAddComponent<AudioSource>(sfxGo);
|
||||
sfxSource.playOnAwake = false;
|
||||
sfxSources[i] = sfxSource;
|
||||
}
|
||||
|
||||
AssignReference(audioManager, "_bgmSourceA", bgmA);
|
||||
AssignReference(audioManager, "_bgmSourceB", bgmB);
|
||||
AssignArrayReferences(audioManager, "_sfxSources", sfxSources, report);
|
||||
report.Add("AudioManager 已生成 2 个 BGM Source 和 6 个 SFX Source,AudioMixer 仍需手工指定。");
|
||||
}
|
||||
|
||||
private static GameObject GetOrCreateRoot(string name)
|
||||
{
|
||||
Scene scene = SceneManager.GetActiveScene();
|
||||
foreach (GameObject rootObject in scene.GetRootGameObjects())
|
||||
{
|
||||
if (rootObject.name == name)
|
||||
return rootObject;
|
||||
}
|
||||
|
||||
GameObject root = new GameObject(name);
|
||||
Undo.RegisterCreatedObjectUndo(root, $"Create {name}");
|
||||
return root;
|
||||
}
|
||||
|
||||
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 T GetOrAddComponent<T>(GameObject go) where T : Component
|
||||
{
|
||||
T component = go.GetComponent<T>();
|
||||
if (component != null)
|
||||
return component;
|
||||
|
||||
return Undo.AddComponent<T>(go);
|
||||
}
|
||||
|
||||
private static GameObject GetOrCreateCanvas(Transform parent, string name, int sortOrder)
|
||||
{
|
||||
GameObject canvasGo = GetOrCreateChild(parent, name).gameObject;
|
||||
Canvas canvas = GetOrAddComponent<Canvas>(canvasGo);
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = sortOrder;
|
||||
GetOrAddComponent<CanvasScaler>(canvasGo);
|
||||
GetOrAddComponent<GraphicRaycaster>(canvasGo);
|
||||
return canvasGo;
|
||||
}
|
||||
|
||||
private static void AssignReference(Object target, string propertyName, Object value)
|
||||
{
|
||||
AssignReference(target, propertyName, value, null);
|
||||
}
|
||||
|
||||
private static void AssignReference(Object target, string propertyName, Object value, List<string> report)
|
||||
{
|
||||
SerializedObject serializedObject = new SerializedObject(target);
|
||||
SerializedProperty property = serializedObject.FindProperty(propertyName);
|
||||
if (property == null)
|
||||
{
|
||||
report?.Add($"{target.GetType().Name}.{propertyName} 字段不存在,未写入引用。");
|
||||
return;
|
||||
}
|
||||
|
||||
property.objectReferenceValue = value;
|
||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static void AssignArrayReferences(Object target, string propertyName, IReadOnlyList<Object> values, List<string> report)
|
||||
{
|
||||
SerializedObject serializedObject = new SerializedObject(target);
|
||||
SerializedProperty property = serializedObject.FindProperty(propertyName);
|
||||
if (property == null || !property.isArray)
|
||||
{
|
||||
report.Add($"{target.GetType().Name}.{propertyName} 不是可写数组字段。");
|
||||
return;
|
||||
}
|
||||
|
||||
property.arraySize = values.Count;
|
||||
for (int i = 0; i < values.Count; i++)
|
||||
property.GetArrayElementAtIndex(i).objectReferenceValue = values[i];
|
||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static void AssignAsset(Object target, string propertyName, List<string> report, bool required, params string[] candidates)
|
||||
{
|
||||
Object asset = FindFirstAsset(candidates);
|
||||
if (asset == null && required)
|
||||
report.Add($"未找到 {target.GetType().Name}.{propertyName} 需要的资产: {string.Join(" / ", candidates)}");
|
||||
|
||||
AssignReference(target, propertyName, asset, report);
|
||||
}
|
||||
|
||||
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 Object FindFirstAssetByType<T>(params string[] candidates) where T : Object
|
||||
{
|
||||
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);
|
||||
T asset = AssetDatabase.LoadAssetAtPath<T>(path);
|
||||
if (asset != null && asset.name == candidate)
|
||||
return asset;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object FindFirstAssetWithExtension(string extension, 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);
|
||||
if (string.IsNullOrEmpty(path) || !path.EndsWith(extension, System.StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
Object asset = AssetDatabase.LoadMainAssetAtPath(path);
|
||||
if (asset != null && asset.name == candidate)
|
||||
return asset;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object EnsureInputReaderAsset(List<string> report)
|
||||
{
|
||||
string[] existing = AssetDatabase.FindAssets("t:InputReaderSO");
|
||||
if (existing != null && existing.Length > 0)
|
||||
{
|
||||
string firstPath = AssetDatabase.GUIDToAssetPath(existing[0]);
|
||||
Object found = AssetDatabase.LoadMainAssetAtPath(firstPath);
|
||||
if (found != null)
|
||||
return found;
|
||||
}
|
||||
|
||||
const string inputFolder = "Assets/_Game/Data/Player/Input";
|
||||
EnsureFolder(inputFolder);
|
||||
|
||||
const string assetPath = "Assets/_Game/Data/Player/Input/InputReader.asset";
|
||||
InputReaderSO created = ScriptableObject.CreateInstance<InputReaderSO>();
|
||||
AssetDatabase.CreateAsset(created, assetPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
report?.Add("未找到 InputReaderSO,已自动创建 Assets/_Game/Data/Player/Input/InputReader.asset。");
|
||||
return created;
|
||||
}
|
||||
|
||||
private static void EnsureFolder(string fullPath)
|
||||
{
|
||||
string[] parts = fullPath.Split('/');
|
||||
if (parts.Length == 0 || parts[0] != "Assets")
|
||||
return;
|
||||
|
||||
string current = "Assets";
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
string next = current + "/" + parts[i];
|
||||
if (!AssetDatabase.IsValidFolder(next))
|
||||
AssetDatabase.CreateFolder(current, parts[i]);
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddScaffoldNote(GameObject go, string message)
|
||||
{
|
||||
AddScaffoldNote(go, message, null);
|
||||
}
|
||||
|
||||
private static void AddScaffoldNote(GameObject go, string message, List<string> report)
|
||||
{
|
||||
// 注意:不再添加 MonoBehaviour 组件,避免 Editor 程序集组件在 Play 模式下出现 Missing Script
|
||||
report?.Add($"{go.name}: {message}");
|
||||
Debug.Log($"[SceneScaffold] {go.name}: {message}");
|
||||
}
|
||||
|
||||
private static void MarkDirtyAndLog(string scaffoldName, GameObject root, List<string> report)
|
||||
{
|
||||
EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
|
||||
Selection.activeGameObject = root;
|
||||
|
||||
if (report.Count == 0)
|
||||
{
|
||||
Debug.Log($"[SceneScaffoldTools] {scaffoldName} 完成。所有可自动补齐的对象与引用均已生成。", root);
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.LogWarning($"[SceneScaffoldTools] {scaffoldName} 完成,但仍有 {report.Count} 项需要手工确认:\n- {string.Join("\n- ", report)}", root);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/Scene/SceneScaffoldTools.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Scene/SceneScaffoldTools.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb2b7f90961ee3344a5f39c68931a26d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user