角色能力,存档

This commit is contained in:
2026-05-19 11:50:21 +08:00
parent d25f237e76
commit 2dcb7a961a
136 changed files with 36035 additions and 27551 deletions

View File

@@ -1,5 +1,6 @@
using System.Reflection;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using Unity.Cinemachine;
using BaseGames.Camera;
@@ -17,7 +18,7 @@ namespace BaseGames.Editor
/// FOV 优先级(降序):
/// 专有 DedicatedCamera.Lens.FieldOfView
/// → CameraLensConfigSO.fieldOfView单一来源无跨场景依赖
/// → CameraStateController._vcamAPersistent 场景已加载时
/// → CameraStateController 活动 VCam 的 FieldOfView编辑器备用
/// → Camera.main.fieldOfView
/// → 60f默认
/// </summary>
@@ -41,6 +42,7 @@ namespace BaseGames.Editor
private bool _foldFollow = true;
private bool _foldLens = false;
private bool _foldCamera = false;
private bool _foldNoise = false;
private bool _foldTools = false;
// ── 折叠标题样式缓存(深色背景 + 白色文字)────────────────────────────
@@ -78,7 +80,7 @@ namespace BaseGames.Editor
EditorGUILayout.PropertyField(confinerProp, new GUIContent("Confiner Collider"));
}
if (!confinerOk)
EditorGUILayout.HelpBox("必须绑定子节点 PolygonCollider2DAreaBoundary否则 Cinemachine 无法限位。", MessageType.Error);
EditorGUILayout.HelpBox("必须绑定子节点 BoxColliderAreaBoundary否则 CinemachineConfiner3D 无法限位。", MessageType.Error);
EditorGUILayout.PropertyField(serializedObject.FindProperty("_visibleBounds"), new GUIContent("Visible Bounds本地坐标"));
}
@@ -86,11 +88,11 @@ namespace BaseGames.Editor
EditorGUILayout.Space(2f);
// ── 跟随参数覆盖 ─────────────────────────────────────────────────
// ── 跟随行为参数 ─────────────────────────────────────────────────
var overrideProp = serializedObject.FindProperty("_overrideFollowBehaviour");
bool overrides = overrideProp.boolValue;
_foldFollow = DrawFoldoutHeader(
overrides ? "跟随参数覆盖 ●" : "跟随参数覆盖 ○ (使用全局默认)", _foldFollow);
overrides ? "相机行为参数 ●" : "相机行为参数 ○ (使用 VCam 默认参数)", _foldFollow);
if (_foldFollow)
{
using (new EditorGUI.IndentLevelScope())
@@ -108,6 +110,25 @@ namespace BaseGames.Editor
EditorGUILayout.PropertyField(serializedObject.FindProperty("_lookaheadSmoothing"),new GUIContent("Lookahead Smoothing"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("_lockHorizontal"), new GUIContent("Lock Horizontal"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("_lockVertical"), new GUIContent("Lock Vertical"));
// ── 方向感知偏置 ──────────────────────────────────
EditorGUILayout.Space(4f);
EditorGUILayout.LabelField("方向感知水平偏置", EditorStyles.miniLabel);
var overrideFacingProp = serializedObject.FindProperty("_overrideFacingBias");
EditorGUILayout.PropertyField(overrideFacingProp, new GUIContent("Override Facing Bias",
"开启后可为此区域单独设置偏置量;关闭则使用 VCam 扩展组件的默认值。"));
if (overrideFacingProp.boolValue)
{
using (new EditorGUI.IndentLevelScope())
{
var biasProp = serializedObject.FindProperty("_facingBiasOverride");
EditorGUILayout.PropertyField(biasProp, new GUIContent("Facing Bias (units)",
"方向感知偏置量世界单位。0 = 禁用此区域的方向偏置。\n" +
"较窄走廊建议设 0防止相机超出 Confiner 边界。"));
if (biasProp.floatValue < 0.01f)
EditorGUILayout.HelpBox("设为 0 将禁用此区域的方向感知偏置。", MessageType.Info);
}
}
}
}
}
@@ -128,8 +149,8 @@ namespace BaseGames.Editor
EditorGUILayout.Space(2f);
// ── 专有相机(可选) ──────────────────────────────────────────────
_foldCamera = DrawFoldoutHeader("专有相机(可选)", _foldCamera);
// ── 虚拟相机 ──────────────────────────────────────────────
_foldCamera = DrawFoldoutHeader("虚拟相机", _foldCamera);
if (_foldCamera)
{
using (new EditorGUI.IndentLevelScope())
@@ -140,7 +161,29 @@ namespace BaseGames.Editor
}
EditorGUILayout.Space(2f);
// ── 相机噪音(氛围震动) ────────────────────────────────────────
_foldNoise = DrawFoldoutHeader("相机噪音(氛围震动)", _foldNoise);
if (_foldNoise)
{
using (new EditorGUI.IndentLevelScope())
{
var noiseProp = serializedObject.FindProperty("_noiseProfile");
EditorGUILayout.PropertyField(noiseProp, new GUIContent("Noise Profile",
"氛围震动配置Noise Settings 资产),留空则禁用噪音。"));
if (noiseProp.objectReferenceValue != null)
{
EditorGUILayout.PropertyField(serializedObject.FindProperty("_noiseAmplitude"),
new GUIContent("Amplitude Gain", "振幅增益0 = 无震动,推荐 0.2 ~ 0.8)。"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("_noiseFrequency"),
new GUIContent("Frequency Gain", "频率增益1 = 资产原始频率)。"));
}
EditorGUILayout.HelpBox(
"专属 VCam 已包含 CinemachineBasicMultiChannelPerlin 组件(使用工具创建时自动附加)。",
MessageType.Info);
}
}
EditorGUILayout.Space(2f);
// ── 可视区域工具 ──────────────────────────────────────────────────
_foldTools = DrawFoldoutHeader("可视区域工具", _foldTools);
if (_foldTools)
@@ -247,7 +290,7 @@ namespace BaseGames.Editor
EditorGUILayout.Space(4f);
DrawLegend("■ 黄色矩形Scene 视图)", kVisibleOutline, "可视区域 — 摄像机视口永不超出此范围");
DrawLegend("■ 蓝色多边Scene 视图)", kConfinerLine, "限位区域 — CinemachineConfiner2D 的运动边界");
DrawLegend("■ 蓝色Scene 视图)", kConfinerLine, "限位区域 — CinemachineConfiner3D 的运动边界");
}
}
@@ -319,7 +362,7 @@ namespace BaseGames.Editor
{
Undo.RecordObject(area, "Edit Visible Bounds");
if (area.ConfinerCollider != null)
Undo.RecordObject(area.ConfinerCollider, "Sync Confiner");
Undo.RecordObject(area.ConfinerCollider, "Sync Confiner"); // BoxCollider
// 世界坐标 → 本地坐标,存入序列化字段
boundsP.rectValue = new Rect(r.x - areaPos.x, r.y - areaPos.y, r.width, r.height);
@@ -380,17 +423,21 @@ namespace BaseGames.Editor
private static void DrawConfinerGizmo(CameraArea area)
{
var poly = area.ConfinerCollider;
if (poly == null || poly.pathCount == 0) return;
var box = area.ConfinerCollider;
if (box == null) return;
int ptCount = poly.GetTotalPointCount();
if (ptCount < 3) return;
// 将 BoxCollider 的 XY 范围投影到 Scene 视图(忽略 Z2D 俯视)
Vector3 centerWorld = box.transform.TransformPoint(box.center);
float hw = box.size.x * 0.5f;
float hh = box.size.y * 0.5f;
var pts2 = new System.Collections.Generic.List<Vector2>(ptCount);
poly.GetPath(0, pts2);
var pts3 = new Vector3[ptCount];
for (int i = 0; i < ptCount; i++)
pts3[i] = poly.transform.TransformPoint(pts2[i]);
var pts3 = new Vector3[]
{
new Vector3(centerWorld.x - hw, centerWorld.y - hh, 0f), // BL
new Vector3(centerWorld.x + hw, centerWorld.y - hh, 0f), // BR
new Vector3(centerWorld.x + hw, centerWorld.y + hh, 0f), // TR
new Vector3(centerWorld.x - hw, centerWorld.y + hh, 0f), // TL
};
DrawPolyGizmo(pts3, kConfinerFill, kConfinerLine, 2.0f);
@@ -510,17 +557,16 @@ namespace BaseGames.Editor
internal static void SyncConfinerFromVisibleBounds(CameraArea area, float vFOV, float aspect)
{
var poly = area.ConfinerCollider;
if (poly == null)
var box = area.ConfinerCollider;
if (box == null)
{
Debug.LogWarning($"[CameraAreaEditor] {area.name}ConfinerCollider 未绑定,无法同步。");
return;
}
// VisibleBounds 已含 transform.position为世界坐标。
// 限位多边形 = 相机中心运动范围 = VisibleBounds 向内收缩视口半尺寸。
// 运行时 ConfigureSlot 设置 OversizeWindow.MaxWindowSize ≈ 0
// 阻止 Cinemachine 再次收缩此多边形,确保边界精确匹配可视区域。
// 限位体积 = 相机中心运动范围 = VisibleBounds 向内收缩视口半尺寸。
// Confiner3D 以 BoxCollider 直接约束相机 3D 位置,无需 OversizeWindow 补偿。
Rect visible = area.VisibleBounds; // 世界坐标
float depth = area.CameraDepth;
float halfH = depth * Mathf.Tan(vFOV * 0.5f * Mathf.Deg2Rad);
@@ -532,25 +578,21 @@ namespace BaseGames.Editor
float yMax = visible.yMax - halfH;
// 小房间:视口大于可视区域时收缩至中心点,相机固定在可视区域中心
const float kMinSize = 0.001f;
if (xMin > xMax) { float cx = visible.center.x; xMin = cx - kMinSize * 0.5f; xMax = cx + kMinSize * 0.5f; }
if (yMin > yMax) { float cy = visible.center.y; yMin = cy - kMinSize * 0.5f; yMax = cy + kMinSize * 0.5f; }
// BoxCollider 允许 size = 0Confiner3D 不需要最小尺寸
if (xMin > xMax) { float cx = visible.center.x; xMin = cx; xMax = cx; }
if (yMin > yMax) { float cy = visible.center.y; yMin = cy; yMax = cy; }
Transform polyT = poly.transform;
Vector2 Local(Vector3 w) => polyT.InverseTransformPoint(w);
// BoxCollider center 以其 Transform 本地坐标表示
// 相机在世界 Z = -depthAreaBoundary 节点在 Z = 0所以 center.z = -depth
Transform boxT = box.transform;
Vector3 centerWorld = new Vector3((xMin + xMax) * 0.5f, (yMin + yMax) * 0.5f, 0f);
Vector3 centerLocal = boxT.InverseTransformPoint(centerWorld);
centerLocal.z = -depth;
Undo.RecordObject(poly, "Sync Confiner from Visible Bounds");
// 顶点必须 CCW逆时针Clipper 对 CW 多边形area<0会取反 delta
// 导致 Confiner 向外膨胀而非向内收缩,相机完全不受限。
// CCW 顺序BL → BR → TR → TL
poly.SetPath(0, new[]
{
Local(new Vector3(xMin, yMin, 0f)), // BL
Local(new Vector3(xMax, yMin, 0f)), // BR
Local(new Vector3(xMax, yMax, 0f)), // TR
Local(new Vector3(xMin, yMax, 0f)), // TL
});
EditorUtility.SetDirty(poly);
Undo.RecordObject(box, "Sync Confiner from Visible Bounds");
box.center = centerLocal;
box.size = new Vector3(xMax - xMin, yMax - yMin, 1f);
EditorUtility.SetDirty(box);
// 记录本次同步所用的 FOV供编辑器过期检测使用
var areaSO = new SerializedObject(area);
@@ -575,6 +617,180 @@ namespace BaseGames.Editor
internal static void SyncConfinerAuto(CameraArea area) =>
SyncConfinerFromVisibleBounds(area, GetFOV(area), GetAspect());
// ── VCam 组件顺序检测 / 修复 ─────────────────────────────────────────
// 必须在 CinemachineConfiner3D 之前运行的扩展类型(修改 RawPosition需被 Confiner 裁剪)
private static readonly System.Type[] s_MustBeforeConfiner =
{
typeof(CameraAsymmetricDampingExtension),
typeof(CameraFallBiasExtension),
typeof(CameraFacingBiasExtension),
typeof(CameraAdaptiveLookaheadExtension),
};
/// <summary>
/// 检查 <paramref name="vcam"/> 的扩展组件顺序是否正确。
/// 返回问题描述字符串;若无问题则返回 <c>null</c>。
/// </summary>
internal static string CheckVCamExtensionOrderIssue(CinemachineCamera vcam)
{
if (vcam == null) return null;
var confiner = vcam.GetComponent<CinemachineConfiner3D>();
if (confiner == null) return "缺少 CinemachineConfiner3D 组件";
Component[] comps = vcam.GetComponents<Component>();
int confinerIdx = System.Array.IndexOf(comps, confiner);
var sb = new System.Text.StringBuilder();
foreach (var t in s_MustBeforeConfiner)
{
var comp = vcam.GetComponent(t);
if (comp == null) continue;
if (System.Array.IndexOf(comps, comp) > confinerIdx)
sb.AppendLine($" · {t.Name} 在 Confiner 之后(偏置绕过限位 → 相机逃出区域)");
}
var axisLock = vcam.GetComponent<CameraAxisLockExtension>();
if (axisLock != null && System.Array.IndexOf(comps, axisLock) < confinerIdx)
sb.AppendLine(" · CameraAxisLockExtension 在 Confiner 之前(轴向锁定会被 Confiner 覆盖失效)");
return sb.Length > 0 ? sb.ToString().TrimEnd() : null;
}
/// <summary>
/// 自动修正 <paramref name="vcam"/> 上扩展组件的挂载顺序:
/// 将偏置扩展移到 <see cref="CinemachineConfiner3D"/> 之前,
/// 将 <see cref="CameraAxisLockExtension"/> 移到之后。
/// </summary>
internal static void FixVCamExtensionOrder(CinemachineCamera vcam)
{
if (vcam == null) return;
var confiner = vcam.GetComponent<CinemachineConfiner3D>();
if (confiner == null)
{
Debug.LogWarning($"[CameraAreaEditor] {vcam.name} 缺少 CinemachineConfiner3D无法修正顺序。");
return;
}
Undo.RegisterCompleteObjectUndo(vcam.gameObject, "Fix VCam Extension Order");
// 将偏置扩展逐个移到 Confiner 之前
foreach (var t in s_MustBeforeConfiner)
{
var comp = vcam.GetComponent(t);
if (comp == null) continue;
// 反复上移,直到位于 Confiner 之前(最多 30 步防死循环)
for (int guard = 0; guard < 30; guard++)
{
Component[] comps = vcam.GetComponents<Component>();
int compIdx = System.Array.IndexOf(comps, comp);
int confinerIdx = System.Array.IndexOf(comps, confiner);
if (compIdx < confinerIdx) break;
UnityEditorInternal.ComponentUtility.MoveComponentUp(comp);
}
}
// 将 AxisLock 移到 Confiner 之后
var axisLock = vcam.GetComponent<CameraAxisLockExtension>();
if (axisLock != null)
{
for (int guard = 0; guard < 30; guard++)
{
Component[] comps = vcam.GetComponents<Component>();
int axisIdx = System.Array.IndexOf(comps, axisLock);
int confinerIdx = System.Array.IndexOf(comps, confiner);
if (axisIdx > confinerIdx) break;
UnityEditorInternal.ComponentUtility.MoveComponentDown(axisLock);
}
}
EditorUtility.SetDirty(vcam.gameObject);
Debug.Log($"[CameraAreaEditor] 已修正 {vcam.name} 的扩展组件顺序。");
}
/// <summary>
/// 为指定 <paramref name="area"/> 创建子节点专有 VCam附加所有必要组件并绑定到
/// <c>_dedicatedCamera</c>。若已有专有 VCam 则直接返回现有实例,不重复创建。
/// </summary>
internal static CinemachineCamera CreateDedicatedVCamForArea(CameraArea area)
{
if (area == null) return null;
if (area.DedicatedCamera != null)
{
Debug.LogWarning(
$"[CameraAreaEditor] {area.name} 已有专有 VCam{area.DedicatedCamera.name},跳过创建。");
return area.DedicatedCamera;
}
string vcamName = $"VCam_{area.gameObject.name}";
var vcamGO = new GameObject(vcamName);
Undo.RegisterCreatedObjectUndo(vcamGO, "Create Dedicated VCam");
vcamGO.transform.SetParent(area.transform);
vcamGO.transform.localPosition = Vector3.zero;
// ── CinemachineCamera ─────────────────────────────────────────────
var vcam = vcamGO.AddComponent<CinemachineCamera>();
vcam.Priority = 0; // 非激活;由 CameraStateController.ActivateDedicated 提升优先级
// ── Body位置合成器 ───────────────────────────────────────────────
var composer = vcamGO.AddComponent<CinemachinePositionComposer>();
// ── 扩展组件管线顺序Body 阶段偏置扩展 → PostBody Confiner → 独立 AxisLock → Noise
// AsymmetricDamping → FallBias → FacingBias → AdaptiveLookahead
// vcamGO.AddComponent<CameraAsymmetricDampingExtension>();
// vcamGO.AddComponent<CameraFallBiasExtension>();
// vcamGO.AddComponent<CameraFacingBiasExtension>();
// vcamGO.AddComponent<CameraAdaptiveLookaheadExtension>();
vcamGO.AddComponent<CinemachinePixelPerfect>();
// ── 限位 ConfinerPostBody 阶段,须在位置偏置扩展之后)────────────
var confiner3d = vcamGO.AddComponent<CinemachineConfiner3D>();
if (area.ConfinerCollider != null)
confiner3d.BoundingVolume = area.ConfinerCollider;
// ── 轴向约束独立PostBody────────────────────────────────────
vcamGO.AddComponent<CameraAxisLockExtension>();
// ── 噪音Noise 阶段,须排在 Confiner / AxisLock 之后)───────────
vcamGO.AddComponent<CinemachineBasicMultiChannelPerlin>();
// ── 应用镜头参数FOV + CameraDistance + Transform Z ─────────────
// 与 CameraStateController.ApplyLensToVcam 逻辑保持一致:
// 1. Lens.FieldOfView
// 2. composer.CameraDistance控制运行时真实 Z 距离,否则管线会覆盖)
// 3. transform.localPosition.z编辑器预览与运行时保持一致
float fov = area.LensConfig != null ? area.LensConfig.fieldOfView : 60f;
float depth = area.CameraDepth > 0f ? area.CameraDepth : 10f;
var lens = vcam.Lens;
lens.FieldOfView = fov;
vcam.Lens = lens;
composer.CameraDistance = depth;
var localPos = vcamGO.transform.localPosition;
localPos.z = -depth;
vcamGO.transform.localPosition = localPos;
// ── 写入 CameraArea._dedicatedCamera及默认优先级───────────────
var so = new SerializedObject(area);
so.Update();
so.FindProperty("_dedicatedCamera").objectReferenceValue = vcam;
var priProp = so.FindProperty("_dedicatedPriority");
if (priProp.intValue == 0) priProp.intValue = 20;
so.ApplyModifiedProperties();
EditorUtility.SetDirty(area);
EditorSceneManager.MarkSceneDirty(area.gameObject.scene);
EditorGUIUtility.PingObject(vcamGO);
Debug.Log($"[CameraAreaEditor] 已为 {area.name} 创建专有 VCam{vcamName}" +
$" FOV={fov:F1}° Depth={depth:F1}");
return vcam;
}
/// <summary>
/// 与 <see cref="SyncConfinerFromVisibleBounds"/> 逻辑相同,但不记录 Undo、不输出日志。
/// 供拖拽 Handle 时每帧调用,避免 Undo 堆积和 Console 刷屏。
@@ -582,8 +798,8 @@ namespace BaseGames.Editor
/// </summary>
private static void SyncConfinerQuiet(CameraArea area, float vFOV, float aspect)
{
var poly = area.ConfinerCollider;
if (poly == null) return;
var box = area.ConfinerCollider;
if (box == null) return;
// VisibleBounds 已含 transform.position为世界坐标。
Rect visible = area.VisibleBounds;
@@ -596,22 +812,17 @@ namespace BaseGames.Editor
float yMin = visible.yMin + halfH;
float yMax = visible.yMax - halfH;
const float kMinSize = 0.001f;
if (xMin > xMax) { float cx = visible.center.x; xMin = cx - kMinSize * 0.5f; xMax = cx + kMinSize * 0.5f; }
if (yMin > yMax) { float cy = visible.center.y; yMin = cy - kMinSize * 0.5f; yMax = cy + kMinSize * 0.5f; }
if (xMin > xMax) { float cx = visible.center.x; xMin = cx; xMax = cx; }
if (yMin > yMax) { float cy = visible.center.y; yMin = cy; yMax = cy; }
Transform polyT = poly.transform;
Vector2 Local(Vector3 w) => polyT.InverseTransformPoint(w);
Transform boxT = box.transform;
Vector3 centerWorld = new Vector3((xMin + xMax) * 0.5f, (yMin + yMax) * 0.5f, 0f);
Vector3 centerLocal = boxT.InverseTransformPoint(centerWorld);
centerLocal.z = -depth;
// CCW 顺序BL → BR → TR → TL同 SyncConfinerFromVisibleBounds
poly.SetPath(0, new[]
{
Local(new Vector3(xMin, yMin, 0f)), // BL
Local(new Vector3(xMax, yMin, 0f)), // BR
Local(new Vector3(xMax, yMax, 0f)), // TR
Local(new Vector3(xMin, yMax, 0f)), // TL
});
EditorUtility.SetDirty(poly);
box.center = centerLocal;
box.size = new Vector3(xMax - xMin, yMax - yMin, 1f);
EditorUtility.SetDirty(box);
}
/// <summary>在 Scene 视图左上角绘制叠加信息面板(屏幕空间)。</summary>
@@ -663,7 +874,7 @@ namespace BaseGames.Editor
// ══ 工具方法 ══════════════════════════════════════════════════════════
/// <summary>
/// 获取用于透视计算的 FOV优先级 VCam → 全局 VCamA → Camera.main → 60f
/// 获取用于透视计算的 FOV优先级 VCam → CameraLensConfigSO → Camera.main → 60f
/// </summary>
private static float GetFOV(CameraArea area)
{
@@ -675,7 +886,7 @@ namespace BaseGames.Editor
if (area.LensConfig != null)
return area.LensConfig.fieldOfView;
// 3. Persistent 场景已加载时,实时读取全局 VCamA兆底
// 3. CameraStateController 存在时,通过 LensConfig 读取 FOV备用底线
#pragma warning disable UNT0023 // FindObjectOfType 在编辑器工具中可接受
var ctrl = Object.FindObjectOfType<CameraStateController>();
#pragma warning restore UNT0023

View File

@@ -17,8 +17,9 @@ namespace BaseGames.Editor
///
/// 新格式:
/// [新 CameraArea GO]CameraArea 组件_visibleBounds = 本地 Rect
/// ├─ AreaBoundaryPolygonCollider2DisTrigger=true,对应旧 Confiner
/// ─ TriggerZoneCameraTriggerZone + PolygonCollider2D对应旧 TriggerRegion
/// ├─ AreaBoundaryBoxCollider对应旧 Confiner
/// ─ TriggerZoneCameraTriggerZone + PolygonCollider2D对应旧 TriggerRegion
/// └─ VCam_xxxCinemachineCamera + 所有扩展组件,专属虚拟相机)
///
/// 菜单BaseGames → Camera → 相机区域迁移工具
/// </summary>
@@ -32,9 +33,10 @@ namespace BaseGames.Editor
}
// ── 设置字段 ──────────────────────────────────────────────────────────
private Transform _sourcesParent; // 旧 Zone_xxx 的父节点(通常名为 Zones
private Transform _targetParent; // 新对象放置位置(留空 = 与旧区域同级)
private CameraLensConfigSO _lensConfig; // 绑定到新 CameraArea._lensConfig
private Transform _sourcesParent; // 旧 Zone_xxx 的父节点(通常名为 Zones
private Transform _targetParent; // 新对象放置位置(留空 = 与旧区域同级)
private CameraLensConfigSO _lensConfig; // 绑定到新 CameraArea._lensConfig
private bool _createDedicatedVCam = true; // 为每个区域创建专属 CinemachineCamera
// ── 运行时状态 ────────────────────────────────────────────────────────
@@ -86,6 +88,12 @@ namespace BaseGames.Editor
new GUIContent("镜头配置 SO", "赋给所有新 CameraArea._lensConfig留空则不赋值"),
_lensConfig, typeof(CameraLensConfigSO), false);
_createDedicatedVCam = EditorGUILayout.Toggle(
new GUIContent("创建专属 VCam",
"为每个迁移区域创建子节点 CinemachineCamera含所有扩展组件\n" +
"并绑定到 CameraArea._dedicatedCamera。"),
_createDedicatedVCam);
EditorGUILayout.Space(8);
@@ -220,17 +228,24 @@ namespace BaseGames.Editor
foreach (Transform child in _sourcesParent)
{
Debug.Log($"[root]:{child.name}");
// 旧 Zone 的标识:子节点直属,且挂有 BoxCollider2D
var box = child.GetComponent<BoxCollider2D>();
if (box == null) continue;
var entry = new ZoneEntry { ZoneObj = child.gameObject, VisibleBox = box };
// 收集触发多边形顶点TriggerRegion 子节点的各个点对象)
Transform triggerRoot = FindChildContaining(child, "TriggerRegion");
if (triggerRoot != null)
foreach (Transform pt in triggerRoot)
// 收集触发多边形顶点——xxx_TriggerRegion 下每个子节点的世界坐标即一个顶点,
// 按子节点顺序依次连线围成多边形触发区域。
Transform triggerRoot = FindChildContaining(child, "Trigger");
if (triggerRoot != null){
Debug.Log($"[trigger]:{triggerRoot.name}");
foreach (Transform pt in triggerRoot){
Debug.Log($"{pt.name}");
entry.TriggerWorldPts.Add((Vector2)pt.position);
}
}
// 读取限位碰撞体Zone_xxx_Confiner 上的 Collider2D
Transform confinerT = FindChildContaining(child, "Confiner");
@@ -298,20 +313,20 @@ namespace BaseGames.Editor
soArea.FindProperty("_lensConfig").objectReferenceValue = _lensConfig;
soArea.ApplyModifiedProperties();
// ── 3. 创建 AreaBoundary限位多边形isTrigger = true──────────
// ── 3. 创建 AreaBoundary限位体积 BoxCollider─────────────────────────────
GameObject boundaryGO = new GameObject($"{zoneGO.name}_AreaBoundary");
Undo.RegisterCreatedObjectUndo(boundaryGO, "Migrate Camera Zone");
boundaryGO.transform.SetParent(areaGO.transform, worldPositionStays: false);
boundaryGO.transform.localPosition = Vector3.zero;
PolygonCollider2D confinerPoly = boundaryGO.AddComponent<PolygonCollider2D>();
confinerPoly.isTrigger = true;
confinerPoly.pathCount = 1;
confinerPoly.SetPath(0, BuildConfinerPath(entry, worldPos, localBounds));
BoxCollider confinerBox = boundaryGO.AddComponent<BoxCollider>();
confinerBox.isTrigger = true;
confinerBox.center = new Vector3(0f, 0f, -10f); // Z 占位符SyncConfiner 会立即重算
confinerBox.size = new Vector3(localBounds.width, localBounds.height, 1f);
// 绑定 _confinerCollider
soArea.Update();
soArea.FindProperty("_confinerCollider").objectReferenceValue = confinerPoly;
soArea.FindProperty("_confinerCollider").objectReferenceValue = confinerBox;
soArea.ApplyModifiedProperties();
EditorUtility.SetDirty(area);
@@ -330,9 +345,10 @@ namespace BaseGames.Editor
// AddComponent 会因 [RequireComponent] 自动添加 PolygonCollider2D
CameraTriggerZone triggerComp = triggerGO.AddComponent<CameraTriggerZone>();
PolygonCollider2D triggerPoly = triggerGO.GetComponent<PolygonCollider2D>();
// 将旧触发多边形路径(本地坐标,相对于新 CameraArea 世界位置)直接赋给 PolygonCollider2D
Vector2[] triggerPath = BuildTriggerPath(entry, worldPos, localBounds);
triggerPoly.isTrigger = true;
triggerPoly.pathCount = 1;
triggerPoly.SetPath(0, BuildTriggerPath(entry, worldPos, localBounds));
triggerPoly.SetPath(0, triggerPath);
// _targetArea → 指向刚创建的 CameraArea
var soTrigger = new SerializedObject(triggerComp);
@@ -340,7 +356,11 @@ namespace BaseGames.Editor
soTrigger.ApplyModifiedProperties();
EditorUtility.SetDirty(triggerComp);
// ── 5. 处理旧对象 ──────────────────────────────────────────────
// ── 5. 创建专属 VCam每区域独立相机─────────────────────────────
if (_createDedicatedVCam)
CameraAreaEditor.CreateDedicatedVCamForArea(area);
// ── 6. 处理旧对象 ──────────────────────────────────────────────
// 先记录原始激活状态,再对旧对象做处理,避免 SetActive(false) 后误读
bool wasActive = zoneGO.activeSelf;
@@ -356,8 +376,8 @@ namespace BaseGames.Editor
Debug.Log($"[迁移工具] {zoneGO.name} → {areaGO.name} " +
$"可视 {localBounds.width:F0}×{localBounds.height:F0} " +
$"触发 {triggerPoly.GetTotalPointCount()} pt " +
$"限位 {confinerPoly.GetTotalPointCount()} pt");
$"触发 PolygonCollider2D ({triggerPath.Length} 点) " +
$"限位 BoxCollider ({confinerBox.size.x:F1}×{confinerBox.size.y:F1})");
}
// ── 限位多边形路径 ────────────────────────────────────────────────────
@@ -405,14 +425,40 @@ namespace BaseGames.Editor
{
if (entry.TriggerWorldPts.Count >= 3)
{
var path = new Vector2[entry.TriggerWorldPts.Count];
for (int i = 0; i < path.Length; i++)
path[i] = entry.TriggerWorldPts[i] - (Vector2)areaWorldPos;
return path;
// 将世界坐标转换为 areaGO 本地坐标
var localPts = new Vector2[entry.TriggerWorldPts.Count];
for (int i = 0; i < localPts.Length; i++)
localPts[i] = entry.TriggerWorldPts[i] - (Vector2)areaWorldPos;
// 按照质心角度排序,确保顶点顺序能够围成合法多边形
return SortPointsByAngle(localPts);
}
return RectToPolygon(fallback);
}
/// <summary>
/// 将一组点按照围绕质心的角度(逆时针)排序,使其能够围成合法的简单多边形。
/// 适用于凸多边形及质心在多边形内部的凹多边形。
/// </summary>
private static Vector2[] SortPointsByAngle(Vector2[] points)
{
// 计算质心
Vector2 centroid = Vector2.zero;
foreach (var p in points)
centroid += p;
centroid /= points.Length;
// 按照相对质心的极角升序排列(逆时针)
var sorted = new System.Collections.Generic.List<Vector2>(points);
sorted.Sort((a, b) =>
{
float angleA = Mathf.Atan2(a.y - centroid.y, a.x - centroid.x);
float angleB = Mathf.Atan2(b.y - centroid.y, b.x - centroid.x);
return angleA.CompareTo(angleB);
});
return sorted.ToArray();
}
/// <summary>
/// 手动计算 BoxCollider2D 的世界 AABB不依赖 .boundsinactive 对象上 .bounds 无效)。
/// </summary>

View File

@@ -98,6 +98,9 @@ namespace BaseGames.Editor
if (GUILayout.Button("↺ 全部同步限位区域", EditorStyles.toolbarButton))
SyncAllConfiners();
if (GUILayout.Button("✔ 批量创建专属 VCam", EditorStyles.toolbarButton))
BatchCreateDedicatedVCams();
}
// ── 创建 CameraArea 面板 ───────────────────────────────────────
@@ -217,6 +220,40 @@ namespace BaseGames.Editor
Debug.Log($"[CameraAreaSetupTool] 已同步 {count} 个 CameraArea 的限位区域。");
}
/// <summary>
/// 为所有尚未配置专有 VCam 的 CameraArea 批量创建专有 CinemachineCamera。
/// 已有 _dedicatedCamera 的区域将跳过。
/// </summary>
private void BatchCreateDedicatedVCams()
{
if (_cameraAreas.Count == 0)
{
Debug.LogWarning("[CameraAreaSetupTool] 场景中无 CameraArea跳过批量创建。");
return;
}
int count = 0;
foreach (var area in _cameraAreas)
{
if (area == null) continue;
if (area.DedicatedCamera != null) continue; // 已有专有 VCam跳过
CameraAreaEditor.CreateDedicatedVCamForArea(area);
count++;
}
if (count > 0)
{
RescanScene();
Debug.Log($"[CameraAreaSetupTool] 已为 {count} 个 CameraArea 创建专有 VCam。");
}
else
{
Debug.Log("[CameraAreaSetupTool] 所有 CameraArea 均已有专有 VCam无需创建。");
}
}
// ── CameraStateController ──────────────────────────────────────────
private void DrawControllerSection()
@@ -240,8 +277,6 @@ namespace BaseGames.Editor
}
SerializedObject so = new SerializedObject(_controller);
DrawFieldCheck(so, "_vcamA", "全局 VCam A (CinemachineCamera)");
DrawFieldCheck(so, "_vcamB", "全局 VCam B (CinemachineCamera)");
DrawFieldCheck(so, "_brain", "CinemachineBrain");
DrawFieldCheck(so, "_onPlayerSpawned", "玩家生成事件 (EVT_PlayerSpawned) → VCam 自动绑 Follow");
DrawFieldCheck(so, "_impulseSource", "CinemachineImpulseSource", optional: true);
@@ -334,7 +369,8 @@ namespace BaseGames.Editor
bool confinerOk = so.FindProperty("_confinerCollider").objectReferenceValue != null;
var boundZones = FindTriggerZonesForArea(area);
bool hasZone = boundZones.Count > 0;
bool allOk = confinerOk && hasZone;
bool hasVCam = so.FindProperty("_dedicatedCamera").objectReferenceValue != null;
bool allOk = confinerOk && hasZone && hasVCam;
using (new EditorGUILayout.VerticalScope(_boxStyle))
{
@@ -353,6 +389,10 @@ namespace BaseGames.Editor
GUI.color = hasZone ? kOk : kError;
GUILayout.Label(hasZone ? $"[{boundZones.Count} 触发器]" : "[无触发器]",
EditorStyles.miniLabel, GUILayout.Width(74f));
GUI.color = hasVCam ? kOk : kError;
GUILayout.Label(hasVCam ? "[VCam ✔]" : "[VCam ✗]",
EditorStyles.miniLabel, GUILayout.Width(54f));
GUI.color = prevC;
if (GUILayout.Button("⊙", GUILayout.Width(24f)))
@@ -366,12 +406,61 @@ namespace BaseGames.Editor
EditorGUILayout.Space(3f);
// ── 绑定字段 ────────────────────────────────────────────────
DrawCheckRow("_confinerCollider可视边界 PolygonCollider2D", confinerOk);
DrawCheckRow("_dedicatedCamera专有 VCam可选",
so.FindProperty("_dedicatedCamera").objectReferenceValue != null, optional: true);
DrawCheckRow("_confinerCollider可视边界 BoxCollider", confinerOk);
// ── 专有 VCam 状态行(创建 / Ping 按鈕)────────────────────────
using (new EditorGUILayout.HorizontalScope())
{
Color prevC2 = GUI.color;
GUI.color = hasVCam ? kOk : kError;
GUILayout.Label(hasVCam ? "●" : "✗", GUILayout.Width(16f));
GUI.color = prevC2;
if (hasVCam)
{
var vcamObj = so.FindProperty("_dedicatedCamera").objectReferenceValue;
EditorGUILayout.LabelField($"_dedicatedCamera{vcamObj.name}",
GUILayout.ExpandWidth(true));
if (GUILayout.Button("⊙", GUILayout.Width(24f)))
EditorGUIUtility.PingObject(vcamObj);
if (GUILayout.Button("选中", GUILayout.Width(36f)))
Selection.activeObject = vcamObj;
}
else
{
EditorGUILayout.LabelField("_dedicatedCamera专有 VCam未创建",
GUILayout.ExpandWidth(true));
if (GUILayout.Button("创建专有 VCam", GUILayout.Width(90f), GUILayout.Height(18f)))
{
CameraAreaEditor.CreateDedicatedVCamForArea(area);
RescanScene();
}
}
}
DrawCheckRow("_blendProfile可选未设则用全局默认",
so.FindProperty("_blendProfile").objectReferenceValue != null, optional: true);
// ── VCam 扩展组件顺序检查 ────────────────────────────────────
// AsymmetricDamping/FallBias/FacingBias 必须在 CinemachineConfiner3D 之前;
// AxisLock 必须在之后。顺序错误会使相机逃出限位区域。
if (hasVCam)
{
var vcam = so.FindProperty("_dedicatedCamera").objectReferenceValue as CinemachineCamera;
string issue = CameraAreaEditor.CheckVCamExtensionOrderIssue(vcam);
if (issue != null)
{
EditorGUILayout.HelpBox(
$"VCam 扩展组件顺序错误!相机会逃出限位区域:\n{issue}",
MessageType.Error);
if (GUILayout.Button("⚙ 自动修正组件顺序", GUILayout.Height(22f)))
{
CameraAreaEditor.FixVCamExtensionOrder(vcam);
RescanScene();
}
}
}
// ── 触发区域列表 ─────────────────────────────────────────────
EditorGUILayout.Space(3f);
using (new EditorGUILayout.HorizontalScope())
@@ -412,16 +501,16 @@ namespace BaseGames.Editor
EditorGUILayout.Space(3f);
if (!confinerOk)
{
// 区分:有非 Trigger 的 PolygonCollider2D 可直接绑定 vs 完全没有 AreaBoundary
var existingBoundary = FindBoundaryPoly(area);
// 区分:有 BoxCollider 可直接绑定 vs 完全没有 AreaBoundary
var existingBoundary = FindBoundaryBox(area);
if (existingBoundary != null)
{
if (GUILayout.Button("修复:绑定子节点 PolygonCollider2D", GUILayout.Height(22f)))
if (GUILayout.Button("修复:绑定子节点 BoxCollider", GUILayout.Height(22f)))
FixConfinerBinding(area);
}
else
{
if (GUILayout.Button("创建 AreaBoundary限位多边形,默认 24 × 12", GUILayout.Height(22f)))
if (GUILayout.Button("创建 AreaBoundary限位体积,默认 24 × 12", GUILayout.Height(22f)))
{
CreateAreaBoundary(area);
RescanScene();
@@ -433,7 +522,7 @@ namespace BaseGames.Editor
Color helpC = GUI.color;
GUI.color = kMuted;
EditorGUILayout.LabelField(
"★ 可视边界:选中子节点的 PolygonCollider2D,在 Scene 视图中编辑顶点。",
"★ 限位体积:选中子节点的 BoxColliderInspector 中编辑 Center / Size。",
EditorStyles.miniLabel);
GUI.color = helpC;
}
@@ -557,34 +646,34 @@ namespace BaseGames.Editor
Debug.LogWarning("[CameraAreaSetupTool] _vcamA/_vcamB 均未绑定,无法赋值 Follow。请先在 Inspector 中绑定。");
}
/// <summary>将子节点中找到的第一个不含 CameraTriggerZone 的 PolygonCollider2D 绑定到 CameraArea._confinerCollider。</summary>
/// <summary>将子节点中找到的第一个不含 CameraTriggerZone 的 BoxCollider 绑定到 CameraArea._confinerCollider。</summary>
private static void FixConfinerBinding(CameraArea area)
{
PolygonCollider2D poly = FindBoundaryPoly(area)
?? area.GetComponentInChildren<PolygonCollider2D>(true);
if (poly == null)
BoxCollider box = FindBoundaryBox(area)
?? area.GetComponentInChildren<BoxCollider>(true);
if (box == null)
{
Debug.LogWarning($"[CameraAreaSetupTool] {area.name}:子节点中未找到 PolygonCollider2D。");
Debug.LogWarning($"[CameraAreaSetupTool] {area.name}:子节点中未找到 BoxCollider。");
return;
}
SerializedObject so = new SerializedObject(area);
so.FindProperty("_confinerCollider").objectReferenceValue = poly;
so.FindProperty("_confinerCollider").objectReferenceValue = box;
so.ApplyModifiedProperties();
Debug.Log($"[CameraAreaSetupTool] {area.name}_confinerCollider → {poly.gameObject.name}");
Debug.Log($"[CameraAreaSetupTool] {area.name}_confinerCollider → {box.gameObject.name}");
}
/// <summary>返回 area 子节点中第一个不含 CameraTriggerZone 的 PolygonCollider2D(即 AreaBoundary 限位体)。</summary>
private static PolygonCollider2D FindBoundaryPoly(CameraArea area)
/// <summary>返回 area 子节点中第一个不含 CameraTriggerZone 的 BoxCollider即 AreaBoundary 限位体)。</summary>
private static BoxCollider FindBoundaryBox(CameraArea area)
{
foreach (var p in area.GetComponentsInChildren<PolygonCollider2D>(true))
if (p.GetComponent<CameraTriggerZone>() == null) return p;
foreach (var b in area.GetComponentsInChildren<BoxCollider>(true))
if (b.GetComponent<CameraTriggerZone>() == null) return b;
return null;
}
/// <summary>
/// 为指定 CameraArea 创建 AreaBoundary 子节点(默认矩形限位多边形isTrigger = false)并绑定到 _confinerCollider。
/// 为指定 CameraArea 创建 AreaBoundary 子节点(默认 BoxCollider 限位体)并绑定到 _confinerCollider。
/// </summary>
private static void CreateAreaBoundary(CameraArea area)
{
@@ -602,27 +691,20 @@ namespace BaseGames.Editor
childGo.transform.localPosition = Vector3.zero;
}
PolygonCollider2D poly = childGo.GetComponent<PolygonCollider2D>()
?? childGo.AddComponent<PolygonCollider2D>();
poly.isTrigger = true; // 限位多边形,仅作为相机约束边界,不产生物理碰撞
poly.pathCount = 1;
poly.SetPath(0, new Vector2[]
{
new Vector2(-12f, -6f),
new Vector2(-12f, 6f),
new Vector2( 12f, 6f),
new Vector2( 12f, -6f),
});
BoxCollider box = childGo.GetComponent<BoxCollider>()
?? childGo.AddComponent<BoxCollider>();
box.center = new Vector3(0f, 0f, -10f); // Z 占位符,绑定 LensConfig 后点击「同步限位区域」更新
box.size = new Vector3(24f, 12f, 1f); // 默认 24 × 12 占位符
EditorUtility.SetDirty(childGo);
SerializedObject so = new SerializedObject(area);
so.Update();
so.FindProperty("_confinerCollider").objectReferenceValue = poly;
so.FindProperty("_confinerCollider").objectReferenceValue = box;
so.ApplyModifiedProperties();
EditorUtility.SetDirty(area);
EditorGUIUtility.PingObject(childGo);
Debug.Log($"[CameraAreaSetupTool] 已为 {area.name} 创建 AreaBoundary矩形 24 × 12。");
Debug.Log($"[CameraAreaSetupTool] 已为 {area.name} 创建 AreaBoundaryBoxCollider 默认 24 × 12。");
}
/// <summary>返回所有以此 area 为激活目标的 CameraTriggerZone。</summary>
@@ -642,18 +724,10 @@ namespace BaseGames.Editor
/// <summary>为指定 CameraArea 创建配对的 CameraTriggerZone自动匹配 Confiner 范围。</summary>
private static void CreateTriggerZoneForArea(CameraArea area)
{
// 用 PolygonCollider2D 包围盒作为放置中心和尺寸;没有则退回到 area 自身位置
Vector3 center = area.transform.position;
Vector2 size = new Vector2(4f, 4f);
var poly = area.GetComponentInChildren<PolygonCollider2D>(true);
if (poly != null)
{
Bounds b = poly.bounds;
center = b.center;
center.z = area.transform.position.z;
size = new Vector2(b.size.x, b.size.y);
}
// 用 VisibleBounds 作为放置中心和初始多边形范围
Rect visible = area.VisibleBounds;
Vector3 center = new Vector3(visible.center.x, visible.center.y, area.transform.position.z);
Vector2 half = visible.size * 0.5f;
var go = new GameObject($"{area.gameObject.name}_TriggerZone");
Undo.RegisterCreatedObjectUndo(go, "Create CameraTriggerZone");
@@ -661,20 +735,21 @@ namespace BaseGames.Editor
go.transform.SetParent(area.transform);
go.transform.position = center;
var col = go.AddComponent<PolygonCollider2D>();
// [RequireComponent] 会自动附加 PolygonCollider2D;先 AddComponent<CameraTriggerZone>
// 再通过 GetComponent 引用,避免顺序依赖问题
var zone = go.AddComponent<CameraTriggerZone>();
var col = go.GetComponent<PolygonCollider2D>();
col.isTrigger = true;
float hw = size.x * 0.5f;
float hh = size.y * 0.5f;
// 以 VisibleBounds 矩形四角为默认路径(可在 Inspector 中进一步编辑顶点)
col.SetPath(0, new Vector2[]
{
new Vector2(-hw, -hh),
new Vector2(-hw, hh),
new Vector2( hw, hh),
new Vector2( hw, -hh),
new Vector2(-half.x, -half.y),
new Vector2(-half.x, half.y),
new Vector2( half.x, half.y),
new Vector2( half.x, -half.y),
});
var zone = go.AddComponent<CameraTriggerZone>();
var so = new SerializedObject(zone);
var so = new SerializedObject(zone);
so.Update();
so.FindProperty("_targetArea").objectReferenceValue = area;
so.ApplyModifiedProperties();