当前位置: 首页 > article >正文

[Unity Demo]从零开始制作空洞骑士Hollow Knight第十九集:制作过场Cutscene系统

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、制作过场Cutscene系统
    • 1.制作基本的视频过场和动画过场
    • 2.制作决定过场系统的播放顺序Sequence以及切换场景以后的逻辑处理
  • 二、制作跳过过场Cutscene的MenuScreen屏幕
  • 总结


前言

         hello大家好久没见,之所以隔了这么久才更新并不是因为我又放弃了这个项目,而是接下来要制作的工作太忙碌了,每次我都花了很长的时间解决完一个部分,然后就没力气打开CSDN写文章就直接睡觉去了,现在终于有时间整理下我这半个月都做了什么内容。

        废话少说,上一期我们已经制作了基本的UI系统,接下来就是将制作过场cutscene系统。

        另外,我的Github已经更新了,想要查看最新的内容话请到我的Github主页下载工程吧:

GitHub - ForestDango/Hollow-Knight-Demo: A new Hollow Knight Demo after 2 years!


一、过场系统Cutscene系统     

1.制作基本的视频过场和动画过场

        OK我们先把两段视频导入到Asset文件夹当中,

然后创建我们上一期没讲到的场景Opening_Sequence,很简单,一个_SceneManager,两个录像的Cutscene视频,

这里我们需要一个可序列化物体可脚本化对象的特性的脚本,它需要记录视频的asset文件路径,音效的asset文件路径,还有我们的video clip:

using System;
using UnityEngine;
using UnityEngine.Video;

[CreateAssetMenu(menuName = "Hollow Knight/Cinematic Video Reference", fileName = "CinematicVideoReference", order = 1000)]
public class CinematicVideoReference : ScriptableObject
{
    [SerializeField] private string videoAssetPath;
    [SerializeField] private string audioAssetPath;
    [SerializeField] private VideoClip embeddedVideoClip;

    public string VideoFileName
    {
	get
	{
	    return name;
	}
    }
    public string VideoAssetPath
    {
	get
	{
	    return videoAssetPath;
	}
    }
    public string AudioAssetPath
    {
	get
	{
	    return audioAssetPath;
	}
    }
    public VideoClip EmbeddedVideoClip
    {
	get
	{
	    return embeddedVideoClip;
	}
    }
}

 这里我们两个视频,创建两个scriptable object:

 还需要创建一个所有电影般的Cinematic抽象类,因为我们游戏中有不一样的cutscene,所以需要一个总的抽象类,里面将包含IsLoading,IsPlaying,IsLooping,Volume,Play(),Stop(),等等最基本的Cinematic功能:

using System;

public abstract class CinematicVideoPlayer : IDisposable
{
    protected CinematicVideoPlayerConfig Config
    {
	get
	{
	    return config;
	}
    }

    public CinematicVideoPlayer(CinematicVideoPlayerConfig config)
    {
	this.config = config;
    }

    public virtual void Dispose()
    {
    }

    public abstract bool IsLoading { get; }
    public abstract bool IsPlaying { get; }
    public abstract bool IsLooping { get; set; }
    public abstract float Volume { get; set; }
    public abstract void Play();
    public abstract void Stop();
    public virtual float CurrentTime
    {
	get
	{
	    return 0f;
	}
    }
    public virtual void Update()
    {
    }

    public static CinematicVideoPlayer Create(CinematicVideoPlayerConfig config)
    {
	return new XB1CinematicVideoPlayer(config);
    }

    private CinematicVideoPlayerConfig config;
}

里面还有几个特别的类,一个是cinematic的播放配置config:

using System;
using UnityEngine;

public class CinematicVideoPlayerConfig
{
    private CinematicVideoReference videoReference;
    private MeshRenderer meshRenderer;
    private AudioSource audioSource;
    private CinematicVideoFaderStyles faderStyle;
    private float implicitVolume;

    public CinematicVideoReference VideoReference
    {
	get
	{
	    return videoReference;
	}
    }
    public MeshRenderer MeshRenderer
    {
	get
	{
	    return meshRenderer;
	}
    }
    public AudioSource AudioSource
    {
	get
	{
	    return audioSource;
	}
    }
    public CinematicVideoFaderStyles FaderStyle
    {
	get
	{
	    return faderStyle;
	}
    }
    public float ImplicitVolume
    {
	get
	{
	    return implicitVolume;
	}
    }
    public CinematicVideoPlayerConfig(CinematicVideoReference videoReference, MeshRenderer meshRenderer, AudioSource audioSource, CinematicVideoFaderStyles faderStyle, float implicitVolume)
    {
	this.videoReference = videoReference;
	this.meshRenderer = meshRenderer;
	this.audioSource = audioSource;
	this.faderStyle = faderStyle;
	this.implicitVolume = implicitVolume;
    }
}
public enum CinematicVideoFaderStyles
{
    Black,
    White
}

然后就是使用类来完成对视频播放器VideoPlayer的全部配置一次搞定:同时它还要实现抽象类CinematicVideoPlayer的全部抽象函数:

using UnityEngine;
using UnityEngine.Video;

