Unity3D RPG Core | 32 跨场景传送
这节我们完成不同场景的传送。
1. 创建新的场景
我们可以点击菜单 File - New Scene 创建一个新的场景。此处使用 Low Poly Dungeons Lite 素材来创建一个地下城场景。
新的场景就是之前学习,我们来重新温习一下:
1. 将素材转化成 URP 版本。可参照文章 《Unity3D RPG Core | 01 创建项目导入素材》。
2. 导航地图烘焙,设置可行走和不可行走区域。可参照文章 《Unity3D RPG Core | 04 智能导航地图烘焙》。
注意地面的标签设置,我们判断鼠标点击内容是通过标签确定的。
3. 摄像机设置,此处我们还是使用 Cinemachine FreeLook 相机。可参照文章 《Unity3D RPG Core | 12 找到 Player 追击》。
4. 放置传送门,注意设置好传送门的属性。
1.1 灯光设置
因为是地下城,所以我们不需要自然光:
1. 如图 1 所示,在相机属性下,把原来的背景色设置为黑色。

2. 去掉原有的天空盒。天空盒相关的设置可参考文章 《Unity3D RPG Core | 02 尝试熟悉基本工具》。
地下城中的灯光来源是壁灯和蜡烛,我们通过设置点光源来实现。如图 2 所示,我们在物体上添加 Light 组件,其中 Type 选择 Point。Emission 下的 Color 属性指定灯光颜色;Intensity 属性指定光的强度。

在 Pipeline Settings 配置文件中对光源的数量有限制。如图 3 所示,我们可以更改 Per Object Limit 属性,最多可以有 8 个光源。

1.2 预制体设置
直接加载场景会报图 4 所示的错误,提示预制体的 Mesh 不能读写。

如图 5 所示,我们通过预制体定位到对应的 Mesh,使能读写。

2. 实现不同场景传送
在上一节相同场景传送的基础上,我们实现不同场景传送的功能,核心方法是 SceneManager.LoadSceneAsync()。
如代码清单 1 所示,我们在协程中不断通过 AsyncOperation.isDone 判断场景是否加载完成。完成加载后,我们通过预制体生成角色人物,生成的位置和相同场景一样通过传送门指定。
- public class SceneController : Singleton<SceneController>
- {
- public GameObject m_playerPrefab = null;
- GameObject m_player = null;
- NavMeshAgent m_agent = null;
- public void TransferToDestination(PortalController portal)
- {
- switch (portal.m_transmissionType)
- {
- case PortalController.TransmissionType.TransmissionSameScene:
- TransferSameScene(portal.m_destinationPortalName);
- break;
- case PortalController.TransmissionType.TransmissionDifferentScene:
- StartCoroutine(TransferDifferentScene(portal.m_sceneName, portal.m_destinationPortalName));
- break;
- }
- }
- void TransferSameScene(string destPortalName)
- {
- if (m_player == null)
- {
- m_player = GameManager.GetInstance().m_playerData.gameObject;
- m_agent = m_player.GetComponent<NavMeshAgent>();
- }
- PortalController destPortal = GetDestinationPortal(destPortalName);
- m_agent.enabled = false;
- m_player.transform.SetPositionAndRotation(
- destPortal.m_point.position,
- destPortal.m_point.rotation);
- m_agent.enabled = true;
- }
- IEnumerator TransferDifferentScene(string sceneName, string destPortalName)
- {
- AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);
- // Wait until the asynchronous scene fully loads
- while (!asyncLoad.isDone)
- {
- yield return null;
- }
- PortalController destPortal = GetDestinationPortal(destPortalName);
- Instantiate(m_playerPrefab,
- destPortal.m_point.position,
- destPortal.m_point.rotation);
- }
- }
以上功能实现后,我们尝试传送,会发现各种空指针错误。加载完场景后,当前场景的内容都会被销毁掉,比如 SceneController 会被销毁掉,无法执行后续逻辑;再比如人物无法使用 GameManager 类。
为了消除以上问题,我们选择在加载新场景时,不销毁相关类。如代码清单 2 所示,可以通过 DontDestroyOnLoad() 指定,我们在 SceneController、GameManager 和 MouseManager 的 Awake() 函数中调用。
- protected override void Awake()
- {
- base.Awake();
- DontDestroyOnLoad(this);
- }
销毁原先场景还会带来一个问题:加了 DontDestroyOnLoad() 后,MouseManager 就变成真正意义上的全局类了,但是之前在 MouseManager 中我们注册了角色人物的移动和攻击等回调函数。此时之前的角色人物已经销毁,再调用相关函数就会出错。
如代码清单 3 所示,在角色人物被销毁时,我们移除之前注册的移动和攻击函数。
- public class PlayerController : MonoBehaviour
- {
- private void OnDisable()
- {
- MouseManager.GetInstance().OnMoveMouseClick -= DoMoveAction;
- MouseManager.GetInstance().OnAttackMouseClick -= DoAttackAction;
- }
- }
2.1 预制体实例化人物带来的其他问题
因为角色人物是预制体实例化生成的,所以摄像头指定的跟随对象还需要再次指定。
如代码清单 4 所示,我们首先通过 FindObjectOfType() 找到 CinemachineFreeLook 摄像头,然后设置其 LookAt 和 Follow 属性。
- public class PlayerController : MonoBehaviour
- {
- void Start()
- {
- MouseManager.GetInstance().OnMoveMouseClick += DoMoveAction;
- MouseManager.GetInstance().OnAttackMouseClick += DoAttackAction;
- GameManager.GetInstance().RegisterPlayerData(m_characterData);
- CinemachineFreeLook cinemachine = FindObjectOfType<CinemachineFreeLook>();
- if (cinemachine != null)
- {
- cinemachine.LookAt = m_LookAtPoint;
- cinemachine.Follow = m_LookAtPoint;
- }
- }
- }
还有一个问题,因为我们旧场景中的角色人物原本就是存在的,当我们从新场景返回旧场景中还会实例化一个人物,就变成了两个。此处把原先的角色人物 Disable 掉,运行游戏时再 Enable 人物。我们先通过这种手动的方式规避掉这个问题,后续主界面场景加载会修复此问题。
3. 实现传送门开启
以上不同场景的传送功能已经实现,接下来我们来实现传送门开启的功能,把石头人打败了之后才开启地下城的传送门。
实现的方式也很简单,我们先不显示传送门,如代码清单 5 所示,待石头人被销毁时,再显示传送门。
- public class GolemController : EnemyController
- {
- public GameObject m_portal = null;
- private void OnDisable()
- {
- if (m_portal != null)
- m_portal.SetActive(true);
- }
- }
Unity 的时序一直是个大问题,需要特别注意。
此处需要注意,切换新场景时,可能在销毁石头人之前,传送门就被销毁了。
图 6 是最终的效果,当击败石头人后会开启传送门,可以传送到地下城,并且可以从地下城传送回来。
