1.17 从0开始学习Unity游戏开发--场景切换

2023-11-20

前面的所有文章我们都在一个固定的游戏场景内进行开发,在最开始介绍场景这个概念的时候就已经提及,这个场景可以是一张地图,或者是一个对战房间等等,所以显然这个场景可以有多个,并且可以从一个场景切换到另外一个场景,那么在Unity中如何进行场景切换,以及如何处理好场景切换时的各个逻辑呢,本章就会详细讲解。

新建第二个场景

还记得最早讲的如何创建场景资源吗?

在Project窗口里面随便哪个你喜欢的位置右键Create->Scene就可以创建一个新的场景资源,我们已经有了一个Demo场景,那么我们创建一个新的场景叫AnotherDemo

OK,接下来我们需要编辑这个场景的内容,那就是双击这个场景资源文件,注意如果当前打开的场景比如你现在打开的是Demo,然后你有一些修改没有保存(比如Demo的Hierarchy窗口里面根节点是带星号的),那么Unity会提示你要不要保存,你自己决定要不要保存,然后才会打开刚刚双击的场景文件。

双击过后,我们就进入到了这个场景里面,一切都回到了原点:

唯独不同的是,Hierarchy窗口的根节点换成了我们新创建的名字AnotherDemo。

ok,这样就理解了如何创建新的场景,并进去编辑内容,我们先换回到Demo场景中,因为我们接下来需要从Demo场景通过代码逻辑切换到AnotherDemo。

场景切换

既然我们要切换场景,肯定是要有什么逻辑触发,我们可以单独写个组件加到空的GameObject上,组件就在Start触发后3秒进行场景切换,而场景切换的API则是:

https://docs.unity3d.com/ScriptReference/SceneManagement.SceneManager.LoadScene.html

我们让gpt帮我们写个:

using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneSwitcher : MonoBehaviour
{
    public string sceneName;
    public float delay = 3f;
    private float startTime = 0f;
    private bool isSwitching = false;

    private void Start()
    {
        startTime = Time.time;
    }

    private void Update()
    {
        if (!isSwitching)
        {
            float elapsedTime = Time.time - startTime;
            if (elapsedTime >= delay)
            {
                SceneManager.LoadScene(sceneName);
                isSwitching = true;
            }
        }
    }
}

写的还不错,delay表示物体开始跑起来后等待多少秒执行场景切换,sceneName则是我们的场景资源文件的名字,Unity相当于收集了我们所有场景文件资源,所以可以通过直接传入名字来切换,聪明的你肯定会想到,如果重名了怎么办,毕竟文件在不同的文件夹下可以拥有一样的名字,Unity的API也给出了详细的说明:

The given sceneNamecan either be the Scene name only, without the .unityextension, or the path as shown in the BuildSettings window still without the .unityextension. If only the Scene name is given this will load the first Scene in the list that matches. If you have multiple Scenes with the same name but different paths, you should use the full path.
Note that sceneName is case insensitive, except when you load the Scene from an AssetBundle.

说的就比较清楚,不要带文件名后缀,我们知道场景资源文件实际在操作系统下的文件名后缀是.unity,我们不需要传入这个后缀,例如就是AnotherDemo,不需要AnotherDemo.unity,如果只传了个名字,会使用第一个找到的,如果传的是路径,则按照路径去找,而路径则是Assets文件夹下的路径,例如Scenes/SampleScene(当然我们创建的并没有在这个文件夹下面)

最后注意这里的路径或者名字是大小写不敏感的,除非是从资源包里面加载,至于资源包是啥,我们后面资源加载的篇章再来聊。

那么我们这里sceneName可以有两种填法:

  1. 直接填AnotherDemo
  2. 填路径AnotherDemo

这两个都没问题,但是我们先创建一个物体把我们的组件挂上去:

Ok,我们跑起来看看,哎,为什么没切换呢?不要着急,有没有注意到编辑器界面底部有一条红色的错误信息?打开Console窗口查看全部的输出:

其实可以看到,说的就是我们需要切换的场景AnotherDemo并没有加入到build settings中,我们在游戏打包的那篇中,有提到过如何将场景加入到我们的打包内容中,其实就是这个操作。

我们从Unity编辑器窗口顶部的菜单File->Build Settings打开:

我们确实没有把AnotherDemo加入进去,那我们还是按照老办法,打开AnotherDemo这个场景文件,然后点击上面这个窗口的右边按钮Add Open Scenes,把场景加进去:

Ok,我们重新打开Demo场景,然后再跑一下,等待3秒后,确实切换到AnotherDemo中一篇虚无的世界了。

场景切换的物体保持

