Skip to content

Instantly share code, notes, and snippets.

@iamtanmay
Forked from AliBharwani/ArrowTipTrigger.cs
Created February 6, 2025 17:35
Show Gist options
  • Save iamtanmay/a36762127f250f18b59fa6fe5c44ca04 to your computer and use it in GitHub Desktop.
Save iamtanmay/a36762127f250f18b59fa6fe5c44ca04 to your computer and use it in GitHub Desktop.
Piercing Arrow in Unity
using UnityEngine;
public class ArrowTipTrigger : MonoBehaviour
{
void OnTriggerEnter(Collider other)
{
SendMessageUpwards("OnArrowTipTriggerEnter", other, SendMessageOptions.RequireReceiver);
}
}
using UnityEngine;
public class PiercingArrow : MonoBehaviour
{
public enum ArrowState
{
Flying,
Penetrating, // After body hits, before creating joint
Embedded
}
// Configuration
public float pierceAngleTolerance = 30f;
public float minImpactVelocity = 5f;
public float maxRelativeVelocityForJoint = 0.5f;
public float penetrationDampingFactor = 5f;
public float rigidbodyVelDampingOnPeneteration = .75f;
public Collider[] tipColliders;
public CapsuleCollider bodyCollider;
public Collider arrowTipTriggerCol;
public bool debug = false;
// Runtime state
public ArrowState currentState = ArrowState.Flying;
private Vector3 penetrationDirection;
// Cached components
private Rigidbody arrowRigidbody;
private ArticulationBody targetArticulationBody;
private void Awake()
{
arrowRigidbody = GetComponent<Rigidbody>();
}
private void HandlePenetrationSlowdown()
{
// Project current velocity onto penetration direction
Vector3 velocityAlongPenetration = Vector3.Project(arrowRigidbody.linearVelocity, penetrationDirection);
Vector3 velocityPerpendicular = arrowRigidbody.linearVelocity - velocityAlongPenetration;
// Apply damping only to the penetration component
Vector3 dampedVelocityAlongPenetration = velocityAlongPenetration / (1f + Time.fixedDeltaTime * penetrationDampingFactor);
// Calculate and apply impulse based on the change in velocity
Vector3 velocityChange = dampedVelocityAlongPenetration - velocityAlongPenetration;
Vector3 impulse = arrowRigidbody.mass * velocityChange;
targetArticulationBody.AddForceAtPosition(impulse, arrowTipTriggerCol.transform.position, ForceMode.Impulse);
// Recombine velocities and apply
arrowRigidbody.linearVelocity = velocityAlongPenetration + velocityPerpendicular;
}
void OnArrowTipTriggerEnter(Collider other) // Called via SendMessageUpwards on arrow tip trigger collider
{
if (debug)
Debug.Log($"OnArrowTipTriggerEnter: {other.gameObject.name}");
if (currentState != ArrowState.Flying)
return;
if // IS NOT AN OBJECT YOU WANT TO PENETRATE
return;
Vector3 relativeVelocity = arrowRigidbody.linearVelocity - other.attachedArticulationBody.linearVelocity;
if (relativeVelocity.magnitude < minImpactVelocity)
{
if (debug)
Debug.Log($"Penetration rejected: velocity {relativeVelocity.magnitude} < {minImpactVelocity}");
return;
}
float impactAngle = _getPiercingAngle(arrowTipTriggerCol, other);
if (impactAngle > pierceAngleTolerance)
{
if (debug)
Debug.Log($"Penetration rejected: angle {impactAngle} > {pierceAngleTolerance}");
return;
}
if (debug)
Debug.Log($"Penetration accepted! collision.articulationBody: {other.attachedArticulationBody}");
targetArticulationBody = other.attachedArticulationBody;
DisableCollisionsWithTarget(other);
currentState = ArrowState.Penetrating;
penetrationDirection = transform.forward;
arrowRigidbody.linearVelocity *= (1 - rigidbodyVelDampingOnPeneteration);
Vector3 impulse = arrowRigidbody.mass * arrowRigidbody.linearVelocity;
targetArticulationBody.AddForceAtPosition(impulse, arrowTipTriggerCol.transform.position, ForceMode.Impulse);
}
private float _getPiercingAngle(Collider triggerCollider, Collider otherCollider)
{
if (Physics.ComputePenetration(
triggerCollider, triggerCollider.transform.position, triggerCollider.transform.rotation,
otherCollider, otherCollider.transform.position, otherCollider.transform.rotation,
out Vector3 direction, out float distance))
{
// direction is pointing from triggerCollider to otherCollider
// We want angle between arrow's forward and the negative of this direction
return Vector3.Angle(transform.forward, -direction);
}
return 180f; // No penetration detected
}
void OnTriggerStay(Collider col)
{
if (currentState == ArrowState.Penetrating && col.attachedArticulationBody != null && col.attachedArticulationBody == targetArticulationBody)
{
HandlePenetrationSlowdown();
float relativeVelocity = (targetArticulationBody.linearVelocity - arrowRigidbody.linearVelocity).magnitude;
if (debug)
Debug.Log($"Penetrating - RelativeVelocity: {relativeVelocity}, Target: {maxRelativeVelocityForJoint}");
if (relativeVelocity <= maxRelativeVelocityForJoint)
{
if (debug)
Debug.Log("Velocity check passed, creating joint");
CreateFixedJoint();
currentState = ArrowState.Embedded;
}
}
}
void OnTriggerExit(Collider col)
{
if (col.attachedArticulationBody == targetArticulationBody)
{
currentState = ArrowState.Flying;
}
}
private void DisableCollisionsWithTarget(Collider targetCollider)
{
foreach (Collider tipCollider in tipColliders)
{
Physics.IgnoreCollision(tipCollider, targetCollider);
}
Physics.IgnoreCollision(bodyCollider, targetCollider);
}
private void CreateFixedJoint()
{
FixedJoint joint = gameObject.AddComponent<FixedJoint>();
joint.connectedArticulationBody = targetArticulationBody;
joint.breakForce = Mathf.Infinity;
joint.breakTorque = Mathf.Infinity;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment