Reputation: 21160
How do I prevent a collision from applying forces in Unity? I am using 2D physics and want an arrow to stick into a crate. I can easily remove the rigid body and collider in the collision callback, but it seems that a frame of collision force is still applied to the arrow, causing slight jumps in position and rotation. Settings isKinematic on the rigid bodies in the collision callback also appears to not prevent this one frame of force being applied.
I am hoping to tell Unity to not apply physics for the collision.
Using kinematic for the life time of the arrow is not an option because the arrow needs to fly realistically until it hits something.
Here is the code for the crate object that handles the collision:
protected virtual void HandleCollision(ArrowScript arrow, Collision2D coll)
{
StickArrow(arrow, coll);
if (DestroyAfterSeconds >= 0.0f)
{
Destroy(arrow.gameObject, DestroyAfterSeconds);
}
}
private void OnCollisionEnter2D(Collision2D coll)
{
ArrowScript script = coll.gameObject.GetComponent<ArrowScript>();
if (script != null)
{
HandleCollision(script, coll);
}
}
private bool StickArrow(ArrowScript arrow, Collision2D coll)
{
Vector2 surfaceNormal = coll.contacts[0].normal;
float surfaceAngle = Mathf.Atan2(surfaceNormal.y, surfaceNormal.x);
float arrowAngle = Mathf.PI + (arrow.transform.eulerAngles.z * Mathf.Deg2Rad);
float angleDifference = Mathf.Abs(BowAndArrowUtilities.DifferenceBetweenAngles(surfaceAngle, arrowAngle));
float penetration = arrow.PercentPenetration * PenetrationPercentageModifier * (1.0f - angleDifference);
if (penetration <= MinimumPenetrationPercentage)
{
arrow.PercentPenetration = 0.0f;
return false;
}
// Make the arrow a child of the thing it's stuck to
arrow.transform.parent = transform;
arrow.gameObject.transform.Translate(new Vector3(-penetration * arrow.Length, 0.0f, 0.0f));
SpriteRenderer thisSpriteRenderer = GetComponent<SpriteRenderer>();
if (thisSpriteRenderer != null)
{
arrow.GetComponent<SpriteRenderer>().sortingLayerID = thisSpriteRenderer.sortingLayerID;
arrow.GetComponent<SpriteRenderer>().sortingOrder = Mathf.Max(0, thisSpriteRenderer.sortingOrder - 1);
}
BowAndArrowUtilities.PlayRandomSound(arrow.CollisionAudioClips, penetration * 5.0f);
// destroy physics objects from the arrow (rigid bodies, colliders, etc.). This unfortunately doesn't prevent this frame from apply force (rotation, position) to the arrow.
arrow.DestroyPhysicsObjects();
return true;
}
Unity version is 5.3.4.
Upvotes: 0
Views: 5899
Reputation: 635
Your problem is that OnCollisionEnter and OnTriggerEnter are called after all the collisions are resolved.
The simplest way to solve this without affecting anything would be to change the weight of the box, arrow, or both.
Set the weight of the crate to a high value, and the weight of the arrow to a low value.
Another way is to use trigger colliders, as you have done. However trigger colliders have problematic side-effects. For example, it doesn't call OnCollisionEnter or OnTriggerEnter on the crate. You will have to do all the logic inside the arrow script, which is not much of a problem.
There are a lot of other ugly hacks however. You could set the velocity of the box to 0 after impact, but it would freeze the crate if you hit it as it was moving. You could use the collision information to cancel the force applied to the crate to solve the collision. You could save the last velocity of the crate every frame, and reapply it to the rigid body during the OnCollision call.
I wouldn't suggest any of these, but they are possible.
Upvotes: 0
Reputation: 21160
I ended up making the arrow head a trigger. Inside of OnTriggerEnter2D, I then perform a circle cast in the direction the arrow is pointing with a width of the arrow head sprite. Triggers do not get affected by Unity physics calculations.
private void OnTriggerEnter2D(Collider2D coll)
{
ArrowScript script = coll.gameObject.GetComponent<ArrowScript>();
if (script != null)
{
Vector2 dir = -script.ArrowHead.transform.right;
// ray cast with the arrow size y value (thickness of arrow)
RaycastHit2D[] hits = Physics2D.CircleCastAll(script.ArrowHead.transform.position, script.Size.y, dir);
foreach (RaycastHit2D hit in hits)
{
// collider2d is a member variable assigned in Start that is the Collider2D for this object
if (hit.collider == collider2d)
{
HandleCollision(script, hit.normal);
break;
}
}
}
}
Upvotes: 1