public class XB1CinematicVideoPlayer : CinematicVideoPlayer
{
    private VideoPlayer videoPlayer;
    private Texture originalMainTexture;
    private RenderTexture renderTexture;
    private const string TexturePropertyName = "_MainTex";
    private bool isPlayEnqueued;

    public XB1CinematicVideoPlayer(CinematicVideoPlayerConfig config) : base(config)
    {
	originalMainTexture = config.MeshRenderer.material.GetTexture("_MainTex");
	renderTexture = new RenderTexture(Screen.width, Screen.height, 0);
	Graphics.Blit((config.FaderStyle == CinematicVideoFaderStyles.White) ? Texture2D.whiteTexture : Texture2D.blackTexture, renderTexture);
	Debug.LogFormat("Creating Unity Video Player......");
	videoPlayer = config.MeshRenderer.gameObject.AddComponent<VideoPlayer>();
	videoPlayer.playOnAwake = false; //开始就播放
	videoPlayer.audioOutputMode = VideoAudioOutputMode.AudioSource; //音效输出模式
	videoPlayer.SetTargetAudioSource(0, config.AudioSource); //设置播放的audiosource游戏对象
	videoPlayer.renderMode = VideoRenderMode.CameraFarPlane; //设置渲染模式
	videoPlayer.targetCamera = GameCameras.instance.mainCamera; //设置渲染目标摄像机
	videoPlayer.targetTexture = renderTexture; //设置目标纹理
	config.MeshRenderer.material.SetTexture(TexturePropertyName, renderTexture); // 设置材质纹理
	VideoClip embeddedVideoClip = config.VideoReference.EmbeddedVideoClip;  //设置播放的clip为config里面的EmbeddedVideoClip
	videoPlayer.clip = embeddedVideoClip;
	videoPlayer.prepareCompleted += OnPrepareCompleted;
	videoPlayer.Prepare(); //准备完成播放
    }

    public override bool IsLoading
    {
	get
	{
	    return false;
	}
    }
    public override bool IsPlaying
    {
	get
	{
	    if (videoPlayer != null && videoPlayer.isPrepared)
	    {
		return videoPlayer.isPlaying;
	    }
	    return isPlayEnqueued;
	}
    }
    public override bool IsLooping
    {
	get
	{
	    return videoPlayer != null && videoPlayer.isLooping;
	}
	set
	{
	    if (videoPlayer != null)
	    {
		videoPlayer.isLooping = value;
	    }
	}
    }
    public override float Volume
    {
	get
	{
	    if (base.Config.AudioSource != null)
	    {
		return base.Config.AudioSource.volume;
	    }
	    return 1f;
	}
	set
	{
	    if (base.Config.AudioSource != null)
	    {
		base.Config.AudioSource.volume = value;
	    }
	}
    }

    public override void Dispose()
    {
	base.Dispose();
	if(videoPlayer != null)
	{
	    videoPlayer.Stop();
	    Object.Destroy(videoPlayer);
	    videoPlayer = null;
	    MeshRenderer meshRenderer = Config.MeshRenderer;
	    if(meshRenderer != null)
	    {
		meshRenderer.material.SetTexture("_MainTex", originalMainTexture);
	    }
	}
	if(renderTexture != null)
	{
	    Object.Destroy(renderTexture);
	    renderTexture = null;
	}
    }
    public override void Play()
    {
	if(videoPlayer != null && videoPlayer.isPrepared)
	{
	    videoPlayer.Play();
	}
	isPlayEnqueued = true;
    }
    public override void Stop()
    {
	if (videoPlayer != null)
	{
	    videoPlayer.Stop();
	}
	isPlayEnqueued = false;
    }
    private void OnPrepareCompleted(VideoPlayer source)
    {
	if (source == videoPlayer && videoPlayer != null && isPlayEnqueued)
	{
	    videoPlayer.Play();
	    isPlayEnqueued = false;
	}
    }
}

最后我们还要制作一个自己的视频播放器脚本就叫CinematicPlayer.cs,

using System;
using System.Collections;
using GlobalEnums;
using UnityEngine;
using UnityEngine.Audio;

[RequireComponent(typeof(AudioSource))]
[RequireComponent(typeof(MeshRenderer))]
public class CinematicPlayer : MonoBehaviour
{
    [SerializeField] private CinematicVideoReference videoClip;
    private CinematicVideoPlayer cinematicVideoPlayer;
    [SerializeField] private AudioSource additionalAudio;
    [SerializeField] private MeshRenderer selfBlanker;

    [Header("Cinematic Settings")]
    [Tooltip("Determines what will trigger the video playing.")]
    public MovieTrigger playTrigger;

    [Tooltip("The speed of the fade in, comes in different flavours.")]
    public FadeInSpeed fadeInSpeed; //淡入速度

    [Tooltip("The amount of time to wait before fading in the camera. Camera will stay black and the video will play.")]
    [Range(0f, 10f)]
    public float delayBeforeFadeIn; //在淡入(0到1)之前延迟几秒才开始

    [Tooltip("Allows the player to skip the video.")] //允许玩家跳过video
    public SkipPromptMode skipMode;

    [Tooltip("Prevents the skip action from taking place until the lock is released. Useful for animators delaying skip feature.")]
    public bool startSkipLocked = false; //开始时强制锁定跳过

