用Unity实现传送门效果(二)

作者:四五二十
大家好。
上一期我们主要讲了多层空间画面是怎么渲染的。在理解了上篇的前提下,我们来继续做余下的功能。

这个项目一共只有5个脚本,抛开角色控制和人物动画管理两个脚本,主要讲解剩下三个:
我们先将两个传送门放到场景中主摄像机照不到的地方:

接着上一篇,为了达到三层空间渲染效果,一共创建四个辅助摄像机和四个Substitute,把它们平均分配给两个传送门作为子物体:

创建一个空物体DoorManager,再创建一个脚本将它们都管理起来:
public class DoorManager : MonoBehaviour
{
public Transform mainCamera; //主摄像机
public Transform[] substitutes; //替身
public Transform[] Cameras; //辅助摄像机
public Door[] doors; //传送门
编辑器里将它们拖进去:

然后在一个方法里同步它们的位置和旋转:
void SetSubstitutePos() //多层空间摄像机渲染
{
//一层空间替身获取主摄像机坐标旋转
substitutes[0].position = substitutes[1].position = mainCamera.position;
substitutes[0].rotation = substitutes[1].rotation = mainCamera.rotation;
//二层空间摄像机获取一层空间替身的本地坐标旋转
Cameras[1].localPosition = substitutes[0].localPosition;
Cameras[1].localRotation = substitutes[0].localRotation;
Cameras[0].localPosition = substitutes[1].localPosition;
Cameras[0].localRotation = substitutes[1].localRotation;
//二层空间替身获取二层空间摄像机的坐标旋转
substitutes[2].position = Cameras[1].position;
substitutes[2].rotation = Cameras[1].rotation;
substitutes[3].position = Cameras[0].position;
substitutes[3].rotation = Cameras[0].rotation;
//三层空间摄像机获取二层空间替身的本地坐标旋转
Cameras[2].localPosition = substitutes[3].localPosition;
Cameras[2].localRotation = substitutes[3].localRotation;
Cameras[3].localPosition = substitutes[2].localPosition;
Cameras[3].localRotation = substitutes[2].localRotation;
}
该方法放到LateUpdate里调用。

开启传送门

开启传送门就是朝墙上发射子弹,如果碰到墙就让该面墙的渲染禁用(看不见),打开碰撞器的触发功能(主角穿梭时不会被阻挡),让传送门出现在该墙的位置,且两扇传送门的本地坐标一个朝外,一个朝里。
我们用一个计数器来记录开门次数,根据计数单双来区别朝外和朝里,创建一个门的脚本Door,在里面写上开门的方法:
public class Door : MonoBehaviour
{
public float angle; //旋转角度
public void OpenDoor(Vector3 pos, Quaternion rota) //获取位置和旋转打开传送门
{
//获取新的位置和旋转
transform.position = pos;
transform.rotation = rota;
transform.Rotate(0, angle, 0);
}
两扇传送门分别都挂上,并且其中一个的angle变量在编辑器里设为180,区分朝里和朝外,然后在DoorManager脚本里调用:
Transform[] walls = new Transform[2]; //保存开门时被隐藏的墙
int number = 0; //计数器
public void AddWall(Transform wall) //获取当前墙
{
int i = number % 2;
if (walls[i] != null) //不为空则将之前隐藏的先显示
ShowWall(walls[i], true);
walls[i] = wall;
if (number > 0)
{
ShowWall(walls[i], false); //隐藏当前
if (number == 1)
ShowWall(walls[0], false);
}
OpenDoor(i); //打开传送门
number++;
}
void ShowWall(Transform wall, bool b) //隐藏墙
{
wall.GetComponent<BoxCollider>().isTrigger = !b; //开关触发器
wall.GetComponent<SpriteRenderer>().enabled = b; //开关渲染器
}
void OpenDoor(int i) //打开传送门
{
doors[i].OpenDoor(walls[i].position, walls[i].rotation);
pm.startColor = i == 0 ? Color.red : Color.blue;
}
AddWall方法会在子弹碰到墙时调用。
接下来我们做子弹的功能,开启传送门的子弹我们在场景中只有一个,当它处于禁用状态时才能开枪发射,然后子弹墙或飞出地图一定距离时再禁用。

子弹我们用了一个移动时能产生拖尾的粒子效果,保留了碰撞器和刚体,为它挂上一个脚本:
public class Bullet : MonoBehaviour //子弹
{
public float speed;
Rigidbody rig;
DoorManager dm; //传送门管理器
Transform wall;
bool open = true; //启动传送门也需要冷却时间
void Start()
{
rig = GetComponent<Rigidbody>();
dm = FindObjectOfType<DoorManager>();
}
void Update()
{
rig.velocity = transform.forward * Time.deltaTime * speed; //前进
//飞出地图一定距离自动禁用(地图放在世界中心)
if (Mathf.Abs(transform.position.z) > 8 || Mathf.Abs(transform.position.x) > 5)
gameObject.SetActive(false);
}
void OnCollisionEnter(Collision other) //碰撞一次
{
if (other.collider.CompareTag("Wall"))
{
//撞到的墙不是刚才的墙,防止两道门开在同一面墙上
if (open && wall != other.transform)
{
open = false;
wall = other.transform;
dm.AddWall(other.transform);
Invoke("ColdOpen", 0.2f); //0.2秒后完成冷却
}
}
gameObject.SetActive(false);
}
void ColdOpen()
{
open = true;
}

传送主角
我们结合旁观者角度看看主角是怎么被传送的:

简单说,就是判断主角与某个传送门之间的位置,达到一定位置条件就将另一个传送门的位置赋给他,让主角出现在另一个传送门的位置。
我们知道主角的位置和旋转给了主摄像机,而主摄像机的位置和旋转又给了第一层空间的两个substitute,而两个substitute又分别是两扇传送门的子物体,所以只需要判断substitute的本地坐标就行了,将传送主角的方法写在DoorManager脚本里:
void DeliveryPlayer() //传送主角
{
if (number >= 2) //有两道门后可以执行传送
{
DeliveryCondition(0, substitutes[0].localPosition.z > 0);
DeliveryCondition(1, substitutes[1].localPosition.z < 0);
}
}
void DeliveryCondition(int i, bool b) //传送主角条件
{
int j = Mathf.Abs(i - 1); //另一道门的索引
//判断某个一层替身与父物体(传送门)的位置关系
if (Mathf.Abs(substitutes[i].localPosition.x) < 0.3f && Mathf.Abs(substitutes[i].localPosition.y) < 1 && b)
{
//将主角传送至另一道门位置
player.position = Cameras[j].position;
Quaternion r = Cameras[j].rotation;
player.rotation = new Quaternion(player.rotation.x, r.y, player.rotation.z, r.w);
}
}
把DeliveryPlayer方法也放在LateUpdate里实时监测。
传送子弹

传送子弹使用触发的方式,当传送门被打开时,原本位置的墙会隐藏并开启触发器,我们之前使用了一个数组专门用来保存隐藏的墙,我们只需要在触发时识别其中一个,然后立刻传送到另一个的位置就行了,传送子弹的方法我们写在DoorManager里:
public void DeliveryBullet(Transform bullet, Transform wall) //传送子弹
{
bullet.parent = wall; //获取该墙成为子弹父物体
//保存自身本地坐标和旋转
Vector3 lp = bullet.localPosition;
Quaternion lr = bullet.localRotation;
//让另一道门成为子弹父物体
if (wall == walls[0])
bullet.parent = walls[1];
else
bullet.parent = walls[0];
//将刚才的本地坐标和旋转再赋予子弹
bullet.localPosition = new Vector3(-lp.x, lp.y, -lp.z);
bullet.localRotation = lr;
bullet.Rotate(0, 180, 0, Space.World);
}
然后在子弹的脚本里使用触发方式调用:
void OnTriggerEnter(Collider other) //触发一次
{
wall = other.transform; //获取被触发的墙
if (wall != transform.parent) //触发的物体(墙)不是自己的父物体
dm.DeliveryBullet(transform, wall); //传送子弹
}
传送门动画


暂停画面会看到在同一个画面中出现了两个门,说明每个门都还有一个替身,当门的位置发生改变时,原来的门会变小,然后以变大的方式呈现,原位置会用假门来替代,假门出现后会缩小并消失,在场景中导入两个假门放在玩家看不见的地方,然后在Door的脚本里添加动画功能:
Vector3 pos; //位置
Vector3 scale;//大小
public float angle; //旋转角度
public Transform CopyDoor; //把假门拖进去
void Start()
{
//记录初始位置和大小
pos = transform.position;
scale = transform.localScale;
}
void Update()
{
//位置发生改变
if (pos != transform.position)
{
//更新位置和旋转信息
pos = transform.position;
transform.localScale = Vector3.zero;
}
//真门变大动画
transform.localScale = Vector3.Lerp(transform.localScale, scale, Time.deltaTime * 10);
if (CopyDoor.gameObject.activeInHierarchy) //如果假门被启用调用展示动画
ShowPrefabDoor();
}
void DisplayDoor() //显示假门
{
//首先复制真门的位置旋转尺寸,然后启用
CopyDoor.position = pos;
CopyDoor.rotation = transform.rotation;
CopyDoor.localScale = scale;
CopyDoor.gameObject.SetActive(true);
}
void ShowPrefabDoor() //展示假门动画
{
//假门变小
CopyDoor.localScale = Vector3.Lerp(CopyDoor.localScale, Vector3.zero, Time.deltaTime * 10);
if (CopyDoor.localScale.x < 0.1f) //小到一定程度就禁用
CopyDoor.gameObject.SetActive(false);
}
我们来看一下慢动作效果:

接下来就说说会遇到的坑,首先就是当我们走到这个位置来的时候:

或者

这些都是第二层空间两个摄像机的渲染层级没设置好造成的。
我们在DoorManager写了一个方法,会根据两个摄像机的位置实时得去调整他们的渲染层级:
void SwitchCameraDepth() //切换二层空间摄像机层级
{
//如果两个一层替身本地高度差不多,哪个替身离父物体Z轴距离近,一起的摄像机层级越低
if (Mathf.Abs(substitutes[0].localPosition.y - substitutes[1].localPosition.y) < 0.1f)
{
if (Mathf.Abs(substitutes[0].localPosition.z) < Mathf.Abs(substitutes[1].localPosition.z))
SetDepth(Cameras[0], Cameras[1]);
else
SetDepth(Cameras[1], Cameras[0]);
}
else //如果高度相差很大,哪个替身与父物体Y轴距离小,一起的子物体摄像机层级越低
{
if (Mathf.Abs(substitutes[0].localPosition.y) < Mathf.Abs(substitutes[1].localPosition.y))
SetDepth(Cameras[0], Cameras[1]);
else
SetDepth(Cameras[1], Cameras[0]);
}
}
void SetDepth(Transform camera1, Transform camera2) //设置二层空间两个摄像机渲染层级
{
camera1.GetComponent<Camera>().depth = -3;
camera2.GetComponent<Camera>().depth = -2;
}
然后放在LateUpdate里调用就可以解决刚才的问题了。
第二个坑是这里,刚才我们提到为什么墙的碰撞盒子是这样:

主要是为了防止这种情况的发生:

当我们走到门口发射时,原计划要打到“前面”的墙,却因为子弹产生的位置碰不到触发器,导致子弹无法被成功传送,这样就不能再“前方”墙上产生新的传送门。
解决办法自然就是加厚碰撞器,让主角走到很边缘的位置也能保证子弹能被传送。

坑点主要就是这两个。
以此为基础可以延伸出许多玩法,大家可以开开脑洞,或者研究一下更复杂地形中传送门功能的实现。
下面是原工程地址,里面有工程素材,以及人物控制和动画的脚本:
https://github.com/wushupei/TransmissionGate
最后想系统学习游戏开发的童鞋,欢迎访问 http://levelpp.com/
游戏开发搅基QQ群:869551769
微信公众号:皮皮关
我们这篇文章暂时就到这里,谢谢各位。