Unity3D仿星露谷物语开发26之创建场景控制管理器
1、目标
创建场景控制管理器,来加载和卸载场景,以实现场景之间的切换。
2、思路
Fade To Back是黑色的过渡场景,透明度逐渐变为1。
Fade To Transparent To Show Scene:黑色消失的过渡场景,透明度逐渐变为0.
事件触发机制:
3、代码编写
(1)优化Enum.cs脚本
创建Scene的枚举类
public enum SceneName
{
Scene1_Farm,
Scene2_Field,
Scene3_Cabin
}
(2)优化EventHandler.cs脚本
添加4个阶段的事件:
// Scene Load Events - in the order they happen
// Before Scene Unload Fade Out Event
public static event Action BeforeSceneUnloadFadeOutEvent;
public static void CallBeforeSceneUnloadFadeOutEvent()
{
if(BeforeSceneUnloadFadeOutEvent != null)
{
BeforeSceneUnloadFadeOutEvent();
}
}
// Before Scene Unload Event
public static event Action BeforeSceneUnloadEvent;
public static void CallBeforeSceneUnloadEvent()
{
if( BeforeSceneUnloadEvent != null)
{
BeforeSceneUnloadEvent();
}
}
// After Scene Loaded Event
public static event Action AfterSceneLoadEvent;
public static void CallAfterSceneLoadEvent()
{
if(AfterSceneLoadEvent != null)
{
AfterSceneLoadEvent();
}
}
// After Scene Load Fade In Event
public static event Action AfterSceneLoadFadeInEvent;
public static void CallAfterSceneLoadFadeInEvent()
{
if(AfterSceneLoadFadeInEvent != null)
{
AfterSceneLoadFadeInEvent();
}
}
(3)创建SceneControllerManager.cs脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class SceneControllerManager : SingletonMonobehaviour<SceneControllerManager>
{
private bool isFading;
[SerializeField] private float fadeDuration = 1f;
[SerializeField] private CanvasGroup faderCanvasGroup = null;
[SerializeField] private Image faderImage = null;
public SceneName startingSceneName;
// This is the main external point of contact and influence from the rest of the project.
// This will be called when the player wants to switch scenes.
// sceneName:目标场景名称
// spawnPosition: 主角出现的位置
public void FadeAndLoadScene(string sceneName, Vector3 spawnPosition)
{
// If a fade isn't happening then start fading and switching scenes.
if (!isFading)
{
StartCoroutine(FadeAndSwitchScenes(sceneName, spawnPosition));
}
}
// This is the coroutine where the 'building blocks' of the script are put together.
private IEnumerator FadeAndSwitchScenes(string sceneName, Vector3 spawnPosition)
{
// Call before scene unload fade out event
EventHandler.CallBeforeSceneUnloadFadeOutEvent();
// Start fading to block and wait for it to finish before continuing.
yield return StartCoroutine(Fade(1f)); // 变黑色
// Set player position
Player.Instance.gameObject.transform.position = spawnPosition;
// Call before scene unload event.
EventHandler.CallBeforeSceneUnloadEvent();
// Unload the current active scene.
yield return SceneManager.UnloadSceneAsync(SceneManager.GetActiveScene().buildIndex);
// Start loading the given scene and wait for it to finish.
yield return StartCoroutine(LoadSceneAndSetActive(sceneName));
// Call after scene load event
EventHandler.CallAfterSceneLoadEvent();
// Start fading back in and wait for it to finish before exiting the function.
yield return StartCoroutine(Fade(0f)); // 变白色
// Call after scene load fade in event
EventHandler.CallAfterSceneLoadFadeInEvent();
}
private IEnumerator Fade(float finalAlpha)
{
// Set the fading flag to true so the FadeAndSwitchScenes coroutine won't be called again.
isFading = true;
// Make sure the CanvasGroup blocks raycasts into the scene so no more input can be accepted.
faderCanvasGroup.blocksRaycasts = true;
// Calculate how fast the CanvasGroup should fade based on it's current alpha,
// it's final alpha and how long it has to change between the two.
float fadeSpeed = Mathf.Abs(faderCanvasGroup.alpha - finalAlpha) / fadeDuration;
// while the CanvasGroup hasn't reached the final alpha yet...
while( !Mathf.Approximately(faderCanvasGroup.alpha, finalAlpha))
{
// ... move the alpha towards it's target alpha.
faderCanvasGroup.alpha = Mathf.MoveTowards(faderCanvasGroup.alpha, finalAlpha,
fadeSpeed * Time.deltaTime);
// Wait for a frame then continue.
yield return null;
}
// Set the flag to false since the fade has finished.
isFading = false;
// Stop the CanvasGroup from blocking raycasts so input is no longer ignored.
faderCanvasGroup.blocksRaycasts = false;
}
private IEnumerator LoadSceneAndSetActive(string sceneName)
{
// Allow the given scene to load over serval frames and add it to the already
// loaded scenes (just the Persistent scene at this point).
yield return SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
// Find the scene that was most recently loaded (the one at the last index of the loaded scenes).
Scene newlyLoadedScene = SceneManager.GetSceneAt(SceneManager.sceneCount - 1);
// Set the newly loaded scene as the active scene(this marks it as the one to be unloaded next).
SceneManager.SetActiveScene(newlyLoadedScene);
}
private IEnumerator Start()
{
// Set the initial alpha to start off with a block screen.
faderImage.color = new Color(0f, 0f, 0f, 1f);
faderCanvasGroup.alpha = 1f;
// Start the first scene loading and wait for it to finish
yield return StartCoroutine(LoadSceneAndSetActive(startingSceneName.ToString()));
// If this event has any subscribers, call it
EventHandler.CallAfterSceneLoadEvent();
// Once the scene is finished loading, start fading in
StartCoroutine(Fade(0f));
}
}
- CanvasGroup的alpha值,其子元素的alpha是子元素alpha和对应CanvasGroup中alpha的乘积。
- 周期函数中的Start方法可以直接当成协程用,使用方法也很简单,直接将返回值void改成IEnumerator即可。Update,LateUpdate,FixedUpdate,Awake,OnEnable等都不能这么用。
(4)优化UIInventorySlot.cs脚本
对于如下的代码:
parentItem = GameObject.FindGameObjectWithTag(Tags.ItemsParentTransform).transform;、
在切换场景的过程中,我们并不能总是找到他们。因为场景加载需要时间,可能场景还没加载完成就在取值了。
所以需要订阅加载完成的事件,在事件中处理元素的获取。
添加如下代码:
private void OnDisable()
{
EventHandler.AfterSceneLoadEvent -= SceneLoaded;
}
private void OnEnable()
{
EventHandler.AfterSceneLoadEvent += SceneLoaded;
}
public void SceneLoaded()
{
parentItem = GameObject.FindGameObjectWithTag(Tags.ItemsParentTransform).transform;
}
同时注释掉Start()中的元素获取方法。
(5)优化SwitchConfineBoundingShape.cs脚本
元素获取基于同样的理由,需要在场景确认加载完毕后才能去获取。
using UnityEngine;
using Cinemachine;
public class SwitchConfine : MonoBehaviour
{
//private void Start()
//{
// SwitchBoundingShape();
//}
private void OnEnable()
{
EventHandler.AfterSceneLoadEvent += SwitchBoundingShape;
}
private void OnDisable()
{
EventHandler.AfterSceneLoadEvent -= SwitchBoundingShape;
}
/// <summary>
/// Switch the collider that cinemachine uses to define edges of the screen
/// </summary>
private void SwitchBoundingShape()
{
// Get the polygon collider on the 'boundsconfiner' gameobject which is used by Cinemachine to prevent the camera going beyond the screen edges
PolygonCollider2D polygonCollider2D = GameObject.FindGameObjectWithTag(Tags.BoundsConfiner).GetComponent<PolygonCollider2D>();
CinemachineConfiner cinemachineConfiner = GetComponent<CinemachineConfiner>();
cinemachineConfiner.m_BoundingShape2D = polygonCollider2D;
// since the confiner bounds have changed need to call this to clear the cache
cinemachineConfiner.InvalidatePathCache();
}
}
(6)优化Player.cs脚本
修改PlayerTestInput()方法。
private void PlayerTestInput()
{
// Trigger Advance Time
if (Input.GetKeyDown(KeyCode.T))
{
TimeManager.Instance.TestAdvanceGameMinute();
}
// Trigger Advance Day
if (Input.GetKeyDown(KeyCode.G))
{
TimeManager.Instance.TestAdvanceGameDay();
}
// Test scene unload / load
if (Input.GetKeyDown(KeyCode.L))
{
SceneControllerManager.Instance.FadeAndLoadScene(SceneName.Scene1_Farm.ToString(), transform.position);
}
}
4、创建对象
(1)FadeImage对象
在PersistentScene -> UI -> MainGameUICanvas -> UICanvasGroup下新建空对象命名为FadeImage。
作用:让屏幕变黑再变透明。
创建完之后,stretch让其铺满屏幕。
添加Image组件。
将颜色调整为黑色,并且将A置为0。
添加Canvas Group组件,并且勾选 Ignore Parent Groups选项,忽略父物体的管理。
(2)SceneControllerManager对象
在PersistentScene下新建空对象命名为SceneControllerManager。
添加SceneControllerManager组件,并且给元素赋值。
5、改变脚本的执行顺序
在多个脚本中,我们无法确定哪个脚本的Start()方法先被执行。
现在,在其他游戏对象开始访问第一个场景之前,我们需要确保SceneControllerManager对象已经在其Start()方法中加载了第一个场景。
Edit -> Project Settings -> Script Execution Order
在Default Time之前保证SceneControllerManager的脚本被执行。
6、卸载场景
卸载已有的Scene1_Farm场景,
点击该场景后选择 Unload Scene。
因为SceneControllerManager的Start方法会加载当前场景一次。
执行程序后,按"L"键后效果如下: