Tick Time Debt
So despite all that fancy pants math I did, I was still having a problem with bullet positions.
Set up to fire at 1/5 second intervals, and with just enough velocity that each shot should come out just touching the tail of the last one, I was still ending up with weird gaps and inconsistent spacing between the shots.
This is my cooldown code to see if we should fire.
And after firing, add time to the cooldown.
(We should probably loop these until Shot Cooldown Time Left is greater than 0. That way if we slow down so much that the cooldown is passed multiple times in one tick, we just fire multiple bullets in one frame.)
Even simplifying the math down to nothing but linear velocity with no acceleration and no angle stuff didn’t seem to help. I was still getting inconsistent spacing. The more inconsistent the framerate was, the more inconsistent the spacing was.
It turns out there was something else about that condition where we pass so much time that we have to fire multiple bullets in one frame: They all end up stacked on top of each other, because they’re all really fired at the same time with the same velocity. It also turns out they get a full tick for the frame they were spawned on.
So there’s our problem. Bullets are actually getting spawned at the time of the tick call instead of our desired every fifth of a second. That’s why everything spawned in one frame is all stacked onto the same spot.
The solution here was to make a “time debt” that bullets need to make up for. This might involve dropping time from its first tick, or adding time to it.
When the shot cooldown timer goes negative, it means we should have fired a shot that many seconds ago, so shots need to make up for this time that they would miss out on. That’s pretty easy to handle. Just add to the time delta for its first keyframe.
void AShooterProjectile::Tick(float DeltaSeconds) {
DeltaSeconds += TickTimeDebt;
TickTimeDebt = 0.0f;
Super::Tick(DeltaSeconds);
... <snip> ...
}
(Projectile code is all done in C++ instead of Blueprints.)
We also need to make up for the fact that it’s going to get the whole time delta for this frame, meaning it’s going to be simulating, effectively, before it ever existed. So I just subtracted the entire time delta for the frame we’re on.
With this code, its first tick (which happens on this frame) will only be the amount of time it should have from the moment it should have been created based on our shot cooldown thingy.
So what does it look like now?
Aww yeah! Pixel perfect spacing, even with a variable timestep! (Minus floating point inaccuracy and whatever.)
Note: This article originally appeared on my Tumblr and was reposted here in 2019.