    [Tooltip("The speed of the fade in, comes in different flavours.")]
    public FadeOutSpeed fadeOutSpeed;

    [Tooltip("Video keeps looping until the player is explicitly told to stop.")]
    public bool loopVideo; //是否循环播放video直到控制它停止

    [Space(6f)]
    [Tooltip("The name of the scene to load when the video ends. Leaving this blank will load the \"next scene\" as set in PlayerData.")]
    public VideoType videoType;

    public CinematicVideoFaderStyles faderStyle;
    private AudioSource audioSource;
    private MeshRenderer myRenderer;
    private GameManager gm;
    private UIManager ui;
    private PlayerData pd;
    private PlayMakerFSM cameraFSM;

    private bool videoTriggered;
    private bool loadingLevel;

    [SerializeField] private AudioMixerSnapshot masterOff;
    [SerializeField] private AudioMixerSnapshot masterResume;

    private void Awake()
    {
	audioSource = GetComponent<AudioSource>();
	myRenderer = GetComponent<MeshRenderer>();
	if (videoType == VideoType.InGameVideo)
	{
	    myRenderer.enabled = false;
	}
    }

    protected void OnDestroy()
    {
	if(cinematicVideoPlayer != null)
	{
	    cinematicVideoPlayer.Dispose();
	    cinematicVideoPlayer = null;
	}
    }

    private void Start()
    {
	gm = GameManager.instance;
	ui = UIManager.instance;
	pd = PlayerData.instance;
	if (startSkipLocked)
	{
	    gm.inputHandler.SetSkipMode(SkipPromptMode.NOT_SKIPPABLE);
	}
	else
	{
	    gm.inputHandler.SetSkipMode(skipMode);
	}
	if (playTrigger == MovieTrigger.ON_START)
	{
	    StartCoroutine(StartVideo());
	}
    }

    private void Update()
    {
	if (cinematicVideoPlayer != null)
	{
	    cinematicVideoPlayer.Update();
	}
	if (Time.frameCount % 10 == 0)
	{
	    Update10();
	}
    }

    private void Update10()
    {
	//每隔十帧检测一下是否动画已经播放完成。
	if ((cinematicVideoPlayer == null || (!cinematicVideoPlayer.IsLoading && !cinematicVideoPlayer.IsPlaying)) && !loadingLevel && videoTriggered)
	{
	    if (videoType == VideoType.InGameVideo)
	    {
		FinishInGameVideo();
		return;
	    }
	    FinishVideo();
	}
    }

    /// <summary>
    /// 影片结束后的行为
    /// </summary>
    private void FinishVideo()
    {
	Debug.LogFormat("Finishing the video.", Array.Empty<object>());
	videoTriggered = false;
	//判断video类型,目前只有OpeningCutscene和OpeningPrologue
	if (videoType == VideoType.OpeningCutscene) 
	{
	    GameCameras.instance.cameraFadeFSM.Fsm.Event("JUST FADE");
	    ui.SetState(UIState.INACTIVE);
	    loadingLevel = true;
	    StartCoroutine(gm.LoadFirstScene());
	    return;
	}
	if(videoType == VideoType.OpeningPrologue)
	{
	    GameCameras.instance.cameraFadeFSM.Fsm.Event("JUST FADE");
	    ui.SetState(UIState.INACTIVE);
	    loadingLevel = true;
	    //gm.LoadOpeningCinematic();
	    return;
	}
	//TODO:
    }

    /// <summary>
    /// 结束游戏内的视频video
    /// </summary>
    private void FinishInGameVideo()
    {
	Debug.LogFormat("Finishing in-game video.", Array.Empty<object>());
	PlayMakerFSM.BroadcastEvent("CINEMATIC END");
	myRenderer.enabled = false;
	selfBlanker.enabled = false;
	if(masterResume != null)
	{
	    masterResume.TransitionTo(0f);
	}
	if(additionalAudio != null)
	{
	    additionalAudio.Stop();
	}
	if(cinematicVideoPlayer != null)
	{
	    cinematicVideoPlayer.Stop();
	    cinematicVideoPlayer.Dispose();
	    cinematicVideoPlayer = null;
	}
	videoTriggered = false;
	gm.gameState = GameState.PLAYING;
    }

