[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里面的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.rhkb.cn/news/465295.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【设计模式系列】桥接模式(十三)

一、什么是桥接模式 桥接模式&#xff08;Bridge Pattern&#xff09;是一种结构型设计模式&#xff0c;其核心目的是将抽象部分与实现部分分离&#xff0c;使它们可以独立地变化。这种模式主要用于处理那些在设计时无法确定实现细节的场合&#xff0c;或者需要在多个实现之间…

基于Multisim光控夜灯LED电路(含仿真和报告)

【全套资料.zip】光控夜灯LED电路设计Multisim仿真设计数字电子技术 文章目录 功能一、Multisim仿真源文件二、原理文档报告资料下载【Multisim仿真报告讲解视频.zip】 功能 光控夜灯LED电路 1.采用纯数字电路&#xff0c;非单片机。 2.通过检测周围光线&#xff0c;光线暗自…

html练习2

实现下列图片的效果 代码&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title><style>* {margin: 0;padding: 0;}#menu {background-color: #0c0048;width: 100%;height: 50px;margin: auto;…

【毫米波雷达(八)】车载毫米波前雷达遮挡检测功能

车载毫米波前雷达遮挡检测功能 一、概念二、功能指标1、遮挡检测功能2、功能流程3、实车验证 一、概念 随着汽车行业智能化发展&#xff0c;车载毫米波雷达在汽车市场应用越来越广泛。在驾驶过程中&#xff0c;当雷达受到泥土、纸巾、冰雪覆盖遮挡后&#xff0c;雷达检测性能受…

小新学习k8s第六天之pod详解

一、资源限制 Pod是k8s中的最小的资源管理组件&#xff0c;pod也是最小化运行容器化应用的资源对象。一个Pod代表着集群中运行的一个进程。k8s中其他大多数组件都是围绕着Pod来进行支撑和扩展Pod功能的&#xff0c;例如&#xff0c;用于管理Pod运行的StatefulSet和Deployment等…

java面试2.0

一.Zookeeper 1.定义 ZooKeeper 是一个开源的分布式协调服务&#xff0c;它的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来&#xff0c;构成一个高效可靠的原语集&#xff0c;并以一系列简单易用的接口提供给用户使用。 ZooKeeper 为我们提供了高可用、高性能…

游戏测试|超越QA的常规:我们如何自动化回归测试

QA测试工作并不单调乏味&#xff0c;它是一项创造性的工作&#xff0c;蕴含着丰富的机会。公平地说&#xff0c;它也有枯燥乏味的一面--回归&#xff08;regression&#xff09;。因此&#xff0c;我们决定将回归测试自动化&#xff0c;具体方法如下。 ​ 在IT行业&#xff0c…

群分解(Swarm Decomposition,SWD)

代码原理 群体分解&#xff08;SWD&#xff09;是一种用于信号处理和数据分析的新兴方法。它通过将复杂的信号分解为多个群体成分&#xff08;Swarm Components&#xff09;&#xff0c;每个成分代表信号中的特定特征或模式。SWD的主要目标是提取信号中的不同特征模式&#xf…

flink实战-- flink任务的火焰图如何使用

火焰图 Flame Graphs 是一种有效的可视化工具,可以帮助我们排查如下问题: 目前哪些方法正在消耗 CPU 资源?一个方法的消耗与其他方法相比如何?哪一系列的堆栈调用导致了特定方法的执行?y 轴表示调用栈,每一层都是一个函数。调用栈越深,火焰就越高,顶部就是正在执行的…

CSS基础知识六(浮动的高度塌陷问题及解决方案)

目录 1.浮动高度塌陷概念 2.下面是几种解决高度塌陷的几种方案&#xff1a; 解决方案一&#xff1a; 解决方案二&#xff1a; 解决方案三&#xff1a; 1.浮动高度塌陷概念 在CSS中&#xff0c;高度塌陷问题指的是父元素没有正确地根据其内部的浮动元素或绝对定位元素来计…

拒绝事后背锅:测试项目中的风险管理一定要知道

在博主的公司中&#xff0c;测试经理除了要管理产品线的质量保障和日常部门事务工作外&#xff0c;另一项比较重要的就是测试项目全流程的管理。 今天不聊整体的测试项目流程如何开展&#xff0c;而是想聊一聊在同行中比较高频出现的一个字眼&#xff1a;风险管理。 什么是风…

基础算法——排序算法(冒泡排序,选择排序,堆排序,插入排序,希尔排序,归并排序,快速排序,计数排序,桶排序,基数排序,Java排序)

1.概述 比较排序算法 算法最好最坏平均空间稳定思想注意事项冒泡O(n)O( n 2 n^2 n2)O( n 2 n^2 n2)O(1)Y比较最好情况需要额外判断选择O( n 2 n^2 n2)O( n 2 n^2 n2)O( n 2 n^2 n2)O(1)N比较交换次数一般少于冒泡堆O( n l o g n nlogn nlogn)O( n l o g n nlogn nlogn)O( n l…

利用pythonstudio写的PDF、图片批量水印生成器,可同时为不同读者生成多组水印

现在很多场合需要将PDF或图片加水印&#xff0c;本程序利用pythonstudio编写。 第一步 界面 其中&#xff1a; LstMask:列表框 PopupMenu:PmnMark LstFiles:列表框 PopupMenu:PmnFiles OdFiles:文件选择器 Filter:PDF文件(.PDF)|.PDF|图像文件(.JPG)|.JPG|图像文件(.png…

如何区分实例化网格中的每个实例

1&#xff09;如何区分实例化网格中的每个实例 2&#xff09;项目在模拟器上切换程序后有概率画面冻结 3&#xff09;Unity工程导入团结引擎&#xff0c;GUID会变化&#xff0c;导致引用关系丢失 4&#xff09;Mask在Android平台下渲染异常 这是第407篇UWA技术知识分享的推送&a…

前端前置——ajax

目标&#xff1a;使用axios库&#xff0c;获取省份列表数据&#xff0c;展示到页面上 axios库地址&#xff1a;https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js 省份数据地址&#xff1a;http://hmajax.itheima.net/api/province axios的使用 axios({ url:目标资源地…

oasys系统代码审计

简述&#xff1a; oasys是一个OA办公自动化系统&#xff0c;使用Maven进行项目管理&#xff0c;基于springboot框架开发的项目&#xff0c;mysql底层数据库&#xff0c;前端采用freemarker模板引擎&#xff0c;Bootstrap作为前端UI框架&#xff0c;集成了jpa、mybatis等框架。…

书生大模型第三关Git 基础知识

关卡编号&#xff1a;L0G3000 任务一 破冰行动 fork仓库&#xff0c;注意这里不要勾选Copy branch Only!!!&#xff0c;因为后面课程中会使用到class分支&#xff1a; 克隆仓库&#xff1a; 移动分支&#xff1a; 创建自己的分支&#xff1a; 创建id.md文档&#xff0c;…

在vue3的vite网络请求报错 [vite] http proxy error:

在开发的过程中 代理proxy报错: [vite] http proxy error: /ranking/hostRank?dateType1 Error: connect ETIMEDOUT 43.xxx.xxx.xxx:443 网络请求是http的: // vite.config.ts import { Agent } from node:http;server: {host: 0.0.0.0,port: port,open: true,https: false,…

初识HTML

什么是HTML呢&#xff1f; HTML是超文本标记语言&#xff0c;HTML代码是由“标签”构成的 超文本&#xff1a;文本、声音、图片、视频、表格、链接 标记&#xff1a;由许许多多的标签组成 HTML页面是运行到浏览器上面的 第一个HTML程序 和C语言从hello world开始一样 HTML可…

DevOps-课堂笔记

各种 aaS 类比于计算机网络的 OSI 参考模型&#xff0c;一个软件应用项目需要不同的支撑层&#xff0c;例如从下至上大概需要&#xff1a; 硬件层面的服务器针对硬件做弹性分配的虚拟化机制&#xff0c;例如虚拟机在虚拟化环境内运行的 OS支撑软件应用的中间件&#xff0c;例…