using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;
///
/// 博物馆场景的视频控制器。
///
/// 播放过程:
/// 1. 视频平面初始隐藏,看起来完全透明。
/// 2. 点击播放后,平面从中间向左右展开。
/// 3. 完全展开后,视频才真正开始播放。
///
/// 停止过程:
/// 1. 先暂停视频。
/// 2. 平面像关闭窗帘一样向中间收拢。
/// 3. 完全关闭后停止视频并隐藏平面。
///
/// 这里使用 Transform 的横向缩放实现窗帘效果,
/// 不需要修改 Unlit 材质,也不需要编写自定义 Shader。
///
[DisallowMultipleComponent]
public class VideoController : MonoBehaviour
{
[Header("场景引用")]
public VideoPlayer videoPlayer;
public Button controlButton;
public Sprite playIcon;
public Sprite pauseIcon;
[SerializeField, Tooltip("VideoPlayer 对象上的 Mesh Renderer;留空时自动查找。")]
private Renderer videoScreenRenderer;
[Header("窗帘动画")]
[SerializeField, Range(0.1f, 2f), Tooltip("窗帘展开和关闭所需的时间。")]
private float curtainDuration = 0.6f;
[Header("手动缩放")]
[SerializeField, Range(0.5f, 1f)]
private float minZoom = 0.8f;
[SerializeField, Range(1f, 2f)]
private float maxZoom = 1.35f;
[SerializeField, Range(0.05f, 0.5f)]
private float zoomStep = 0.1f;
// VideoPlayer.Prepare 是异步操作,因此需要记录准备状态。
private bool isPreparing;
private bool playAfterPrepare;
private bool isPaused;
private bool curtainIsOpen;
private bool curtainIsMoving;
private Image buttonImage;
private Transform videoTransform;
private Vector3 originalVideoScale;
private float zoom = 1f;
private Coroutine curtainCoroutine;
public bool IsPlaying => videoPlayer != null && videoPlayer.isPlaying;
public float CurrentZoom => zoom;
private void Awake()
{
FindSceneObjects();
if (videoPlayer == null || controlButton == null || videoScreenRenderer == null)
{
Debug.LogError("VideoController:场景引用不完整。", this);
enabled = false;
return;
}
buttonImage = controlButton.image;
videoTransform = videoScreenRenderer.transform;
originalVideoScale = videoTransform.localScale;
ConfigureVideoPlayer();
// 初始时关闭并隐藏视频平面。
SetCurtainImmediately(false);
SetButtonIcon(false);
}
private void OnEnable()
{
if (videoPlayer != null)
{
videoPlayer.prepareCompleted += OnVideoPrepared;
videoPlayer.loopPointReached += OnVideoFinished;
videoPlayer.errorReceived += OnVideoError;
}
if (controlButton != null)
{
controlButton.onClick.AddListener(TogglePlayPause);
}
}
private void Start()
{
// 防止场景中误勾 Play On Awake 后自动播放。
videoPlayer.Stop();
SetCurtainImmediately(false);
SetButtonIcon(false);
// 提前准备第一帧,点击时可以立即展开窗帘。
PrepareVideo();
}
private void OnDisable()
{
if (videoPlayer != null)
{
videoPlayer.prepareCompleted -= OnVideoPrepared;
videoPlayer.loopPointReached -= OnVideoFinished;
videoPlayer.errorReceived -= OnVideoError;
}
if (controlButton != null)
{
controlButton.onClick.RemoveListener(TogglePlayPause);
}
}
private void OnApplicationPause(bool paused)
{
// 用户摘下头显或应用进入后台时暂停视频。
if (paused && videoPlayer != null && videoPlayer.isPlaying)
{
PauseVideo();
}
}
///
/// 中央按钮入口:播放或等待播放时执行停止,否则开始播放。
///
public void TogglePlayPause()
{
if (videoPlayer.isPlaying || playAfterPrepare || curtainIsMoving)
{
StopVideo();
}
else
{
PlayVideo();
}
}
/// 展开窗帘,完全展开后开始播放。
public void PlayVideo()
{
// 暂停后继续播放时,窗帘本来就是打开的,不需要重复展开。
if (isPaused && curtainIsOpen)
{
isPaused = false;
SetButtonIcon(true);
videoPlayer.Play();
return;
}
if (videoPlayer.isPrepared)
{
StartOpeningCurtain();
}
else
{
playAfterPrepare = true;
SetButtonIcon(true);
PrepareVideo();
}
}
/// 暂停视频,但保持窗帘打开。
public void PauseVideo()
{
playAfterPrepare = false;
isPaused = true;
videoPlayer.Pause();
SetButtonIcon(false);
}
/// 暂停视频,关闭窗帘,最后停止并隐藏画面。
public void StopVideo()
{
playAfterPrepare = false;
isPreparing = false;
isPaused = false;
if (videoPlayer.isPlaying)
{
videoPlayer.Pause();
}
SetButtonIcon(false);
// 准备期间取消播放时,画面还没有打开,可以直接停止。
if (!curtainIsOpen && !curtainIsMoving)
{
videoPlayer.Stop();
SetCurtainImmediately(false);
return;
}
StartCurtainAnimation(false);
}
/// 关闭后重新从头播放。
public void RestartVideo()
{
if (videoPlayer.isPrepared && videoPlayer.canSetTime)
{
videoPlayer.time = 0d;
}
isPaused = false;
PlayVideo();
}
private void PrepareVideo()
{
if (videoPlayer.isPrepared)
{
if (playAfterPrepare)
{
StartOpeningCurtain();
}
return;
}
if (!isPreparing)
{
isPreparing = true;
videoPlayer.Prepare();
}
}
private void OnVideoPrepared(VideoPlayer source)
{
isPreparing = false;
if (playAfterPrepare)
{
StartOpeningCurtain();
}
}
private void StartOpeningCurtain()
{
playAfterPrepare = false;
// 视频播完后再次播放,需要先回到开头。
if (!videoPlayer.isLooping &&
videoPlayer.canSetTime &&
videoPlayer.length > 0d &&
videoPlayer.time >= videoPlayer.length - 0.05d)
{
videoPlayer.time = 0d;
}
SetButtonIcon(true);
StartCurtainAnimation(true);
}
private void OnVideoFinished(VideoPlayer source)
{
if (!source.isLooping)
{
// 播放结束后自动关上窗帘。
StopVideo();
}
}
private void OnVideoError(VideoPlayer source, string message)
{
isPreparing = false;
playAfterPrepare = false;
isPaused = false;
source.Stop();
SetCurtainImmediately(false);
SetButtonIcon(false);
Debug.LogError("VideoController:视频播放失败:" + message, this);
}
#region 手动缩放
public void ZoomIn()
{
SetZoom(zoom + zoomStep);
}
public void ZoomOut()
{
SetZoom(zoom - zoomStep);
}
public void ResetZoom()
{
SetZoom(1f);
}
public void SetZoom(float newZoom)
{
zoom = Mathf.Clamp(newZoom, minZoom, maxZoom);
// 窗帘关闭时横向宽度必须保持为 0。
videoTransform.localScale = GetCurtainScale(curtainIsOpen);
}
#endregion
#region 窗帘动画
private void StartCurtainAnimation(bool open)
{
if (curtainCoroutine != null)
{
StopCoroutine(curtainCoroutine);
}
curtainCoroutine = StartCoroutine(AnimateCurtain(open));
}
private IEnumerator AnimateCurtain(bool open)
{
curtainIsMoving = true;
controlButton.interactable = false;
// 展开前先显示 Renderer;关闭完成后再隐藏。
if (open)
{
videoScreenRenderer.enabled = true;
}
Vector3 startScale = videoTransform.localScale;
Vector3 targetScale = GetCurtainScale(open);
float elapsed = 0f;
while (elapsed < curtainDuration)
{
elapsed += Time.unscaledDeltaTime;
float progress = Mathf.Clamp01(elapsed / curtainDuration);
// SmoothStep 让窗帘起步和停止更加柔和。
progress = Mathf.SmoothStep(0f, 1f, progress);
videoTransform.localScale =
Vector3.Lerp(startScale, targetScale, progress);
yield return null;
}
videoTransform.localScale = targetScale;
curtainIsOpen = open;
curtainIsMoving = false;
curtainCoroutine = null;
controlButton.interactable = true;
if (open)
{
// 关键点:窗帘完全展开后才开始播放。
videoPlayer.Play();
}
else
{
// 完全关闭后停止并隐藏,画面看起来就是透明的。
videoPlayer.Stop();
videoScreenRenderer.enabled = false;
}
}
private void SetCurtainImmediately(bool open)
{
if (curtainCoroutine != null)
{
StopCoroutine(curtainCoroutine);
curtainCoroutine = null;
}
curtainIsOpen = open;
curtainIsMoving = false;
videoTransform.localScale = GetCurtainScale(open);
videoScreenRenderer.enabled = open;
if (controlButton != null)
{
controlButton.interactable = true;
}
}
private Vector3 GetCurtainScale(bool open)
{
// 只把 X 轴压缩到 0,因此效果是从中间向左右展开或关闭。
return new Vector3(
open ? originalVideoScale.x * zoom : 0f,
originalVideoScale.y * zoom,
originalVideoScale.z * zoom);
}
#endregion
#region 初始化
private void FindSceneObjects()
{
// VideoControllerUI 和 VideoPlayer 是两个独立根对象。
if (videoPlayer == null)
{
videoPlayer = FindObjectOfType();
}
if (controlButton == null)
{
controlButton = GetComponentInChildren