304 lines
13 KiB
C#
304 lines
13 KiB
C#
using Base;
|
||
using Map;
|
||
using UnityEngine;
|
||
|
||
namespace CameraControl
|
||
{
|
||
/// <summary>
|
||
/// 控制游戏摄像机的移动、缩放和维度切换。
|
||
/// 继承自 MonoSingleton 以确保场景中只有一个实例,并实现 ITick 和 ITickUI 接口以在游戏循环和UI循环中更新。
|
||
/// </summary>
|
||
public class CameraControl : Utils.MonoSingleton<CameraControl>, ITick, ITickUI
|
||
{
|
||
// 摄像机移动相关变量
|
||
[SerializeField] private float _zoomSpeed = 5f; // 摄像机缩放速度
|
||
[SerializeField] private float _minZoom = 2f; // 摄像机最小缩放级别(正交摄像机的 orthographicSize)
|
||
[SerializeField] private float _maxZoom = 20f; // 摄像机最大缩放级别
|
||
[SerializeField] private float _focusLerpSpeed = 5f; // 摄像机跟随目标时的平滑插值速度
|
||
|
||
private Vector3 _dragOrigin; // 拖拽操作的起始世界坐标
|
||
private bool _isDragging; // 标记摄像机是否正在被拖拽
|
||
|
||
private Camera _camera; // 当前场景中的主摄像机引用
|
||
|
||
private int dimensionId; // 当前摄像机控制器关注的维度索引
|
||
private string[] dimensionList; // 维度名称列表
|
||
|
||
/// <summary>
|
||
/// 当脚本实例被销毁时调用。
|
||
/// 用于取消订阅 Program.Instance 的事件,防止内存泄漏。
|
||
/// </summary>
|
||
private void OnDestroy()
|
||
{
|
||
// 在OnDestroy时进行Program.Instance检查是合理的,因为Program实例可能已被销毁。
|
||
if (Program.Instance != null)
|
||
{
|
||
Program.Instance.OnFocusedDimensionChanged -= Init;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 脚本实例在第一次帧更新之前被启用时调用。
|
||
/// 用于订阅 Program.Instance 的事件,并进行初始设置。
|
||
/// </summary>
|
||
private void Start()
|
||
{
|
||
// 在Start时进行Program.Instance检查是合理的,防止CameraControl比Program实例更早启动。
|
||
if (Program.Instance != null)
|
||
{
|
||
Program.Instance.OnFocusedDimensionChanged += Init; // 订阅聚焦维度改变事件
|
||
Init(); // 首次调用初始化,根据当前聚焦维度设置摄像机状态
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError("CameraControl 的 Start 方法中 Program.Instance 为空。请检查 Program 单例的初始化顺序。摄像机控制功能将无法初始化。");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据指定的维度对象初始化摄像机控制器。
|
||
/// 主要用于在聚焦维度改变时更新摄像机状态和内部的维度ID。
|
||
/// </summary>
|
||
/// <param name="obj">当前聚焦的维度对象。</param>
|
||
private void Init(Dimension obj)
|
||
{
|
||
// 修改点 1: 统一 Unity 对象 `null` 检查,移除冗余 `_camera == null`
|
||
// 确保相机引用有效,如果为空则尝试获取主相机
|
||
if (!_camera)
|
||
{
|
||
_camera = Camera.main;
|
||
if (!_camera) // 再次检查获取是否成功
|
||
{
|
||
_camera = FindFirstObjectByType<Camera>();
|
||
}
|
||
}
|
||
|
||
if (!_camera) // 如果到此为止仍未获取到相机,记录错误并返回
|
||
{
|
||
Debug.LogError("场景中未找到摄像机!CameraControl 功能将无法完全初始化。");
|
||
// 如果没有相机,后续依赖相机的逻辑都无法执行,直接返回。
|
||
// dimensionList 的获取如果逻辑上不依赖相机,可以放在此处之外。
|
||
// 但当前上下文中,Init的主要目的是初始化相机视角等,无相机则无意义。
|
||
return;
|
||
}
|
||
|
||
// 修改点 2: 移除对 `Program.Instance` 的冗余 `null` 检查
|
||
// 修改点 3: `Init` 方法中 `dimensionList` 的获取位置
|
||
// 总是获取最新的维度列表,因为外部维度状态可能会变化
|
||
dimensionList = Program.Instance.Dimensions;
|
||
|
||
// 处理 obj 为 null 的情况
|
||
if (!obj)
|
||
{
|
||
Debug.LogWarning("Init 方法在聚焦维度为空的情况下被调用。维度列表已更新,但摄像机状态未根据特定维度设置。");
|
||
// 此时 dimensionId 仍然是默认值或上次设置的值,不进行 cameraPosition 的设置。
|
||
return;
|
||
}
|
||
|
||
// 根据当前聚焦维度同步 CameraControl 内部的 dimensionId
|
||
int focusedIndex = System.Array.IndexOf(dimensionList, obj.name);
|
||
if (focusedIndex != -1)
|
||
{
|
||
dimensionId = focusedIndex;
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"聚焦维度 '{obj.name}' 未在维度列表中找到。回退到 ID 0。");
|
||
dimensionId = 0; // 找不到时,回退到第一个维度,避免数组越界
|
||
}
|
||
|
||
// 设置摄像机位置到当前聚焦维度的位置 (移除 Program.Instance 的 null 检查)
|
||
_camera.transform.position = Program.Instance.FocusedDimension.cameraPosition;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 调用带参数的 Init 方法,使用 Program.Instance 中当前聚焦的维度。
|
||
/// </summary>
|
||
private void Init()
|
||
{
|
||
// 移除 Program.Instance 的 null 检查
|
||
Init(Program.Instance.FocusedDimension);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 切换到下一个维度。
|
||
/// 会保存当前摄像机位置到当前维度,然后加载下一个维度的摄像机位置。
|
||
/// </summary>
|
||
public void NextDimension()
|
||
{
|
||
// 修改点 1: 统一 Unity 对象 `null` 检查
|
||
if (!_camera)
|
||
{
|
||
Debug.LogWarning("摄像机引用为空,无法切换维度。");
|
||
return;
|
||
}
|
||
if (dimensionList == null || dimensionList.Length == 0)
|
||
{
|
||
Debug.LogWarning("维度列表为空。");
|
||
return;
|
||
}
|
||
|
||
// 1. 保存当前摄像机的实际位置到当前维度 (移除 Program.Instance 的 null 检查)
|
||
if (dimensionId >= 0 && dimensionId < dimensionList.Length)
|
||
{
|
||
var currentDimension = Program.Instance.GetDimension(dimensionList[dimensionId]);
|
||
if (currentDimension != null) // currentDimension 可能是普通 C# 对象,所以此处 null 检查是必要的
|
||
{
|
||
currentDimension.cameraPosition = _camera.transform.position;
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"无法找到 ID 为 {dimensionId} ({dimensionList[dimensionId]}) 的维度对象,无法保存摄像机位置。");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"当前维度ID ({dimensionId}) 超出范围,无法保存摄像机位置 (维度列表长度: {dimensionList.Length})。");
|
||
}
|
||
|
||
// 2. 更新 dimensionId,形成循环切换
|
||
dimensionId = (dimensionId + 1) % dimensionList.Length;
|
||
|
||
// 3. 更新聚焦维度,摄像机位置的更新将由事件触发的 Init 方法处理
|
||
SetCameraPositionForDimension(dimensionId);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置 Program.Instance 中聚焦的维度。
|
||
/// 摄像机位置的实际更新将通过 Program.Instance.OnFocusedDimensionChanged 事件在 Init(Dimension obj) 方法中处理。
|
||
/// </summary>
|
||
/// <param name="id">要设置的维度ID。</param>
|
||
private void SetCameraPositionForDimension(int id)
|
||
{
|
||
// 修改点 1: 统一 Unity 对象 `null` 检查
|
||
if (!_camera)
|
||
{
|
||
Debug.LogWarning("摄像机引用为空,无法设置摄像机位置。");
|
||
return;
|
||
}
|
||
if (dimensionList == null || id < 0 || id >= dimensionList.Length)
|
||
{
|
||
Debug.LogWarning($"维度ID {id} 超出范围或维度列表为空。");
|
||
return;
|
||
}
|
||
// 移除 Program.Instance 的 null 检查
|
||
Program.Instance.SetFocusedDimension(dimensionList[id]);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在游戏循环中每帧调用,用于处理摄像机跟随聚焦实体和维度切换。
|
||
/// </summary>
|
||
public void Tick()
|
||
{
|
||
// 修改点 1: 统一 Unity 对象 `null` 检查
|
||
if (!_camera) return; // 确保相机存在
|
||
|
||
// 当没有拖拽且存在聚焦实体时,摄像机跟随聚焦实体 (移除 Program.Instance 的 null 检查)
|
||
if (!_isDragging && Program.Instance.FocusedEntity)
|
||
{
|
||
var targetPosition = new Vector3(
|
||
Program.Instance.FocusedEntity.Position.x,
|
||
Program.Instance.FocusedEntity.Position.y,
|
||
_camera.transform.position.z);
|
||
|
||
// 使用 deltaTime 进行平滑的摄像机跟随
|
||
_camera.transform.position = Vector3.Lerp(
|
||
_camera.transform.position,
|
||
targetPosition,
|
||
Time.deltaTime * _focusLerpSpeed);
|
||
}
|
||
|
||
// 按下 Tab 键时切换到下一个维度
|
||
if (Input.GetKeyDown(KeyCode.Tab))
|
||
{
|
||
NextDimension();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在UI更新循环中每帧调用,用于处理用户界面的摄像机操作,如鼠标拖拽和缩放。
|
||
/// </summary>
|
||
public void TickUI()
|
||
{
|
||
// 修改点 1: 统一 Unity 对象 `null` 检查
|
||
if (!_camera) // 确保相机存在
|
||
return;
|
||
|
||
HandleMiddleMouseDrag(); // 处理鼠标中键拖拽
|
||
HandleMouseZoom(); // 处理鼠标滚轮缩放
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置摄像机的世界坐标位置。
|
||
/// </summary>
|
||
/// <param name="position">要设置的摄像机位置。</param>
|
||
public void SetPosition(Vector3 position)
|
||
{
|
||
// 修改点 1: 统一 Unity 对象 `null` 检查
|
||
if (_camera) // if (!_camera) 比 if (_camera == null) 更优雅
|
||
_camera.transform.position = position;
|
||
else
|
||
Debug.LogWarning("摄像机引用为空,无法设置位置。");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理鼠标中键拖拽摄像机的逻辑。
|
||
/// </summary>
|
||
private void HandleMiddleMouseDrag()
|
||
{
|
||
// 修改点 1: 统一 Unity 对象 `null` 检查
|
||
if (!_camera) return; // 确保相机存在
|
||
|
||
// 开始拖拽:检测鼠标中键按下
|
||
if (Input.GetMouseButtonDown(2)) // 鼠标中键
|
||
{
|
||
_dragOrigin = _camera.ScreenToWorldPoint(Input.mousePosition);
|
||
_isDragging = true;
|
||
// 如果有聚焦实体,则在开始拖拽时取消聚焦,暂停跟随 (移除 Program.Instance 的 null 检查)
|
||
if (Program.Instance.FocusedEntity)
|
||
{
|
||
Program.Instance.SetFocusedEntity(null);
|
||
}
|
||
}
|
||
|
||
// 拖拽中:根据鼠标移动更新摄像机位置
|
||
if (Input.GetMouseButton(2) && _isDragging)
|
||
{
|
||
var difference = _dragOrigin - _camera.ScreenToWorldPoint(Input.mousePosition);
|
||
_camera.transform.position += difference;
|
||
}
|
||
|
||
// 结束拖拽:检测鼠标中键抬起
|
||
if (Input.GetMouseButtonUp(2))
|
||
{
|
||
_isDragging = false;
|
||
// 拖拽结束后,当前代码不会自动重新聚焦实体。如果需要此行为,应在此处添加逻辑。
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理鼠标滚轮缩放摄像机的逻辑。
|
||
/// </summary>
|
||
private void HandleMouseZoom()
|
||
{
|
||
// 修改点 1: 统一 Unity 对象 `null` 检查
|
||
if (!_camera) return; // 确保相机存在
|
||
|
||
var scroll = Input.GetAxis("Mouse ScrollWheel");
|
||
if (scroll == 0) return; // 没有滚轮滚动,则返回
|
||
|
||
// 根据滚轮输入调整正交摄像机的尺寸,并限制在最小和最大缩放之间
|
||
var newSize = _camera.orthographicSize - scroll * _zoomSpeed;
|
||
_camera.orthographicSize = Mathf.Clamp(newSize, _minZoom, _maxZoom);
|
||
}
|
||
|
||
/// <summary>
|
||
/// MonoSingleton 的 OnStart 方法,可在单例首次创建时添加额外初始化逻辑。
|
||
/// </summary>
|
||
protected override void OnStart()
|
||
{
|
||
// 当前为空,是合理的。
|
||
}
|
||
}
|
||
}
|