在这一部分,我们将实现进入传送门内物品的传送。
关于物品的传送
首先,使用碰撞检测判断进入传送门内的物品是否可以被传送。其次,在可被传送物品在进入传送门的同时,应该关闭传送门背后墙体与可传送物品间的碰撞。最后,通过对物品传送后的位置、旋转和运动方向进行计算并赋予物品从而实现效果。
计算物品传送后的位置、旋转和运动方向
物品传送后的位置、旋转和运动方向,简单示意如图。
将物品进入传送门时的位置、旋转和运动方向旋转180°,就可以计算物品传送后物品所对应的位置、旋转和运动方向。
PortalableObject脚本
现在,创建脚本并命名为PortalableObject,并为脚本添加依赖。可传送的物品一定需要渲染同时要实现物理特性,因此可被传送的物品应该添加与渲染和物理相关的依赖。
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(Collider))]
public class PortalableObject : MonoBehaviour
{
}
接着定义rigidbody、collider、进入传送门次数、进入的传送门、离开的传送门和半圈的旋转便于之后的计算。(Portal脚本将在下文解释)
private int inPortalCount = 0;
private Portal inPortal;
private Portal outPortal;
private new Rigidbody rigidbody;
protected new Collider collider;
//定义半圈的旋转
private static readonly Quaternion halfTurn = Quaternion.Euler(0.0f, 180.0f, 0.0f);
protected virtual void Awake()
{
rigidbody = GetComponent<Rigidbody>();
collider = GetComponent<Collider>();
}
编写 SetIsInPortal方法,确定进入、离开的传送门和墙面的碰撞体。
public void SetIsInPortal(Portal inPortal, Portal outPortal, Collider wallCollider)
{
this.inPortal = inPortal;
this.outPortal = outPortal;
//忽略物品和墙体的碰撞
Physics.IgnoreCollision(collider, wallCollider);
++inPortalCount;
}
编写 ExitPortal方法,恢复墙面和物品间的碰撞。
public void ExitPortal(Collider wallCollider)
{
Physics.IgnoreCollision(collider, wallCollider, false);
--inPortalCount;
}
编写Warp方法,计算物品传送后的位置、旋转和运动方向。
public virtual void Warp()
{
var inTransform = inPortal.transform;
var outTransform = outPortal.transform;
// 计算物品的Pos
Vector3 relativePos = inTransform.InverseTransformPoint(transform.position);
relativePos = halfTurn * relativePos;
transform.position = outTransform.TransformPoint(relativePos);
// 计算物品的Rot
Quaternion relativeRot = Quaternion.Inverse(inTransform.rotation) * transform.rotation;
relativeRot = halfTurn * relativeRot;
transform.rotation = outTransform.rotation * relativeRot;
// 计算物品的运动方向
Vector3 relativeVel = inTransform.InverseTransformDirection(rigidbody.velocity);
relativeVel = halfTurn * relativeVel;
rigidbody.velocity = outTransform.TransformDirection(relativeVel);
// 替换进出传送门的引用
var tmp = inPortal;
inPortal = outPortal;
outPortal = tmp;
}
PortalableObject脚本的编写暂时已经完成。
Portal脚本
创建脚本并命名为Portal,该脚本将通过碰撞检测(Trigger)实现对物品是否可以传送进行判断。首先,为Portal脚本定义所需要的变量和属性。
[RequireComponent(typeof(BoxCollider))]
public class Portal : MonoBehaviour
{
//另一传送门
[SerializeField]
private Portal otherPortal;
//墙面的碰撞体
[SerializeField]
private Collider wallCollider;
//传送门是否被放置
[SerializeField]
private bool isPlaced;
//存储被传送的物品
private List<PortalableObject> portalObjects = new List<PortalableObject>();
private Material material;
private new Renderer renderer;
private new BoxCollider collider;
private void Awake()
{
collider = GetComponent<BoxCollider>();
renderer = GetComponent<Renderer>();
material = renderer.material;
}
public Portal GetOtherPortal()
{
return otherPortal;
}
public void SetMaskID(int id)
{
material.SetInt("_MaskID", id);
}
/// <summary>
/// 当前传送门是否被渲染
/// </summary>
/// <returns></returns>
public bool IsRendererVisible()
{
return renderer.isVisible;
}
}
上篇文章中对于传送门相关属性的调用也可以改为如上脚本中的属性。想更改的同学就自己修改和添加,我就不过多赘述了。
通过Trigger检测进入传送门的物体。
private void OnTriggerEnter(Collider other)
{
//通过碰撞检测判断进入传送门的物品能否被传送
var obj = other.GetComponent<PortalableObject>();
if (obj != null)
{
portalObjects.Add(obj);
obj.SetIsInPortal(this, otherPortal, wallCollider);
}
}
private void OnTriggerExit(Collider other)
{
//通过碰撞检测判断离开传送门的物品能否被传送
var obj = other.GetComponent<PortalableObject>();
if(portalObjects.Contains(obj))
{
portalObjects.Remove(obj);
obj.ExitPortal(wallCollider);
}
}
最后,在update中更新可传送物品的位置、旋转和运动方向。
private void Update()
{
for (int i = 0; i < portalObjects.Count; ++i)
{
Vector3 objPos = transform.InverseTransformPoint(portalObjects[i].transform.position);
//根据物品相对于传送门的Pos,判断物品是否进入传送门
if (objPos.z > 0.0f)
{
//计算物品传送后的各个属性
portalObjects[i].Warp();
}
}
}
场景搭建测试
简单调整上一章中已经搭建完成的场景,并新建一个Cube为其添加PortalableObject脚本,为先前的两个传送门添加BoxCollider、Portal脚本,调整传送门BoxCollider的大小并勾选isTrigger。
当前效果如下。
创建复制体
当前已经可以实现物品的传送,但是仔细观察可以发现如下问题。
在物品传送的过程中,因为传送门摄像机近视锥剔除了还未完全离开传送门的物体,所以我们可以在进行传送时创建传送中物体的复制体,从而避免上述的问题出现。
首先,在PortalableObject脚本中添加对应的变量,并修改部分代码。
public class PortalableObject : MonoBehaviour
{
//***省略前文变量***
private GameObject cloneObject;
protected virtual void Awake()
{
cloneObject = new GameObject();
cloneObject.SetActive(false);
var meshFilter = cloneObject.AddComponent<MeshFilter>();
var meshRenderer = cloneObject.AddComponent<MeshRenderer>();
meshFilter.mesh = GetComponent<MeshFilter>().mesh;
meshRenderer.materials = GetComponent<MeshRenderer>().materials;
cloneObject.transform.localScale = transform.localScale;
//***省略前文***
}
private void LateUpdate()
{
//在LateUpdate更新复制体的位置等属性
if (inPortal == null || outPortal == null)
{
return;
}
if (cloneObject.activeSelf && inPortal.IsPlaced() && outPortal.IsPlaced())
{
var inTransform = inPortal.transform;
var outTransform = outPortal.transform;
Vector3 relativePos = inTransform.InverseTransformPoint(transform.position);
relativePos = halfTurn * relativePos;
cloneObject.transform.position = outTransform.TransformPoint(relativePos);
Quaternion relativeRot = Quaternion.Inverse(inTransform.rotation) * transform.rotation;
relativeRot = halfTurn * relativeRot;
cloneObject.transform.rotation = outTransform.rotation * relativeRot;
}
else
{
cloneObject.transform.position = new Vector3(-1000.0f, 1000.0f, -1000.0f);
}
}
public void SetIsInPortal(Portal inPortal, Portal outPortal, Collider wallCollider)
{
//***省略前文***
cloneObject.SetActive(true);
}
public void ExitPortal(Collider wallCollider)
{
//***省略前文***
if (inPortalCount == 0)
{
cloneObject.SetActive(false);
}
}
}
增加复制体后效果。
后续
还未完成发射传送门、传送门内画面迭代和玩家的传送等功能。