Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

Gun Fire-rate Is Frame-rate Dependent

Discussion in 'Editor & General Support' started by WorldWideGlide, Apr 15, 2019.

  1. WorldWideGlide

    WorldWideGlide

    Joined:
    Nov 3, 2013
    Posts:
    26
    I've taken several different approaches to instantiating a projectile from a gun and have ran into an issue where the fire rate of the gun decreases significantly if my frame-rate dips.
    • Animation event method
      • A button press triggers an animation which contains events that instantiate the projectile, plays sound effects and instantiate particles.
      • Animation triggering is affected by frame-rate???
    • Coroutine based method
      • A button press starts a coroutine which stages the projectile firing along with and sound effects or using yield return new WaitForSeconds()
      • I assume this doesn't work because WaitForSeconds() is not based on deltaTime?
    Both of these methods work fine unless there is a dip in frame-rate which results in a nearly proportional dip in fire-rate. Creating a handful of timers and incrementing them by time.delta time fixes the issue, but this seems really over-complicated for such a simple operation.

    Is the timer method the only way to handle this?
     
  2. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,202
    Well, ultimately, your fire rate will always be somewhat frame-rate dependent, but the goal is to minimize that. Consider an extreme example where your desired fire rate is one shot every 0.25 seconds, but your frame rate is so low that you're only rendering 1 FPS. In that case, Unity will only execute the Update() methods on your scripts once per second, dropping your fire rate to one shot per second as well.

    A coroutine-based approach should be fine. Code executed in a coroutine, using WaitForSeconds, is definitely based on deltaTime. However, it's important to note that WaitForSeconds doesn't wait for an exact number of seconds. It waits at least that long, but it could wait longer. For example, let's say your framerate was 10 FPS, and your Coroutine waits via WaitForSeconds(0.25f). Unity will check if your coroutine can continue at 0.2 seconds, and it will decide that it should not continue yet. Then it will check again at 0.3 seconds (because the framerate is 10 FPS in this example), and now it will decide that enough time has passed, so it will continue the rest of the coroutine. But now .3 seconds has passed instead of the desired 0.25 seconds.

    I think either of your two approaches are ultimately at the mercy of how long your game goes between rendering frames. The shorter the interval between shots, the more likely it is that frame rate drops will result in a longer than desired interval between shots. I don't see how a timer and a coroutine would behave any differently here. But the approach I usually take is just to capture the Time.timeSinceLevelLoad when I fire a shot. Later, when checking whether another shot should be fired, I just make sure that (Time.timeSinceLevelLoad - _lastShotTime) is greater than some amount.

    Depending on the kind of game you're making, one way to get around this would be some custom behavior where on any given frame, you might actually fire more than once. This would likely only be practical for games where your shoot a relatively slow-moving projectile, rather than the kind of game where you perform a raycast and instantly shoot a target when firing. But in this kind of approach, you'd see how much time has passed since the last update. If enough time has passed between updates to allow two shots to fire, you could instantiate two shots, and place one of that far enough ahead of the player to make it behave as though the shot was fired "between frames".
     
    mrsorry78 and WorldWideGlide like this.
  3. WorldWideGlide

    WorldWideGlide

    Joined:
    Nov 3, 2013
    Posts:
    26
    I like the idea of firing more than once per frame, I could implement a counter and check it to make sure enough rounds have been fired after a given time.

    By the way I'm firing maybe 10 projectiles per second and am running 60-200 FPS depending on what is in the scene. The difference in fire rate between 200 FPS and 60 FPS is huge when using the coroutine, like less than 5 projectiles per second. But with time.deltaTime timers the difference is negligible during the dips.
     
  4. Antony-Blackett

    Antony-Blackett

    Joined:
    Feb 15, 2011
    Posts:
    1,778
    You can make it non-framerate dependant.

    Use Time.deltaTime to see how much time has passed. Calculate how many bullets should have been fired in that span and also for each bullet calculate how long ago they should have been fired, then spawn them with a lifetime less that time that has passed and in a position equivalent to them having been fired that long ago.

    Even with the example above you would then have 4 bullets spawn on a single frame each spaced the correct distance apart.

    To make it even more accurate you should also track the muzzle location each frame and take the muzzle motion into account when figuring out where projectiles should have spawned in the past.

    Or here's the easy way:

    Ensure your firerate is never faster than fixed update and any incrementation of your firerate is always a multiple of your fixed update rate. Then use fixed update to fire your bullets. Unity will ensure that fixed update is always called the appropriate amount of times and you won't need to do any position interpolation for spawn points.
     
    WorldWideGlide likes this.
  5. WorldWideGlide

    WorldWideGlide

    Joined:
    Nov 3, 2013
    Posts:
    26
    I am on the same page as you guys on making sure I don't try to fire faster than the frame-rate. I changed my coroutine yields and it fixed the problem.

    Instead of:
    FireProjectile();
    yield return new WaitForSeconds(FireCooldownTime);

    I changed it to:
    FireProjectile();
    While(FireCooldownTimer < FireCooldownTime)
    {
    FireCooldownTimer += Time.deltaTime;
    yield return new WaitForEndOfFrame();
    }

    It looks like WaitForSeconds was not taking frame time into consideration?
     
  6. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,202
    If you're trying to fire 10 shots per second, and you're seeing a big different in actual shots per second between 60 FPS and 200 FPS, I think something must be wrong. The code you posted there doesn't really make sense to me. There must be another outer while look, perhaps? Maybe post the actual code?

    I'm assuming that your firing logic is based on whether the player has the key/button pressed, so that they keep firing as long as the button is held? Is so, I'd expect something like the following. I'd do this in Update. No need to use a Coroutine, but it could be done that way as well if you prefer.

    Code (CSharp):
    1. void Update() {
    2.     HandleFiring();
    3. }
    4.  
    5. // How long between shots.
    6. private const float _firingRate = 0.1f;
    7. // When was the last shot fired. Init to a negative value so that we're ready to fire immediately.
    8. private float _lastShotFired = -_firingRate;
    9. void HandleFiring()
    10. {
    11.     // Replace input handling with whatever logic you're using to test for held firing input
    12.     if (Input.GetMouseButton(0))
    13.     {
    14.         if (Time.timeSinceLevelLoad - _lastShotFired >= _firingRate)
    15.         {
    16.             // We've waited long enough since the last shot. Shoot again.
    17.             Debug.Log("Fire");
    18.             // TODO: Instantiate projectile or perform firing logic
    19.  
    20.             // Update LastShotFired
    21.             _lastShotFired = Time.timeSinceLevelLoad;
    22.         }
    23.     }
    24. }
    With that code, I'd expect almost no difference in the firing rate as you go from 60 to 200 FPS.
     
  7. Antony-Blackett

    Antony-Blackett

    Joined:
    Feb 15, 2011
    Posts:
    1,778
    I’m not sure about that. I would expect both of these to be almost same. Only difference is execution at the end of frame instead of update().

    I think the issues you’re seeing is you don’t account for overflow of the timer.
     
  8. WorldWideGlide

    WorldWideGlide

    Joined:
    Nov 3, 2013
    Posts:
    26
    Sorry that was just for reference, here is the actual code for the original coroutine:
    Code (CSharp):
    1. IEnumerator LaunchProjectileTest()
    2.     {
    3.         var shotsFired = 0;
    4.         firing = true;
    5.         anim.SetBool("Shooting", true);
    6.         //CHARGE
    7.         if (chargeParticle)
    8.             Instantiate(chargeParticle, launchPoint.position, launchPoint.rotation, launchPoint);
    9.         yield return new WaitForSeconds(chargeTime);
    10.         //FIRE
    11.         while (shotsFired < numberShots)
    12.         {
    13.             anim.SetTrigger("Shoot1");
    14.             shotsFired++;
    15.             FireSound();
    16.             if (transform.tag == "Player")
    17.             {
    18.                 if (OnPlayerFire != null)
    19.                     OnPlayerFire();
    20.             }
    21.             stat.RHClipCountCurrent -= config.ammoPerFire;
    22.             Instantiate(muzzleFlash, launchPoint.position, launchPoint.rotation, launchPoint.transform);
    23.             for (int i = 0; i < numberProjectiles; i++)
    24.             {
    25.                 var p = Instantiate(projectileObj) as GameObject;
    26.                 p.GetComponent<Projectile>().Launch(launchPoint.position, this.transform, projectileSpeed, projectileSpread, projectileSize, angleOffset, 0, transform.tag == "Player", hitForce, EquipmentSlot.Rifle);
    27.             }
    28.             yield return new WaitForSeconds(shotTime);
    29.             shotTimer = 0f;
    30.         }
    31.         //DISCHARGE  
    32.         yield return new WaitForSeconds(dischargeTime);
    33.         if (dischargeParticle)
    34.             Instantiate(dischargeParticle, launchPoint.position, launchPoint.rotation, launchPoint);
    35.         //COOLDOWN
    36.         yield return new WaitForSeconds(fireCooldownTime);
    37.        
    38.         firing = false;
    39.  
    40.     }
    And this coroutine is called in Update by:
    Code (CSharp):
    1. if (Input.GetKey(KeyCode.Mouse0))
    2.         {
    3.             if (firing == false)
    4.             {
    5.                 StartCoroutine(LaunchProjectile());
    6.             }
    7.             else
    8.             {
    9.                 anim.ResetTrigger("Shoot1");
    10.             }
    11.         }
    If I replace all of these yields:
    Code (CSharp):
    1. yield return new WaitForSeconds(chargeTime);
    with while loops:
    Code (CSharp):
    1. while(chargeTimer < chargeTime)
    2.         {
    3.             chargeTimer += Time.deltaTime;
    4.             yield return new WaitForEndOfFrame();
    5.         }  
    The slow down disappears. Not really sure why but that is what I am experiencing. I'm sure your method will also work I guess the issue is that I'm trying to sequence up a lot of different effects on each shot which makes coroutines or animation events convenient.
     
  9. WorldWideGlide

    WorldWideGlide

    Joined:
    Nov 3, 2013
    Posts:
    26
    Yeah def could be overflow
     
  10. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,202
    Okay. So this is kind of a mini-gun style weapon. A coroutine-based approach should be fine. The only reason I can think of why the while-loop timer you're using would result in different timing than the WaitForSeconds approach would be is something else in your code changing your timer values at the same time, causing them to increment faster than expected. Otherwise there really should be no difference. But a couple of observations about your code. I don't know if any of these are actually the source of your issue, but may be worth looking into.

    The 'else' statement in your Update statement, where you reset the Shoot1 trigger, seems a odd to me, and it seems like it's going to conflict with what you're doing in your Coroutine's while loop. Essentially, the first frame the user is holding the Mouse0 button, you'll start the coroutine. But for all subsequence frames, you're going to call ResetTrigger on the animator, even on frames where you're calling SetTrigger for the same parameter. What's the purpose of resetting the trigger manually, when triggers are typically used for one-shot events? (It almost looks like your trigger used to be a boolean parameter, where you needed to set/unset it. But triggers work differently.) Are there any animation events in your shooting animation that maybe aren't getting reached before you're resetting the trigger?

    On line 29 you're setting shotTimer to 0. Maybe that's just old code? I don't see what it does, but it's worth checking whether it's doing something unintended.

    Anyway, sorry if this isn't much help. I don't really see why switching out the coroutine for the while loop should change the behavior.
     
  11. WorldWideGlide

    WorldWideGlide

    Joined:
    Nov 3, 2013
    Posts:
    26
    You are correct, the else in the Update statement does not need to be there, I think that's something that was left over from a previous iteration of code that I forgot to remove, same with the shotTimer. I do sometimes reset triggers to prevent animations that are triggered from GetKey() from looping when they aren't supposed to.

    Yeah still not sure what's causing the difference, there are no other references to the charge/discharge/cooldown timers. If it's overflow then the fire-rate should only be marginally affected. I'll stick to the while loops for now as they are working until I have more time to debug.