TUTORIAL: An Explanation of the "Half-Tic" Trick

In my recent ZDoom-based mod, Police Brutality: Weasel Presents Terrorists!, I experimented with some subtle effects on weapons. I'm not talking about the upgrade system; that's anything but subtle and beyond the scope of this tutorial. (Maybe if there's enough demand I'll explain it in more depth.) No, the effect in question is what I like to call the "half-tic" - though it might be more accurate to call it a "sub-tic" as it's not actually measured by the in-game tic counter.

I'll take my explanation from the beginning. As you might know already, Doom (and by extension, ZDoom) runs the game at 35 tics per second. Now, this was nice and smooth back in 1993 when it first came out, and it's still at least five frames a second faster than more contemporary modern games (*cough* Halo *cough*), but for more precise timing, it might be tricky to work with.

Let's take, for example, a simple machine gun. I tend to measure the fire rates of my machine guns by the number of tics between individual bullets. For example, Doom's chaingun is a "4-tic" refire, as it fires one bullet, waits 4 tics, then fires another, until the fire button is finally released. Strife's assault rifle is a 3-tic refire, which is noticeably faster than the chaingun. Skulltag's minigun is a 2-tic refire - by now we're actually firing at double the speed of Doom's chaingun.

Actual machine guns are measured in rounds per minute (RPM), which is a much more precise unit of measurement, but it'll take some number crunching to pare down the tic-rates into RPM rates. So let's figure that out right now, using the known fire rate of a real gun - we'll say a Colt M4A1 - which according to my preferred firearms guide, Modern Firearms & Ammunition, ranges from 700-950 RPM. We'll stick with 700 as our baseline.

So we know Doom's chaingun fires once every 4 tics, and Doom runs at 35 tics per second. We'll take 35 - which represents one second - and divide it by 4, getting 8.75 shots per second. Multiply that by 60, and we get the per-minute count, which is a very modest 525 RPM, which is more akin to, say, a Browning M2 or a Bren gun.

Let's do that again with Strife's assault rifle:
35 tics per second / 3 tic refire delay = ~11.66666667 * 60 seconds = 700 RPM.
That means Strife's assault rifle is just on the low end of what an actual assault rifle can manage, as far as cyclic rate.

And one more time with Skulltag's minigun:
35 TPS / 2-tic refire = 17.5 RPS * 60 secs = 1,050 RPM, or noticeably faster than the quickest-cycling M4A1, and exactly double that of the chaingun.

As you can see, one tic of refire makes a load of difference, so if you want your guns to fire at a rate somewhere in between the three "extremes", you're going to need a time unit more precise than a tic. But Doom doesn't have any more precise ways to measure time, you're probably thinking. That's where I came up with a special trick that I called the "half-tic." I was actually inspired by tasvideos.org's guide to Sonic the Hedgehog's physics, which goes into great detail about how these 16-bit platformers do not measure player movement in raw pixels, but rather "sub-pixels," so that a greater precision may be exacted over player movement, resulting in much smoother speed changes.

So how would I handle something similar with tics? I notice that the article is actually not storing the player's location in sub-pixels, but as a raw x/y pixel coordinate coupled with two sub-pixel counters for x and y. Well, since Doom's ticker is a strictly linear affair, I only need one such counter, and for simplicity's sake, I really only need it to be a boolean, i.e. 1 or 0.

Let's have a look at Doom's chaingun Fire state.
     CHGG AB 4 A_FireCGun
     CHGG B 0 A_ReFire
     Goto Ready
Pretty simple; fires one bullet every 4 tics as mentioned before. Well, here's what I'm going to add to this. First, a sub-tic counter.
ACTOR HalfTicCounter : Inventory { Inventory.MaxAmount 1 }
(Oh, did I forget to show you how you can condense simple actors to just one line? Sometimes you can do that. Not often, but sometimes.)

Now, this might get a little cluttered. It was recently suggested on the ZDoom forums that one could use the A_SetTics function in Decorate to handle this, but I do not think that you can use the value of an inventory item in a Decorate expression, so we're going to go do this the hard way, with A_JumpIfInventory.

   CHGG A 0 A_JumpIfInventory("HalfTicCounter", 1, "FireHalftic")
   CHGG A 4 A_FireCGun
   CHGG A 0 A_GiveInventory("HalfTicCounter", 1)
   Goto Ready
   CHGG A 0 A_TakeInventory("HalfTicCounter", 1)
   CHGG B 3 A_FireCGun
   Goto Ready

So here's what it's doing: the weapon checks the half-tic counter first, then having failed the check, fires at the "normal" 4-tic rate on the first shot and adds to the counter. When the next shot is fired, it will pass the half-tic check and go to the FireHalftic state instead, where the weapon fires at the quicker 3-tic rate, then takes the counter away again. This effectively sets the weapon's fire rate at 3.5 tics per shot, or going by our formula again:
35 TPS / 3.5 tics = 10 RPS * 60 secs = 600 RPM, or roughly halfway in between the chaingun's 525 RPM and the Strife assault gun's 700.

Now, there is a drawback, for those with sensitive ears: since this is obviously not a "true" fire rate (this would not be the case if Doom ran at 70 FPS, where a fire rate of 7 tics per shot is more natural), the sound made when firing the gun at this rate may seem to stutter, or fire at an uneven rate. With faster fire rates, this is less noticeable, but it's honestly a small price to pay for the added precision involved. You could potentially get around this issue by perhaps replacing your weapon's firing sound with a looping one, similar to the ones used in more modern games like Far Cry or Rainbow Six Vegas.

I imagine half-tics could easily be expanded into different ranges of sub-tics to achieve different fractions; by increasing the maximum value of the half-tic counter to 2, you could have an extra 1/3-tic, or even beyond.

(Weasel's Note: Regarding the example I used above, I know I could easily just write it like this...)
   CHGG A 4 A_FireCGun
   CHGG B 3 A_FireCGun
(But that's not really the point of the tutorial, is it?)