摄像机区域的优化

This commit is contained in:
2026-05-17 07:56:12 +08:00
parent f264329751
commit d25f237e76
62 changed files with 25774 additions and 5450 deletions

View File

@@ -0,0 +1,109 @@
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace BaseGames.Editor
{
/// <summary>
/// 编辑器 Edit Mode 辅助:打开任意场景时自动将 Persistent 场景 Additive 加入 Hierarchy。
///
/// 职责范围(仅限 Edit Mode
/// 让设计师在编辑房间场景时Inspector 中可直接看到并配置 GameManager / SceneService
/// 等 Persistent 场景内的组件,无需手动 Open Additive。
///
/// 运行时Play Mode / 发行版 Build的保证由 GameBootstrapRuntime 程序集)负责,
/// 本脚本与 Play Mode 状态无关,不监听 playModeStateChanged。
///
/// 菜单BaseGames/Tools/Edit Mode: Auto-Open Persistent Scene
/// </summary>
[InitializeOnLoad]
public static class PersistentSceneAutoLoader
{
// ── 常量 ─────────────────────────────────────────────────────────────
private const string MenuPath = "BaseGames/Tools/Edit Mode: Auto-Open Persistent Scene";
private const string PrefKey = "BaseGames_EditAutoOpen_Persistent";
private const string PersistentSceneName = "Scene_Persistent";
// ── 构造Editor 启动时执行)──────────────────────────────────────────
static PersistentSceneAutoLoader()
{
EditorSceneManager.sceneOpened += OnSceneOpened;
// 启动时补一次检查Editor 已打开但 Persistent 不在 Hierarchy 的场景
EditorApplication.delayCall += EnsurePersistentInHierarchyEditMode;
}
// ── 菜单 ─────────────────────────────────────────────────────────────
[MenuItem(MenuPath, validate = false, priority = 301)]
private static void ToggleEnabled()
{
bool current = EditorPrefs.GetBool(PrefKey, true);
EditorPrefs.SetBool(PrefKey, !current);
}
[MenuItem(MenuPath, validate = true)]
private static bool ToggleEnabledValidate()
{
Menu.SetChecked(MenuPath, EditorPrefs.GetBool(PrefKey, true));
return true;
}
// ── 场景打开回调 ──────────────────────────────────────────────────────
private static void OnSceneOpened(Scene scene, OpenSceneMode mode)
{
// 若是 Persistent 本身被打开,无需额外处理
if (IsPersistentScene(scene.name)) return;
// Single 模式(替换当前场景)或 Additive 加入新场景时,确保 Persistent 也在 Hierarchy 中
// 使用 delayCall 避免在场景加载中途调用 OpenScene
EditorApplication.delayCall += EnsurePersistentInHierarchyEditMode;
}
// ── 核心逻辑 ──────────────────────────────────────────────────────────
private static void EnsurePersistentInHierarchyEditMode()
{
// 仅在 Edit Mode 执行Play Mode 由 GameBootstrap 负责)
if (Application.isPlaying) return;
if (!EditorPrefs.GetBool(PrefKey, true)) return;
// 若 Persistent 已在 Hierarchy无需操作
for (int i = 0; i < SceneManager.sceneCount; i++)
if (IsPersistentScene(SceneManager.GetSceneAt(i).name)) return;
// 查找并 Additive 打开 Persistent 场景
string path = FindPersistentScenePath();
if (string.IsNullOrEmpty(path))
{
Debug.LogWarning(
$"[PersistentAutoLoader] 未找到 '{PersistentSceneName}' 场景。" +
"请确认场景已添加到 Build Settings 或可在 Assets 中搜索到。");
return;
}
EditorSceneManager.OpenScene(path, OpenSceneMode.Additive);
}
// ── 工具函数 ──────────────────────────────────────────────────────────
private static bool IsPersistentScene(string sceneName)
=> sceneName == PersistentSceneName || sceneName == "Persistent";
private static string FindPersistentScenePath()
{
// 优先从 Build Settings 查找(保证与 GameBootstrap 使用同一文件)
foreach (var buildScene in EditorBuildSettings.scenes)
{
if (!buildScene.enabled) continue;
string name = System.IO.Path.GetFileNameWithoutExtension(buildScene.path);
if (IsPersistentScene(name)) return buildScene.path;
}
// 回退:在 AssetDatabase 中搜索
string[] guids = AssetDatabase.FindAssets($"t:Scene {PersistentSceneName}");
if (guids.Length > 0) return AssetDatabase.GUIDToAssetPath(guids[0]);
guids = AssetDatabase.FindAssets("t:Scene Persistent");
return guids.Length > 0 ? AssetDatabase.GUIDToAssetPath(guids[0]) : null;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1b5ab9e5f153fb148817239307245e00
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -385,46 +385,101 @@ namespace BaseGames.Editor
MarkDirtyAndLog("Room Transition", go, report);
}
[MenuItem("BaseGames/Scene/Place/Room Camera", priority = 140)]
public static void PlaceRoomCamera()
[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)");
GameObject go = new GameObject("RoomCamera");
Undo.RegisterCreatedObjectUndo(go, "Place Room Camera");
go.transform.position = GetDropPosition();
Vector3 pos = GetDropPosition();
CinemachineCamera cinemachine = GetOrAddComponent<CinemachineCamera>(go);
RoomCamera roomCamera = GetOrAddComponent<RoomCamera>(go);
CinemachineConfiner2D confiner = GetOrAddComponent<CinemachineConfiner2D>(go);
// ── 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");
// RoomBoundary child — defines the camera confinement area
Transform boundaryT = GetOrCreateChild(go.transform, "RoomBoundary");
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),
new Vector2(-12f, 6f),
new Vector2( 12f, 6f),
new Vector2( 12f, -6f),
new Vector2(-12f, -6f), // BL
new Vector2( 12f, -6f), // BR
new Vector2( 12f, 6f), // TR
new Vector2(-12f, 6f), // TL
});
RoomVisibleArea visibleArea = GetOrAddComponent<RoomVisibleArea>(boundaryT.gameObject);
AssignReference(roomCamera, "_visibleArea", visibleArea, report);
AssignReference(confiner, "m_BoundingShape2D", boundaryCollider, report);
AssignReference(cameraArea, "_confinerCollider", 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); }
// ── CameraTriggerZone配对─────────────────────────────────────────
GameObject zoneGo = new GameObject($"{areaName}_TriggerZone");
Undo.RegisterCreatedObjectUndo(zoneGo, "Place Camera Trigger Zone");
zoneGo.transform.position = pos;
SetLayer(zoneGo, "TriggerZone", report);
report.Add("将 Player/CameraFollowTarget Transform 拖入 CinemachineCamera.Follow 字段以跟随玩家(或使用 Room Camera Setup 工具批量赋值)。");
report.Add("调整 RoomBoundary PolygonCollider2D 顶点以匹配房间边界。");
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("Room Camera", go, report);
MarkDirtyAndLog($"Camera Area (+ TriggerZone): {areaName}", go, report);
}
[MenuItem("BaseGames/Scene/Place/Ground Platform", priority = 150)]
@@ -534,28 +589,6 @@ namespace BaseGames.Editor
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()
{

View File

@@ -86,6 +86,29 @@ namespace BaseGames.Editor
CameraStateController cameraStateController = GetOrAddComponent<CameraStateController>(cameraStateGo);
CinemachineImpulseSource impulseSource = GetOrAddComponent<CinemachineImpulseSource>(cameraStateGo);
// 垂直窥视系统独立节点CameraStateController 持引用
GameObject lookSystemGo = GetOrCreateChild(camera, "CameraLookSystem").gameObject;
CameraLookSystem lookSystem = GetOrAddComponent<CameraLookSystem>(lookSystemGo);
GameObject vcamAGo = GetOrCreateChild(camera, "VCamA").gameObject;
CinemachineCamera vcamA = GetOrAddComponent<CinemachineCamera>(vcamAGo);
GetOrAddComponent<CinemachineConfiner2D>(vcamAGo);
GetOrAddComponent<CameraAxisLockExtension>(vcamAGo);
GetOrAddComponent<CameraAsymmetricDampingExtension>(vcamAGo);
GetOrAddComponent<CameraAdaptiveLookaheadExtension>(vcamAGo);
// CinemachinePositionComposerBody 阶段组件必须存在ConfigureSlot 依赖它写入所有相机跟随参数
var composerA = GetOrAddComponent<CinemachinePositionComposer>(vcamAGo);
ApplyComposerDefaults(composerA);
GameObject vcamBGo = GetOrCreateChild(camera, "VCamB").gameObject;
CinemachineCamera vcamB = GetOrAddComponent<CinemachineCamera>(vcamBGo);
GetOrAddComponent<CinemachineConfiner2D>(vcamBGo);
GetOrAddComponent<CameraAxisLockExtension>(vcamBGo);
GetOrAddComponent<CameraAsymmetricDampingExtension>(vcamBGo);
GetOrAddComponent<CameraAdaptiveLookaheadExtension>(vcamBGo);
var composerB = GetOrAddComponent<CinemachinePositionComposer>(vcamBGo);
ApplyComposerDefaults(composerB);
GameObject uiRootGo = GetOrCreateChild(ui, "UIRoot").gameObject;
UIManager uiManager = GetOrAddComponent<UIManager>(uiRootGo);
@@ -146,6 +169,11 @@ namespace BaseGames.Editor
AssignReference(cameraStateController, "_brain", brain);
AssignReference(cameraStateController, "_impulseSource", impulseSource);
AssignReference(cameraStateController, "_lookSystem", lookSystem);
AssignReference(cameraStateController, "_vcamA", vcamA);
AssignReference(cameraStateController, "_vcamB", vcamB);
AssignAsset(cameraStateController, "_onPlayerSpawned", report, true, "EVT_PlayerSpawned");
AssignAsset(cameraStateController, "_lensConfig", report, false, "CAM_LensConfig", "LensConfig", "CameraLensConfig");
AssignReference(uiManager, "_hudRoot", hudRootGo);
AssignReference(uiManager, "_pauseMenuRoot", pauseRootGo);
@@ -189,13 +217,12 @@ namespace BaseGames.Editor
// ── [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);
// CameraArea — 定义相机区域(限位 + 混合配置 + 可选专有 VCam
GameObject cameraAreaGo = GetOrCreateChild(cameraGroup, "CameraArea").gameObject;
CameraArea cameraArea = GetOrAddComponent<CameraArea>(cameraAreaGo);
// RoomBoundary — defines visible area and confiner polygon
Transform boundaryT = GetOrCreateChild(roomCameraGo.transform, "RoomBoundary");
// AreaBoundary — 提供 CinemachineConfiner2D 所需的限位多边形
Transform boundaryT = GetOrCreateChild(cameraAreaGo.transform, "AreaBoundary");
PolygonCollider2D boundaryCollider = GetOrAddComponent<PolygonCollider2D>(boundaryT.gameObject);
boundaryCollider.pathCount = 1;
boundaryCollider.SetPath(0, new Vector2[]
@@ -203,16 +230,8 @@ namespace BaseGames.Editor
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); }
AssignReference(cameraArea, "_confinerCollider", boundaryCollider);
// ── [SpawnPoints] ──────────────────────────────────────────────
Transform spawnGroup = GetOrCreateChild(root.transform, "[SpawnPoints]");
@@ -250,7 +269,7 @@ namespace BaseGames.Editor
GetOrCreateChild(root.transform, "[Transitions]");
// ── Wire RoomController ────────────────────────────────────────
AssignReference(roomController, "_roomCamera", roomCamera);
AssignReference(roomController, "_cameraArea", cameraArea);
SerializedObject roomSO = new SerializedObject(roomController);
SerializedProperty spawnArrayProp = roomSO.FindProperty("_spawnPoints");
@@ -263,8 +282,7 @@ namespace BaseGames.Editor
// ── 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("调整 AreaBoundary PolygonCollider2D 顶点以匹配实际房间大小。");
report.Add("使用 Tile Palette 在 Ground Tilemap 上绘制地形,然后在 NavSurface Inspector 中点击 Bake。");
report.Add("[Transitions] 子节点下使用 BaseGames/Scene/Place/Room Transition 添加过渡点。");
@@ -561,6 +579,34 @@ namespace BaseGames.Editor
Debug.LogWarning($"[SceneScaffoldTools] {scaffoldName} 完成,但仍有 {report.Count} 项需要手工确认:\n- {string.Join("\n- ", report)}", root);
}
/// <summary>
/// 为 VCam 上的 CinemachinePositionComposer 写入初始默认展示参数。
/// 这些値与 <see cref="CameraArea"/> 的默认値一致,确保脆架生成后 Scene 预览即有正确感觉。
/// 运行时 CameraStateController.ConfigureSlot 会在每次 SwitchArea 时用 per-area 配置覆写。
/// </summary>
private static void ApplyComposerDefaults(CinemachinePositionComposer composer)
{
if (composer == null) return;
// 屏幕位置:玩家稍低于中心,上方有更多视野
var comp = composer.Composition;
comp.ScreenPosition = new Vector2(0f, -0.15f);
comp.DeadZone.Enabled = true;
comp.DeadZone.Size = new Vector2(0.15f, 0.05f);
composer.Composition = comp;
// 阻尼X 轻度缓冲Y = 0由 CameraAsymmetricDampingExtension 接管非对称 Y 阻尼)
composer.Damping = new Vector3(0.5f, 0f, 0f);
// Lookahead水平引领预测开启IgnoreY = true平台游戏 Y 轴不预测,避免起跳时镜头猛拉)
var lah = composer.Lookahead;
lah.Enabled = true;
lah.Time = 0.28f;
lah.Smoothing = 5f;
lah.IgnoreY = true;
composer.Lookahead = lah;
}
}
}