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

news/2024/11/6 11:44:10/

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

文章目录

  • 前言
  • 一、制作过场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里面的EmbeddedVideoClipvideoPlayer.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.")] //允许玩家跳过videopublic 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和OpeningPrologueif (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.ppmy.cn/news/1544809.html

相关文章

Webserver(3.3)生产者消费者模型

目录 生产者消费者简单模型条件变量信号变量 生产者消费者简单模型 //生产者消费者模型#include <stdio.h> #include<pthread.h> #include<stdlib.h> #include<unistd.h>struct Node{int num;struct Node * next; }; //头结点 struct Node * headNULL…

unreal engine5动画重定向

UE5系列文章目录 文章目录 UE5系列文章目录前言一、下载动画资源二、创建IK Rig&#xff08;IK绑定&#xff09; 前言 在Unreal Engine 5.4中&#xff0c;动画重定向&#xff08;Animation Retargeting&#xff09;和动作匹配&#xff08;Motion Matching&#xff09;是两种不…

PL端:LED闪烁

实验环境 vivado2024.1 实验任务 LED闪烁 引脚关系 硬件配置 新建一个vivado实验 创建 Verilog HDL 文件点亮 LED 点击 Project Manager 下的 Add Sources 图标&#xff08;或者使用快捷键 AltA&#xff09; 编辑led.v module led(input sys_clk,input rst_n,outp…

C语言中如何实现动态内存分配

在C语言中&#xff0c;动态内存分配是通过标准库中的malloc、calloc和free函数实现的。这些函数允许程序在运行时请求内存&#xff0c;从而提供灵活性&#xff0c;尤其是在不知道所需内存大小的情况下。下面是对这三个函数的详细解释和使用示例。 1. malloc malloc&#xff0…

electron 中 ipcRenderer 作用

1. 理解 IPC&#xff08;进程间通信&#xff09;的背景 在 Electron 应用中&#xff0c;有主进程&#xff08;main process&#xff09;和渲染进程&#xff08;renderer process&#xff09;之分。 主进程&#xff1a;负责管理应用程序的生命周期、创建和管理窗口等核心任务。…

【已解决】element-plus配置主题色后,sass兼容问题。set-color-mix-level() is...in Dart Sass 3

项目&#xff1a;vue3vite "scripts": {"dev": "vite","build": "vite build","preview": "vite preview"},"dependencies": {"element-plus/icons-vue": "^2.3.1",&quo…

51单片机教程(八)- 数码管的静态显示

1、项目分析 使用数码管显示指定的字符、数字和符号。 2、技术准备 1、显示器及其接口 单片机系统中常用的显示器有&#xff1a; 发光二极管LED&#xff08;Light Emitting Diode&#xff09;显示器、液晶LCD&#xff08;Liquid Crystal Display&#xff09;显示器、CRT显…

【Vue】在 Vue 组件的 methods 中,箭头函数和不带箭头函数中的this的区别

具体说明 箭头函数在定义时就绑定了它的 this&#xff0c;这个 this 通常是组件定义环境的上下文&#xff08;即创建 Vue 实例之前的环境&#xff09;&#xff0c;而不是 Vue 实例本身。这意味着在 Vue 组件的 methods 中使用箭头函数时&#xff0c;this 通常不会指向 Vue 实例…