iOS device - tilt controls
september 28, 2011 10:27pm
inputTiltAngle = Mathf.Atan2( -Input.acceleration.y, -Input.acceleration.x ) * Mathf.Rad2Deg;
output = Mathf.Clamp(inputTiltAngle, -maxAngle, maxAngle);
Couldn't have been easier right?
So the programmer in me tests it out, and confirms, it maps the wheels to the angle. Nice, job well done.
If I would've wanted to make a doodle jump (or more recently, goatUp, one that my girlfriend enjoys a lot), this would've done he job.
Unfortunately, it wasn't as easy as that (why else would I have chosen this as topic for a Blog then). After handing my girlfriend the device, giving it a test, I dont think my heart could've been trampled on more. She described into tiny detail how much she disliked the controls, and how it did not work. Obviously, I acted as if I had planned more work on it, and she should just get used to it for now, "You're not a gamer" I believe I even replied (she is though! And a darn good one at it too, she keeps kicking my ass... I'll save that for another time).
After that I started analysing what was wrong with the current implementation.
When someone is holding the device, there is no way they can hold the device at a perfect zero-degree angle (assuming the device is held in front of the player like a wheel, and the home button is in the right hand), there is always some movement, and as it's mapped 1-on-1, the wheels will start rotating as well a bit, making it completely impossible to keep going in a straight line without balancing the device on a spirit level. Besides that, we might also not want the same amount of tilt to convert into the same amount of rotation of the wheels, so I needed a way to remap that as well.
Look at this diagram, it shows the input, and the output value.
To counter this, some form of deadzone is needed, a certain angle under which the player can rotate all he wants, but it does not affect the steering.
When something like that is implemented, our previous diagram now looks more like this.
Nice, now if we test something like that ingame, it works, there is a certain angle under which there is no input, and after that it starts to behave as before. And again, before was not good enough, so I went to think about another implementation of this. the results after the deadzone increased too much too fast, therefor making the wheel angles feel very snappy.
We had to make sure that output would be smoothed out, so it would look more like the following.
I had to use Maths again in a long while (Honestly this was never really my strongest point). I used a java powered math calculator (
this one to be exact). To make it easier, here's the equation I ended up using:
((x-2)/5)^3
This converts into the following with some more readable variable names:
inputTiltAngle = ((input-deadzone)/distance)^falloffAngle
Sweet! That makes no sense to some though. So writing that down into Unity code (Java Script, those that understand the above, are smart enough to convert it themself into C#).
inputTiltAngle = Mathf.Pow(Mathf.Clamp(Mathf.Abs(inputTiltAngle)-deadzone, 0, maxDistance)/(maxDistance-deadzone), falloffAngle)
that returns a value between 0 and 1, where our max wheel angle should be more then 1 ofcourse ;) However, simply multiplying wont do, as it only gives you range towards one side, so we have to make sure the maxAngle also works the other way around. To solve all this, the final piece of code will look like the following.
inputTiltAngle = Mathf.Clamp(Mathf.Atan2( -Input.acceleration.y, -Input.acceleration.x ) * Mathf.Rad2Deg, -maxTiltDeviceAngle, maxTiltDeviceAngle); // clamp values to max
outputAngle = Mathf.Pow(Mathf.Clamp(Mathf.Abs(inputTiltAngle)-deadzone, 0, maxTiltDeviceAngle)/(maxTiltDeviceAngle-deadzone), falloffAngle) * maxTiltDeviceAngle;
outputDevice = Mathf.Clamp(maxTiltDeviceAngle/inputTiltAngle, -1, 1) * outputAngle;
output = (outputDevice/maxTiltDeviceAngle) * maxTargetAngle; // retarget it to new angle
Quick explanation on the variables:
inputTiltAngle = value in degrees the device is being held (raw input)
maxTiltDeviceAngle = max angle the device takes input from
outputAngle = value returned from new input calculation
deadzone = amount of degrees input should not be calculated under
maxAngle = limit how far the output maximum could be
falloffAngle = strength of the curve calculated after the deadzone area
outputDevice = output angle based on device rotation (input)
maxTargetAngle = max angle the object can rotate
output = new output value based on input, in range between minus and plus of maxTargetAngle
The next addition is more of a personal taste, but when I asked around on twitter if people had a good solution themself, Jayenkai (
follow him on twitter) reminded me of another very useful piece of code.
use = use - ((use-aim)/step)
This snippet in short basically interpolates any value you put in there towards the value you want it to be (in our use, that would make the wheels smoothly rotate towards the final rotation they should be, based on the input tilt controls). Luckily there is the Mathf class we can use, which contains Lerp, and that already does exactly that!
If we change the last piece of code to make use of this, it would result in this.
inputTiltAngle = Mathf.Clamp(Mathf.Atan2( -Input.acceleration.y, -Input.acceleration.x ) * Mathf.Rad2Deg, -maxTiltDeviceAngle, maxTiltDeviceAngle); // clamp values to max
outputAngle = Mathf.Pow(Mathf.Clamp(Mathf.Abs(inputTiltAngle)-deadzone, 0, maxTiltDeviceAngle)/(maxTiltDeviceAngle-deadzone), falloffAngle) * maxTiltDeviceAngle;
outputDevice = Mathf.Clamp(maxTiltDeviceAngle/inputTiltAngle, -1, 1) * outputAngle;
outputLerp = Mathf.Lerp(outputLerp, outputDevice, lerpSpeed * Time.deltaTime); // lerpSpeed variable is defined as 2.5 in my version but can be any value you feel is best
output = (outputLerp/maxTiltDeviceAngle) * maxTargetAngle; // retarget it to new angle
Woohoo, this covers the entire code for controls! we can leave it here, and we'd have a great set of controls.
Ofcourse, if some people want more feedback on the controls, they can make the camera rotate over the Z-axis in reverse of the input tilt.
That makes the horizon stay in line, even though the device is being rotated around.
The following is a bit of pseudo code, as we have it implemented in our code in a different way, but you should be able to figure out by yourself how this works if you understood all the previous ;)
Camera.main.transform.eulerAngles.z = outputDevice*-1;
That is all we used for implementation of our tilt controls in our upcoming title, keep an eye out for more on that from us! We hope this helped all the developers that have been looking, like us, for a proper implementation of tilt input values.
Keep in mind, all the values used in the Blog are just examples, you will have to tweak them yourself to get the best result out of it!