You’re in the middle of a fight—dodging rockets, cores, shock combos. In the chaos you dodge straight into a combo that leads to your death. You swear you didn’t double tap in that direction. “F*cking auto-dodge,” you say in chat followed by gaslighting from your peers, “I never auto-dodge.” Is it just you? Maybe you did accidentally double tap in the chaos. Well, I’m here to say you’re not crazy. Auto-dodging, or what I prefer to call unintentional dodging, is a thing in the Unreal Tournament franchise—always has been.

An unintentional dodge is when your character dodges without a distinct double tap in the dodge direction. You can easily reproduce this with the following key combination,

Hold Forward,
Double Tap Back
↳ Dodge Forward

Or,

Hold Right
Double Tap Left
↳ Dodge Right

However, you can’t reproduce it if you change the directions! For example,

Hold Backward,
Double Tap Forward

Does not lead to a backward dodge. Why? Let’s dig into that.

Axis

Unreal Tournament does not directly translate a key press to a movement action. Instead, UT maps key presses as a change in an “Axis”. Put simply, an Axis is a 1-dimensional unit representing speed. If you look at your user.ini, you may have seen these lines and wondered what they mean:

Aliases[2]=(Command="Axis aBaseY  Speed=+300.0",Alias="MoveForward")
Aliases[3]=(Command="Axis aBaseY  Speed=-300.0",Alias="MoveBackward")
Aliases[4]=(Command="Axis aBaseX Speed=-40.0",Alias="TurnLeft")
Aliases[5]=(Command="Axis aBaseX  Speed=+40.0",Alias="TurnRight")
Aliases[6]=(Command="Axis aStrafe Speed=-300.0",Alias="StrafeLeft")
Aliases[7]=(Command="Axis aStrafe Speed=+300.0",Alias="StrafeRight")
Aliases[8]=(Command="Jump | Axis aUp Speed=+300.0",Alias="Jump")
Aliases[9]=(Command="Button bDuck | Axis aUp Speed=-300.0",Alias="Duck")

The Move and Strafe aliases modify the aBaseY and aStrafe axis values, respectively, to a speed of ± 300.0.

If you press W, bound to MoveForward, then aBaseY = +300.0. If you press S, then aBaseY = -300.0, and if you press both W and S then aBaseY = 0.0—theoretically, more on this later!

Dodge Detection

UT uses the axis to determine movement direction, and this is where the logic starts to get faulty. Let’s take a look at PlayerInput.uc to see how double taps are detected.

// Check for Double click move
// flag transitions
bEdgeForward = (bWasForward ^^ (aBaseY > 0));
bEdgeBack = (bWasBack ^^ (aBaseY < 0));
bEdgeLeft = (bWasLeft ^^ (aStrafe < 0));
bEdgeRight = (bWasRight ^^ (aStrafe > 0));
bWasForward = (aBaseY > 0);
bWasBack = (aBaseY < 0);
bWasLeft = (aStrafe < 0);
bWasRight = (aStrafe > 0);

Further down these values are used to determine dodge direction,

if (bEdgeForward && bWasForward)
    DoubleClickDir = DCLICK_Forward;
else if (bEdgeBack && bWasBack)
    DoubleClickDir = DCLICK_Back;
else if (bEdgeLeft && bWasLeft)
    DoubleClickDir = DCLICK_Left;
else if (bEdgeRight && bWasRight)
    DoubleClickDir = DCLICK_Right;

The Axis aBaseY and aStrafe encode movement direction as positive/negative speeds.

The bEdge variables represent an “edge” event. The easiest way to visualize this is to consider a digital signal graph,

Edge/Level Diagram

The dodge detection code looks for two edge events that lead to a level event. This is how it infers a “double tap”.

Edge events for dodge

So far everything seems good. Now let’s consider the unintentional forward dodge scenario.

Forward key (W) Move forward graph

Backward key (S) Move backward graph

Because forward and back keys are aggregated into a single axis, aBaseY, the true result is a combination of these graphs.

Combined graph

What do you notice in this graph? That’s right, two dodge triggering forward edge events! Despite double tapping the back key, S, we are propelled forward due to a bug in the dodge detection algorithm.

Forward dodge edge events

Floating Point Errors

OK, so we know the condition under which an unintentional forward dodge occurs. But why does this not occur in reverse? If we hold down the back key and double tap the forward key, why do we not dodge backwards? The reason lies in floating point arithmetic. It’s time to dive into the Unreal Engine code, particularly the input processing.

Let’s start off with the code that processes keyboard input for Axis variables,

else if( GetInputAction() == IST_Hold )
{
    *Axis += GetInputDelta() * Speed * Invert;
}

Nothing special, it simply adds to the referenced axis variable. In this case, GetInputDelta() = DeltaSeconds and Invert = 1.0.

Later on, the Axis value is scaled up before sending it to the UnrealScript code for further processing.

FLOAT Scale = (DeltaSeconds != -1.f) ? (20.f / DeltaSeconds) : 0.f;

Put these calculations together and the Axis value on forward key press becomes,

Axis = DeltaSeconds * Speed * 20.0 / DeltaSeconds
     = 300.0 * 20.0
     = 6000.0

Seems reasonable. Now how about when both forward and backwards keys are pressed?

// Forward key
Axis = DeltaSeconds * 300.0 * 20.0 / DeltaSeconds

// Backward key
Axis += DeltaSeconds * -300.0 * 20.0 / DeltaSeconds

So the end result should be Axis = 0.0, right? Turns out, it’s not!

Because DeltaSeconds < 1.0 (DeltaSeconds = 0.004 at 250 fps), floating point precision starts rearing its ugly head. These aggregate operations result in a case where Axis is really close to 0.0 but not exactly 0.0.

Perhaps this is why the bug was never noticed at development. With DeltaSeconds having millisecond resolution, pressing both directional keys yields -0.0001 < Axis < 0.01. So when you press and hold back, then double tap forward, the dodge detection doesn’t detect a change in direction because the Axis value is always negative!

Conclusion

Diving into the dodge code was a fun journey. I ended up patching the axis code in the engine to fix this in my custom UT2004 binary.

My solution took inspiration from the case where an unintentional dodge does not occur. When both keys are pressed, set the axis to a very small positive or negative number depending on the last directional key pressed.


  1. This does not hold true when DeltaSeconds is a higher resolution, which I found out real quick in my timer patch for UT2004. At higher resolution, -0.0001 < Axis < +0.0001, which means it fluctuates between +/-. This is why I had to add special logic to fix this dodge bug in the engine. ↩︎