Game Development Community

Uneven Automatic Weapon Timing vs. a useRemainderDT Bug

by Henry Todd · in Torque 3D Professional · 05/10/2013 (12:21 pm) · 7 replies

If you use automatic weapons in Torque you may have noticed that they don't fire particularly evenly. As in, the delay between shots is not consistent. A long time ago I used to think this was caused by the state system itself or the relatively low default tick rate (for a shooter) of 32ms.

So let's start with this: Because of how the state system works your refire delays need to be multiples of 0.032. Fire events only happen at increments of 32ms to begin with (the length of a tick) so any other value is just confusing and can result in uneven numbers of ticks between shots. In the Lurker the values in question are:
stateTimeoutValue[5]
stateTimeoutValue[6]
Make sure they're both a multiple of 0.032 (or your (TickMs / 1000) if you've changed it).

The problem is that even if you set your "Fire" and "NewRound" timeout delays to multiples of 32ms your automatic fire won't be even. Turns out there's a fairly simple code bug causing this.

In shapeimage.cpp you'll find these lines at the end of ShapeBase::updateImageState
if ( image.rDT > 0.0f && image.delayTime > 0.0f && imageData.useRemainderDT && dt != 0.0f )
      goto TICKAGAIN;

Aside from the fact that I had forgotten there was such a thing as the goto command in C++ (I'm saying you don't see it in use very often) there's a fundamental bug here:

imageData.useRemainderDT's function is to allow leftover delta time to be used immediately so that the tick represents a full 32ms even if the active state at the start of the update had less than 32ms of time remaining.

The problem is that this code will run another full 32ms instead of only using the remaining delta time (image.rDT) because it doesn't alter the variable "dt" before going back and running the image tick again. In fact if your timeout values are very small this system can actually get stuck running way too many ticks all at once and generally messing up your image state timings completely. I believe the correct code would be:

if ( image.rDT > 0.0f && image.delayTime > 0.0f && imageData.useRemainderDT && dt != 0.0f )
   {
      dt = image.rDT;
      goto TICKAGAIN;
   }

I've tested this change and it solves all of those weapon timing errors that make Torque's shooter mechanics look so unstable.

Technically it's also not a great idea to check "image.rDT > 0.0f" because I've actually seen cases where this causes reticks when it shouldn't (if remaining time on this state was exactly 0.032 the floating point subtraction of a dt value of 0.032 will result in a number like 0.000001, causing an erroneous "TICKAGAIN" to occur). That said, the fix above solves those problems because the re-tick will use a dt value of 0.000001 (or whatever) and essentially do nothing. It's wasted code execution but generally harmless.

You can also set "useRemainderDT = false;" in your weapon datablocks to even out the fire delays, however I assume useRemainderDT is usually enabled for a reason (its actual function is to make sure that the state machine processes real time without losing any dt when a state is changed during an update).