我们成功的切换了之后,可以很显然的理解,旧场景里面的所有GameObject都被自动销毁了,这很好,但是有一些GameObject我们是希望跨场景的,最常见的就是UI,很显然我们不能因为切换了一个场景就导致我们的UI全部消失。

那么我们需要将需要在场景切换后仍然保持存在的GameObject使用特殊API进行标记:

Object.DontDestroyOnLoad​docs.unity3d.com/ScriptReference/Object.DontDestroyOnLoad.html

没错,就是这么简单,我们只需要用这个DontDestoryOnLoad来传入我们需要保持的GameObject即可。我们修改一下场景切换的脚本,允许我们设置哪些GameObject可以被保持:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneSwitcher : MonoBehaviour
{
    public string sceneName;
    public float delay = 3f;
    public List<GameObject> gameObjectToStayAlive;
    private float startTime = 0f;
    private bool isSwitching = false;

    private void Start()
    {
        startTime = Time.time;
    }

    private void Update()
    {
        if (!isSwitching)
        {
            float elapsedTime = Time.time - startTime;
            if (elapsedTime >= delay)
            {
                if (gameObjectToStayAlive != null)
                {
                    foreach (GameObject go in gameObjectToStayAlive)
                    {
                        DontDestroyOnLoad(go);
                    }
                }
                
                SceneManager.LoadScene(sceneName);
                isSwitching = true;
            }
        }
    }
}

可以看到,我们新增了一个GameObject数组gameObjectToStayAlive来存放我们希望切换场景时会继续带到下个场景的GameObject,在切换场景之前我们通过DontDestroyOnLoad来设置这些GameObject不要因为场景切换而销毁。

然后我们可以看到编辑器面板上就能通过点击数组的加减符号来新增一个数组元素:

这其实是Unity编辑器默认给数组成员画的一个更方便一点的编辑方法,如果用过更老一些版本的Unity应该能知道以前的编辑方法很落后,不好用。

这里我们将UI显示所需的元素加进去,注意父元素设置DontDestroyOnLoad对子元素也是生效的,所以我们这里没必要将Canvas物体下的所有子元素都加进去。

Ok,那我们跑起来看看?

切换场景后,我们看到UI还能生效,并且可以观察到,我们设置过的GameObject都跑到了一个奇怪的地方:

他们都跑到了一个DontDestroyOnLoad下面,这其实就是Unity实现DontDestroyOnLoad方案,也就是我们实际打开了两个场景,一个是AnotherDemo场景,一个是DontDestroyOnLoad场景,这两个场景同时生效,而我们设置了DontDestroyOnLoad的GameObject会待在DontDestroyOnLoad场景内,所以即使我们切换其他场景也不会影响在DontDestroyOnLoad场景内的物体的销毁逻辑。

不过现在我们发现了另外一个问题,Console窗口里面疯狂报错:

看起来都是同一个错误,仔细看一下,其实就能理解,这个是我们的UI准心上,挂了一个AimController组件,这个组件有引用场景中的相机来确定我们开火的方向,而我们切换场景了,之前引用的Demo场景里面的相机已经不复存在,那么我们继续引用的话,肯定就报错了。同理的还有FireController组件的引用也是一样,我们切换了场景后,这个被引用的组件所在的GameObject也已经销毁了。

所以我们要解决这个问题的话,就需要在切换场景的时候清理掉旧的引用,并且做好判空的逻辑:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneSwitcher : MonoBehaviour
{
    public string sceneName;
    public float delay = 3f;
    public List<GameObject> gameObjectToStayAlive;
    public AimController aimController;
    private float startTime = 0f;
    private bool isSwitching = false;

    private void Start()
    {
        startTime = Time.time;
    }

    private void Update()
    {
        if (!isSwitching)
        {
            float elapsedTime = Time.time - startTime;
            if (elapsedTime >= delay)
            {
                if (gameObjectToStayAlive != null)
                {
                    foreach (GameObject go in gameObjectToStayAlive)
                    {
                        DontDestroyOnLoad(go);
                    }
                }

                if (aimController != null)
                {
                    aimController.mainCamera = null;
                    aimController.fireController = null;
                }
                
                SceneManager.LoadScene(sceneName);
                isSwitching = true;
            }
        }
    }
}

我们朴素的加了一个aimController成员,然后期望有人能在编辑器里面赋值好这个组件,这样我们在场景切换的时候就能够顺利的清理掉带不走的引用。

同理,我们在AimController脚本里面也需要对两个成员进行判空处理:

using UnityEngine;

public class AimController : MonoBehaviour
{
    public Camera mainCamera;
    public FireController fireController;

