Ben Bowen's Blog [Xenoprimate] • Home / Blog • • About • • Subscribe •

Table of Contents

Top
Comments

Post Details

Wednesday 10th May 2017
Software Engineering
Postmortems
c#

Recommended Posts

(C#)
(Multithreading, Performance)
(Multithreading, Performance)
(Multithreading, Performance)
(Multithreading, Performance)
(Performance, C#)
(.NET, Performance)
(OOP / API Design, C#)

Postmortems - Absolutely Wrong 

When handling input for XBOX controllers in Escape Lizards, I had to tilt the level/environment according to the direction the left analog stick was being held. The left-analog stick's position is returned via the XInput API as two 16-bit signed integers (shorts), one for each axis (x and y).


When the left-stick is in its default center position, x and y are 0. If the stick is moved fully to the left, x will be -32768 (e.g. Int16.MinValue). When fully to the right, it will be 32767 (e.g. Int16.MaxValue). y is the same, but with up/down instead of left/right being the reported axis.

My callback event would be fired whenever the value for either x or y changed since the last frame/poll. Inside the callback I would do some maths and then call a different method to actually tilt the gameboard that had a signature like this:

private static void TiltGameboard(GameboardTiltDirection tiltDir, float tiltAmount) { /* ... */ }
View Code Fullscreen • "Gameboard Tilt Method Signature"
The tiltAmount variable is meant to be a value between 0f and 1f that indicates how far the gameboard should tilt towards the provided tiltDir, as a fraction of the maximum possible amount.

Anyway, my first implmentation for tilting the gameboard according to controller input looked something like this (with some code removed for clarity):

// This anonymous callback is called when the player moves the left-stick
currentRegisteredGameplayLeftStickEvent = gameIPB.RegisterLeftStickEvent((x, y, _) => {
	switch (currentGameState) {
		case OverallGameState.LevelPlaying:
			float tiltDeadzone = Config.BoardTiltDeadZone;
			float maxAxialTilt = 32767f - tiltDeadzone;
			float tiltFractionX = (Math.Abs(x) - tiltDeadzone)) / maxAxialTilt;
			float tiltFractionY = (Math.Abs(y) - tiltDeadzone)) / maxAxialTilt;
			
			if (x < -tiltDeadzone) {
				TiltGameboard(GameboardTiltDirection.Left, tiltFractionX);
			}
			else if (x > tiltDeadzone) {
				TiltGameboard(GameboardTiltDirection.Right, tiltFractionX);
			}

			if (y < -tiltDeadzone) {
				TiltGameboard(GameboardTiltDirection.Backward, tiltFractionY);
			}
			else if (y > tiltDeadzone) {
				TiltGameboard(GameboardTiltDirection.Forward, tiltFractionY);
			}
			break;
	}
});
View Code Fullscreen • "Initial Input Handling"
However, the first time I went to test that code, it worked for about 5 seconds and then crashed! In fact, there's a subtle bug in the code above that I'd written- can you spot it? Here's a clue: Recall how I mentioned that x will be -32768 when the left-stick is pushed all the way to the left. What do you think will happen when attempting to calculate tiltFractionX?

If you still don't see it, here's what's going wrong: Math.Abs has an overload for short parameters. The absolute value for -32768 is of course, 32768. However, the maximum value a short can represent is 32767, which is one less. This means that when I push the analog stick all the way to the left, x is set to -32768, and I try to call Math.Abs on the one 16-bit signed value that can't be represented by a positive magnitude!

Luckily, the .NET Math library detects this and throws an exception explaining the error.

Below is the eventual code for the relevant section that shipped in Escape Lizards, fixed to avoid the problem:

currentRegisteredGameplayLeftStickEvent = gameIPB.RegisterLeftStickEvent((x, y, _) => {
	switch (currentGameState) {
		case OverallGameState.LevelPlaying:
			float tiltDeadzone = Config.BoardTiltDeadZone;
			float maxAxialTilt = 32767f - tiltDeadzone;
			
			if (x < -tiltDeadzone) {
				TiltGameboard(GameboardTiltDirection.Left, (x + tiltDeadzone) / -maxAxialTilt);
			}
			else if (x > tiltDeadzone) {
				TiltGameboard(GameboardTiltDirection.Right, (x - tiltDeadzone) / maxAxialTilt);
			}

			if (y < -tiltDeadzone) {
				TiltGameboard(GameboardTiltDirection.Backward, (y + tiltDeadzone) / -maxAxialTilt);
			}
			else if (y > tiltDeadzone) {
				TiltGameboard(GameboardTiltDirection.Forward, (y - tiltDeadzone) / maxAxialTilt);
			}

			break;
	}
});
View Code Fullscreen • "Fixed Input Handling"