    /// <summary>
    /// 开启视频video
    /// </summary>
    /// <returns></returns>
    private IEnumerator StartVideo()
    {
	if(masterOff != null)
	{
	    masterOff.TransitionTo(0f);
	}
	videoTriggered = true;
	if(videoType == VideoType.InGameVideo)
	{
	    gm.gameState = GameState.CUTSCENE;
	    if(cinematicVideoPlayer == null)
	    {
		Debug.LogFormat("Creating new CinematicVideoPlayer for in game video", Array.Empty<object>());
		cinematicVideoPlayer = CinematicVideoPlayer.Create(new CinematicVideoPlayerConfig(videoClip, myRenderer, audioSource, faderStyle, GameManager.instance.GetImplicitCinematicVolume()));
	    }
	    Debug.LogFormat("Waiting for CinematicVideoPlayer in game video load...", Array.Empty<object>());
	    while (cinematicVideoPlayer != null && cinematicVideoPlayer.IsLoading)
	    {
		yield return null;
	    }
	    Debug.LogFormat("Starting cinematic video player in game video.", Array.Empty<object>());
	    if(cinematicVideoPlayer != null)
	    {
		cinematicVideoPlayer.IsLooping = loopVideo;
		cinematicVideoPlayer.Play();
		myRenderer.enabled = true;
	    }
	    if (additionalAudio)
	    {
		additionalAudio.Play();
	    }
	    yield return new WaitForSeconds(delayBeforeFadeIn);
	    if (fadeInSpeed == FadeInSpeed.SLOW)
	    {
		GameCameras.instance.cameraFadeFSM.Fsm.Event("FADE SCENE IN SLOWLY");
	    }
	    else if (fadeInSpeed == FadeInSpeed.NORMAL)
	    {
		GameCameras.instance.cameraFadeFSM.Fsm.Event("FADE SCENE IN");
	    }
	}
	else if(videoType == VideoType.StagTravel)
	{
	    //TODO:
	}
	else
	{
	    Debug.LogFormat("Start the Video");
	    if (cinematicVideoPlayer == null)
	    {
		cinematicVideoPlayer = CinematicVideoPlayer.Create(new CinematicVideoPlayerConfig(videoClip, myRenderer, audioSource, faderStyle, GameManager.instance.GetImplicitCinematicVolume()));
	    }
	    while (cinematicVideoPlayer != null && cinematicVideoPlayer.IsLoading)
	    {
		yield return null;
	    }
	    if (cinematicVideoPlayer != null)
	    {
		cinematicVideoPlayer.IsLooping = loopVideo;
		cinematicVideoPlayer.Play();
		myRenderer.enabled = true;
	    }
	    yield return new WaitForSeconds(delayBeforeFadeIn);
	    if(fadeInSpeed == FadeInSpeed.SLOW)
	    {
		GameCameras.instance.cameraFadeFSM.Fsm.Event("FADE SCENE IN SLOWLY");
	    }
	    else if(fadeInSpeed == FadeInSpeed.NORMAL)
	    {
		GameCameras.instance.cameraFadeFSM.Fsm.Event("FADE SCENE IN");
	    }
	}
    }

    /// <summary>
    /// 跳过视频
    /// </summary>
    /// <returns></returns>
    public IEnumerator SkipVideo()
    {
	if (videoTriggered)
	{
	    if(videoType == VideoType.InGameVideo)
	    {
		if(fadeOutSpeed != FadeOutSpeed.NONE)
		{
		    float duration = 0f; 
		    if (fadeOutSpeed == FadeOutSpeed.NORMAL)
		    {
			duration = 0.5f;
		    }
		    else if (fadeOutSpeed == FadeOutSpeed.SLOW)
		    {
			duration = 2.3f;
		    }
		    selfBlanker.enabled = true;
		    float timer = 0f;
		    while (videoTriggered)
		    {
			if (timer >= duration)
			{
			    break;
			}
			float a = Mathf.Clamp01(timer / duration);
			selfBlanker.material.color = new Color(0f, 0f, 0f, a);
			yield return null;
			timer += Time.unscaledDeltaTime;
		    }
		}
		else
		{
		    yield return null;
		}
	    }
	    else if(fadeOutSpeed == FadeOutSpeed.NORMAL)
	    {
		PlayMakerFSM.BroadcastEvent("JUST FADE");
		yield return new WaitForSeconds(0.5f);
	    }
	    else if (fadeOutSpeed == FadeOutSpeed.SLOW)
	    {
		PlayMakerFSM.BroadcastEvent("START FADE");
		yield return new WaitForSeconds(2.3f);
	    }
	    else
	    {
		yield return null;
	    }
	    if(cinematicVideoPlayer != null)
	    {
		cinematicVideoPlayer.Stop();
	    }
	}
    }

    public enum MovieTrigger
    {
	ON_START,
	MANUAL_TRIGGER
    }

    public enum FadeInSpeed
    {
	NORMAL,
	SLOW,
	NONE
    }

    public enum FadeOutSpeed
    {
	NORMAL,
	SLOW,
	NONE
    }

    public enum VideoType
    {
	OpeningCutscene,
	StagTravel,
	InGameVideo,
	OpeningPrologue,
	EndingA,
	EndingB,
	EndingC,
	EndingGG
    }
}

然后就是添加上参数:

至此视频的过场系统我已经实现好了,做到这里我突然想到了好像空洞骑士新游戏一开始还有教师蒙诺膜的一首诗,这个就是接下来要讲的动画过场:

首先我们先制作好五个textmeshpro,然后把诗句的内容打填上去,

然后需要一些黑幕和粒子系统:

至于动画animator,就搁个120帧显示一段字就好了:

OK我们已经制作了最基本的视频过场和动画过场了。

2.制作决定过场系统的播放顺序Sequence以及切换场景以后的逻辑处理

看到这里你可能会想,既然我已经制作了三个片段,那我怎么决定他们的播放顺序呢?这就要用到我们Sequence相关的脚本了:

这里我们可以先写一个抽象类,表明你的播放序列Sequence里的都是可以跳过的Sequence,因为空洞骑士的结局过场都是不可跳过的,所以得区分一下:

using System;
using UnityEngine;

