Extensible Physics Damage Effects - Shoot, Grenade, etc

Earlier in the course I was experimenting with different damage effects for the ShootAction. Now with the GrenadeAction I decided to make it more generic and extensible. There are three different damage effects now (two of my own + the original from Hugo). I provide all code.

bullet effect showing opponent being knocked by the hit

grenade explosion

I noted some similarity in my approach to what this person did.

~~
The main body of work
A new DamageContext class plus calls within ShootAction and GrenadeProjectile to set up the DamageContext Object. The DamageContext object is then passed down through the various calls and events until you get to the UnitRagdoll script where you “activate” the DamageContext object against the ragdoll. Tons of code examples below
~~
Step by step:

  1. Copy my DamageContext class (I’m guessing this could be further improved and refactored to match the Factory pattern). If any experienced engineers want to weight in here, please do.
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;

public class DamageContext
{
    //explosions
    private float explosiveForceMagnitude; // how strong the explosion force is. 
    private float explosionRadius; //if its an explosive force, how far does it reach

    //bullets
    private Vector3 impulseForceVector; // direction and magnitude of force

    //explosions and bullets
    private float upwardForceMagnitude;
    private Vector3 target; // the target i.e. the position where the effect will land or impact
    private DamageEffect damageEffect;

//note that I also played with trying to get rotation / torque effects around Y or up axis but none of them worked out


    public enum DamageEffect { NotConfigured, Bullet, Explosion }

    public DamageContext()
    {
        damageEffect = DamageEffect.NotConfigured;
    }

    public void SetUpAsExplosion(float explosiveForceMagnitude, Vector3 targetPosition, float explosionRadius, float upwardForceMagnitude)
    {
        this.explosiveForceMagnitude = explosiveForceMagnitude;
        this.explosionRadius = explosionRadius;
        this.upwardForceMagnitude = upwardForceMagnitude;
        this.target = targetPosition;
        damageEffect = DamageEffect.Explosion;
    }

    public void SetUpAsBullet(Vector3 origin, Vector3 target, float impulseForceMagnitude, float upwardForceMagnitude)
    {
        this.target = target;
        this.impulseForceVector = impulseForceMagnitude*((target - origin).normalized);
        this.upwardForceMagnitude = upwardForceMagnitude;
        this.damageEffect = DamageContext.DamageEffect.Bullet;
    }

    public void ApplyDamageEffectToRagdoll(Transform ragdollRootBone, Rigidbody ragdollMainRigidbody)
    {
        switch (damageEffect)
        {
            case DamageContext.DamageEffect.Explosion:
                ragdollMainRigidbody.AddExplosionForce(explosiveForceMagnitude, target, explosionRadius, upwardForceMagnitude);
                break;
            case DamageContext.DamageEffect.Bullet:
                ragdollMainRigidbody.AddForce(impulseForceVector, ForceMode.Impulse);
                ragdollMainRigidbody.AddForce(new Vector3(0, upwardForceMagnitude, 0), ForceMode.Impulse);
                break;
            default:
                Vector3 randomDir = new Vector3(Random.Range(-1f, 1f), 0, Random.Range(-1f, 1f));
                ApplyExplosionToRagdoll(ragdollRootBone, 300f, ragdollRootBone.position + randomDir, 10f);
                break;
        }
    }

    private void ApplyExplosionToRagdoll(Transform root, float explosionForce, Vector3 explosionPosition, float explosionRange)
    {
        foreach (Transform child in root)
        {
            if (child.TryGetComponent<Rigidbody>(out Rigidbody childRigidbody))
            {
                childRigidbody.AddExplosionForce(explosionForce, explosionPosition, explosionRange);
            }

            ApplyExplosionToRagdoll(child, explosionForce, explosionPosition, explosionRange);
        }
    }

}
  1. In ShootAction call it like so at the end of the Shoot() method. ShootPointTransform is a serialized field for the “ShootPoint” (end up barrel) .
        DamageContext damageContext = new DamageContext();
// 80f works well for impuleForceMagnitude and 50f for the upwardForce
        damageContext.SetUpAsBullet(shootPointTransform.position, targetPoint, impulseForceMagnitude, upwardForceMagnitude);
        targetUnit.Damage(40, damageContext);

  1. Within Grenade Projectile set up the DamageContext inside the if statement. Full code below.
    Note: this code also makes two other fixes to get the trail to look right. One in Setup() and another to find whether it ended up up at the destination.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

public class GrenadeProjectile : MonoBehaviour
{
    public static event EventHandler OnAnyGrenadeExploded;

    [SerializeField] private Transform grenadeExplodeVfxPrefab;
    [SerializeField] private TrailRenderer trailRenderer;
    [SerializeField] private AnimationCurve arcYAnimationCurve;


    private Vector3 targetPosition;
    private Action onGrenadeBehaviourComplete;
    private float totalDistance;
    private Vector3 positionXZ; //current XZ position of the projectile

    [SerializeField] private float explosiveForce;
    [SerializeField] private float upwardForce;

