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