public abstract class SkippableSequence : MonoBehaviour
{
    public abstract void Begin();
    public abstract bool IsPlaying { get; }
    public abstract void Skip();
    public abstract bool IsSkipped { get; }
    public abstract float FadeByController { get; set; }
}

然后就到了我们的视频过场序列,创建一个名字CinematicSequence.cs继承它

using System;
using UnityEngine;
using UnityEngine.Audio;

[RequireComponent(typeof(AudioSource))]
public class CinematicSequence : SkippableSequence
{
    private AudioSource audioSource;
    [SerializeField] private AudioMixerSnapshot atmosSnapshot;
    [SerializeField] private float atmosSnapshotTransitionDuration;
    [SerializeField] private CinematicVideoReference videoReference; //视频引用
    [SerializeField] private bool isLooping; //循环播放
    [SerializeField] private MeshRenderer targetRenderer;
    [SerializeField] private MeshRenderer blankerRenderer;

    private CinematicVideoPlayer videoPlayer;
    private bool didPlay;
    private bool isSkipped; //是否跳过
    private int framesSinceBegan; //视频的第几帧
    private float fadeByController;
    public CinematicVideoPlayer VideoPlayer
    {
	get
	{
	    return videoPlayer;
	}
    }
    public override bool IsPlaying
    {
	get
	{
	    bool flag = framesSinceBegan < 10 || !didPlay;
	    return !isSkipped && (flag || (videoPlayer != null && videoPlayer.IsPlaying));
	}
    }
    public override bool IsSkipped
    {
	get
	{
	    return isSkipped;
	}
    }
    public override float FadeByController
    {
	get
	{
	    return fadeByController;
	}
	set
	{
	    fadeByController = value;
	    if (videoPlayer != null)
	    {
		videoPlayer.Volume = fadeByController;
	    }
	    UpdateBlanker(1f - fadeByController);
	}
    }

    protected void Awake()
    {
	audioSource = GetComponent<AudioSource>();
	fadeByController = 1f;
    }

    protected void OnDestroy()
    {
	if (videoPlayer != null)
	{
	    videoPlayer.Dispose();
	    videoPlayer = null;
	}
    }

    protected void Update()
    {
	if (videoPlayer != null)
	{
	    framesSinceBegan++;
	    videoPlayer.Update();
	    if (!videoPlayer.IsLoading && !didPlay)
	    {
		didPlay = true;
		if (atmosSnapshot != null)
		{
		    atmosSnapshot.TransitionTo(atmosSnapshotTransitionDuration);
		}
		Debug.LogFormat(this, "Started cinematic '{0}'", new object[]
		{
		    videoReference.name
		});
		videoPlayer.Play();
	    }
	    if (!videoPlayer.IsPlaying && !videoPlayer.IsLoading && framesSinceBegan >= 10)
	    {
		Debug.LogFormat(this, "Stopped cinematic '{0}'", new object[]
		{
		    videoReference.name
		});
		videoPlayer.Dispose();
		videoPlayer = null;
		targetRenderer.enabled = false;
		return;
	    }
	    if (isSkipped)
	    {
		Debug.LogFormat(this, "Skipped cinematic '{0}'", new object[]
		{
		    videoReference.name
		});
		videoPlayer.Stop();
	    }
	}
    }
    public override void Begin()
    {
	if (videoPlayer != null && videoPlayer.IsPlaying)
	{
	    Debug.LogErrorFormat(this, "Can't play a cinematic sequence that is already playing", Array.Empty<object>());
	    return;
	}
	if (videoPlayer != null)
	{
	    videoPlayer.Dispose();
	    videoPlayer = null;
	    targetRenderer.enabled = false;
	}
	targetRenderer.enabled = true;
	videoPlayer = CinematicVideoPlayer.Create(new CinematicVideoPlayerConfig(videoReference, targetRenderer, audioSource, CinematicVideoFaderStyles.Black, GameManager.instance.GetImplicitCinematicVolume()));
	videoPlayer.IsLooping = isLooping;
	videoPlayer.Volume = FadeByController;
	isSkipped = false;
	framesSinceBegan = 0;
	UpdateBlanker(1f - fadeByController);
	Debug.LogFormat(this, "Started cinematic '{0}'", new object[]
	{
	    videoReference.name
	});
    }

    public override void Skip()
    {
	isSkipped = true;
    }

    private void UpdateBlanker(float alpha)
    {
	if (alpha > Mathf.Epsilon)
	{
	    if (!blankerRenderer.enabled)
	    {
		blankerRenderer.enabled = true;
	    }
	    blankerRenderer.material.color = new Color(0f, 0f, 0f, alpha);
	    return;
	}
	if (blankerRenderer.enabled)
	{
	    blankerRenderer.enabled = false;
	}
    }
}

回到Unity编辑器中,我们来给两个视频过场添加好参数:

然后是视频动画,这里更加简单,只需要开始时打开动画,然后等动画播放到一定阶段就关掉,接入下一个过场播放

using System;
using UnityEngine;

public class AnimatorSequence : SkippableSequence
{
    [SerializeField] private Animator animator;
    [SerializeField]private string animatorStateName;
    [SerializeField] private float normalizedFinishTime;