    private void Update()
    {
        Vector3 moveDir = (targetPosition - positionXZ).normalized; // works because targetPosition also has 0 for y value
        float moveSpeed = 15f;
        positionXZ += moveDir * (moveSpeed * Time.deltaTime);

        float distance = Vector3.Distance(positionXZ, targetPosition);
        float distanceNormalized = 1 - distance / totalDistance; // how far along from start to destination

        float maxHeight = totalDistance / 4f;
        float positionY = arcYAnimationCurve.Evaluate(distanceNormalized) * maxHeight; // gets the requisite Y value.
        transform.position = new Vector3(positionXZ.x, positionY, positionXZ.z);
        Vector3 nextFramemoveDir = targetPosition - positionXZ;

        if (Vector3.Dot(moveDir, nextFramemoveDir) <0)
        {
            float damageRadius = 4f; //note this is 4 units in WORLD position not grid space
            Collider[] colliderArray = Physics.OverlapSphere(targetPosition, damageRadius);

            foreach (Collider collider in colliderArray)
            { 
                if (collider.TryGetComponent<Unit>(out Unit targetUnit))
                {
                    DamageContext damageContext = new DamageContext();
// use [SerializedField] to experiment.  I liked 5000f for explosiveForce and 0f for upwardForve
                    damageContext.SetUpAsExplosion(explosiveForce, targetPosition + Vector3.up * 1f, damageRadius, upwardForce);
                    targetUnit.Damage(30, damageContext);
                }
            }
            OnAnyGrenadeExploded?.Invoke(this, EventArgs.Empty);

            trailRenderer.transform.parent = null; 
            Instantiate(grenadeExplodeVfxPrefab, targetPosition + Vector3.up * 1f, Quaternion.identity);
            Destroy(gameObject);

            onGrenadeBehaviourComplete();
        }
    }

    public void Setup(GridPosition targetGridPosition, Action onGrenadeBehaviourComplete)
    {
        this.onGrenadeBehaviourComplete = onGrenadeBehaviourComplete;
        targetPosition = LevelGrid.Instance.GetWorldPosition(targetGridPosition);

        positionXZ = transform.position;
        positionXZ.y = 0;
        float shoulderHeight = 1.25f; // should approximate the value of arcYAnimationCurve.Evaluate(0) * maxHeight
        transform.position = new Vector3(positionXZ.x, shoulderHeight, positionXZ.z); 
        totalDistance = Vector3.Distance(positionXZ, targetPosition);
    }
}

  1. Minor changes in unit script
    public void Damage(int damageAmount, DamageContext damageContext)
    {
        healthSystem.Damage(damageAmount, damageContext);
    }

    private void HealthSystem_OnDead(object sender, DamageContext damageContext)
    {
        LevelGrid.Instance.RemoveUnitAtGridPosition(gridPosition, this);

        Destroy(gameObject);

        OnAnyUnitDead?.Invoke(this, EventArgs.Empty);
    }

  1. Minor changes in Health System
    public void Damage(int damageAmount, DamageContext damageContext)
    {
        health -= damageAmount;

        if (health < 0)
        {
            health = 0;
        }

        OnDamaged?.Invoke(this, EventArgs.Empty);

        if (health == 0)
        {
            Die(damageContext);
        }
    }

    private void Die(DamageContext damageContext)
    {
        OnDead?.Invoke(this, damageContext);
    }

  1. Minor change in RagdollSpawner
    private void HealthSystem_OnDead(object sender, DamageContext damageContext)
    {
        Transform ragdollTransform = Instantiate(ragdollPrefab, transform.position, transform.rotation);
        UnitRagdoll unitRagdoll = ragdollTransform.GetComponent<UnitRagdoll>();
        unitRagdoll.Setup(originalRootBone, damageContext);
    }

  1. Minor change in Ragdoll. Note the code that was in ApplyExplosionToRagdoll() is now in DamageContext
    public void Setup(Transform originalRootBone, DamageContext damageContext)
    {
        MatchAllChildTransforms(originalRootBone, ragdollRootBone);
        damageContext.ApplyDamageEffectToRagdoll(ragdollRootBone, ragdollMainRigidbody);
    }

Later on in the destructible crate lesson you’ll need a simple means to call the DamageContext with some default values. I used a very simple solution. Add a static method to the DamageContextObject:

 public static void ApplyDefaultDamageEffect(Transform objectToDestroy, float explosiveForce, float explosionRange)
    {
        Vector3 randomDir = new Vector3(Random.Range(-1f, 1f), 0, Random.Range(-1f, 1f));
        ApplyExplosionToChildren(objectToDestroy, explosiveForce, objectToDestroy.position + randomDir, explosionRange);
    }

You can then also call this from within ApplyDamageEffectToRagdoll() method’s switch statement so that both use the same exact code.

Within DestructibleCrate your code will look like this:


    public void Damage()
    {
        Transform crateDestroyedTransform = Instantiate(crateDestroyedPrefab, transform.position, transform.rotation);

        Destroy(gameObject);

        OnAnyDestroyed?.Invoke(this, EventArgs.Empty);

        DamageContext.ApplyDefaultDamageEffect(crateDestroyedTransform, 150f, 10f);
    }

Having previously refactored this code to use a DamageContext it was super easy to make this modification. I’m sure as we continue to add features and explosion types, more refactoring will be needed but hopefully should be very simple going forward.

Privacy & Terms