之前写过一篇文章——《Unity第一人称可视化传送门制作》,实现了传送门的基本操作,而且,可以通过传送门观察对面的画面,但一个缺陷是无法跨场景传送。那么,如何实现跨场景的传送呢?
事实上,所谓跨场景传送,本质上就是场景加载,关键是要传送到什么位置去,在在老场景中事先确定好位置,然后新场景加载后,把角色设置到新的目标位置和朝向即可。本质上,这就是跨场景传送数据的问题,Unity中,有个DontDestroyOnLoad
方法,可以将一个物体设置为“切换场景时不要销毁”,那么这个物体就会保留到下一个场景中,我们需要的数据就可以保存在这个物体上,等数据使用完了,再把这个物体销毁即可。但是,跨场景的传送门,无法像“同场景中的传送门”一样,可以提前观察到对面的情况,因为在没有进入门之前,场景还没有加载,肯定是无法观察到的,这也是没有办法的。
注意观察视频中的Hierarchy
窗口,每次传送,场景都真实的切换了。
Unity跨场景传送门
public class ProtalDoor : MonoBehaviour
{
public LayerMask TransLayer;
public string TargetSceneName;
public string TargetDoorTag;
private void OnTriggerEnter(Collider other)
{
if (((1 << other.gameObject.layer ) & TransLayer) != 0)
{
if (TransDataToScene.IsExists)
{
// 如果存在数据,表示此时为玩家进入传送后新场景的门
// 启动玩家控制,销毁跨场景数据
PlayerController.Active(true);
TransDataToScene.Destroy();
}
else
{
// 数据不存在,表示玩家刚进入传送门,此为传送前。
// 需要禁用玩家控制,然后创建跨场景数据,加载新场景
PlayerController.Active(false);
TransDataToScene.SaveData(transform, other.transform, TargetDoorTag);
HxSceneLoader.LoadScene(TargetSceneName);
}
}
}
}
门的逻辑起始很简单,如果玩家进入了门的碰撞器范围内,就分两种情况:
一是跨场景的数据不存在,那么此时就是传送之前,玩家想要进行传送了。此时,就创建好需要跨场景保存的数据,比如玩家相对门的位置,玩家想要去哪个场景,如果目标场景中有多个门,那要明确目标门的TAG,然后就加载场景。
二是如果跨场景的数据已经存在了,那么就表示此时为传送之后了(传送之后,玩家会被设置到目标门附近,所以也会触发OnTriiggerEnter),此时要做的就是销毁数据,以便下次传送。
public class TransDataToScene : MonoBehaviour
{
private static TransDataToScene _instance;
public static bool IsExists => _instance != null;
public static Vector3 localPos { get; private set; }
public static Vector3 localRot { get; private set; }
public static string targetDoorTag { get; private set; }
public static ProtalDoor targetDoor => string.IsNullOrWhiteSpace(targetDoorTag)
? null
: GameObject.FindWithTag(targetDoorTag)?.GetComponent<ProtalDoor>();
public static void Destroy()
{
if (IsExists)
{
Destroy(_instance.gameObject);
_instance = null;
}
}
public static void SaveData(Transform door, Transform player, string targetTag)
{
if (_instance == null)
{
GameObject obj = new GameObject("TransData");
_instance = obj.AddComponent<TransDataToScene>();
DontDestroyOnLoad(obj);
}
localPos = door.InverseTransformPoint(player.position);
localRot = door.InverseTransformDirection(player.eulerAngles);
targetDoorTag = targetTag;
}
}
因为玩家一次只可能使用一个门,所以,这个跨场景传送数据的物体,被设计为单例模式,他唯一的用途,就是在老场景中被创建出来,然后记录玩家相对于源门的相对位置和相对朝向,以及目标门的TAG,然后新场景加载完成后,用它记录好的数据,找到新场景中的目标门物体,最后进行玩家位置的恢复,恢复到和目标门一样的相对位置。
public static void LoadScene(string name)
{
// 首先启用加载器,然后启动加载协程。
Instance.gameObject.SetActive(true);
Instance.StartCoroutine(Instance.RealLoad(name));
}
private IEnumerator RealLoad(string sceneName)
{
// 播放界面淡入动画,显示进度条,开始加载
_animator.SetTrigger(StartLoad);
var asy = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);
asy.allowSceneActivation = true;
while (!asy.isDone)
{
_text.text = ((asy.progress + 0.09f) * 100f).ToString("0");
_slider.value = asy.progress + 0.09f;
yield return null;
}
// 加载完成,设置玩家在新场景中的位置,然后UI淡出。
PlayerController.SetTransform();
_animator.SetTrigger(LoadDone);
}
场景的加载,有个过渡动画,并且有个进度条,但是我这三个场景都太小了,加载是秒加载的,所以,这个过渡就会一闪而过,如果场景足够大,加载时间长一点,那么这个过渡动画就变的很有必要了。
异步加载使用LoadSceneAsync
API。
玩家恢复数据的代码:
public static void SetTransform()
{
// 获取目标门
ProtalDoor door = TransDataToScene.targetDoor;
if (!door )
return;
// 参照玩家相对源门的位置、朝向,恢复到目标门的相对位置和朝向。
Instance.transform.position = door.transform.TransformPoint(TransDataToScene.localPos);
Instance.transform.rotation = Quaternion.Euler(door.transform.TransformDirection(TransDataToScene.localRot));
}
这个门的模型是自己用Blender做的,美术功底不太好。但这不重要,特效分两部分,一个是门内平面本身的渲染,用Shader Graph连了简单的半透明效果:
然后就是一个粒子特效,用Visual Effect Graph连的:
起始特效方面,完全可以做到更炫酷,比如第三人称的话,可以让角色消融、变成粒子飞升。。。