This commit is contained in:
2026-07-06 02:10:51 +08:00
parent deaffb85bf
commit b538c5ab93
7 changed files with 822 additions and 1 deletions
+251
View File
@@ -0,0 +1,251 @@
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;
using System.Collections;
namespace GameFramework
{
/// <summary>
/// 剧情 PV 播放器 —— 点击 StoryReplayButton 后弹出全屏视频覆盖层播放 PV。
/// 播放完毕自动关闭,也可点击"跳过"按钮立即关闭。
///
/// 搭建:
/// 1. 在 MainMenu Canvas 下创建 VideoOverlay(初始 SetActive(false)
/// 2. 子物体:BG (全屏黑色 Image)、VideoDisplay (RawImage)、SkipButton (Button)
/// 3. 挂 VideoPlayer 组件,VideoClip 赋值为 pv.mp4
/// 4. 挂本脚本,Inspector 中关联各引用
/// </summary>
public class StoryPVPlayer : MonoBehaviour
{
[Header("覆盖层")]
[Tooltip("视频覆盖层根节点(初始隐藏)")]
[SerializeField] private GameObject videoOverlay;
[Header("视频显示")]
[Tooltip("全屏 RawImage 用于显示视频画面")]
[SerializeField] private RawImage videoDisplay;
[Header("跳过按钮")]
[Tooltip("右下角跳过按钮")]
[SerializeField] private Button skipButton;
[Header("视频播放器")]
[Tooltip("VideoPlayer 组件(挂在 VideoOverlay 或自身上)")]
[SerializeField] private VideoPlayer videoPlayer;
private RenderTexture _renderTexture;
private bool _isPlaying = false;
/// <summary>
/// 懒加载:确保所有引用都已初始化。
/// 使用 GetComponentsInChildren(true) 递归搜索(包括 inactive 子物体)。
/// </summary>
private void EnsureReferences()
{
// videoOverlay:默认就是自身(StoryPVPlayer 挂在 VideoOverlay 上)
if (videoOverlay == null)
{
// 递归搜索子物体中名为 "VideoOverlay" 的 GameObject
var transforms = GetComponentsInChildren<Transform>(true);
foreach (var t in transforms)
{
if (t.name == "VideoOverlay")
{
videoOverlay = t.gameObject;
break;
}
}
// 如果没找到,说明本脚本就挂在 VideoOverlay 上
if (videoOverlay == null)
videoOverlay = gameObject;
}
// 查找 RawImage(递归搜索 videoOverlay 的子物体,包括 inactive
if (videoDisplay == null && videoOverlay != null)
{
var displays = videoOverlay.GetComponentsInChildren<RawImage>(true);
if (displays.Length > 0)
videoDisplay = displays[0];
}
// 查找 SkipButton(递归搜索,按名称匹配避免找到其他按钮)
if (skipButton == null && videoOverlay != null)
{
var buttons = videoOverlay.GetComponentsInChildren<Button>(true);
foreach (var b in buttons)
{
if (b.name == "SkipButton")
{
skipButton = b;
skipButton.onClick.AddListener(Stop);
break;
}
}
}
// 查找 VideoPlayer:先查自身,再查 videoOverlay
if (videoPlayer == null)
videoPlayer = GetComponent<VideoPlayer>();
if (videoPlayer == null && videoOverlay != null)
videoPlayer = videoOverlay.GetComponent<VideoPlayer>();
// 订阅播放完成事件(只订阅一次)
if (videoPlayer != null)
videoPlayer.loopPointReached -= OnVideoFinished;
if (videoPlayer != null)
videoPlayer.loopPointReached += OnVideoFinished;
}
/// <summary>
/// 播放 PV:先激活覆盖层 → 初始化引用 → 创建 RenderTexture → 等一帧 → 开始播放。
/// </summary>
public void Play()
{
Debug.Log($"[StoryPVPlayer] Play() called, _isPlaying={_isPlaying}, gameObject.activeSelf={gameObject.activeSelf}");
if (_isPlaying)
{
Debug.Log("[StoryPVPlayer] 已在播放中,忽略本次点击");
return;
}
// 关键:本脚本所在的 GameObject 可能初始为 inactive,必须先激活才能启动协程。
// 这里先把 gameObject.active 设为 true,让 MonoBehaviour 能正常工作。
gameObject.SetActive(true);
// 确保所有引用已初始化(可能在 Inspector 中未赋值,通过递归搜索查找)
EnsureReferences();
// 确保 videoOverlay 被赋值
if (videoOverlay == null)
videoOverlay = gameObject;
// 激活覆盖层
if (videoOverlay != null && videoOverlay != gameObject)
videoOverlay.SetActive(true);
Debug.Log($"[StoryPVPlayer] 引用检查:videoOverlay={videoOverlay != null}, videoDisplay={videoDisplay != null}, videoPlayer={videoPlayer != null}");
if (videoPlayer == null || videoDisplay == null || videoOverlay == null)
{
Debug.LogError($"[StoryPVPlayer] 引用未配置!videoOverlay={videoOverlay != null}, videoDisplay={videoDisplay != null}, videoPlayer={videoPlayer != null}。请检查 Inspector。");
if (videoOverlay != null) videoOverlay.SetActive(false);
return;
}
// 检查视频文件
if (videoPlayer.clip == null)
{
Debug.LogWarning("[StoryPVPlayer] VideoClip 未设置,尝试从 Resources 加载...");
videoPlayer.clip = Resources.Load<VideoClip>("pv");
if (videoPlayer.clip == null)
{
Debug.LogError("[StoryPVPlayer] 无法加载 pv 视频文件!请将 pv.mp4 放入 Resources 文件夹,或在 Inspector 中赋值 VideoClip。");
videoOverlay.SetActive(false);
return;
}
}
// 创建 RenderTexture(匹配视频分辨率,如果未知则用屏幕分辨率)
int width = videoPlayer.clip != null ? (int)videoPlayer.clip.width : Screen.width;
int height = videoPlayer.clip != null ? (int)videoPlayer.clip.height : Screen.height;
if (_renderTexture != null)
{
_renderTexture.Release();
Destroy(_renderTexture);
}
_renderTexture = new RenderTexture(width, height, 0);
videoPlayer.targetTexture = _renderTexture;
videoDisplay.texture = _renderTexture;
Debug.Log("[StoryPVPlayer] 启动协程,延迟一帧播放");
// 启动协程,等待一帧后再播放(Unity VideoPlayer 在 SetActive 同帧内调用 Play 无效)
StartCoroutine(PlayVideoCoroutine());
}
/// <summary>
/// 延迟一帧后播放视频(解决 VideoPlayer 在激活同帧内调用 Play 无效的问题)。
/// </summary>
private IEnumerator PlayVideoCoroutine()
{
_isPlaying = true;
Debug.Log($"[StoryPVPlayer] PlayVideoCoroutine started, activeSelf={gameObject.activeSelf}");
// 等待一帧,让 VideoPlayer 完成初始化
yield return null;
Debug.Log($"[StoryPVPlayer] 协程等待一帧后:videoPlayer={videoPlayer != null}, isActive={gameObject.activeSelf}");
if (videoPlayer == null)
{
Debug.LogError("[StoryPVPlayer] 协程中 videoPlayer 为 null,无法播放");
_isPlaying = false;
yield break;
}
// 播放
videoPlayer.Play();
Debug.Log($"[StoryPVPlayer] 已调用 videoPlayer.Play(), isPlaying={videoPlayer.isPlaying}");
// 暂停背景音乐
if (AudioManager.Instance != null)
AudioManager.Instance.PauseBGM();
Debug.Log("[StoryPVPlayer] 开始播放 PV");
}
/// <summary>
/// 停止播放:停止 VideoPlayer → 释放 RenderTexture → 隐藏覆盖层。
/// </summary>
public void Stop()
{
_isPlaying = false;
if (videoPlayer != null)
videoPlayer.Stop();
if (_renderTexture != null)
{
_renderTexture.Release();
Destroy(_renderTexture);
_renderTexture = null;
}
if (videoDisplay != null)
videoDisplay.texture = null;
if (videoOverlay != null)
videoOverlay.SetActive(false);
// 恢复背景音乐
if (AudioManager.Instance != null)
AudioManager.Instance.UnpauseBGM();
Debug.Log("[StoryPVPlayer] PV 播放已停止");
}
/// <summary>
/// 视频播放完毕回调(自动关闭覆盖层)。
/// </summary>
private void OnVideoFinished(VideoPlayer source)
{
Debug.Log("[StoryPVPlayer] PV 播放完毕");
Stop();
}
private void OnDestroy()
{
if (videoPlayer != null)
videoPlayer.loopPointReached -= OnVideoFinished;
if (_renderTexture != null)
{
_renderTexture.Release();
Destroy(_renderTexture);
}
}
}
}