    private float fadeByController;
    private bool isSkipped;
    public override bool IsPlaying 
    {
	 get
	 {
	    return animator.isActiveAndEnabled && animator.GetCurrentAnimatorStateInfo(0).normalizedTime < Mathf.Min(normalizedFinishTime, 1f - Mathf.Epsilon);
	 }
    }
    public override bool IsSkipped
    {
	get
	{
	    return isSkipped;
	}
    }
    public override float FadeByController
    {
	get
	{
	    return fadeByController;
	}
	set
	{
	    fadeByController = value;
	}
    }

    protected void Awake()
    {
	fadeByController = 1f;
    }

    protected void Update()
    {
	if(animator.isActiveAndEnabled && animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= Mathf.Min(normalizedFinishTime, 1f - Mathf.Epsilon))
	{
	    animator.gameObject.SetActive(false);
	}
    }

    public override void Begin()
    {
	animator.gameObject.SetActive(true);
	animator.Play(animatorStateName, 0, 0f);
    }

    public override void Skip()
    {
	isSkipped = true;
	animator.Update(1000);
    }
}

最后就是用一个总的sequence管理这三个分开的sequence:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ChainSequence : SkippableSequence
{
    [SerializeField] private SkippableSequence[] sequences;
    private int currentSequenceIndex;
    private float fadeByController;
    private bool isSkipped;

    private SkippableSequence CurrentSequence
    {
	get
	{
	    if (currentSequenceIndex < 0 || currentSequenceIndex >= sequences.Length)
	    {
		return null;
	    }
	    return sequences[currentSequenceIndex];
	}
    }
    public bool IsCurrentSkipped
    {
	get
	{
	    return CurrentSequence != null && CurrentSequence.IsSkipped;
	}
    }
    public override bool IsPlaying
    {
	get
	{
	    return currentSequenceIndex < sequences.Length - 1 || (!(CurrentSequence == null) && CurrentSequence.IsPlaying);
	}
    }
    public override bool IsSkipped
    {
	get
	{
	    return isSkipped;
	}
    }
    public override float FadeByController
    {
	get
	{
	    return fadeByController;
	}
	set
	{
	    fadeByController = Mathf.Clamp01(value);
	    for (int i = 0; i < sequences.Length; i++)
	    {
		sequences[i].FadeByController = fadeByController;
	    }
	}
    }

    public delegate void TransitionedToNextSequenceDelegate();
    public event TransitionedToNextSequenceDelegate TransitionedToNextSequence;

    protected void Awake()
    {
	fadeByController = 1f;
    }

    protected void Update()
    {
	if(CurrentSequence != null && !CurrentSequence.IsPlaying && !isSkipped)
	{
	    Next();
	}
    }

    public override void Begin()
    {
	isSkipped = false;
	currentSequenceIndex = -1;
	Next();
    }

    private void Next()
    {
	SkippableSequence currentSequence = CurrentSequence;
	if(currentSequence != null)
	{
	    currentSequence.gameObject.SetActive(false);
	}
	currentSequenceIndex++;
	if (!isSkipped)
	{
	    if(CurrentSequence != null)
	    {
		CurrentSequence.gameObject.SetActive(true);
		CurrentSequence.Begin();
	    }
	    if(TransitionedToNextSequence != null)
	    {
		TransitionedToNextSequence();
	    }
	}
    }

    public override void Skip()
    {
	isSkipped = true;
	for (int i = 0; i < sequences.Length; i++)
	{
	    sequences[i].Skip();
	}
    }

    public void SkipSingle()
    {
	if (CurrentSequence != null)
	{
	    CurrentSequence.Skip();
	}
    }
}

最后的最后,我们还需要在cinematic过场播放的时候让后面的教学关卡和小骑士关卡都已经加载完成,也就是我们要异步的加载后面的场景,所以还需要一个脚本,

using System;
using System.Collections;
using GlobalEnums;
using UnityEngine;
using UnityEngine.SceneManagement;

public class OpeningSequence : MonoBehaviour
{
    [SerializeField] private ChainSequence chainSequence;
    [SerializeField] private ThreadPriority streamingLoadPriority;
    [SerializeField] private ThreadPriority completedLoadPriority;
    [SerializeField] private float skipChargeDuration; //跳过不同Sequence之间的冷却时间
    private bool isAsync;
    private bool isLevelReady;
    private AsyncOperation asyncKnightLoad;
    private AsyncOperation asyncWorldLoad;
    private float skipChargeTimer; // 计时器

    protected void OnEnable()
    {
	chainSequence.TransitionedToNextSequence += OnChangingSequences;
    }

    protected void OnDisable()
    {
	chainSequence.TransitionedToNextSequence -= OnChangingSequences;
    }

    protected IEnumerator Start()
    {
	isAsync = Platform.Current.FetchScenesBeforeFade;
	if (isAsync)
	{
	    return StartAsync();
	}
	return StartAsync();
    }

    protected void Update()
    {
	skipChargeTimer += Time.unscaledDeltaTime;
    }

    private static bool IsLevelReady(AsyncOperation operation)
    {
	return operation.progress >= 0.9f;
    }