    private RectTransform rectTransform;

    private void Start()
    {
        rectTransform = GetComponent<RectTransform>();
    }

    private void Update()
    {
        rectTransform.anchoredPosition = Input.mousePosition;

        if (mainCamera == null || fireController == null)
        {
            return;
        }
        
        // 获取屏幕上当前鼠标位置(也就是准心位置)所在的3D空间位置
        Vector3 aimWorldPosition = mainCamera.ScreenToWorldPoint(new Vector3(Input.mousePosition.x,
            Input.mousePosition.y, mainCamera.nearClipPlane));
        // 通过坐标相减可以得到方向向量
        Vector3 fireDirection = aimWorldPosition - mainCamera.transform.position;
        // 归一化后传递给开火控制脚本
        fireController.SetDirection(fireDirection.normalized);
    }
}

Ok,那我们在场景里面赋值一下aimController:

再跑一下,错误消失。

从这里我们就可以看到,场景切换保持物体的存在虽然用起来很简单,但是实际上我们仍然需要小心的管理所有对于旧场景的引用,并且所有设置了DontDestroyOnLoad的物体,已经不会因为切换场景而销毁,所以理所当然的需要我们自己管理什么时候去Destroy它。

思考题

  1. 我们切换场景后,抛弃了旧场景里面的相机和FireController,但是我们在新场景里面肯定也是希望可以瞄准+射击的,新场景里面已经有了一个自带的Main Camera,所以现在我们该怎么做来让切换场景后也能正常射击?
  2. 在上面代码里面,我们简单的加入了一个AimController来实现清理旧引用的逻辑,但是作为一个合格的程序员肯定第一时间想到的是职责单一的设计原则,所以想想看,一个合理的解耦写法应该是如何?
  3. 在切换场景的API的官方说明里面,其实已经有明确的提示,这个切换API是阻塞同步操作,应该使用异步API,那么如果使用异步API,我们又应该如何改写代码呢?

下一章

本章我们详细的了解了一下如何切换场景以及切换场景如果要保持GameObject存活要怎么做,以及这么做了之后遇到的一些现实问题。

切换场景的时候其实已经从官方文档中开始了解到有AssetBundle这样的字眼,其实这就是Unity在正式游戏包中的资源管理办法,当我们需要动态加载资源而不是像现在都是引用来搞定的时候,就需要动态加载资源了,所以下一章我们将会讲解Unity内部动态加载资源的几种办法和适用场景。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

1.17 从0开始学习Unity游戏开发--场景切换 的相关文章

  • caffe源码 之 CPU与GPU数据同步类

    本文主要解析caffe源码文件 src caffe SycedMem cpp 该文件主要实现cpu与gpu的内存同步 先看SycedMem hpp中SycedMem的类定义 ifndef CAFFE SYNCEDMEM HPP define

