Files
Gen_Hack-and-Slash-Roguelite/Client/Assets/Scripts/CameraControl/CameraControl.cs

304 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using 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()
{
// 当前为空,是合理的。
}
}
}