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

news/2024/11/6 7:42:25 标签: unity, 游戏引擎, playmaker, c#, visualstudio

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

文章目录

  • 前言
  • 一、制作过场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.niftyadmin.cn/n/5740581.html

相关文章

elementUI 点击弹出时间 date-picker

elementUI的日期组件&#xff0c;有完整的UI样式及弹窗&#xff0c;但是我的页面不要它的UI样式&#xff0c;点击的时候却要弹出类似的日期选择器&#xff0c;那怎么办呢&#xff1f; 以下是elementUI自带的UI风格&#xff0c;一定要一个输入框来触发。 这是我的项目中要用到的…

Docker-- cgroups资源控制实战

上一篇&#xff1a;容器化和虚拟化 什么是cgroups&#xff1f; cgroups是Linux内核中的一项功能&#xff0c;最初由Google的工程师提出&#xff0c;后来被整合进Linux内核; 它允许用户将一系列系统任务及其子任务整合或分隔到按资源划分等级的不同组内&#xff0c;从而为系统…

qt QFontDialog详解

1、概述 QFontDialog 是 Qt 框架中的一个对话框类&#xff0c;用于选择字体。它提供了一个可视化的界面&#xff0c;允许用户选择所需的字体以及相关的属性&#xff0c;如字体样式、大小、粗细等。用户可以通过对话框中的选项进行选择&#xff0c;并实时预览所选字体的效果。Q…

蓝桥杯2021年题解(IP补充)

问题&#xff1a;&#xff08;填空题&#xff09; 小蓝的IP地址为 192.168.*.21&#xff0c;其中 * 是一个数字&#xff0c;请问这个数字最大可能是多少&#xff1f; 答案&#xff1a;255 在IP地址中&#xff0c;每个数字的范围是0&#xff5e;255&#xff0c;所以*最大是25…

鸿蒙网络编程系列43-仓颉版HttpRequest下载文件示例

1. HttpRequest文件下载简介 在本系列的第10篇文章《鸿蒙网络编程系列10-使用HttpRequest下载文件到本地示例》中&#xff0c;使用ArkTS语言在API 9环境下演示了基于HttpRequest进行文件下载的功能&#xff0c;本章将使用仓颉语言在API 12环境下实现类似的功能。因为本示例使用…

[mysql]修改表和课后练习

目录 DDL数据定义语言 添加一个字段 添加一个字段到最后一个 添加到表中的第一个一个字段 选择其中一个位置: 修改一个字段:数据类型,长度,默认值(略) 重命名一个字段 删除一个字段 重命名表 删除表 清空表 DCL中事务相关内容 DCL中COMMIT和ROLLBACK的讲解 对比TR…

Zookeeper运维秘籍:四字命令基础、详解及业务应用全解析

文章目录 一、四字命令基础二、四字命令详解三、四字命令的开启与配置四、结合业务解读四字命令confconsenvi命令Stat命令MNTR命令ruok命令dump命令wchswchp ZooKeeper&#xff0c;作为一款分布式协调服务&#xff0c;提供了丰富的四字命令&#xff08;也称为四字短语&#xff…

Flutter下拉刷新上拉加载的简单实现方式一

方式一&#xff1a;RefreshIndicatorListView实现 import package:flutter/material.dart;class SimpleRefreshDemoPage extends StatefulWidget {const SimpleRefreshDemoPage({super.key});overrideState<StatefulWidget> createState() {return _SimpleRefreshDemoPa…