Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Unreliable Hit Detection for Fast Projectiles

Discussion in 'Scripting' started by SpaceTako, Aug 16, 2020.

  1. SpaceTako

    SpaceTako

    Joined:
    Feb 15, 2018
    Posts:
    6
    I'm working on a game that deals with fast moving projectiles so I decided to use a linecast from the projectiles current position to their previous position on the last frame and check to see if a collision happen, and if so execute the proper collision code. The Issue comes when I switched it over to a spherecast solution since the linecast was only a thin line and wouldnt register a hit even if the projectile grazed the enemy since the linecast would only work if the collision occured on the center of the projectile. This is how I wanted the sphere cast to work (basically like a thicker raycast)
    the top image being a linecast that I was using prev. and the bottom being how I want the raycast detection to work.
    Code (CSharp):
    1. //fromPos = this.transform.position;
    2.             fromPos = transform.position;
    3.             toPos = previousPosition;
    4.             direction = toPos - fromPos;
    5.  
    6.             if (Physics.SphereCast(fromPos, sphereRadius, direction, out var hitInfo, direction.magnitude, obsticlesMask)) //dont want to ignore triggers cus the limbs are triggers
    7.             {
    8.                 //Debug.Break();
    9.                 Debug.Log(message: "checking DMG SPHERECAST");
    10.              
    11.                 if (hitInfo.transform.gameObject.tag != pj.owner.gameObject.tag)
    12.                 {
    13.                  
    14.                     //make the enemy take damage
    15.                     Debug.Log("HITCHA");
    16.                     Debug.Log(hitInfo.transform.name);
    17.                     potentialVictim = hitInfo.transform;
    18.  
    19.                     if (potentialVictim.GetComponent<LimbDamage>() != null && hasDamaged == false)
    20.                     {
    21.                         potentialVictim.GetComponent<LimbDamage>().TotalDamage(projectileDmg, Camera.main.transform.forward * projectileKnockback, chanceToDismember);
    22.                         hasDamaged = true;
    23.                         hasPushed = true;
    24.                     }
    25.  
    26.                     if (potentialVictim.GetComponent<HealthSystem>() != null && hasDamaged == false)
    27.                     {
    28.                         potentialVictim.GetComponent<HealthSystem>().TakeDamage(projectileDmg);
    29.                         hasDamaged = true;
    30.                     }
    31.                     //then mark yourself to be disabled cus you just collided
    32.                     if (projectileSND != null)
    33.                     {
    34.                         projectileSND.loop = false;
    35.                         projectileSND.clip = projectileImpactSND;
    36.                         projectileSND.Play();
    37.                     }
    38.                     ObjectPooler.Instance.AddBackToPool(this.gameObject, pj.poolTag);
    39.  
    40.                     if (this.GetComponent<HealthSystem>() != null)
    41.                         this.GetComponent<HealthSystem>().TakeDamage(100); //switch this to an internal method instead of using the health system
    42.                 }
    43.             }
    this is the current code I'm using for collisions but as I stated it doesnt work %100 of the time and I cant help but feel that I'm using the spherecast wrong and I'm just not understanding how to reach the solution I seek.
    Is there any way I can fix this collision detection and get it working %100 of the way instead of the unreliable mess it currently is? This is the rest of the code just incase it's needed for more context
    Code (CSharp):
    1. public abstract class Projectile : MonoBehaviour
    2. {
    3.  
    4.     //every projectile takes a movement code
    5.     //placeholder for how movement is going to work
    6.  
    7.  
    8.     //every projectile inflicts damage if it collides
    9.     //every projectile dies by collision or timer
    10.     public ProjectileBase pj;
    11.  
    12.     //attacking
    13.     [SerializeField] float projectileDmg;
    14.     [SerializeField] float projectileKnockback;
    15.     [SerializeField] float chanceToDismember = 30;
    16.  
    17.     public bool hasDamaged = false;
    18.     public bool hasPushed = false;
    19.  
    20.     Vector3 previousPosition;
    21.     public LayerMask obsticlesMask;
    22.     public float sphereRadius;
    23.     public float DeathsphereRadius;
    24.  
    25.     public Transform potentialVictim;
    26.  
    27.     Vector3 fromPos;
    28.     Vector3 toPos;
    29.     Vector3 direction;
    30.  
    31.     bool readyToFire;
    32.  
    33.     public float lifeSpan;
    34.     float timeAlive;
    35.  
    36.     //Seems like removing "fromPos" in "from + direction" in the sphere cast code fixed the collision issues ? WTF
    37.     //BUG : Collision Code isn't Firing Reliably
    38.     public float speed;
    39.  
    40.     AudioSource projectileSND;
    41.  
    42.     public AudioClip projectileLoopSND;
    43.     public AudioClip projectileImpactSND;
    44.  
    45.     [HideInInspector]
    46.     public bool hasDirectionVector;
    47.  
    48.     public abstract void Move(Vector3 _start, Vector3 _destination, float _speed); //implement this in a sub class
    49.     private void Awake()
    50.     {
    51.         pj = this.GetComponent<ProjectileBase>();
    52.         projectileSND = this.GetComponent<AudioSource>();
    53.         pj.projectileFinishedInitEvent += ReadyToFire;
    54.     }
    55.  
    56.     public virtual void FixedUpdate()
    57.     {
    58.         if (readyToFire == true)
    59.         {
    60.             Move(pj.bulletEmitter.position, pj.projectileDestination, speed);
    61.         }
    62.     }
    63.  
    64.     private void ReadyToFire()
    65.     {
    66.         timeAlive = lifeSpan;
    67.         hasPushed = false;
    68.         hasDamaged = false;
    69.         potentialVictim = null;
    70.         //Debug.Break();
    71.         previousPosition = this.transform.position;
    72.  
    73.         if(projectileSND != null)
    74.         {
    75.             projectileSND.clip = projectileLoopSND;
    76.             projectileSND.loop = true;
    77.             projectileSND.Play();
    78.         }
    79.  
    80.         readyToFire = true;
    81.     }
    82.  
    83.     private void OnDisable()
    84.     {
    85.         hasDirectionVector = false;
    86.         readyToFire = false;
    87.         //StopAllCoroutines();
    88.     }
    89.  
    90.     private void Update()
    91.     {
    92.         //calculate if a collision has occured
    93.         if (readyToFire == true)
    94.         {
    95.             //fromPos = this.transform.position;
    96.             fromPos = transform.position;
    97.             toPos = previousPosition;
    98.             direction = toPos - fromPos;
    99.  
    100.             if (Physics.SphereCast(fromPos, sphereRadius, direction, out var hitInfo, direction.magnitude, obsticlesMask)) //dont want to ignore triggers cus the limbs are triggers
    101.             {
    102.                 //Debug.Break();
    103.                 Debug.Log(message: "checking DMG SPHERECAST");
    104.              
    105.                 if (hitInfo.transform.gameObject.tag != pj.owner.gameObject.tag)
    106.                 {
    107.                  
    108.                     //make the enemy take damage
    109.                     Debug.Log("HITCHA");
    110.                     Debug.Log(hitInfo.transform.name);
    111.                     potentialVictim = hitInfo.transform;
    112.  
    113.                     if (potentialVictim.GetComponent<LimbDamage>() != null && hasDamaged == false)
    114.                     {
    115.                         potentialVictim.GetComponent<LimbDamage>().TotalDamage(projectileDmg, Camera.main.transform.forward * projectileKnockback, chanceToDismember);
    116.                         hasDamaged = true;
    117.                         hasPushed = true;
    118.                     }
    119.  
    120.                     if (potentialVictim.GetComponent<HealthSystem>() != null && hasDamaged == false)
    121.                     {
    122.                         potentialVictim.GetComponent<HealthSystem>().TakeDamage(projectileDmg);
    123.                         hasDamaged = true;
    124.                     }
    125.                     //then mark yourself to be disabled cus you just collided
    126.                     if (projectileSND != null)
    127.                     {
    128.                         projectileSND.loop = false;
    129.                         projectileSND.clip = projectileImpactSND;
    130.                         projectileSND.Play();
    131.                     }
    132.                     ObjectPooler.Instance.AddBackToPool(this.gameObject, pj.poolTag);
    133.  
    134.                     if (this.GetComponent<HealthSystem>() != null)
    135.                         this.GetComponent<HealthSystem>().TakeDamage(100); //switch this to an internal method instead of using the health system
    136.                 }
    137.             }
    138.  
    139.             //draw a ray
    140.             Debug.DrawLine(previousPosition, transform.position);
    141.             //update the previous position
    142.             previousPosition = transform.position; //this works
    143.  
    144.             //timer to disable yourself
    145.             timeAlive -= Time.deltaTime;
    146.             if (timeAlive <= 0)
    147.             {
    148.                 if (projectileSND != null)
    149.                 {
    150.                     projectileSND.loop = false;
    151.                     projectileSND.clip = projectileImpactSND;
    152.                     projectileSND.Play();
    153.                 }
    154.                 ObjectPooler.Instance.AddBackToPool(this.gameObject, pj.poolTag);
    155.  
    156.                 if (this.GetComponent<HealthSystem>() != null)
    157.                     this.GetComponent<HealthSystem>().TakeDamage(100);
    158.             }
    159.         }
    160.     }
    161.  
    162.     void OnDrawGizmos()
    163.     {
    164.         //attack collider --------------------------------
    165.  
    166.         Gizmos.color = Color.blue;
    167.         Debug.DrawLine(fromPos, direction * direction.magnitude);
    168.         //Gizmos.DrawLine(fpscam.transform.position, fpscam.transform.forward * grabRange + fpscam.transform.position);
    169.         Gizmos.DrawWireSphere(fromPos + direction * direction.magnitude, sphereRadius);
    170.         /*
    171.         //death collider ---------------------------------------
    172.         Gizmos.color = Color.red;
    173.         Debug.DrawLine(fromPos, fromPos + direction * direction.magnitude);
    174.         //Gizmos.DrawLine(fpscam.transform.position, fpscam.transform.forward * grabRange + fpscam.transform.position);
    175.         Gizmos.DrawWireSphere(fromPos + direction * direction.magnitude, DeathsphereRadius);*/
    176.     }
    177. }
     
  2. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,589
    Are the projectiles rigidbodies? There is a continuous collision detection mode for exactly these cases. It's obviously more performance intensive than the default detection mode, but so is manually casting lines/spheres and checking for collisions.
    https://docs.unity3d.com/ScriptReference/Rigidbody-collisionDetectionMode.html

    Other than that there are two potential problems with your approach:
    • Raycasts (and i imagine all other casts like spheres) will not register a collision from within the collider (more specifically, they wont register hitting the 'backside' of a collision mesh). That's probably why you wont register 100% of the collisions - if your bullet is inside the object, it wont be registered. Not an expert, but i think colliders only have a sort of border / margin where they check for collisions, so the normal collision detection system would miss this kind of collision aswell. You may wanna read up on the last part tho.
    • Spherecasts will return not only objects behind you, but also in front you.. and in all other directions, requiring even more calculations for finding out if the hit object(s) are in fact in a direct line towards your previous position, effectively requiring a new cast again, which requires more performance. I'd just go with the continuous rigidbody collisiondetection.
     
    Last edited: Aug 16, 2020
  3. SpaceTako

    SpaceTako

    Joined:
    Feb 15, 2018
    Posts:
    6
    The problem with the rigidbody approach is that it's more unreliable that what I have currently, but what I did notice is that in some rare instance the rigidbody approach would catch a collision that my spherecast approach wouldn't. It never crossed my mind how my collision detection would work if the projectile collision occurred inside the collider so that could help me towards a solution
     
    PraetorBlue likes this.
  4. exiguous

    exiguous

    Joined:
    Nov 21, 2010
    Posts:
    1,749
    If you just want a "thicker" line why not use a Capsule Cast? This is at least what you drawed in your example and should look like the lower part of it. Spherecast is wrong in this scenario IMO.
     
  5. SpaceTako

    SpaceTako

    Joined:
    Feb 15, 2018
    Posts:
    6
    Yeah I tried Capsule Cast but I was just getting some weird collisions, I'm pretty sure it's just that I don't understand how to use these different casts but I think I've stumbled upon a solution for this problem. I switched the SphereCast to SphereCastAll and it seems to be working now. The problem I was getting previously was that since the projectile was raycasting from it's current position to its position last frame, I would get instances where the bullet would pass through the Enemy and the Wall, but SphereCast only returns the 1st hit (at least that's what it seems like to me) that being the wall and not the enemy that technically it passed through 1st
     
    PraetorBlue likes this.
  6. SpaceTako

    SpaceTako

    Joined:
    Feb 15, 2018
    Posts:
    6
    I still can't say for sure if it's been %100 fixed as I'd need to get some more playtesting done before I can claim that the problems fixed
     
  7. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,722
    Just a heads up, SphereCastAll that returns an array will create a lot of garbage if you are running it every frame for every projectile. There's a SphereCastAllNonAlloc that lets you pass an existing array into that will populate your array with the results. This will not create garbage. Just use a reasonable sized array for how many hits you expect as naturally the array cannot be resized:

    https://docs.unity3d.com/2020.1/Documentation/ScriptReference/Physics.SphereCastNonAlloc.html
     
  8. SpaceTako

    SpaceTako

    Joined:
    Feb 15, 2018
    Posts:
    6
    I'll definitely look into this as I have noticed that what I have isn't very optimized. Thanks for letting me know about this!
     
  9. Hobby-Game-Developer

    Hobby-Game-Developer

    Joined:
    Aug 27, 2017
    Posts:
    14
    If you use rigidbody's for collision detection, then set the detection option not to discrete or continuous, use continuous dynamically. That worked for me and I use really fast Maschine gun bullets.