实现场景切换
1、所需使用知识点
1.1单列模式
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问(方便全局访问),不需要实例化该类的对象。
优点:在内存里面只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类只关心内部逻辑情况,而不关心外面怎么样来实例化。
注意
1.单例类只能有一个实例;
2.单例类必须自己创建自己的唯一实例;
3.单例类必须给所有其它对象提供这一实例。
泛型单例模式模板,可以直接套用。
using UnityEngine;
public class MySingleton<T> : MonoBehaviour where T: MySingleton<T>
{
private static T instance;
public static T GetInstance
{
get { return instance; }
}
protected virtual void Awake()
{
if(instance != null)
{
Destroy(gameObject);
}
else
{
instance = (T)this;
}
}
//判断当前单例模式下的Manager是否已经初始化生成了
public static bool IsInitialized
{
get { return instance != null; }//如果不为空,就是当前Manager已经生成了,返回True
}
//如果一个场景中有多个单例模式,需要将他销毁
protected virtual void OnDestroy()
{
if(instance == this)
{
instance = null;
}
}
}
这样一来,场景中需要单例化的脚本,只需要简单继承这个类就可以了
public class Manager : MySingleton<Manager>
{
public string testText = "hello Sinngleton!";
}
那么我们就不用实例化对象来调用Manager中一些方法了:
public class SingletonTest : MonoBehaviour {
void Start () {
Debug.Log(Manager.GetInstance.testText);
}
}
使用单例模式的情景:
在游戏中一定会有很多场景,比如每个场景都有背景音乐,我们就可以场景一个AudioManager脚本来管理所有音乐资源。如果想播放音乐,在其它脚本中我们只需要写AudioManager.GetInstance.PlayerSound();
如果不使用单例模式,在其它脚本中调用方法,我们需要先声明AudioManager,会加大运算时间。
1.2协程使用
协程在Unity中是一个很重要的概念,我们知道,在使用Unity进行游戏开发时,一般(注意是一般)不考虑多线程。
协程与线程区别?
- 对于协程而言,同一时间只能执行一个协程,而线程它是并发的,可以同时有多个线程运行。
- 两者在内存上使用是相同的,共享堆,不共享栈。
什么是协程?
协程协程字面意思就是帮助程序运行,在主任务运行的同时,需要一些分支任务来配合工作完成来达到最终效果。
稍微形象的解释一下,想象一下,在进行主任务的过程中我们需要一个对资源消耗极大的操作时候,如果在一帧中实现这样的操作,游戏就会变得十分卡顿,这个时候,我们就可以通过协程,在一定帧内完成该工作的处理,同时不影响主任务的进行。
协程原理
协程不是线程,但是协程仍然在主线程中运行。
协程是通过迭代器来实现功能的,通过关键字IEnumerator
来定义一个迭代方法,注意使用的是IEnumerator
,而不是IEnumerable
:
两者之间的区别:
IEnumerator
:是一个实现迭代器功能的接口IEnumerable
:是在IEnumerator
基础上的一个封装接口,有一个GetEnumerator()
方法返回IEnumerator
在迭代器中呢,最关键的是yield的使用,这是我们实现协程的主要途径。通过该关键方法,可以实现协程的运行暂停,记录下一次运行的时间和位置等。
协程在代码中运用
首先通过迭代器关键字IEnumerator来定义一个方法,然后在对应的程序中通过StartCoroutine来开启协程方法。
在正式代码之前先了解StartCoroutine的重载方式:
- StartCoroutine(协程的方法名):这种是没有参数的情况,直接通过方法名(字符串形式)来开启协程-
- StartCoroutine(IEnumerator routine):通过方法形式调用
- StartCoroutine(协程的方法名,参数):带参数的通过方法名进行调用
协程开启示例代码:
//通过迭代器定义一个方法
IEnumerator Demo(int i)
{
//代码块
yield return 0;
//代码块
}
//在程序种调用协程
public void Test()
{
//第一种与第二种调用方式,通过方法名与参数调用
StartCoroutine("Demo", 1);
//第三种调用方式, 通过调用方法直接调用
StartCoroutine(Demo(1));
}
协程有开启的方法,同样有协程结束的方法。StopCoroutine与StopAllCoroutines两种方式,但是需要注意的是,两者的使用需要遵循一定的规则,在介绍规则之前,同样介绍一下关于StopCoroutine重载:
StopCoroutine(string methodName)
:通过方法名(字符串)来进行StopCoroutine(IEnumerator routine)
:通过方法形式来调用StopCoroutine(Coroutine routine)
:通过指定的协程来关闭
刚刚我们说到他们的使用是有一定的规则的,那么规则是什么呢,答案是前两种结束协程方法的使用上,如果我们是使用StartCoroutine(string methodName)来开启一个协程的,那么结束协程就只能使用StopCoroutin(string methodName)和StopCoroutine(Coroutine routine)来结束协程。
关于yield
yield在Unity生命周期中的一些执行方法,不同的yield方法处于生命周期不同的位置。
由图可以看出yield的一些方法存在于Update和LateUpdate之间。
首先解释一下位于Update与LateUpdate之间这些yield 的含义:
yield return null; 暂停协程等待下一帧继续执行
yield return 0或其他数字; 暂停协程等待下一帧继续执行
yield return new WairForSeconds(时间); 等待规定时间后继续执行
yield return StartCoroutine(“协程方法名”);开启一个协程(嵌套协程)
通过代码来理解yield一些方法执行的顺序:
void Update()
{
Debug.Log("第一次");
StartCoroutine(myyield());
}
private void LateUpdate()
{
Debug.Log("第四次");
}
private IEnumerator myyield()
{
Debug.Log("第二次");
yield return 0;
Debug.Log("已经在下一帧执行");
}
运行结果:
可以很清晰的看出,协程虽然是在Update
中开启,但是关于yield return null
后面的代码会在下一帧运行,并且是在Update执行完之后才开始执行,但是会在LateUpdate
之前执行。
2、代码逻辑思路
2.1新建一个Teleport类
新建一个teleportToscene方法
public class Teleport : MonoBehaviour
{
[SceneName]public string sceneForm;
[SceneName]public string sceneTo;
public void teleportToscene()
{
TransitonManager.GetInstance.Transiton(sceneForm, sceneTo);
}
}
2.2具体场景转换方法
而实现场景转换的具体方法我们设置为一个单例类,以便后续我们直接访问调用。
public class TransitonManager : MySingleton<TransitonManager>
{
private bool IsFade;
public CanvasGroup FadeCanvasGroup;
public float fadeDuration;
public void Transiton(string from,string to)
{
StartCoroutine(TransitionToScene(from, to));
}
注:通过自己写的单例模板,MySingleton<自己的类名>就能直接变成单列类。
同时引入切换场景的命名空间:
using UnityEngine.SceneManagement;
2.3协程的运用
//协程
private IEnumerator TransitionToScene(string from,string to)
{
yield return Fade(1);
yield return SceneManager.UnloadSceneAsync(from);
yield return SceneManager.LoadSceneAsync(to, LoadSceneMode.Additive);
//设置新场景为激活场景
Scene newScene = SceneManager.GetSceneAt(SceneManager.sceneCount - 1);
SceneManager.SetActiveScene(newScene);
yield return Fade(0);
}
关于场景的API官方文档:SceneManagement.SceneManager - Unity 脚本 API
2.4实现场景切换的动态遮罩
通过控制画布的Alpha值来淡入淡出整个窗口
private IEnumerator Fade(float targetAlpha)
{
IsFade = true;
FadeCanvasGroup.blocksRaycasts = true;
float speed = Mathf.Abs(FadeCanvasGroup.alpha - targetAlpha) / fadeDuration;
while (!Mathf.Approximately(FadeCanvasGroup.alpha, targetAlpha)){
FadeCanvasGroup.alpha = Mathf.MoveTowards(FadeCanvasGroup.alpha, targetAlpha, speed * Time.deltaTime);
yield return null;
}
FadeCanvasGroup.blocksRaycasts = false;
IsFade = false;
}
在切换场景的协程方法中再引用这这个方法 yield return Fade(1):实现黑色遮罩的淡入;yield return Fade(0):实现黑色遮罩的淡出。
本次数学函数官方文档:UnityEngine.Mathf - Unity 脚本 API
2.5实现鼠标点击切换
创建一个鼠标点击类CursorManager类,因为此游戏主要是鼠标点击交互,要判断点击的游戏对象就专门写一个类来实现。
public class CursorManager: MonoBehaviour
{
private Vector3 mouseWorldPos => Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, 0));
private bool canClick;
private void Update()
{
canClick = ObjectAtMousePosition();
if(canClick && Input.GetMouseButtonDown(0))
{
//检测鼠标互动
clickAction(ObjectAtMousePosition().gameObject);
}
Debug.Log(canClick);
}
private void clickAction(GameObject clickObject)
{
switch (clickObject.tag)
{
case "Teleport":
var teleport = clickObject.GetComponent<Teleport>();
teleport?.teleportToscene();
break;
}
}
private Collider2D ObjectAtMousePosition()
{
//返回一个与鼠标坐标点重叠的碰撞体
return Physics2D.OverlapPoint(mouseWorldPos);
}
}
- 给被点击物体挂载上Collider2D组件,并且勾选上Is Trigger;
- 将鼠标的屏幕坐标转换成游戏世界坐标,并创建一个返回Collider2D的方法,OverlapPoint返回一个与鼠标坐标重合的碰撞体;
- 给不同碰撞体类划分不同Tag标签,在clickAction方法中检测点击的物体以此来调用不同的方法;
声明:本文档只用于本人学习记录,文中若出现错误望各位指正。更多详情关注B站麦扣老师:《迷失岛2》游戏框架开发00:项目介绍|Unity教程_哔哩哔哩_bilibili