第 14 课
实现视频的播放(下)
本课将在上一课自动播放的基础上加入完整控制逻辑。你会从脚本结构开始,逐段理解字段、生命周期、事件、异步准备和协程动画,再把 VideoController 与场景中的播放器、按钮、图标和视频屏幕连接起来。
用 C# 完成视频按钮控制与幕布动画
编写 VideoController 脚本,把按钮点击、视频准备、播放状态、图标切换和 Quad 横向缩放连接成一套稳定的视频控制流程。
学完你将能够
学习目标
- 说明 using、类、字段、属性和方法分别解决什么问题。
- 说明代码文件为什么不会从第一行自动执行到最后一行。
- 解释一帧和 Update 的含义,并判断什么时候不需要每帧执行代码。
- 区分 public、private 与可序列化私有字段。
- 读懂 true、false、null、if、else、return 和常用逻辑运算符。
- 根据准备、暂停和幕布状态判断一次按钮点击应该进入哪条路径。
- 说出 Awake、OnEnable、Start 和 OnDisable 的主要执行顺序。
- 为按钮和 Video Player 事件成对添加、移除监听。
- 区分 Play、Pause 和 Stop 对播放位置的影响。
- 解释 Prepare 为什么需要等待完成事件。
- 使用 IEnumerator、SmoothStep 和 Lerp 制作逐帧幕布动画。
- 使用 Renderer.enabled 隐藏画面,同时保留 Video Player 对象。
- 在 Inspector 中正确指定播放器、按钮和两个状态图标。
- 使用自动寻找作为单播放器场景的补充,并理解多播放器场景必须明确指定引用。
- 根据运行现象定位引用、准备、材质输出或动画状态方面的问题。
- 把项目背景、功能要求、实现约束和完成标准写成一份可执行的人工智能编程提示词。
开始操作前
准备工作
- 确认第 12 课制作的 VideoControllerUI、VideoControlBtn、VideoPlay 和 VideoCancel 仍然存在。
- 确认第 13 课的 VideoPlayer、VideoMat 和 Quad 可以正常显示视频。
- 确认 Video Player 使用 Material Override,VideoMat 使用 URP Unlit。
- 确认 Unity 可以正常编译 C# 脚本,并且已经安装 Visual Studio Code。
- 保存 MainScene,打开 Project、Hierarchy、Inspector、Game 和 Console 窗口。
本课学习路线
按照学习模块逐步完成
每个模块都包含理解、跟随操作和独立实践。先建立代码结构,再判断按钮路径
建立脚本并读懂控制状态
本课逻辑由按钮点击、视频事件和协程触发,不需要空的 Update 每帧运行。先建立脚本文件,再用状态变量描述播放器和使用者当前的真实意图。下载完整 VideoController 脚本
你可以直接下载完整脚本并放入 Assets/_Scripts,也可以按页面顺序连接七段主线代码。两种方式得到的 VideoController.cs 内容完全相同。
- 文件名必须与类名 VideoController 一致
- 脚本不依赖第三方动画插件
- 适用于本课程当前的单播放器场景
创建 _Scripts 与 VideoController.cs
- 01
在 Edit → Preferences → External Tools 中,把 Visual Studio Code 设为外部脚本编辑器。
- 02
在 Assets 下建立 _Scripts 文件夹。
- 03
在 _Scripts 中选择 Create → C# Script。
- 04
把文件命名为 VideoController。
- 05
双击脚本,确认由 Visual Studio Code 打开。
- 06
删除不需要的空 Update 方法。
代码文件不是从上到下自动执行
C# 脚本不是进入运行模式后从第一行一直执行到最后一行。using、类、字段和属性主要用于说明脚本能使用什么功能、保存什么数据;方法大括号中的语句只有在方法被调用时才会执行。
Awake、OnEnable、Start 等生命周期方法由 Unity 在规定阶段自动调用。PlayVideo、StopVideo、PrepareVideo 等普通方法则需要由按钮、事件或其他代码主动调用。阅读代码时,先问“谁会调用这个方法”,再沿着方法内部从上向下推演。
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;
/// <summary>
/// 博物馆场景的视频控制器。
///
/// 播放过程:
/// 1. 视频平面初始隐藏,看起来完全透明。
/// 2. 点击播放后,平面从中间向左右展开。
/// 3. 完全展开后,视频才真正开始播放。
///
/// 停止过程:
/// 1. 先暂停视频。
/// 2. 平面像关闭窗帘一样向中间收拢。
/// 3. 完全关闭后停止视频并隐藏平面。
///
/// 这里使用 Transform 的横向缩放实现窗帘效果,
/// 不需要修改 Unlit 材质,也不需要编写自定义 Shader。
/// </summary>
[DisallowMultipleComponent]
public class VideoController : MonoBehaviour
{主线代码 1/7:从文件开头开始并打开 VideoController 类,本段不关闭类。using 引入协程、Unity 基础类型、界面和视频功能;继承 MonoBehaviour 后,脚本才能挂到游戏对象并使用 Unity 生命周期。
先读懂类声明中的冒号
class 用来定义一种新的类型。类型规定一份数据能够保存什么、可以执行哪些操作;VideoController 是本课定义的类型,MonoBehaviour 是 Unity 提供的组件基础类型。
public class VideoController : MonoBehaviour 中的冒号表示“继承”。VideoController 在保留自己视频控制功能的同时,也获得了挂载到游戏对象、启用与禁用,以及接收 Unity 生命周期函数调用的能力。这里的冒号不是赋值符号。
一份 Unity C# 脚本的基本结构
先辨认代码属于哪一层,再理解这一层具体做什么。
左右滑动表格,可查看完整内容
| 代码结构 | 主要作用 | 本课示例 |
|---|---|---|
| using | 引入已有功能,使代码可以使用较短的类型名称 | UnityEngine.Video |
| class | 定义一个组件包含的数据与行为 | VideoController |
| 字段与属性 | 保存场景引用、参数和运行状态 | videoPlayer、isPaused、IsPlaying |
| 方法 | 在被调用时执行一组有顺序的操作 | Awake、PlayVideo、StopVideo |
| { } | 标出类、方法或条件分支的范围 | 最外层大括号包住整个类 |
| // 或 /// | 写给阅读者看的注释,不会成为运行步骤 | 说明脚本用途和关键顺序 |
脚本的五项职责
完整脚本虽然较长,但可以先按职责拆成五组理解。
左右滑动表格,可查看完整内容
| 职责 | 负责的内容 | 代表方法 |
|---|---|---|
| 场景引用 | 找到播放器、按钮、图标和 Renderer | FindSceneObjects |
| 播放控制 | 播放、暂停、停止和重新播放 | PlayVideo、StopVideo |
| 视频准备 | 等待解码器和第一帧准备完成 | PrepareVideo |
| 幕布动画 | 逐帧改变 Quad 的横向缩放 | AnimateCurtain |
| 状态恢复 | 处理结束、错误、禁用和应用暂停 | OnVideoError 等 |
什么是一帧,Update 为什么可以删除
运行中的画面由一张接一张的画面组成。Unity 完成一次画面更新,可以理解为经过了一帧;设备性能和场景复杂度会改变每秒帧数,因此一帧不是固定时长。
组件启用时,Unity 会在每一帧调用一次 Update。只有需要持续检查或持续变化的逻辑才应放进去;空 Update 每帧都会被调用,却没有完成任何工作,所以应删除。
本课的按钮点击、视频准备完成和播放结束都在事情发生时通知脚本,幕布动画则由协程逐帧推进,因此不需要再用一个空 Update 反复询问状态。
从顶部点击事件开始阅读:正在播放、等待准备或幕布运动时进入停止路径;其他情况再根据暂停与准备状态选择继续播放、等待准备或展开幕布。
手机上可左右滑动;点击图片可以查看完整图bool 用 true 和 false 记录两种状态
bool(布尔值)只保存 true 或 false。true 可以理解为“是、正在进行或条件成立”,false 可以理解为“否、没有进行或条件不成立”。
isPreparing、isPaused 和 curtainIsOpen 都是在回答一个只有两种结果的问题。把每个问题分别保存,按钮被点击时才能根据它们的组合选择正确路径。
! 表示把布尔结果取反,例如 !curtainIsOpen 表示“幕布当前没有打开”。
五个关键状态变量
每个变量只回答一个问题,组合起来才能准确描述当前流程。
左右滑动表格,可查看完整内容
| 变量 | 它回答的问题 | 属于哪类状态 |
|---|---|---|
| isPreparing | 播放器是否正在准备视频 | 播放器状态 |
| playAfterPrepare | 准备完成后使用者是否仍想播放 | 使用者意图 |
| isPaused | 是否保留当前位置等待继续 | 播放状态 |
| curtainIsOpen | 幕布是否已经完全展开 | 界面状态 |
| curtainIsMoving | 幕布动画是否正在运行 | 动画状态 |
描述四条按钮路径
对照流程图,用自己的话说明首次播放、准备期间取消、播放时停止和暂停后继续。
- 能够区分 isPreparing 与 playAfterPrepare。
- 能够说明准备尚未完成时为什么允许取消播放。
- 能够指出幕布运动期间按钮需要暂时禁止重复操作。
- 能够说明本课逻辑为什么不依赖空 Update。
先判断一份数据属于哪种职责
理解场景引用、参数、状态与属性
字段看起来都是变量,但用途不同:有些连接场景对象,有些允许调节动画,有些记录运行状态,还有一些缓存经常使用的组件。 [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;
主线代码 2/7:紧接第一段,继续写在 VideoController 类的大括号内。阅读时按场景引用、可调参数、状态变量、缓存对象和只读属性五组观察。当前文件仍未完整,继续下一段,不要先返回 Unity 编译。
从声明中区分类型、字段和属性
先确认一行代码在声明什么,再判断它保存的是对象、数值还是运行状态。
左右滑动表格,可查看完整内容
| 概念 | 怎样理解 | 本课示例 |
|---|---|---|
| 类型 | 规定数据可以保存什么,以及能够使用哪些功能 | VideoPlayer、Renderer、bool、float |
| 字段 | 写在类中、方法外,用来保存对象引用、参数或运行状态 | private bool isPaused; |
| 属性 | 为其他代码提供受控制的读取或写入入口 | IsPlaying、CurrentZoom |
只读表达式属性怎样计算结果
public bool IsPlaying => videoPlayer != null && videoPlayer.isPlaying; 表示每次读取 IsPlaying 时,都计算箭头右侧的表达式,并把结果作为属性值。这里没有提供赋值入口,因此其他代码只能读取,不能直接改写 IsPlaying。
&& 表示“并且”,左右两边都成立时结果才是 true。程序先检查 videoPlayer != null;如果播放器引用为空,右侧就不再读取 videoPlayer.isPlaying,从而避免访问一个不存在的对象。
逐段读懂一行字段声明
以 [SerializeField] private float curtainDuration = 0.6f; 为例,从左到右辨认每个部分。
左右滑动表格,可查看完整内容
| 代码部分 | 作用 | 怎样理解 |
|---|---|---|
| [SerializeField] | 交给 Unity 保存并显示 | 可以在 Inspector 中调整 |
| private | 限制访问范围 | 只允许 VideoController 内部直接使用 |
| float | 规定数据类型 | 这个字段保存单精度小数 |
| curtainDuration | 字段名称 | 后续用这个名称读取动画时长 |
| = 0.6f | 设置初始值 | 首次添加组件时默认使用 0.6;f 表示 float 小数 |
| ; | 结束一条语句 | 下一条声明从分号之后开始 |
场景引用对应关系
这些字段把脚本与上一课建立的场景对象连接起来。
左右滑动表格,可查看完整内容
| 字段 | 指定对象 | 脚本中的用途 |
|---|---|---|
| videoPlayer | VideoPlayer 上的 Video Player 组件 | 准备、播放、暂停和停止 |
| controlButton | VideoControlBtn 的 Button | 接收点击并控制可操作状态 |
| playIcon | VideoPlay 图片资源 | 未播放时显示 |
| pauseIcon | VideoCancel 或暂停图标 | 播放或等待播放时显示 |
| videoScreenRenderer | VideoPlayer 的 Mesh Renderer | 显示、隐藏并取得视频屏幕 Transform |
public、private 与 SerializeField
public 字段可以被其他脚本访问,并会显示在 Inspector 中。private 字段只允许当前类直接访问。
[SerializeField] private 是 Unity 常用组合:代码层面对其他类保持私有,同时允许在 Inspector 中设置并保存数值或引用。
动画和缩放参数
Range 会在 Inspector 中显示滑块,帮助把数值控制在合理范围。
左右滑动表格,可查看完整内容
| 参数 | 代码默认值 | 作用 |
|---|---|---|
| curtainDuration | 0.6 | 幕布展开或关闭的持续时间 |
| minZoom | 0.8 | 允许的最小缩放倍数 |
| maxZoom | 1.35 | 允许的最大缩放倍数 |
| zoomStep | 0.1 | 每次放大或缩小的变化量 |
为什么要保存 originalVideoScale
第 13 课已经把 Quad 调整为正确宽高比。Awake 必须先保存当前 localScale,再把幕布横向宽度设为 0。
展开时回到 originalVideoScale,而不是固定回到 1、1、1,才能避免视频画面被重新拉成正方形。
给字段按职责分组
在自己的代码旁标注每个字段属于场景引用、可调参数、运行状态还是缓存数据。
- 能够说出五个公开或序列化引用分别拖入什么对象。
- 能够解释五个布尔变量分别记录什么事实。
- 能够说明 originalVideoScale 为什么必须在关闭幕布前保存。
- 能够解释 IsPlaying 和 CurrentZoom 为什么只提供读取。
初始化、监听与清理各放在合适时机
使用生命周期连接和清理事件
Awake 负责确认对象与初始状态,OnEnable 负责添加当前启用期间的监听,Start 负责预准备视频,OnDisable 则把监听成对移除。本课使用的生命周期顺序
生命周期方法由 Unity 自动调用;普通控制方法只有在按钮、事件或其他代码调用时才会执行。
左右滑动表格,可查看完整内容
| 方法 | 调用时机 | 可能次数 | 本课主要工作 |
|---|---|---|---|
| Awake | 脚本实例载入时 | 每个实例一次 | 寻找对象、确认引用、缓存缩放并设置初始界面 |
| OnEnable | 组件每次进入启用状态时 | 可以多次 | 订阅视频事件和按钮点击 |
| Start | 组件第一次启用后、首个 Update 之前 | 每个实例一次 | 停止误开启的自动播放并提前 Prepare |
| OnDisable | 组件每次进入停用状态时 | 可以多次 | 移除视频事件和按钮点击 |
| OnApplicationPause | 应用进入后台或恢复时 | 可以多次 | 进入后台时暂停并保留播放位置 |
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();
}
}
主线代码 3/7:紧接字段与只读属性。注意 += 与 -=、AddListener 与 RemoveListener 必须使用同一个响应方法,形成完整的添加与清理配对。当前文件还缺少控制方法,继续下一段,不要先返回 Unity 编译。
方法、参数和方法调用怎样配合
方法先定义一组操作,真正被调用时,大括号中的语句才会从上向下执行。
左右滑动表格,可查看完整内容
| 概念 | 怎样理解 | 本课示例 |
|---|---|---|
| 方法定义 | 写明方法名称、返回类型、参数和需要执行的语句 | private void OnApplicationPause(bool paused) |
| void | 表示这个方法完成操作后不返回一个计算结果 | private void Awake() |
| 参数 | 调用方交给方法使用的数据;类型写在前,参数名写在后 | bool paused 表示接收暂停或恢复状态 |
| 方法调用 | 写出方法名和圆括号,立即进入该方法;结束后再回到调用处继续 | FindSceneObjects();、PauseVideo(); |
| 通过对象调用 | 点号左边是对象引用,右边是要执行的方法 | videoPlayer.Stop(); 表示让当前播放器执行停止 |
第一次阅读条件分支时要认识的符号
先把符号翻译成中文,再判断当前代码会进入哪条路径。
左右滑动表格,可查看完整内容
| 写法 | 怎样读 | 本课中的含义 |
|---|---|---|
| = | 把右侧结果保存到左侧 | isPaused = true:记录为暂停状态 |
| == | 判断两边是否相等 | videoPlayer == null:播放器引用是否为空 |
| != | 判断两边是否不相等 | buttonImage != null:按钮图片引用是否存在 |
| ! | 把 true 与 false 取反 | !curtainIsOpen:幕布没有打开 |
| || | 或者;任一条件成立即可 | 任一关键引用为空就不能继续初始化 |
| && | 并且;两边都成立才成立 | 应用暂停并且视频正在播放时才暂停 |
| null | 没有指向可使用的对象 | 需要先补全引用或停止继续访问 |
if、else、return 与大括号怎样控制顺序
if 后面的圆括号是条件。条件为 true 时执行紧随其后的大括号内容;条件为 false 时跳过这部分。如果后面有 else,就改为执行 else 的大括号内容。
return 只结束当前这一次方法调用,不会关闭游戏,也不会让脚本永久停止。Awake 发现关键引用不完整时先禁用当前组件,再用 return 阻止后续代码继续访问空引用。
阅读一段条件代码时,先找到 if 对应的结束大括号,再确认下一条语句是在条件里面还是外面。这样可以避免把只在某种状态下执行的代码误认为每次都会执行。
事件是通知,回调是收到通知后执行的方法
事件可以理解为程序发出的通知,例如视频准备完成、播放到结尾或按钮被点击。回调方法则规定收到通知后要做什么,例如 OnVideoPrepared 或 TogglePlayPause。
prepareCompleted += OnVideoPrepared 表示登记回调,此时方法名后没有圆括号,所以不会立即执行;事件真正发生时,Video Player 才会调用它。-= 负责移除同一个登记,AddListener 与 RemoveListener 也是相同的配对关系。
OnVideoPrepared(VideoPlayer source) 中的 source 是事件传入的参数。方法签名必须符合事件要求,即使某次实现暂时没有读取这个参数,也不能随意删掉。
引用不完整时为什么提前返回
Awake 中只要播放器、按钮或 Renderer 有一项为空,就输出学生能够定位对象的错误、禁用脚本并 return。
提前返回能够阻止后续代码继续访问空引用,避免一条场景配置错误引发更多连锁错误。
三个 Video Player 事件
事件表示某件事已经发生,响应方法负责继续对应流程。
左右滑动表格,可查看完整内容
| 事件 | 发生时机 | 响应方法 |
|---|---|---|
| prepareCompleted | Prepare 完成 | OnVideoPrepared |
| loopPointReached | 播放到结尾 | OnVideoFinished |
| errorReceived | 播放器报告错误 | OnVideoError |
画出生命周期与监听关系
按照 Awake → OnEnable → Start 的顺序写出主要工作,再把 OnEnable 与 OnDisable 中的每组监听连线。
- prepareCompleted 的添加和移除使用 OnVideoPrepared。
- loopPointReached 的添加和移除使用 OnVideoFinished。
- errorReceived 的添加和移除使用 OnVideoError。
- Button 的 AddListener 与 RemoveListener 都使用 TogglePlayPause。
一次点击先判断状态,再选择播放或停止路径
控制播放、暂停、停止与重新播放
Play、Pause 和 Stop 不是同一件事。播放按钮需要同时考虑是否正在播放、是否等待准备、幕布是否运动,以及是否从后台暂停状态恢复。Play、Pause 与 Stop 的区别
三种方法对画面状态和播放位置产生不同结果。
左右滑动表格,可查看完整内容
| 方法 | 播放位置 | 本课用途 |
|---|---|---|
| Play | 从当前位置开始或继续 | 幕布展开完成后播放,或暂停后恢复 |
| Pause | 保留当前位置 | 应用进入后台,或关闭幕布前冻结当前画面 |
| Stop | 结束并回到开头 | 幕布完全关闭后结束本次播放 |
/// <summary>
/// 中央按钮入口:播放或等待播放时执行停止,否则开始播放。
/// </summary>
public void TogglePlayPause()
{
if (videoPlayer.isPlaying || playAfterPrepare || curtainIsMoving)
{
StopVideo();
}
else
{
PlayVideo();
}
}
/// <summary>展开窗帘,完全展开后开始播放。</summary>
public void PlayVideo()
{
// 暂停后继续播放时,窗帘本来就是打开的,不需要重复展开。
if (isPaused && curtainIsOpen)
{
isPaused = false;
SetButtonIcon(true);
videoPlayer.Play();
return;
}
if (videoPlayer.isPrepared)
{
StartOpeningCurtain();
}
else
{
playAfterPrepare = true;
SetButtonIcon(true);
PrepareVideo();
}
}
/// <summary>暂停视频,但保持窗帘打开。</summary>
public void PauseVideo()
{
playAfterPrepare = false;
isPaused = true;
videoPlayer.Pause();
SetButtonIcon(false);
}
/// <summary>暂停视频,关闭窗帘,最后停止并隐藏画面。</summary>
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);
}
/// <summary>关闭后重新从头播放。</summary>
public void RestartVideo()
{
if (videoPlayer.isPrepared && videoPlayer.canSetTime)
{
videoPlayer.time = 0d;
}
isPaused = false;
PlayVideo();
}
主线代码 4/7:紧接生命周期与事件监听。重点观察 StopVideo:它先取消待播放意图并暂停当前画面,幕布完全关闭后才真正 Stop。当前文件还缺少准备与动画方法,继续下一段,不要先返回 Unity 编译。
TogglePlayPause 的三个停止条件
videoPlayer.isPlaying 表示视频已经在播放;playAfterPrepare 表示使用者已经提出播放要求但可能仍在等待;curtainIsMoving 表示幕布动画正在执行。
三项中任意一项为 true,再次点击都进入 StopVideo。全部为 false 时,才进入 PlayVideo。
PlayVideo 的三条分支
相同的播放请求会根据当前状态选择不同路径。
左右滑动表格,可查看完整内容
| 当前状态 | 处理方式 | 为什么 |
|---|---|---|
| 暂停且幕布已打开 | 清除暂停并直接 Play | 不需要重复展开幕布 |
| 视频已经准备完成 | 开始展开幕布 | 展开结束后再 Play |
| 视频尚未准备完成 | 记录待播放意图并 Prepare | 等待完成事件后继续 |
逐行跟踪四种控制结果
分别跟踪首次播放、播放时停止、应用后台暂停和暂停后继续,写出图标、幕布和播放位置的变化。
- 首次播放尚未准备时,playAfterPrepare 设为 true。
- 关闭幕布前先 Pause,收拢期间画面保持不变。
- 完全关闭后再 Stop,并隐藏 Renderer。
- 暂停恢复直接 Play,不重复运行展开动画。
播放器准备完成,不代表使用者仍然想播放
处理异步准备与视频事件
Prepare 需要打开文件、初始化解码器并准备第一帧,不能保证当前帧立即完成。脚本发出请求后继续运行,完成时由 prepareCompleted 事件通知。把系统状态与使用者意图区分开
isPreparing 描述播放器是否正在准备;playAfterPrepare 描述准备完成后是否仍然需要播放。
使用者可以在等待期间再次点击停止。即使底层准备稍后完成,也必须读取 playAfterPrepare 的最新值,不能擅自重新打开幕布。
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);
}
主线代码 5/7:紧接播放控制方法。PrepareVideo 防止重复准备;OnVideoPrepared 读取最新意图;播放结束与错误都复用已有恢复流程。当前文件还没有完成幕布动画,继续下一段,不要先返回 Unity 编译。
异步过程中的关键时刻
同一准备请求可能因为使用者操作得到不同结果。
左右滑动表格,可查看完整内容
| 时刻 | isPreparing | playAfterPrepare | 下一步 |
|---|---|---|---|
| 首次点击且未准备 | true | true | 等待 prepareCompleted |
| 准备期间再次点击 | 控制流程清除 | false | 不再自动展开 |
| 准备完成且仍想播放 | false | true | StartOpeningCurtain |
| 准备完成但已经取消 | false | false | 保持关闭 |
错误处理必须恢复可操作状态
OnVideoError 不只输出错误,还会清除准备、待播放和暂停状态,停止播放器,立即隐藏画面并恢复播放图标。
这样即使视频文件无法读取,按钮和界面也不会一直停留在等待状态。
模拟准备期间取消播放
从首次点击开始写出 isPreparing 与 playAfterPrepare 的变化,再假设准备完成事件晚到一步。
- 首次点击后记录待播放意图。
- 再次点击后把 playAfterPrepare 改为 false。
- 准备完成事件只在最新意图仍为 true 时展开。
- 能够说明播放失败后界面怎样回到初始状态。
把一次缩放变化分散到多个画面帧
用协程制作视频幕布动画
关闭状态把 Quad 的 Scale X 压到 0;展开状态恢复原始 X 并乘以缩放倍数。协程逐帧计算中间值,让屏幕从中心柔和展开或收拢。幕布动画中的关键工具
时间、进度、速度曲线和插值分别承担不同职责。
左右滑动表格,可查看完整内容
| 工具 | 作用 | 本课中的结果 |
|---|---|---|
| Time.unscaledDeltaTime | 累计真实经过时间 | 游戏时间暂停时动画仍可完成 |
| Mathf.Clamp01 | 把进度限制在 0 到 1 | 防止最后一帧超过目标 |
| Mathf.SmoothStep | 调整开始和结束速度 | 起步与停止更柔和 |
| Vector3.Lerp | 在起点和终点之间取中间缩放 | 逐帧改变 Quad 大小 |
| yield return null | 暂停协程到下一帧 | 让动画真正分散到多帧 |
#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
主线代码 6/7:紧接异步准备与视频事件。代码块可以纵向滚动,重点观察动画开始、while 逐帧计算和动画结束三个阶段。类仍未闭合,继续最后一段,不要先返回 Unity 编译。
读懂协程中的新写法
这些写法共同把一次缩放变化拆到多个画面帧中完成。
左右滑动表格,可查看完整内容
| 写法 | 含义 | 本课中的执行方式 |
|---|---|---|
| IEnumerator | 协程方法的返回类型,表示这段过程可以暂停后继续 | AnimateCurtain 由 StartCoroutine 启动 |
| while(条件) | 每轮开始先检查条件,成立就再执行一轮,不成立就退出循环 | elapsed 小于 curtainDuration 时继续计算缩放 |
| yield return null | 暂停当前协程,把后续语句留到下一帧继续 | 下一帧从暂停处之后继续,再回到 while 检查条件 |
| float elapsed = 0f | 声明只在这次方法调用中使用的局部变量 | 从 0 开始记录本次动画已经经过的时间 |
| elapsed += Time.unscaledDeltaTime | 把本帧经过时间累加回 elapsed | 这里的 += 是数值累加,不是前面用于登记事件的 += |
| 条件 ? A : B | 条件成立时选择 A,否则选择 B | open 为 true 时使用目标宽度,否则把 X 设为 0 |
| new Vector3(x, y, z) | 创建一个包含三个轴数值的 Vector3 | 组合幕布本帧需要使用的 X、Y、Z 缩放 |
| #region 与 #endregion | 给一组代码加上可折叠标题,只帮助阅读 | 整理手动缩放、幕布动画和初始化代码,不改变运行顺序 |
幕布协程每一帧怎样继续
StartCurtainAnimation 先停止旧协程,再启动 AnimateCurtain。协程第一次进入时记录状态、锁定按钮并取得起点与终点,然后进入 while 循环。
每一轮先累计本帧经过的时间,再计算进度和缩放,遇到 yield return null 后暂停。下一帧不会从方法开头重来,而是继续检查 while 条件;条件不再成立后,才准确设置最终缩放、恢复按钮,并根据 open 播放或停止视频。
为什么动画期间暂时禁用按钮
协程开始时把 controlButton.interactable 设为 false,能够阻止连续点击创建互相冲突的展开和关闭请求。
动画结束后再恢复 true。如果需要中途反向动画,应设计更完整的状态机,而不是让两个协程同时修改同一个 Transform。
为什么结束后还要准确赋值
帧间隔不会刚好整除幕布动画时长,while 结束时可能残留很小误差。
最后再次执行 videoTransform.localScale = targetScale,能够保证 X 真正到达 0 或完整宽度。关闭后再禁用 Renderer,可以避免留下细线。
Renderer.enabled 与 SetActive 的区别
本课只隐藏视频网格,不关闭整个 VideoPlayer 对象。
左右滑动表格,可查看完整内容
| 方式 | 影响范围 | 本课是否使用 |
|---|---|---|
| Renderer.enabled = false | 停止绘制网格,其他组件仍存在 | 使用 |
| gameObject.SetActive(false) | 禁用整个对象和所有组件 | 不用于视频屏幕 |
用数值模拟一次展开
假设原始 Scale X 为 3.7,分别写出 progress 为 0、0.5 和 1 时画面所处位置,并说明最终赋值的必要性。
- 能够说明 elapsed 怎样转成 0 到 1 的 progress。
- 能够区分 SmoothStep 调速度与 Lerp 算位置。
- 能够说明 yield return null 为什么让动画可见。
- 能够说明展开前显示 Renderer、关闭后隐藏 Renderer 的顺序。
手动引用优先,自动寻找作为单播放器补充
自动配置播放器并连接场景引用
脚本先使用 Inspector 中已经指定的对象;只有字段为空时才尝试自动寻找。这样既方便当前单播放器场景,也为以后多个视频屏幕保留明确配置方式。 #region 初始化
private void FindSceneObjects()
{
// VideoControllerUI 和 VideoPlayer 是两个独立根对象。
if (videoPlayer == null)
{
videoPlayer = FindObjectOfType<VideoPlayer>();
}
if (controlButton == null)
{
controlButton = GetComponentInChildren<Button>(true);
}
if (videoScreenRenderer == null && videoPlayer != null)
{
videoScreenRenderer = videoPlayer.targetMaterialRenderer;
if (videoScreenRenderer == null)
{
videoScreenRenderer = videoPlayer.GetComponent<Renderer>();
}
}
}
private void ConfigureVideoPlayer()
{
videoPlayer.playOnAwake = false;
videoPlayer.waitForFirstFrame = true;
videoPlayer.skipOnDrop = true;
// 把视频写入 Unlit 材质的主贴图。
if (videoPlayer.renderMode == VideoRenderMode.MaterialOverride)
{
videoPlayer.targetMaterialRenderer = videoScreenRenderer;
Material material = videoScreenRenderer.sharedMaterial;
videoPlayer.targetMaterialProperty =
material != null && material.HasProperty("_BaseMap")
? "_BaseMap"
: "_MainTex";
}
}
private void SetButtonIcon(bool playing)
{
if (buttonImage == null)
{
return;
}
Sprite icon = playing ? pauseIcon : playIcon;
if (icon != null)
{
buttonImage.sprite = icon;
}
}
#endregion
}主线代码 7/7:紧接幕布协程,并在末尾关闭代码区域和 VideoController 类。Material Override 使用 URP Unlit 时通常写入 _BaseMap;其他常见材质可能使用 _MainTex。
自动寻找的优先顺序
Inspector 已有引用时不会覆盖,字段为空时才继续寻找。
左右滑动表格,可查看完整内容
| 对象 | 第一选择 | 备用选择 |
|---|---|---|
| Video Player | Inspector 中指定 | 场景中的第一个 VideoPlayer |
| 控制按钮 | Inspector 中指定 | 脚本对象及子对象中的 Button |
| 视频 Renderer | Inspector 中指定 | targetMaterialRenderer 或同对象 Renderer |
把 VideoController 加到界面对象
- 01
保存 VideoController.cs,返回 Unity 等待编译完成。
- 02
选中 VideoControllerUI 或它的 Canvas。
- 03
把 VideoController 脚本拖到 Inspector。
- 04
确认组件没有显示脚本错误。

依次指定 Video Player、VideoControlBtn、VideoPlay 和 VideoCancel。当前结构中 Video Screen Renderer 可以留空自动寻找,也可以手动拖入 Mesh Renderer。
Inspector 字段应该拖入什么
场景中存在多个同类对象时,建议全部明确指定。
左右滑动表格,可查看完整内容
| 字段 | 拖入对象 | 完成后观察 |
|---|---|---|
| Video Player | VideoPlayer 对象的 Video Player 组件 | 脚本能控制正确视频 |
| Control Button | VideoControlBtn 的 Button 组件 | 按钮点击进入控制流程 |
| Play Icon | _Textures/VideoPlay | 未播放时显示播放图标 |
| Pause Icon | _Textures/VideoCancel 或暂停图标 | 播放或等待时提示再次点击可停止 |
| Video Screen Renderer | VideoPlayer 的 Mesh Renderer | 用于动画和画面显隐 |
脚本自动设置的 Video Player 参数
仍应在 Inspector 中观察它们与场景结构是否一致。
左右滑动表格,可查看完整内容
| 参数 | 脚本设置 | 作用 |
|---|---|---|
| Play On Awake | 关闭 | 不再进入场景就自动播放 |
| Wait For First Frame | 启用 | 等待第一帧准备好再播放 |
| Skip On Drop | 启用 | 设备较慢时优先保持视频时间同步 |
| Render Mode | 要求 Material Override | 把画面写入 VideoMat |
| Material Property | _BaseMap 或 _MainTex | 选择材质主纹理入口 |
完成脚本与场景连接
挂载组件、指定四项主要引用,并确认 Renderer 与材质属性能够正确找到。
- VideoController 挂在 VideoControllerUI 或 Canvas。
- 播放器、按钮、播放图标和停止图标引用正确。
- Video Screen Renderer 能够手动指定或自动找到。
- 按钮 Inspector 的 On Click 不再重复绑定同一方法。
- Play On Awake 已关闭,Material Override 保持不变。
按现象回到最早异常的代码层
综合运行并定位常见问题
完整运行时不要只看视频是否出现,还要观察初始状态、按钮可操作性、幕布方向、播放时机、图标变化和错误恢复。按顺序观察视频控制结果
- 01
保存 MainScene 并进入运行模式。
- 02
确认视频屏幕初始隐藏,按钮显示播放图标。
- 03
第一次点击按钮,观察按钮暂时不可再次点击。
- 04
观察幕布从中间向左右展开。
- 05
确认幕布完全展开后视频才开始播放。
- 06
再次点击,观察当前画面先冻结。
- 07
确认幕布向中间收拢,完全关闭后 Renderer 隐藏。
- 08
再次播放,确认视频能够从开头开始。
- 09
让视频播放到结尾,观察它是否自动关闭。
- 10
确认 Console 没有新增错误。
常见现象与处理方法
从最早异常的位置开始处理,不要同时改动多个状态。
左右滑动表格,可查看完整内容
| 现象 | 优先检查 | 处理方法 |
|---|---|---|
| 脚本无法挂载 | 文件名、类名和编译状态 | 保持 VideoController.cs 与类名一致,并先解决 Console 编译错误 |
| 点击没有反应 | Button、Control Button 与 XR 点击链路 | 确认按钮引用、EventSystem 和射线没有被遮挡 |
| 一次点击执行两次 | 重复监听或 On Click | 保留脚本 AddListener,清除重复手动绑定 |
| 进入场景仍自动播放 | Play On Awake 和其他脚本 | 关闭自动播放,确认 ConfigureVideoPlayer 执行 |
| 图标变化但视频不出现 | Video Clip、Renderer 与材质属性 | 检查 Material Override、_BaseMap 和 Renderer.enabled |
| 幕布直接出现 | curtainDuration 与协程入口 | 确认使用 StartCoroutine,而不是立即打开 |
| 展开后比例变形 | originalVideoScale 保存顺序 | 先保存原始缩放,再关闭幕布 |
| 准备期间取消后仍播放 | playAfterPrepare | 停止时清除意图,准备完成后再次判断 |
| 关闭后留下细线 | 最终缩放和 Renderer | 准确赋值 targetScale,再禁用 Renderer |
| PICO 中播放失败 | 视频编码与设备解码能力 | 使用常见 H.264,并降低分辨率或码率对比 |
七段代码怎样组成完整脚本
页面中的七个主线代码块全部直接取自本课 VideoController.cs,按页面出现顺序连接后就是完整文件。
第一段打开 VideoController 类,第二至第六段依次加入字段、生命周期、播放控制、异步准备和幕布协程,第七段加入自动配置并用最后一个大括号关闭类。
主线代码拼接顺序
从第一段开始依次向下连接,不需要重复添加 using、类声明或大括号。
左右滑动表格,可查看完整内容
| 顺序 | 代码块 | 在完整文件中的位置 |
|---|---|---|
| 1 | 命名空间与类声明 | 文件开头,并打开 VideoController 类 |
| 2 | 字段与只读属性 | 类内字段区域 |
| 3 | 生命周期与事件监听 | 字段区域之后 |
| 4 | 播放控制公开方法 | 生命周期方法之后 |
| 5 | 异步准备与视频事件 | 播放控制方法之后 |
| 6 | 缩放控制与幕布协程 | 异步事件方法之后 |
| 7 | 自动寻找、播放器配置与图标切换 | 类末尾,并关闭 VideoController 类 |
本课成果清单
逐项确认后,视频控制、异步准备、幕布动画和状态恢复已经形成完整功能。
- _Scripts/VideoController.cs 能够正常编译。
- VideoController 的播放器、按钮和图标引用正确。
- 视频屏幕初始隐藏,按钮显示播放图标。
- 首次点击后幕布展开,完全展开后视频才开始播放。
- 动画期间按钮不可重复点击。
- 再次点击后画面冻结、幕布收拢并停止播放。
- 收拢完成后 Renderer 隐藏,不留下细线。
- 再次播放能够回到开头。
- 准备期间取消不会在稍后自动播放。
- 播放结束和播放失败都能恢复安全界面状态。
- 应用进入后台时正在播放的视频会暂停。
- MainScene 已保存,Console 没有新增错误。
把编程需求写成可以直接执行的任务说明
用人工智能编程助手完成 VideoController
人工智能编程助手(AI Agent)是能够读取并修改项目文件的智能工具。它能加快脚本编写,但前提是你要准确说明项目环境、目标行为、技术限制和完成标准。提示词不是一句“帮我写代码”
如果只说“写一个视频控制器”,人工智能编程助手无法知道场景中有哪些对象、视频何时开始、幕布怎样运动,也不知道哪些方案不能使用。
一份可执行的提示词应像任务说明书:先交代当前项目,再描述最终效果,随后列出必须实现的成员和边界情况,最后明确什么样的代码才算完成。
完整编程提示词的四个部分
这四部分越具体,生成结果越容易与当前 Unity 项目正确连接。
左右滑动表格,可查看完整内容
| 组成部分 | 要回答的问题 | 本课示例 |
|---|---|---|
| 项目背景 | 代码运行在哪里,已有对象是什么 | Unity 2022.3、VideoPlayer、Quad 和控制按钮 |
| 目标行为 | 使用者操作后应看到什么 | 展开后播放,停止时先收拢再隐藏 |
| 实现约束 | 必须或不能采用哪些做法 | 使用协程,不安装第三方动画插件,不使用空 Update |
| 完成标准 | 怎样判断文件可以使用 | 脚本完整可编译,事件成对清理,异常后能够恢复 |
从一个新脚本开始
- 01
在 Assets/_Scripts 中新建 C# Script,并命名为 VideoController。
- 02
确认当前文件只是 Unity 创建的默认脚本,还没有加入本课代码。
- 03
在人工智能编程助手中打开 Unity 项目根目录,使它能够找到 Assets/_Scripts/VideoController.cs。
- 04
复制下面的完整提示词并发送,让编程助手直接修改这个文件。
- 05
完成后返回 Unity,等待脚本编译,再根据本课前面的场景引用表连接对象。
请直接完成 Unity 项目中的 Assets/_Scripts/VideoController.cs。
当前状态:
- 这是一个刚创建的 C# 脚本,可以删除默认模板后完整重写。
- 项目使用 Unity 2022.3、通用渲染管线和 UnityEngine.Video。
- 场景中已经存在 VideoPlayer、显示视频的 Quad、VideoControllerUI、控制按钮、播放图标和暂停图标。
- VideoPlayer 使用 Material Override,把视频输出到 Quad 的 Unlit 材质。
任务目标:
编写一个可直接挂到 VideoControllerUI 上的 VideoController 组件。单个按钮负责播放和停止;播放前让视频 Quad 从中间向左右展开,完全展开后才播放;停止时先暂停当前画面,再把 Quad 向中间收拢,完全关闭后停止并隐藏。
文件与依赖约束:
1. 文件名必须是 VideoController.cs,类名必须是 VideoController,并继承 MonoBehaviour。
2. 只修改这个文件,不创建其他脚本,不修改场景,不安装或调用第三方动画插件。
3. 使用 Unity 协程、Time.unscaledDeltaTime、Mathf.SmoothStep 和 Vector3.Lerp 完成动画。
4. 不编写自定义 Shader,不使用 Update 轮询。
5. 引入 System.Collections、UnityEngine、UnityEngine.UI 和 UnityEngine.Video。
6. 为类添加 DisallowMultipleComponent。
必须提供的场景引用与参数:
- public VideoPlayer videoPlayer
- public Button controlButton
- public Sprite playIcon
- public Sprite pauseIcon
- 可序列化私有 Renderer videoScreenRenderer;未指定时自动寻找
- curtainDuration,默认 0.6,范围 0.1 到 2
- minZoom,默认 0.8,范围 0.5 到 1
- maxZoom,默认 1.35,范围 1 到 2
- zoomStep,默认 0.1,范围 0.05 到 0.5
必须维护的状态:
- isPreparing:是否正在异步准备视频
- playAfterPrepare:准备完成后是否仍然需要播放
- isPaused:是否保留当前位置等待继续
- curtainIsOpen:幕布是否完全展开
- curtainIsMoving:幕布是否正在运动
- 缓存 Button 的 Image、视频 Transform、原始 localScale、当前 zoom 和正在运行的 Coroutine
- 提供只读属性 IsPlaying 和 CurrentZoom
生命周期与事件要求:
1. Awake 中自动补全引用;引用不完整时输出清楚的错误并禁用组件。
2. Awake 中缓存组件与原始缩放,配置播放器,关闭并隐藏初始幕布,显示播放图标。
3. OnEnable 中监听 prepareCompleted、loopPointReached、errorReceived 和按钮 onClick。
4. OnDisable 中移除完全相同的事件与按钮监听,避免重复响应。
5. Start 中停止误开启的自动播放、恢复关闭状态,并提前调用 Prepare 准备第一帧。
6. 应用进入后台或使用者摘下头显时,如果视频正在播放,就暂停视频。
必须实现的公开方法:
- TogglePlayPause()
- PlayVideo()
- PauseVideo()
- StopVideo()
- RestartVideo()
- ZoomIn()
- ZoomOut()
- ResetZoom()
- SetZoom(float newZoom)
播放控制要求:
1. 第一次点击时,如果视频尚未准备好,记录播放意图并调用 Prepare;准备完成事件到达后再次读取最新意图。
2. 准备期间再次点击停止时,必须清除播放意图,不能在稍后突然播放。
3. 暂停后继续播放时保留当前位置;完全停止后再次播放时从头开始。
4. 非循环视频播放到结尾后自动关闭幕布。
5. 播放失败时清理准备、播放和暂停状态,停止播放器,关闭并隐藏幕布,恢复播放图标,并输出包含错误信息的日志。
幕布动画要求:
1. 只改变 Quad 的 localScale.x;y 和 z 保持原始比例并乘以当前 zoom。
2. 展开前先启用 Renderer,完全展开后才调用 videoPlayer.Play()。
3. 关闭前先暂停视频;完全关闭后调用 Stop() 并禁用 Renderer。
4. 动画期间禁用控制按钮,完成后恢复。
5. 新动画开始前停止尚未结束的旧协程,避免两个协程同时修改缩放。
6. 提供立即打开或关闭幕布的方法,用于初始化和错误恢复。
7. 必须先保存 Quad 的原始缩放,再把横向缩放设为 0。
自动配置要求:
1. videoPlayer 未指定时,在场景中寻找 VideoPlayer。
2. controlButton 未指定时,在当前对象及子对象中寻找 Button,包括未激活对象。
3. videoScreenRenderer 未指定时,优先使用 targetMaterialRenderer,再尝试 VideoPlayer 同对象上的 Renderer。
4. 关闭 playOnAwake,开启 waitForFirstFrame 和 skipOnDrop。
5. Material Override 模式下指定目标 Renderer;材质具有 _BaseMap 时使用 _BaseMap,否则使用 _MainTex。
6. 根据播放状态把按钮图标切换为 pauseIcon 或 playIcon;图标为空时不能报错。
代码质量要求:
- 输出完整、可编译的 VideoController.cs,不能只给方法片段或伪代码。
- 所有大括号、事件签名、协程返回类型和命名空间必须完整。
- 对异步准备、最新播放意图、动画开始与结束、错误恢复等关键位置添加简洁中文注释。
- 不保留空的 Update 方法。
请直接把完整实现写入 Assets/_Scripts/VideoController.cs。完成后只需简要说明已经实现的功能,并提醒我返回 Unity 查看 Console 是否成功编译。点击右上角复制整段提示词。它从新建文件的状态出发,已经包含本课功能、字段、事件、协程动画、异常处理和完成标准。
完成一次人工智能辅助编程
使用提示词生成脚本,并把生成结果与本课七段主线代码逐项比较。
- VideoController.cs 是完整文件,不是零散方法。
- 脚本没有安装或调用第三方动画插件。
- 所有公开字段和可调参数都能在 Inspector 中看到。
- 按钮、视频事件和应用暂停事件都使用正确的方法响应。
- 准备期间取消后不会自动播放。
- 幕布动画只改变横向缩放,并在正确时机控制 Renderer。
- Unity 完成编译,Console 没有新增错误。
把知识变成作品
本课练习
请按顺序完成以下任务,并保存自己的学习成果。- 01
把 playAfterPrepare 的变化写成一条状态时间线,模拟首次点击、准备期间取消和准备完成三个时刻。
- 02
暂时把 curtainDuration 调大,观察 SmoothStep 和 Lerp 的作用,再恢复到适合当前场景的数值。
- 03
在不改变原始宽高比的前提下调用 ZoomIn、ZoomOut 和 ResetZoom,观察幕布打开与关闭时的缩放结果。
- 04
使用本课最后的提示词从新建脚本生成 VideoController.cs,再与七段主线代码比较关键状态和事件是否完整。
把本课进度保存下来
登录后可以保存浏览内容、有效学习时间、完成状态和答题参与记录。