镜头转换菜单

说明

实现一个鼠标悬停主界面菜单时,自动变更转换镜头的效果。

  • 鼠标悬停时转换镜头
  • 空闲时进入游览模式

效果预览

使用Cinemachine的轨道相机,可以实现游览模式。通过修改虚拟相机的权重就可以自动转换当前使用的虚拟相机;Cinemachine系统会自动进行过渡切换。

源码实现

鼠标悬停菜单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;

public class Move : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
private TextMeshProUGUI text;

private void Awake()
{
text = GetComponent<TextMeshProUGUI>();
}

public int index;

public void OnPointerEnter(PointerEventData eventData)
{
Debug.Log($"进入{index}");

text.color = new Color(0.4f, 0.4f, 0.7f);

CameraController.Instance.MoveToStart(index);
}

public void OnPointerExit(PointerEventData eventData)
{
text.color = Color.white;
}
}
摄像机控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
using System;
using UnityEngine;
using Cinemachine;

public class CameraController : MonoBehaviour
{
public static CameraController Instance;

[Header("Virtual Cameras")]
[SerializeField] private CinemachineVirtualCamera[] virtualCameras;

[Header("Settings")]
[SerializeField] private float transitionTime = 1f;

[Header("Idle Settings")]
[SerializeField] private float idleTimeBeforeLoop = 30f;
[SerializeField] private bool enableIdleDetection = true;

private CinemachineBrain cinemachineBrain;

[SerializeField]
private CinemachineVirtualCamera loopVirtualCamera;

private float lastActivityTime;
private bool isIdleModeActive;
private int previousActiveCameraIndex = -1;

private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}

Instance = this;

cinemachineBrain = GetComponent<CinemachineBrain>() ?? Camera.main.GetComponent<CinemachineBrain>();

SetTransitionTime(1f);

if (cinemachineBrain == null)
{
Debug.LogError("CinemachineBrain not found! Please add CinemachineVirtualCamera to your scene.");
}
}

private void Start()
{
lastActivityTime = Time.time;
isIdleModeActive = false;
}

private void Update()
{
if (!enableIdleDetection || loopVirtualCamera == null)
return;

if (Input.anyKeyDown || Input.GetAxis("Mouse X") != 0 || Input.GetAxis("Mouse Y") != 0)
{
lastActivityTime = Time.time;

if (isIdleModeActive)
{
ExitIdleMode();
}
}

if (!isIdleModeActive && Time.time - lastActivityTime >= idleTimeBeforeLoop)
{
EnterIdleMode();
}
}

public void MoveToStart(int index)
{
if (virtualCameras == null || virtualCameras.Length == 0)
{
Debug.LogWarning("No virtual cameras assigned!");
return;
}

if (index < 0 || index >= virtualCameras.Length)
{
Debug.LogError($"Invalid camera index: {index}. Valid range: 0 - {virtualCameras.Length - 1}");
return;
}

if (virtualCameras[index] != null)
{
previousActiveCameraIndex = index;
virtualCameras[index].Priority = 100;

for (int i = 0; i < virtualCameras.Length; i++)
{
if (i != index && virtualCameras[i] != null)
{
virtualCameras[i].Priority = 0;
}
}

lastActivityTime = Time.time;
}
else
{
Debug.LogError($"Virtual camera at index {index} is null!");
}
}

public void SetTransitionTime(float time)
{
transitionTime = time;
if (cinemachineBrain != null)
{
cinemachineBrain.m_DefaultBlend.m_Time = time;
}
}

private void EnterIdleMode()
{
isIdleModeActive = true;

for (int i = 0; i < virtualCameras.Length; i++)
{
if (virtualCameras[i] != null)
{
virtualCameras[i].Priority = 0;
}
}

loopVirtualCamera.Priority = 100;
}

private void ExitIdleMode()
{
isIdleModeActive = false;

loopVirtualCamera.Priority = 0;

previousActiveCameraIndex = -1;
}

public void ResetIdleTimer()
{
lastActivityTime = Time.time;
}
}