解释
Unity 中的连续碰撞检测不使用光线投射。结果,非常快速移动(和/或相对较小)的物体(我们现在称之为那种物体projectile
)仍然穿过物体而不会检测到碰撞。著名的DontGoThroughThings组件为 3D 修复了该问题。有一些不一致之处,但是如果您知道自己在做什么,它就可以完成工作。
这是我对它的 2D 改编。
我添加了一些功能,使其对不擅长编码或游戏物理的每个人都更加用户友好。
如何使用它
- 将此组件添加到快速移动的对象中,它们将始终
OnTriggerEnter2D
在击中某物时触发事件。
- 您也可以选择发送不同的自定义消息(通过更改
MessageName
变量)。由于下面解释的警告,我实际上建议这样做。
- 发送消息是脚本的主要用例。它不会神奇地使射弹在物理意义上正确运行。
- 该
triggerTarget
变量确定它是否将消息发送给自己(如果您将命中处理脚本附加到弹丸),发送到被击中的对象(如果您将命中处理附加到应该被弹丸击中的对象) ),或两者的任何变体。
- 与原始版本不同,此脚本还允许在冲击时施加力,这可以通过
momentumTransferFraction
变量进行调整。当两个物体发生碰撞时,所产生的力是两个物体之间动量(质量乘以速度)传递的结果。我这样做的方式非常简陋,并且缺少很多促成因素,但是让射弹在撞击时推动物体就足够了。就像在现实世界中一样,您的弹丸越快或越重,施加的力就越大。
还有一些注意事项(其中大部分也适用于原始版本)
- 仅适用于非常快速移动的物体。你使用的越少越好,因为它比正常的碰撞检测在计算上要昂贵得多。
- 虽然碰撞检测更准确,但碰撞分辨率只是非常初级。它不如物理引擎默认的效果。
- 在当前版本中,总是事后检测到碰撞。这就是为什么在记录碰撞时您可能会看到射弹已经穿过对象。我想在不久的将来解决这个问题。
- 如果你在子弹或其他形式的射弹等物体上使用它,在第一次击中后基本上停止工作,你可以设置
momentumTransferFraction
为 1 让子弹物理推动物体(通过将所有动量应用到第一个击中的物体上)而不让子弹被击中影响了自己。
- 出于某种原因,您不能仅针对一个对象禁用默认碰撞检测。这意味着,如果您非常(不)幸运,并且碰巧通过 Unity 的默认碰撞检查记录了碰撞,您可能会
OnTriggerEnter2D
在同一个对象上多次开火,或者(如果 collider 不是触发器)在命中时施加力目标(除了这个脚本施加的目标)。但是,由于这会有些随机且非常不一致,因此除了打开IsTrigger
在您的射弹对撞机上,我建议使用自定义消息名称来处理射弹冲击。这样,默认碰撞检测随机检测到的碰撞不会有任何意外的副作用 [请记住,默认碰撞检测与这些类型的对象不一致是添加此脚本的实际原因]。仅供参考:从 Unity 5 开始,防止默认碰撞检测的唯一两种方法是IgnoreCollision和IgnoreLayerCollision。
代码
using UnityEngine;
using System.Collections;
using System.Linq;
/// <summary>
/// 2D adaption of the famous DontGoThroughThings component (http://wiki.unity3d.com/index.php?title=DontGoThroughThings).
/// Uses raycasting to trigger OnTriggerEnter2D events when hitting something.
/// </summary>
/// <see cref="http://stackoverflow.com/a/29564394/2228771"/>
public class ProjectileCollisionTrigger2D : MonoBehaviour {
public enum TriggerTarget {
None = 0,
Self = 1,
Other = 2,
Both = 3
}
/// <summary>
/// The layers that can be hit by this object.
/// Defaults to "Everything" (-1).
/// </summary>
public LayerMask hitLayers = -1;
/// <summary>
/// The name of the message to be sent on hit.
/// You generally want to change this, especially if you want to let the projectile apply a force (`momentumTransferFraction` greater 0).
/// If you do not change this, the physics engine (when it happens to pick up the collision)
/// will send an extra message, prior to this component being able to. This might cause errors or unexpected behavior.
/// </summary>
public string MessageName = "OnTriggerEnter2D";
/// <summary>
/// Where to send the hit event message to.
/// </summary>
public TriggerTarget triggerTarget = TriggerTarget.Both;
/// <summary>
/// How much of momentum is transfered upon impact.
/// If set to 0, no force is applied.
/// If set to 1, the entire momentum of this object is transfered upon the first collider and this object stops dead.
/// If set to anything in between, this object will lose some velocity and transfer the corresponding momentum onto every collided object.
/// </summary>
public float momentumTransferFraction = 0;
private float minimumExtent;
private float sqrMinimumExtent;
private Vector2 previousPosition;
private Rigidbody2D myRigidbody;
private Collider2D myCollider;
//initialize values
void Awake()
{
myRigidbody = GetComponent<Rigidbody2D>();
myCollider = GetComponents<Collider2D> ().FirstOrDefault();
if (myCollider == null || myRigidbody == null) {
Debug.LogError("ProjectileCollisionTrigger2D is missing Collider2D or Rigidbody2D component", this);
enabled = false;
return;
}
previousPosition = myRigidbody.transform.position;
minimumExtent = Mathf.Min(myCollider.bounds.extents.x, myCollider.bounds.extents.y);
sqrMinimumExtent = minimumExtent * minimumExtent;
}
void FixedUpdate()
{
//have we moved more than our minimum extent?
var origPosition = transform.position;
Vector2 movementThisStep = (Vector2)transform.position - previousPosition;
float movementSqrMagnitude = movementThisStep.sqrMagnitude;
if (movementSqrMagnitude > sqrMinimumExtent) {
float movementMagnitude = Mathf.Sqrt(movementSqrMagnitude);
//check for obstructions we might have missed
RaycastHit2D[] hitsInfo = Physics2D.RaycastAll(previousPosition, movementThisStep, movementMagnitude, hitLayers.value);
//Going backward because we want to look at the first collisions first. Because we want to destroy the once that are closer to previous position
for (int i = 0; i < hitsInfo.Length; ++i) {
var hitInfo = hitsInfo[i];
if (hitInfo && hitInfo.collider != myCollider) {
// apply force
if (hitInfo.rigidbody && momentumTransferFraction != 0) {
// When using impulse mode, the force argument is actually the amount of instantaneous momentum transfered.
// Quick physics refresher: F = dp / dt = m * dv / dt
// Note: dt is the amount of time traveled (which is the time of the current frame and is taken care of internally, when using impulse mode)
// For more info, go here: http://forum.unity3d.com/threads/rigidbody2d-forcemode-impulse.213397/
var dv = myRigidbody.velocity;
var m = myRigidbody.mass;
var dp = dv * m;
var impulse = momentumTransferFraction * dp;
hitInfo.rigidbody.AddForceAtPosition(impulse, hitInfo.point, ForceMode2D.Impulse);
if (momentumTransferFraction < 1) {
// also apply force to self (in opposite direction)
var impulse2 = (1-momentumTransferFraction) * dp;
hitInfo.rigidbody.AddForceAtPosition(-impulse2, hitInfo.point, ForceMode2D.Impulse);
}
}
// move this object to point of collision
transform.position = hitInfo.point;
// send hit messages
if (((int)triggerTarget & (int)TriggerTarget.Other) != 0 && hitInfo.collider.isTrigger) {
hitInfo.collider.SendMessage(MessageName, myCollider, SendMessageOptions.DontRequireReceiver);
}
if (((int)triggerTarget & (int)TriggerTarget.Self) != 0) {
SendMessage(MessageName, hitInfo.collider, SendMessageOptions.DontRequireReceiver);
}
}
}
}
previousPosition = transform.position = origPosition;
}
}