    private IEnumerator StartAsync()
    {
	GameCameras.instance.cameraFadeFSM.Fsm.Event("FADE SCENE IN");

	PlayMakerFSM.BroadcastEvent("START FADE OUT");
	Debug.LogFormat(this, "Starting opening sequence.", Array.Empty<object>());
	GameManager.instance.ui.SetState(UIState.CUTSCENE);
	GameManager.instance.inputHandler.SetSkipMode(SkipPromptMode.NOT_SKIPPABLE_DUE_TO_LOADING);
	chainSequence.Begin();
	ThreadPriority lastLoadPriority = Application.backgroundLoadingPriority;
	Application.backgroundLoadingPriority = streamingLoadPriority;
	asyncKnightLoad = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync("Knight_Pickup", LoadSceneMode.Additive);
	asyncKnightLoad.allowSceneActivation = false;
	asyncWorldLoad = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync("Tutorial_01", LoadSceneMode.Single);
	asyncWorldLoad.allowSceneActivation = false;
	isLevelReady = false;
	while (chainSequence.IsPlaying)
	{
	    if (!isLevelReady)
	    {
		isLevelReady = (IsLevelReady(asyncKnightLoad) && IsLevelReady(asyncWorldLoad));
		if (isLevelReady)
		{
		    Debug.LogFormat(this, "Levels are ready before cinematics are finished. Cinematics made skippable.", Array.Empty<object>());
		}
	    }
	    SkipPromptMode skipPromptMode;
	    if(chainSequence.IsCurrentSkipped || skipChargeTimer < skipChargeDuration)
	    {
		skipPromptMode = SkipPromptMode.NOT_SKIPPABLE;
	    }
	    else if (!isLevelReady)
	    {
		skipPromptMode = SkipPromptMode.NOT_SKIPPABLE_DUE_TO_LOADING;
	    }
	    else
	    {
		skipPromptMode = SkipPromptMode.SKIP_PROMPT;
	    }
	    if(GameManager.instance.inputHandler.skipMode != skipPromptMode)
	    {
		GameManager.instance.inputHandler.SetSkipMode(skipPromptMode);
	    }
	    yield return null;
	}
	if (!isLevelReady)
	{
	    Debug.LogFormat(this, "Cinematics are finished before levels are ready. Blocking.", Array.Empty<object>());
	}
	Application.backgroundLoadingPriority = completedLoadPriority;
	GameManager.instance.inputHandler.SetSkipMode(SkipPromptMode.NOT_SKIPPABLE);
	yield return new WaitForSeconds(1.2f);
	asyncKnightLoad.allowSceneActivation = true;
	yield return asyncKnightLoad;
	asyncKnightLoad = null;
	GameManager.instance.OnWillActivateFirstLevel();
	asyncWorldLoad.allowSceneActivation = true;
	GameManager.instance.nextSceneName = "Tutorial_01";
	yield return asyncWorldLoad;
	asyncWorldLoad = null;
	Application.backgroundLoadingPriority = lastLoadPriority;
	UnityEngine.SceneManagement.SceneManager.UnloadSceneAsync(gameObject.scene);
	GameManager.instance.SetupSceneRefs(true);
	GameManager.instance.BeginScene();
	GameManager.instance.OnNextLevelReady();
    }

    private IEnumerator StartSync()
    {
	GameCameras.instance.cameraFadeFSM.Fsm.Event("FADE SCENE IN");

	PlayMakerFSM.BroadcastEvent("START FADE OUT");
	Debug.LogFormat(this, "Starting opening sequence.", Array.Empty<object>());
	GameManager.instance.ui.SetState(UIState.CUTSCENE);
	chainSequence.Begin();
	while (chainSequence.IsPlaying)
	{
	    SkipPromptMode skipPromptMode;
	    if (chainSequence.IsCurrentSkipped || skipChargeTimer < skipChargeDuration)
	    {
		skipPromptMode = SkipPromptMode.NOT_SKIPPABLE;
	    }
	    else
	    {
		skipPromptMode = SkipPromptMode.SKIP_PROMPT;
	    }
	    if (GameManager.instance.inputHandler.skipMode != skipPromptMode)
	    {
		GameManager.instance.inputHandler.SetSkipMode(skipPromptMode);
	    }
	    yield return null;
	}
	GameManager.instance.inputHandler.SetSkipMode(SkipPromptMode.NOT_SKIPPABLE);
	AsyncOperation asyncOperation = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync("Knight_Pickup", LoadSceneMode.Additive);
	asyncOperation.allowSceneActivation = true;
	yield return asyncOperation;
	GameManager.instance.OnWillActivateFirstLevel();
	GameManager.instance.nextSceneName = "Tutorial_01";
	AsyncOperation asyncOperation2 = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync("Tutorial_01", LoadSceneMode.Single);
	asyncOperation2.allowSceneActivation = true;
	yield return asyncOperation2;
	UnityEngine.SceneManagement.SceneManager.UnloadSceneAsync(gameObject.scene);
	GameManager.instance.SetupSceneRefs(true);
	GameManager.instance.BeginScene();
	GameManager.instance.OnNextLevelReady();

    }

    public IEnumerator Skip()
    {
	Debug.LogFormat("Opening sequience skipping.", Array.Empty<object>());
	chainSequence.SkipSingle();
	while (chainSequence.IsCurrentSkipped)
	{
	    skipChargeTimer = 0f;
	    yield return null;
	}
	yield break;
    }

