Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Projectiles pass through objects (using SphereCast, non physics based)

Discussion in 'Editor & General Support' started by FantasticGlass, Jan 9, 2019.

  1. FantasticGlass

    FantasticGlass

    Joined:
    May 31, 2016
    Posts:
    38
    I have an infrequent issue in my little game.
    Very rarely my projectiles pass through enemy colliders without hitting them.
    Every frame that the projectile is active I do a SphereCast from it's last position to it's current position.
    It works fine 90% of the time, but every so often a projectile will pass right through an enemy like it wasn't even there.
    I've paused the game and checked the projectile variables as it's passing through an enemy.
    The active bool is always correctly set so the CheckForHit() is being called.
    The Target LayerMask is correctly set to "Enemy" and the Enemy's layer is correctly set to "Enemy"
    Projectiles are set on layer "Projectiles" which has been set to not collide with anything else.
    Projectiles have a collider, but I'm not using it in this script.

    Any help would be appreciated.
    And if you catch something newbish in my code, please let me know, I'm always trying to get better.


    Projectile.cs
    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6. public class Projectile : MonoBehaviour
    7. {
    8.     [SerializeField]
    9.     private GameManager.Faction faction;
    10.     public GameManager.Faction Faction { get { return faction; } }
    11.  
    12.     [SerializeField]
    13.     private bool alive = false;
    14.     public bool Alive { get { return alive; } }
    15.  
    16.     [SerializeField]
    17.     private float lifeTime = 5f;
    18.     private float deathTime;
    19.  
    20.  
    21.     [Space(10)]
    22.     [Header("STATS")]
    23.     [SerializeField]
    24.     private bool enableSplashDamage = false;
    25.     [SerializeField]
    26.     private bool enableFriendlyFire = false;
    27.     [SerializeField]
    28.     private float directDamage = 10f;
    29.     public float DirectDamage { get { return directDamage; } }
    30.     [SerializeField]
    31.     private float splashDamage = 5f;
    32.     public float SplashDamage { get { return splashDamage; } }
    33.     [Space(5)]
    34.     [SerializeField]
    35.     private float directRadius = 0.5f;
    36.     [SerializeField]
    37.     private float splashRadius = 4f;  
    38.     [Space(5)]
    39.     [SerializeField]
    40.     private float speed = 12f;
    41.  
    42.     [Space(5)]
    43.     [SerializeField]
    44.     private bool enableRotation = false;
    45.     [SerializeField]
    46.     private float rotationSpeed = 30f;
    47.  
    48.     private Vector3 lastPosition;
    49.     private Vector3 direction;
    50.  
    51.     [Space(5)]
    52.     [SerializeField]
    53.     private bool ignoreWalls = false;
    54.     [SerializeField]
    55.     private LayerMask targetMask;
    56.     [SerializeField]
    57.     private LayerMask allEntitiesLayerMask;
    58.     [SerializeField]
    59.     private GameObject graphic;
    60.     [SerializeField]
    61.     private bool fadeOverLifeTime = false;
    62.     [SerializeField]
    63.     private MeshRenderer[] graphicMeshRenderer;
    64.     [SerializeField]
    65.     private Color graphicOriginalColor;
    66.  
    67.  
    68.     [Space(8)]
    69.     [SerializeField]
    70.     private bool explodeOnLifeEnd = false;
    71.     [SerializeField]
    72.     private Light explosionLight;
    73.     [SerializeField]
    74.     private float explosionLightIntensity = 140f;
    75.     [SerializeField]
    76.     private float explosionLightDuration = 0.6f;
    77.  
    78.  
    79.     [Space(8)]
    80.     [SerializeField]
    81.     private AudioClip deathSound;
    82.  
    83.  
    84.     [Space(10)]
    85.     [Header("REFERENCES")]
    86.     [SerializeField]
    87.     private ParticleSystem deathParticles;
    88.     private AudioSource audioSource;
    89.     public ProjectilePool pool;
    90.  
    91.  
    92.  
    93.  
    94.     private void Awake ()
    95.     {
    96.         graphic = transform.GetChild(0).gameObject;
    97.         graphicMeshRenderer = new MeshRenderer[2];
    98.         graphicMeshRenderer[0] = graphic.transform.GetChild(0).GetComponent<MeshRenderer>();
    99.         graphicMeshRenderer[1] = graphic.transform.GetChild(1).GetComponent<MeshRenderer>();
    100.         graphicOriginalColor = graphicMeshRenderer[0].material.color;
    101.  
    102.         audioSource = GetComponent<AudioSource>();
    103.     }
    104.  
    105.     private void Start ()
    106.     {
    107.         allEntitiesLayerMask = LayerMask.GetMask("Player") + LayerMask.GetMask("Enemy");
    108.         direction = transform.forward;
    109.     }
    110.  
    111.     private void Update ()
    112.     {
    113.         if (!alive) { return; }
    114.  
    115.         FadeGraphic();
    116.  
    117.         if (enableRotation)
    118.         {
    119.             graphic.transform.Rotate(Vector3.forward, rotationSpeed * 10f * Time.deltaTime);
    120.         }
    121.  
    122.         lastPosition = transform.position;
    123.  
    124.         Vector3 newPosition = transform.position + direction * speed * Time.deltaTime;
    125.         transform.position = newPosition;
    126.  
    127.         CheckForHit();
    128.  
    129.         if (Time.time > deathTime)
    130.         {
    131.             if (explodeOnLifeEnd)
    132.             {
    133.                 HitSomething();
    134.             }
    135.             else
    136.             {
    137.                 Die();
    138.             }
    139.         }
    140.     }
    141.  
    142.  
    143.  
    144.  
    145.     public void SetFaction (GameManager.Faction newFaction)
    146.     {
    147.         faction = newFaction;
    148.     }
    149.  
    150.     public void SetMask (LayerMask newTargetMask)
    151.     {
    152.         if (ignoreWalls)
    153.         {
    154.             targetMask = newTargetMask;
    155.         }
    156.         else
    157.         {
    158.             targetMask = LayerMask.GetMask("Wall") + LayerMask.GetMask("Ground") + LayerMask.GetMask("Neutral") + newTargetMask;
    159.         }
    160.     }
    161.  
    162.     public void SetSpeed (float newSpeed)
    163.     {
    164.         speed = newSpeed;
    165.     }
    166.  
    167.     public void SetDamage (float newDirectDamage, float newSplashDamge = 0f)
    168.     {
    169.         directDamage = newDirectDamage;
    170.         splashDamage = newSplashDamge;
    171.  
    172.         if (splashDamage == 0)
    173.         {
    174.             enableSplashDamage = false;
    175.         }
    176.     }
    177.  
    178.     public void SetLifeTime (float newLifeTime)
    179.     {
    180.         lifeTime = newLifeTime;
    181.     }
    182.  
    183.     public void Fire (Vector3 newDirection)
    184.     {
    185.         alive = true;
    186.  
    187.         lastPosition = transform.position;
    188.         transform.forward = direction = newDirection;
    189.  
    190.         if (fadeOverLifeTime)
    191.         {
    192.             graphicMeshRenderer[0].material.color = graphicOriginalColor;
    193.             graphicMeshRenderer[1].material.color = graphicOriginalColor;
    194.         }
    195.  
    196.         graphic.SetActive(true);
    197.        
    198.         deathTime = Time.time + lifeTime;
    199.     }
    200.  
    201.     public void Die (Entity hitEntity = null)
    202.     {
    203.         alive = false;
    204.         graphic.SetActive(false);
    205.  
    206.         ReturnToPool(0);
    207.     }
    208.  
    209.  
    210.     private void CheckForHit ()
    211.     {
    212.         RaycastHit hitInfo;
    213.         Vector3 dir = transform.position - lastPosition;
    214.         Ray ray = new Ray(lastPosition, dir.normalized);
    215.  
    216.         if (Physics.SphereCast(ray, directRadius, out hitInfo, dir.magnitude, targetMask, QueryTriggerInteraction.Ignore))
    217.         {
    218.             transform.position = hitInfo.point;
    219.  
    220.             Entity hitEntity = hitInfo.collider.GetComponent<Entity>();
    221.  
    222.             if (hitEntity != null)
    223.             {
    224.                 hitEntity.TakeDamage(directDamage);
    225.             }
    226.  
    227.             HitSomething(hitEntity);
    228.         }
    229.  
    230.  
    231.     }
    232.    
    233.     private void HitSomething (Entity hitEntity = null)
    234.     {
    235.         if (enableSplashDamage)
    236.         {
    237.             DealSplashDamage(hitEntity);
    238.         }
    239.  
    240.         alive = false;
    241.         graphic.SetActive(false);
    242.  
    243.         if (deathSound != null)
    244.         {
    245.             audioSource.PlayOneShot(deathSound);
    246.         }
    247.  
    248.         if (deathParticles != null)
    249.         {
    250.             deathParticles.Play();
    251.             ReturnToPool(deathParticles.main.startLifetimeMultiplier);
    252.         }
    253.         else
    254.         {
    255.             ReturnToPool(0);
    256.         }
    257.  
    258.         if (explosionLight != null)
    259.         {
    260.             StartCoroutine(FlashLight(explosionLightDuration));
    261.         }
    262.     }
    263.    
    264.     private void DealSplashDamage (Entity hitEntity)
    265.     {
    266.         LayerMask mask = (enableFriendlyFire == true) ? allEntitiesLayerMask : targetMask;
    267.         Collider[] hitColliders = Physics.OverlapSphere(transform.position, splashRadius, mask, QueryTriggerInteraction.Ignore);
    268.  
    269.         if (hitColliders.Length > 0)
    270.         {
    271.             for (int i = 0; i < hitColliders.Length; i++)
    272.             {
    273.                 Entity splashHitEntity = hitColliders[i].GetComponent<Entity>();
    274.  
    275.                 if (splashHitEntity == hitEntity) { continue; }
    276.  
    277.                 if (splashHitEntity != null)
    278.                 {
    279.                     splashHitEntity.TakeDamage(splashDamage);
    280.                 }
    281.             }
    282.         }
    283.     }
    284.  
    285.     private void ReturnToPool (float delay = 0f)
    286.     {
    287.  
    288.         if (delay > 0)
    289.         {
    290.             StartCoroutine(ReturnToPoolAfterDelay(delay));
    291.             return;
    292.         }
    293.  
    294.         direction = Vector3.zero;
    295.         pool.ProjectileReturn(this);      
    296.     }
    297.  
    298.     private void FadeGraphic ()
    299.     {
    300.         if (!fadeOverLifeTime) { return; }
    301.         if (graphicMeshRenderer == null) { return; }
    302.  
    303.         Color newColor = graphicMeshRenderer[0].material.color;
    304.  
    305.         newColor.a = Mathf.Clamp01(newColor.a -= Time.deltaTime / 1.5f);
    306.  
    307.         graphicMeshRenderer[0].material.color = newColor;
    308.         graphicMeshRenderer[1].material.color = newColor;
    309.  
    310.     }
    311.  
    312.  
    313.  
    314.  
    315.  
    316.     private IEnumerator ReturnToPoolAfterDelay (float duration)
    317.     {
    318.         yield return new WaitForSeconds(duration);
    319.  
    320.         ReturnToPool();
    321.     }
    322.  
    323.     private IEnumerator FlashLight (float duration)
    324.     {
    325.         explosionLight.enabled = true;
    326.         explosionLight.intensity = explosionLightIntensity;
    327.  
    328.         float currentDuration = duration;
    329.  
    330.         while (currentDuration > 0)
    331.         {
    332.             explosionLight.intensity = explosionLightIntensity * (currentDuration / duration);
    333.             currentDuration -= Time.deltaTime;
    334.             yield return null;
    335.         }
    336.  
    337.         explosionLight.enabled = false;
    338.     }
    339.  
    340.  
    341.  
    342.  
    343.     private void OnDrawGizmosSelected ()
    344.     {
    345.         Gizmos.color = Color.green;
    346.         Gizmos.DrawWireSphere(transform.position, directRadius);
    347.         Gizmos.DrawWireSphere(transform.position, directRadius);
    348.  
    349.         if (alive)
    350.         {
    351.             Gizmos.color = Color.magenta;
    352.             Gizmos.DrawWireSphere(lastPosition, directRadius);
    353.         }
    354.     }
    355.  
    356.  
    357. }
     
  2. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847
    If I were to venture a guess I'd say you're probably getting a hiccup in your frame rate for whatever unrelated reason, and between frames the projectile has passed to the other side of the object you expect to hit.
     
  3. FantasticGlass

    FantasticGlass

    Joined:
    May 31, 2016
    Posts:
    38
    Hey sorry for the late response.
    Thanks for replying!

    It's entirely possible you are correct.
    However, the frame rate is around 700 fps in editor, and I don't think the projectiles are traveling fast enough to do so.
    The SphereCast radius is decently large, and the enemy collider is quite large.
    Maybe I'll screen cap a video of this to share.

    I have a feeling I'm missing something simple, or something is wrong with my code.
    But right now I have no idea.
     
  4. FantasticGlass

    FantasticGlass

    Joined:
    May 31, 2016
    Posts:
    38
    I think I might have found the source of my problem.
    https://answers.unity.com/questions/1255321/raycast-or-linecast.html

    There might be some times when the start of the ray, my projectiles' lastPosition, is inside the enemy collider, and so passes right through.

    I thought it still would have collided when coming out the other side, but maybe not.
    Also my slower projectile's currentPosition and lastPosition might be only ever both inside or both outside the enemy collider.
    If so it would not register a hit, and would look the same as my problem.
    I have noticed that my super fast projectiles never pass through an enemy collider.
    Further evidence.

    I'll investigate further.
    I'll have to search where to go from here.