While this works, it’s not very consistent throughout different frames per second. At around 20fps, it shoots around 100 bullets in 20 seconds. At 40-50fps, it shoots around 150 bullets in 20 seconds.
Moving it to FixedUpdate makes the difference between the two a little less, but still, not very consistent.
Also notable: at a locked 60fps, it’s around 160 bullets in 20 seconds. Shouldn’t it be 200 at 60fps?
What could I do to achieve a consistent fire rate? Note: I don’t want to make the fps fixed…
The problem is that your call to fire() is frame dependent. Now, you were pretty smart about this and locked the actual firing time to a fixed amount but there is still a variable amount of time between the point that fireDelay is reset to 0 and the next call to fire().
A more accurate approach might be to set the fire rate in terms of ‘bullets per second’ and then use a timer along with the delta time of the frame to ensure that you don’t exceed this fire rate.
Thanks for your reply! Not sure if I’m following here… How would this work exactly? Because in my example above, it’s shooting too few bullets per second, so it’s definitely not exceeding.
Well, I think your math is wrong. You are definitely getting too many - though I’m not sure why other than the possiblity that the frame rate isn’t quite fixed. At 60 frames per second for 20 seconds you will call fire() approximately 1200 times. You have a firing time of 0.1 seconds so divide that 1200 by 10 to get 120 bullets max.
Anyway, my suggestion is to remove the co-routine altogether. It’s not going to give precise results here and will likely harm performance on top of that. Instead, calculate everything in Update(). Use a variable to store the number of bullets to fire per second - BPS. From here, in each call to Update() you should add Time.deltaTime to a time accumulator. When this accumulator exceeds your BPS: 1) temporarily store the difference of the accumulator and the target BPS, 2) reset the accumulator, 3) add the previously stored difference back to the accumulator, 4) fire your bullet. For each update that the the accumulator doesn’t exceed, simply add the Time.deltaTime to it and early-out.
WaitForSeconds is evaluated each frame (don’t get confused by it being a coroutine, it’s still being evaluated with all updates, so per frame).
So it’s not waiting exactly 100ms, it’s waiting at least 100ms. And the 100ms starts counting from the end of the frame you fire it.
Hopefully this terrible graph can help:
So what ends up happening is that you’re firing at approximately 150ms (or 200ms if you dip slightly below 20fps), which is not what you want.
At higher framerates, since update is evaluated more often, the wasted time is less, so you’re getting more shots and closer to your desired waiting time. But that’s not a good solution.
The solution:
Get rid of the coroutine.
There are many approaches, here is one:
if (Time.time > nextfire)
{
nextfire += 0.1f;
Debug.Log("fire!");
}
nextfire is the time you want to fire. By adding 0.1f to that value instead of the current time, we compensate for the fact that one shot might happen a bit late. So if a shot happens at 1000ms, next fire is 1100ms. If the next shot happens at 1150ms, the next one will happen much sooner (1100+100= 1200ms, so in 50ms) compensating for the fact that one happened late.
like you suggested. I feels better in terms of consistency between different frame rates, but I’m still not able to get the 200 shots in 20 seconds. It’s around 180 now. Shouldn’t 0.1f be exactly 200 shots or am I missing something here?
Using +0.03f gives me around 420 shots in 20 seconds on 40fps.
Edit:
using nextFire +=0.1f fires around 220 shots in 20 seconds…
That’s actually not what he suggested. Though it seems like those two snippets are practically identical, your snippet doesn’t allow for “rollover”, while his does. What happens when you reset nextFire to Time.time plus the delay is that you are discarding the amount by which Time.time was over nextFire. This will introduce some discrepancy and cause it to trigger slightly less frequently, as you’ve noticed. In order to be the most consistent possible in terms of number of bullets in a given period of time over basically any frame rate, you want to do something like this:
Thanks, both solutions seem to work! But now I’m having troubles hooking up my spacebar press to initiate the shooting. Where do I put the if (Input.GetKey(KeyCode.Space))? The shooting has to stop when releasing it, of course. A rookie question, I know, still very new to Unity and coding
There are many ways you could approach that. You could try something like this code. Enabling the Test class above will restart the timing sequence in its OnEnable:
public class GunController : MonoBehaviour
{
void Start()
{
m_firing = GetComponent<Test>();
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
m_firing.enabled = true;
else if (Input.GetKeyUp(KeyCode.Space))
m_firing.enabled = false;
}
Test m_firing;
}
Ok, before we get into what the problem is likely to be, can I just check what you have done to narrow down the problem?
For example, if you were to take another look at the OnEnable method, do you have any thoughts on what could be happening there? If not, what could you do to find out? Are you comfortable with using the debugger or debug logging?
Correct me if I’m wrong, but I was thinking the enabling worked, but it just had to wait on the delay the first bullet too. That’s not necessary of course, that’s why I added a shot fire in the OnEnabled too. Seems to work now, but again, correct me if I’m misinterpreting something :p.
One question still remains: should I use the FixedUpdate() or Update() cycle in the Test class? In GunController it’s Update() I assume, but I was thinking about moving it to the FixedUpdate() cycle in the Test class to ensure consistency with different frame rates.
Ok, if that is working for you, then great. However, I was thinking more along the lines of removing line 4 in that excerpt (the m_time += m_rate;). That line means it is starting with a delay (rather than an immediate shot) which is not what you want.
I guess that will depend on exactly what code you actually have inside the method. Remember that FixedUpdate is not necessarily synchronised with screen refreshes though.
The code only reduces the bullet count (i.e. removing it from the clip) and a new bullet is recorded to be created (but not yet rendered).
The code is adding / removing muzzle flashes.
The first case could be fine for FixedUpdate. It is not directly doing anything with the visuals, is purely mathematical and preparing for the next frame update when the necessary firing animations can be drawn (or even ignored if necessary).
The second case is not a good fit for FixedUpdate because you could potentially end up with a muzzle flash image remaining on screen for a (small but noticable) period of time longer than expected. Basically, it can end up creating slightly jarring visuals.
Well, I call a fire function instead of the ++m_fired; which does everything: play the sound & animation, do a muzzle flash using a Coroutine, decrease the ammo, …
so I guess I’m unlucky :(.
There probably isn’t a way around the FixedUpdate part, and still maintain consistent fire rates?
If it is running at or near the limit of your frame rate, then yes it could cause issues that would need some form of working around. For example, with the sound, you could play a continuous audio clip for the duration of the firing rather than a “bang per shot” clip. It could enhance the illusion to the player of a consistent rate.
Thanks for the advance! As it turns out, the Update() cycle seems to be pretty consistent now… same fire rate at 20fps as at 60fps, so I guess I’m sticking with the Update() cycle for now. Thanks for all the help, I learned a lot!!!