随机推荐

  • 简单的连接数据库的Web登录界面

    简单的连接数据库的Web登录界面 一 需求分析 实现在登录界面输入用户名和密码 连接数据库 与数据库信息进行比对 若用户名和密码相互匹配 则显示登陆成功 若不正确 选择重新输入 二 工具 1 MySql 2 Tomcat 3 Java EE
  • spring boot 定时任务参数设置

    cron参数 分别对应 秒 分 时 日 月 年 0 0 10 14 16 每天上午10点 下午2点 4点 逗号之间连接起来算一个 0 0 12 每天中午12点触发 0 0 5 0 每5分钟执行一次 0 10 14 16 每天上午10点 下午
  • 程序删除自身

    Windows平台下删除自身的方法 通过bat文件删除 echo off loop del access exe if exist access exe goto loop del DelMe bat 用C C 语言表示创建DelMe ba
  • Python 变量与赋值

    一 变量及类型 1 变量可以是任意的数据类型 在程序中用一个变量名表示 2 变量名区分大小写 3 变量名必须是大小写英文 数字和下划线 的组合 且不能以数字开头 如 gt gt gt a 1 变量a是一个整数 gt gt gt t 007
  • 执行存储过程 获得 table 返回结果

    调用存偖过程 写入投诉信息 SqlConnection conn db GetCon 连接数据库 conn Open 并打开了连接 SqlCommand sqlcmd new SqlCommand 存偖过程名称 conn 使用存偖过程 sq
  • 如果老板要求你的系统接入春晚大流量活动,你会心慌慌吗?

    目录 回头看看 原始系统技术架构 基于CDN的活动静态页面缓存方案 基于Nginx Tomcat Redis的多级缓存方案 超高并发写请求RocketMQ削峰填谷方案 系统限流防雪崩体系架构方案 今天给大家分享一个话题 就是如果要是你老板突
  • MyBatis实现Mapper配置并查询数据

    什么是Mapper 在MyBatis工程搭建 中我们主要讲解的是 MyBatis 如何连接数据库 具体执行 SQL 语句使用的是 JDBC 方式 但在实际应用中是不会选择 JDBC 来执行 SQL 的 MyBatis 提供了 Mapper
  • MeterSphere接口测试cookie提取正则表达式

    在接口自动化测试中经常需要提取header cookie信息 MeterSphere接口自动化测试 添加cookie提取方法如下 0 开启场景共享cookie 1 选择要提取cookie的请求步骤 2 点击后置操作 3 添加参数提取 类型选
  • Vuetify笔记(3):v-btn按钮和v-text-field文本

    1 v btn按钮 在UI组件中v btn组件是用一个material design主题和多个选项来替换标准的html按钮 任何色彩辅助类都可以用来改变背景或文字的颜色 v btn按钮常用属性 1 flat 移除按钮的背景色 布尔值类型 默
  • 蓝桥杯真题:迷宫

    目录 题目描述 运行限制 dfs bfs 结果 题目描述 本题为填空题 只需要算出结果后 在代码中使用输出语句将所填结果输出即可 下图给出了一个迷宫的平面图 其中标记为 11 的为障碍 标记为 00 的为可以通行的地方 010000 000
  • 前端趋势 2022

    大家好 我是若川 持续组织了近一年的源码共读活动 感兴趣的可以 加我微信lxchuan12 参与 每周大家一起学习200行左右的源码 共同进步 同时极力推荐订阅我写的 学习源码整体架构系列 包含20余篇源码文章 历史面试系列 另外 目前建有
  • js判断字符串以某字符开头或结尾

    一 substr start length 函数 1 概述 substr start length 从start开始往后截取length位 下标从0开始 长度从开始下标计算 2 举例 var str 我是一只快乐的小青蛙 str subst
  • XSS(跨站脚本攻击)详解

    XSS攻击通常指的是通过利用网页开发时留下的漏洞 通过巧妙的方法注入恶意指令代码到网页 使用户加载并执行攻击者恶意制造的网页程序 这些恶意网页程序通常是JavaScript 但实际上也可以包括Java VBScript ActiveX Fl
  • 从零开始zynq linux AXI DMA传输

    本文从0开始叙述过程 使用的工具为vivado2016 4 sdk也是2016 4 准备工作 首先下载如下的目标文件 1 下载xilinx官方的bootloader文件 git clone https github com Xilinx u
  • zookeeper集群安装+集群值启动+source命令+export命令

    1zookeeper集群安装 1安装到3台虚拟机上 mini2 mini3 mini4 2先要安装好JDK 步骤 1先给mini2 mini3 mini4创建用户hadoop 密码也是hadoop adduser hadoop passwd
  • python数据科学(十):pandas基础—— 数据导入导出

    数据科学 十 数据格式 csv 载入数据到 Pandas 分隔 缺失 指定读取 指定列名 指定行索引 指定多层索引 逐块读取 处理 正则表达式 缺失值处理 统计次数 保存数据到磁盘 不写索引 不写列名 指定分隔符 只写一部分 pickle
  • 【ffmpeg基础】ffmpeg音频编码

    一 aac编码 输入raw音频编码为AAC ffmpeg i input wav acodec aac y input aac 通过 acodec来指定音频编码器 视频编码器为 vcodec 也可以使用 c a来指定音频编码器 ffmpeg
  • 企业架构LNMP学习笔记29

    Nginx负载均衡配置 架构分析 1 用户访问请求Nginx负载均衡服务器 2 Nginx负载均衡服务器再分发请求到Web服务器 实际配置负载均衡 只需修改作为负载均衡服务器的Nginx即可 当前架构中的server04 在客户端解析域名到
  • 华为OD机试 - 日志首次上报最多积分(Java)

    题目描述 日志采集是运维系统的的核心组件 日志是按行生成 每行记做一条 由采集系统分批上报 如果上报太频繁 会对服务端造成压力 如果上报太晚 会降低用户的体验 如果一次上报的条数太多 会导致超时失败 为此 项目组设计了如下的上报策略 每成功
  • 1.17 从0开始学习Unity游戏开发--场景切换

    前面的所有文章我们都在一个固定的游戏场景内进行开发 在最开始介绍场景这个概念的时候就已经提及 这个场景可以是一张地图 或者是一个对战房间等等 所以显然这个场景可以有多个 并且可以从一个场景切换到另外一个场景 那么在Unity中如何进行场景切