    private void OnChangingSequences()
    {
	Debug.LogFormat("Opening sequience changing sequences.", Array.Empty<object>());
	skipChargeTimer = 0f;
	if (isAsync && asyncKnightLoad != null && !asyncKnightLoad.allowSceneActivation)
	{
	    asyncKnightLoad.allowSceneActivation = true;
	}
    }
}

这个Knight_Pickup场景究竟是啥呢?其实就是只有一个小骑士的场景:然后还要再加一个playmaker Unity 2D,不然看到红色的报错眼睛就烦了

二、制作跳过过场Cutscene的MenuScreen屏幕

              

        最后我们还需要制作能够跳过过场的文字提示,也就是UIManager底下新的Screen屏幕,我们先来制作好:

 

这里有个脚本名字叫:CinematicSkipPopup.cs:我们用淡入淡出的手法显示可跳过提示的文字:并根据你按任意键的持续时间来显示这段提示文字等等

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CinematicSkipPopup : MonoBehaviour
{
    private CanvasGroup canvasGroup;

    [SerializeField] private GameObject[] textGroups;
    [SerializeField] private float fadeInDuration;
    [SerializeField] private float holdDuration;
    [SerializeField]private float fadeOutDuration;

    private bool isShowing;
    private float showTimer;

    protected void Awake()
    {
	canvasGroup = GetComponent<CanvasGroup>();
    }

    protected void Update()
    {
	if (isShowing)
	{
	    float alpha = Mathf.MoveTowards(canvasGroup.alpha, 1f, Time.unscaledDeltaTime / fadeInDuration);
	    canvasGroup.alpha = alpha;
	    return;
	}
	float num = Mathf.MoveTowards(canvasGroup.alpha, 0f, Time.unscaledDeltaTime / fadeOutDuration);
	canvasGroup.alpha = num;
	if (num < Mathf.Epsilon)
	{
	    Hide();
	    gameObject.SetActive(false);
	}
    }

    public void Show(Texts texts)
    {
	Debug.LogFormat("Show the CinematicSkipPopup");
	base.gameObject.SetActive(true);
	for (int i = 0; i < textGroups.Length; i++)
	{
	    textGroups[i].SetActive(i == (int)texts);
	}
	StopCoroutine("ShowRoutine");
	StartCoroutine("ShowRoutine");
    }

    protected IEnumerator ShowRoutine()
    {
	isShowing = true;
	yield return new WaitForSecondsRealtime(fadeInDuration);
	yield return new WaitForSecondsRealtime(holdDuration);
	isShowing = false;
	yield break;
    }

    public void Hide()
    {
	StopCoroutine("ShowRoutine");
	isShowing = false;
    }

    public enum Texts
    {
	Skip,
	Loading
    }
}

回到UIManager.cs中,用show和hide两个函数制作:

 [Header("Cinematics")]
    [SerializeField] private CinematicSkipPopup cinematicSkipPopup;

  public void ShowCutscenePrompt(CinematicSkipPopup.Texts text)
    {
	cinematicSkipPopup.gameObject.SetActive(true);
	cinematicSkipPopup.Show(text);
    }

    public void HideCutscenePrompt()
    {
	cinematicSkipPopup.Hide();
    }

 


总结

最后我们来看看效果吧,回到上一期写完的选择存档场景:

点击,场景淡出,进入Opening_Sequence:

播放诗歌:

播放第一个视频片段:

播放第二个视频片段:

然后这些都是可以跳过的,最后就来到了教学关卡 了:

OK我们终于完成了开场的过场顺序的播放,也制作了一个相对完善的过场系统。 


http://www.kler.cn/a/381463.html

相关文章:

  • Multimodal Reasoning with Multimodal Knowledge Graph
  • GD32H7XX软件触发中断
  • 你知道Mac也能拥有丰富的右键菜单栏吗?
  • ElasticSearch学习篇16_《检索技术核心20讲》进阶篇之空间检索
  • sql server 文件备份恢复
  • 虚拟化环境中的精简版 Android 操作系统 Microdroid
  • Java 实训第11天 枚举
  • 北京交通大学机器学习实验
  • winform 加载 office excel 插入QRCode图片如何设定位置
  • selenium操作已开启的浏览器,方便调试
  • Python数据分析NumPy和pandas(二十四、数据整理--连接、合并和重塑 之一:分层索引)
  • 51单片机教程(八)- 数码管的静态显示
  • 云轴科技ZStack 联合 OpenCloudOS 完成技术兼容互认证
  • 目标检测YOLO实战应用案例100讲-基于深度学习的人眼视线检测
  • 【云原生开发】如何通过client-go来操作K8S集群
  • 排序算法详细总结
  • 导师双选系统开发新解:Spring Boot技术
  • 练手之基于python的新闻爬虫
  • github.com port 22
  • 基于TRIZ理论的便携式光伏手机充电装置创新
  • 《Linux系统编程篇》消息队列(Linux 进程间通信(IPC))——基础篇
  • docker配置与基础操作
  • Go语言组合和转发
  • 通过自然语言表达你的想法。GitHub Spark让任何人都能使用人工智能,为自己创建软件...
  • Spring Boot环境下的导师双选流程优化
  • 鸿蒙ArkTS中的布局容器组件(Column、Row、Flex、 Stack、Grid)