Anyway, I think after making this change (and assuming you're using timeouts which are multiples of 0.032) you'll find that Torque's automatic weapons don't feel nearly so much like they need to be stripped, cleaned and lubricated.

Edit:
In case anyone's wondering, a total delay of 64 ms produces a fire rate of about 930rpm, while 96ms gets you 625rpm. Without changing the tick rate there's nothing in between.

#1
05/10/2013 (1:34 pm)
Henry,

Nice catch. I will take a look at this... If it works out correctly I will see about adding it to the next release.

Ron
#2
05/10/2013 (4:30 pm)
:O

Have we finally found the source of the awful bad timing in weapons?? This very bug has actually at times made me re-think even using Torque for some of my planned projects as it is very, very obvious. This must be merged in the next release if it is in-fact good to go.
#3
05/10/2013 (7:33 pm)
useRemainderDt has always been a bit dangerous. If the weapon had a very fast reload rate and/or lots of weapons were being fired at the same time the engine would randomly freeze or crash. Vince talked about this here: https://www.garagegames.com/community/forums/viewthread/131027

No one was able to nail down the cause but it seems like this fix could solve that issue as well.
#4
05/10/2013 (9:19 pm)
I just tried this fix and using the Lurker, and to me it sounds like the fire rate still hops around.

Edit:
I changed stateTimeoutValue[5] and stateTimeoutValue[6] to 0.064.
#5
05/10/2013 (11:01 pm)
@Adam:
And made the code change? You'll need to do both.

(Edit: reduced verbosity... just imagine how it was before)

I also hear some minor variation in the sounds, however I can confirm that the server fire rate is perfect using debug console output from the update function. And the performance is subjectively much improved, at least in my testing.

I can think of two possible causes for any remaining RoF variation on the client:

-Some of it could be the sound engine being difficult. High RoF weapons can overload it, resulting in the beginning of additional fire sounds being cut off, which can make the RoF sound uneven. And at least on my system fire sounds sometimes get strange position and Doppler effects. You might want to try muting the sound and watching the flare effects, as they seem quite even to me after the fix.

-The second potential cause, which I might have a fix for below, is that the client fire rate can technically be off slightly because the client's shapeImageUpdate is running at frame rate instead of a fixed tick interval. Basically, while we can match timeout values to tick intervals (0.032) it's not possible to also match the client intervals since they're unknown. How much of an offset this creates depends on the frame rate.

For example at a frame rate of 70 the delta time on the client is about 0.014, which is never going to add up to exactly an interval of 0.032. So no matter what the update code does you either have to execute the state event 4ms early or 10ms late (it will be 10ms late because that's how the system works).

From my testing it's still much better with the fix in place (even at lower framerates) than it was before, but it occurs to me that you might improve it again by making the client updateImageState run at tick intervals as well instead of frame rate.

As an experiment I just pulled the call to updateImageState from advanceTime (client frame rate update event -- make sure to leave updateImageAnimation there) and altered shapeBase::processTick to call it for the client at the same 32ms interval it uses on the server. It doesn't seem to break anything, and to be honest I'm not sure updateImageState actually should be updated at frame rate; it's part of the simulation, and generally simulation events should all run asynchronously from the rendering rate (that's what the sim tick system is for in the first place).

In shapeBase::processTick find this:
// Advance images
   if (isServerObject())
   {
      for (int i = 0; i < MaxMountedImages; i++)
      {
         if (mMountedImageList[i].dataBlock)
            updateImageState(i, TickSec);
      }
   }
and get rid of if (isServerObject()) so that it will run updateImageState for the client as well. Then go to shapeBase::advanceTime and find this:

for (int i = 0; i < MaxMountedImages; i++)
      if (mMountedImageList[i].dataBlock)
      {
         updateImageState(i, dt);
         updateImageAnimation(i, dt);
      }

and comment out the updateImageState line so that it no longer update image states at the rendering frame rate.

Let me know if that cleans it up a bit more. I'm uncertain at the moment if it's making a notable improvement or not (definitely not making it worse). My console debug output is now reporting that the client is using a stable number of ticks between shots now (3, just like the server, with a total delay of 96ms between fire/newround states). Using the original frame rate method it was getting slightly different numbers of ticks between shots, however those extra ticks generally represented a very small offset of several ms.

This is a fairly significant and largely untested change to how clients process Image state updates, so I'm not specifically recommending it go with the original fix (at least for now).
#6
05/10/2013 (11:06 pm)
Double posting because I've exceeded the reply size trying to reply to everyone in one msg.

@Ron:
Thanks, sounds good.

@Chris:
You might be right; I ran into one infinite loop situation when I was testing this. Imagine a state machine that loops 3 states waiting for input. Each state has a timeout of less than 0.032. If useRemainderDT is enabled the stock code will get stuck cycling those 3 states forever, never releasing for the next actual tick of the engine. It's because after each update carries over for another tick it will use the original full dt of 0.032, which will complete the next state, resulting in another state change and carry over tick that does the same thing, etc.
#7
05/15/2013 (7:27 pm)
@Henry

Those changes seem to fix the client side issues.