引言
游戏中很多地方用得到传送门,就跟哆啦A梦的任意门一样,从这边走进去,从那边走出来,用Unity实现一下它吧。
先来个效果图,请原谅没有给门添加漂亮的门框、或者特效,因为手头没有合适的素材,但这不影响任意门的功能,也不影响介绍它的原理。另外,机器上也没有按装gif截屏,所以不带动态,但图中的“能量波动花纹”是带动态的哦,请自行脑补。
这里是另一个,传送门嘛,至少得两个。
从一端走进去,就会从另一端走出来。效果还是可以的。
下面介绍一下原理和制作方法:
原理是这样的,用一个摄像机,观察传送门另一边的图像,然后渲染到本端传送门的平面上,这样,就能在本端门内看到远端的画面了。
一、创建shader
用ShaderGraph制作一个Shader如下图:
这个shader的原理比较简单,事实上最关键的是下面两个节点,上面的那一堆都是为了加能量波动的效果的。将摄像机画面也就是一张纹理连接到BaseColor节点即可,最关键的是UV节点,要使用屏幕空间,否则的话,摄像机画面会被压缩到门的尺寸,就变形了。还要注意,这个shader勾选一下双面渲染,否则跑到门的后面就看不到了。
然后用这个Shader制作一个材质,右键刚刚建好的ShaderGraph文件,选创建材质即可。
二、创建门
建一个Quad平面,作为传送门。将上面的材质赋给它。然后在门的下面创建一个相机、两个空物体,结构如下图:
由于要在门上显示另一个门外的场景,所以,本地门材质上显示的是远端门相机的画面。另外需要注意的是,上面shader中所说过了,一定要使用屏幕空间作为纹理UV,这还有一个问题,那就是纹理尺寸,要跟屏幕尺寸相符,至少比例要一致,否则门上的画面是要变形的。这个其实很容易解决,纹理使用动态创建的就好,根据屏幕尺寸动态建一张纹理即可:
// 获取门底下的相机组件
Camera cam = m_ProtalCamera.GetComponent<Camera>();
// 根据屏幕尺寸建一张纹理
ProtalCameraTexture = new RenderTexture(Screen.width, Screen.height, 32)
{
name = "ProtalCameraTargetTexture"
};
// 将门下相机的渲染目标设置为该纹理
cam.targetTexture = ProtalCameraTexture;
// 设置相机不要观察门本身
cam.cullingMask &= ~(1 << gameObject.layer);
然后就是将纹理设置到门的材质了,也是用代码:
if( TargetDoor != null )
{
// 获取到门的渲染组件
MeshRenderer renderer = GetComponent<MeshRenderer>();
foreach (var m in renderer.materials)
{
// 将本地门的材质纹理,设置为目标门摄像机的渲染纹理
m.SetTexture("_MainTex", TargetDoor.ProtalCameraTexture);
}
}
那么,为啥还要有两个空物体呢?介绍一下原理:
如上图所示,由于传送门的两端朝向是任意的,所以搞清楚摄像机的相对位置尤为重要,否则在A门看到的B门处的图像角度就会不正确。有了上面那个空物体,这一切就变得简单多了,甚至不需要计算,也不需要什么数学知识。
// 将主相机(角色)的世界坐标和朝向赋予A门表示主相机的空物体
m_MainCameraPos.position = m_Camera.position;
m_MainCameraPos.rotation = m_Camera.rotation;
// 将A门下表示主相机的空物体相对于A门的相对位置,赋予B门的相机,使得B门相机相对于B的相对位置与主相机相对于A门的相对位置相同
TargetDoor.m_ProtalCamera.localPosition = m_MainCameraPos.localPosition;
TargetDoor.m_ProtalCamera.localRotation = m_MainCameraPos.localRotation;
请看上述代码中的注释,文字有点绕,但道理比较容易理解。
三、实现传送
上面步骤中共建了两个空物体,但目前只说了其中一个的用途,另一个是干嘛的呢?试想一下,当角色被传送时,从A门离开时相对于A的位置,应该与他到达B门时相对于B门的相对位置是相同的。这样才不会显得突兀,画面跳转也会比较正常。那么剩下的这个空物体,就是解决这个的,原理跟上面相机相对位置的原理是一模一样的:
// 当角色进入门的碰撞范围,触发碰撞器
private void OnTriggerEnter(Collider other)
{
// 将角色的世界位置和朝向赋予门记录目标位置的空物体
m_TargetPos.position = other.transform.position;
m_TargetPos.rotation = other.transform.rotation;
// 使目标门用于记录角色位置的空物体相对于目标门的相对位置与源门的相同
TargetDoor.m_TargetPos.localPosition = m_TargetPos.localPosition;
TargetDoor.m_TargetPos.localRotation = m_TargetPos.localRotation;
// 将角色传送过去
other.transform.position = TargetDoor.m_TargetPos.position;
other.transform.rotation = TargetDoor.m_TargetPos.rotation;
}
四、完善一下
是不是很简单,完事了么?NO,还需要处理几个小问题。试想一下,如果角色进入A门的触发器,那么A门会把它传送到B门去,但是,角色一旦到达B门,就会立即触发B门的触发器,然后B门又会把他传送到A,然后,你的玩家就会被两个门传来传去。。。怎么解决这个问题呢?还有,测试中发现,如果你的角色控制器用的是unity自带的CharacterController组件的话,那么就会有点小问题,一并解决一下:
private void OnTriggerEnter(Collider other)
{
// 加一个接受传送的变量,只有在接受传送时,角色才能被传过去。
// 另外,顺便判断一下目标物体是否允许被传送(通过层来判断)
if (bAcceptTrans && (((1 << other.gameObject.layer) & TransformTargetLayer) != 0))
{
// 暂时关闭目标门传送触发
TargetDoor.bAcceptTrans = false;
// 计算角色相对本地门的相对位置,保证传送到目标门后,与目标门的相对位置一致。
m_TargetPos.position = other.transform.position;
m_TargetPos.rotation = other.transform.rotation;
TargetDoor.m_TargetPos.localPosition = m_TargetPos.localPosition;
TargetDoor.m_TargetPos.localRotation = m_TargetPos.localRotation;
// 如果是CharacterController组件控制的角色,先禁用一下,传送完了再打开
// 如果你的代码不是用这个组件,可以删掉
CharacterController cc = other.GetComponent<CharacterController>();
if (cc != null)
cc.enabled = false;
other.transform.position = TargetDoor.m_TargetPos.position;
other.transform.rotation = TargetDoor.m_TargetPos.rotation;
if (cc != null)
cc.enabled = true;
}
}
五、更多的门
好了,把传送门做成预制体,尽情在你的游戏中摆放吧。。A传到B,B再传回A,或者A传到B,B传到C,C又传到D…只要你愿意,你可以一直这么摆下去。摆完之后,只要把目标门拖到Inspector面板即可。如下图:
Transform Target Layer :允许被传送的物体层,比如玩家层、子弹层…
Target Door:这个门的目标门(要把待传送物体传送到哪个门,就把哪个门拖到这里即可)
使用起来很简单吧。。
六、还能完善吗
本文到这里就结束了,它已经可以实现基本的传送门的功能,但仍有很多优化和完善的余地,比如,shader可以写的更炫酷一点,可以添加启用或者禁用传送门的效果和机关,传送的瞬间添加一些特效…如果要继续完善,那么就尽情的发挥想象力吧。