This is commonly done with collision.relativeVelocity, something like this:
void OnCollisionEnter(Collision) {
Vector3 impactVelocity = collision.relativeVelocity;
// Subtracting a minimum threshold can avoid tiny scratches at negligible speeds.
float magnitude = Mathf.Max(0f, impactVelocity.magnitude - minimumDamageThreshold);
// Using sqrMagnitude can feel good here,
// making light taps less damaging and high-speed strikes devastating.
float damage = magnitude * collisionDamageScale;
healthComponent.TakeDamage(damage);
}
But this version will apply symetrically to both participants. (From the view of either A or B, the other object is approacing just as fast, so we should take the same damage)
If you want an object to take damage only when it hits something that's not moving away (at a slower speed), then we'll need to reconstruct the absolute velocities the objects had in the world's inertial frame just before this collision (the velocities you see in the collision handler are post-resolution).
Note that this isn't how real-world physics works, where there's no privileged inertial frame, but sometimes breaking the rules makes for more intuitive behaviour for game purposes.
// Cache this in Start()
Rigidbody _body;
void OnCollisionEnter(Collision collision) {
Vector3 normal = collision.GetContact(0).normal;
Vector3 impulse = collision.impulse;
// Both bodies see the same impulse. Flip it for one of the bodies.
if (Vector3.Dot(normal, impulse) < 0f)
impulse *= -1f;
Vector3 myIncidentVelocity = _body.velocity - impulse / _body.mass;
Vector3 otherIncidentVelocity = Vector3.zero;
var otherBody = collision.rigidbody;
if(otherBody != null) {
otherIncidentVelocity = otherBody.velocity
if(!otherBody.isKinematic)
otherIncidentVelocity += impulse / otherBody.mass;
}
// Compute how fast each one was moving along the collision normal,
// Or zero if we were moving against the normal.
float myApproach = Mathf.Max(0f, Vector3.Dot(myIncidentVelocity, normal));
float otherApproach = Mathf.Max(0f, Vector3.Dot(otherIncidentVelocity, normal));
float damage = Mathf.Max(0f, otherApproach - myApproach - minimumDamageThreshold);
healthComponent.TakeDamage(damage * collisionDamageScale);
}
Note that this particular formula also deals no damage to an object that strikes a stationary target (though the target takes damage)
In 2D, you have to total up the impulse to use yourself:
static Vector2 ComputeTotalImpulse(Collision2D collision) {
Vector2 impulse = Vector2.zero;
int contactCount = collision.contactCount;
for(int i = 0; i < contactCount; i++) {
var contact = collision.GetContact(i);
impulse += contact.normal * contact.normalImpulse;
impulse.x += contact.tangentImpulse * contact.normal.y;
impulse.y -= contact.tangentImpulse * contact.normal.x;
}
return impulse;
}
One benefit of this is that each object in the collision sees its own version of the impulse, so you don't need the flipping line I included in the 3D version.