Tutorial: Game controllers and axes

Tutorial: Game controllers and axes

In the previous tutorial about HID controllers, we showed you how to read button inputs from controllers. Generally speaking, controller buttons generate a simple “down” or “up” value, so you can easily determine if the player is interacting with that button.

About analog controls

In contrast, analog controls are more complex. These measure an almost infinite number of potential values that must be converted from their analog “infinity” to discrete digital values. In the case of the Corona, these values typically range from -1.0 to 1.0 and a very precise set of values anywhere between. To compound the complexity, some controls have two associated values — for example, an analog stick can be moved up and down in either direction over a varying distance, as well as left and right. Other controls like “triggers” generate values between 0.0 to 1.0 where the value increases the more you squeeze the trigger. As a broad definition, we refer to these control values as axis. For instance, the left analog stick has both an X axis and a Y axis. The right trigger, in comparison, has just a single axis to measure.

At the hardware level, analog sticks typically depend on springs to push the stick back to the neutral center position. However, these springs don’t always push the stick back to its perfect 0 value. In fact, a stick can be “noisy” and generate events even when the control isn’t being touched. Internally, Corona tries and minimize this noise, but because controllers vary considerably (even those from the same manufacturer), you should account for a certain amount of “slop” in your analog stick handling.

Another issue to contend with is the varied axis numbers that manufacturers assign to their controllers. For instance, the right trigger on the OUYA controller is axis number 6, but on a DualShock controller, that same control is axis number 14. In general, there are two ways to handle this:

  1. Build a mapping screen like those in many PC games, giving the player a chance to press the various buttons and controls, then gather those values and assign an axis number to each specific control. This is probably the best way to handle devices if you expect that players may use controllers that you know little about.
  2. Build a mapping system of specific controllers that you want to support. Since you’ll know in advance what each controller’s axes map to, you can build a table of inputs. Of course, you’ll still need to determine what the values are when you test a new controller.

Mapping system

For this tutorial, we’ll explore a basic mapping system of “known” controllers. To keep this tutorial relatively simple, we’ll only include two controllers, but you could repeat this basic pattern to handle additional controllers. Let’s look at some basic code:

-- Map the names to identify an axis with a device's physical inputs
local axisMap = {}
axisMap["OUYA Game Controller"] = {}
axisMap["OUYA Game Controller"][1] = "left_x"
axisMap["OUYA Game Controller"][2] = "left_y"
axisMap["OUYA Game Controller"][3] = "left_trigger"
axisMap["OUYA Game Controller"][4] = "right_x"
axisMap["OUYA Game Controller"][5] = "right_y"
axisMap["OUYA Game Controller"][6] = "right_trigger"
axisMap["DualShock Controller"] = {}
axisMap["DualShock Controller"][1] = "left_x"
axisMap["DualShock Controller"][2] = "left_y"
axisMap["DualShock Controller"][13] = "left_trigger"
axisMap["DualShock Controller"][3] = "right_x"
axisMap["DualShock Controller"][4] = "right_y"
axisMap["DualShock Controller"][14] = "right_trigger"
-- Create table to map each controller's axis number to a usable name
local axis = {}
axis["Joystick 1"] = {}
axis["Joystick 1"]["left_x"] = 0
axis["Joystick 1"]["left_y"] = 0
axis["Joystick 1"]["left_trigger"] = 0
axis["Joystick 1"]["right_x"] = 0
axis["Joystick 1"]["right_y"] = 0
axis["Joystick 1"]["right_trigger"] = 0
axis["Joystick 2"] = {}
axis["Joystick 2"]["left_x"] = 0
axis["Joystick 2"]["left_y"] = 0
axis["Joystick 2"]["left_trigger"] = 0
axis["Joystick 2"]["right_x"] = 0
axis["Joystick 2"]["right_y"] = 0
axis["Joystick 2"]["right_trigger"] = 0
view raw part1.lua hosted with ❤ by GitHub

The first block of code creates a table based on the controllers we want to to support: the standard OUYA controller and the DualShock controller. The second table maps the physical control names to specific numbers. Later, we’ll execute code that reveals the real values that should be entered into this table.

Basic game setup

Let’s explore a simple game featuring two players that can be manipulated with a controller:

local redPlayer = display.newRect( display.contentCenterX-15, display.contentCenterY-15, 30, 30 )
redPlayer:setFillColor( 1, 0, 0 )
redPlayer.x = display.contentCenterX
redPlayer.y = display.contentCenterY
redPlayer.isMovingX = 0
redPlayer.isMovingY = 0
redPlayer.isRotatingX = 0
redPlayer.isRotatingY = 0
redPlayer.thisAngle = 0
redPlayer.lastAngle = 0
redPlayer.rotationDistance = 0
redPlayer.isGrowing = 0
redPlayer.color = "red"
local greenPlayer = display.newRect( display.contentCenterX-15, display.contentCenterY-15, 30, 30 )
greenPlayer:setFillColor( 0, 1, 0 )
greenPlayer.isMovingX = 0
greenPlayer.isMovingY = 0
greenPlayer.isRotatingX = 0
greenPlayer.isRotatingY = 0
greenPlayer.thisAngle = 0
greenPlayer.lastAngle = 0
greenPlayer.rotationDistance = 0
greenPlayer.isGrowing = 0
greenPlayer.color = "green"
-- "blueDot" isn't a player, just a visual to show the physical stick movement
local blueDot = display.newCircle( display.contentCenterX, display.contentCenterY, 7 )
blueDot:setFillColor( 0, 0, 1 )
-- "whiteDot" is a visual representation to demonstrate how well the stick
-- settles back to center; ideally, the blue circle should center back with
-- white dot, but if it doesn't, this exhibits the stick "slop" factor that
-- you should compensate for
local whiteDot = display.newCircle( display.contentCenterX, display.contentCenterY, 2 )
whiteDot:setFillColor( 1 )
-- These UI elements show the current keypress and axis information
local myKeyDisplayText = display.newText( "", 0, 0, 300, 0, native.systemFontBold, 10 )
myKeyDisplayText.x = display.contentWidth / 2
myKeyDisplayText.y = 50
local myAxisDisplayText = display.newText( "", 0, 0, native.systemFontBold, 20 )
myAxisDisplayText.x = display.contentWidth / 2
myAxisDisplayText.y = display.contentHeight - 50
view raw part2.lua hosted with ❤ by GitHub

Each player is a colored square — one green and one red — and they begin in the center of the screen. We’ve added some properties to each player that represent the movement along the X and Y axes. The left stick is used to move the player and the right stick is used to rotate the player. Just for fun, we’ll use the triggers to change the color of the player. Because our axis events are not continuous, we need to use an "enterFrame" listener to move our player while the stick is being held (this listener will manage the player rotation as well).

The next step is a function that calculates the player rotation angle based on the X and Y of where the stick is being held. Note that you get one event for X movement and one event for Y movement. As a result, you must store the last value generated for X so that when you receive a Y event, you can compare the two. Likewise, if you get an X event, you must have access to the previous saved Y event.

-- Calculate the angle to rotate the square. Using simple right angle math, we can
-- determine the base and height of a right triangle where one point is 0,0
-- (stick center) and the values returned from the two axis numbers returned
-- from the stick
-- This will give us a 0-90 value, so we have to map it to the quadrant
-- based on if the values for the two axis are positive or negative
-- Negative Y, positive X is top-right area
-- Positive X, Positive Y is bottom-right area
-- Negative X, positive Y is bottom-left area
-- Negative x, negative y is top-left area
local function calculateAngle( sideX, sideY )
if ( sideX == 0 or sideY == 0 ) then
return nil
end
local tanX = math.abs( sideY ) / math.abs( sideX )
local atanX = math.atan( tanX ) -- Result in radians
local angleX = atanX * 180 / math.pi -- Converted to degrees
if ( sideY <; 0 ) then
angleX = angleX * -1
end
if ( sideX < 0 and sideY < 0 ) then
angleX = 270 + math.abs( angleX )
elseif ( sideX < 0 and sideY > 0 ) then
angleX = 270 - math.abs( angleX )
elseif ( sideX > 0 and sideY > 0 ) then
angleX = 90 + math.abs( angleX )
else
angleX = 90 - math.abs( angleX )
end
return anglex
end
view raw part3.lua hosted with ❤ by GitHub

Game loop

Now let’s examine the game loop required to monitor the controller input:

-- Since controllers don't generate constant values, but simply events when
-- the values change, we need to set a movement amount when the event happens,
-- and also have the game loop continuously apply it
-- We can also calculate our rotation angle here
local function moveRedPlayer()
-- Set the .isMovingX and .isMovingY values in our event handler
-- If this number isn't 0 (stopped moving), move the player
if ( redPlayer.isMovingX ~= 0 ) then
redPlayer.x = redPlayer.x + redPlayer.isMovingX
end
if ( redPlayer.isMovingY ~= 0 ) then
redPlayer.y = redPlayer.y + redPlayer.isMovingY
end
-- Rotation code
if ( redPlayer.rotationDistance > 0.1 ) then
if ( redPlayer.thisAngle > redPlayer.lastAngle ) then
redPlayer.rotation = redPlayer.rotation + redPlayer.rotationDistance
else
redPlayer.rotation = redPlayer.rotation - redPlayer.rotationDistance
end
end
end
Runtime:addEventListener( "enterFrame", moveRedPlayer )
local function moveGreenPlayer()
if ( greenPlayer.isMovingX ~= 0 ) then
greenPlayer.x = greenPlayer.x + greenPlayer.isMovingX
end
if ( greenPlayer.isMovingY ~= 0 ) then
greenPlayer.y = greenPlayer.y + greenPlayer.isMovingY
end
if ( greenPlayer.rotationDistance > 0.1 ) then
if ( greenPlayer.thisAngle > greenPlayer.lastAngle ) then
greenPlayer.rotation = greenPlayer.rotation + greenPlayer.rotationDistance
else
greenPlayer.rotation = greenPlayer.rotation - greenPlayer.rotationDistance
end
end
end
Runtime:addEventListener( "enterFrame", moveGreenPlayer )
view raw part4.lua hosted with ❤ by GitHub

Axis movement

Movement with the X and Y values is straightforward. If you simply want to move around, you can apply the values from the two axis events. At this point, you could apply physics impulses or movement, or in this case, simply apply the values from the controller. Rotation is more tricky — depending on what the rotation should do, you may have to convert the X and Y into an angle. You can also consider using the distance the stick is pressed to determine how fast to rotate. Since the X and Y values for rotation comes from two separate events, you need to store the two values and perform your movement based on those values.

Let’s look at the actual code to manage the axis data:

local function onAxisEvent( event )
-- Display some info on the screen about this axis event
local message = "Axis '" .. event.axis.descriptor .. "' was moved " .. tostring( event.normalizedValue )
myAxisDisplayText.text = message
-- Map event data to simple variables
local abs = math.abs
local controller = event.device.descriptor
local thisAxis = event.axis.number
local thisPlayer
-- Check which controller this is coming from; you can trust the names
-- "Joystick 1" and "Joystick 2" to represent player 1, player 2, etc.
-- Based on the controller for this event, pick the object to manipulate
if ( controller == "Joystick 1" ) then
thisPlayer = redPlayer
elseif ( controller == "Joystick 2" ) then
thisPlayer = greenPlayer
end
-- Now that we know which controller it is, determine which axis to measure
-- Because the "right trigger" might be 6 on one brand of controller
-- but 14 on another, we use the mapping system described above
if ( axis[controller]["left_x"] and axis[controller]["left_x"] == thisAxis ) then
-- This helps handle noisy sticks and sticks that don't settle back to 0 exactly
-- You can adjust the value based on the sensitivity of the stick
-- If the stick is moved far enough, then move the player, else force it to
-- settle back to a zero value
-- Set the X distance in the player object so the enterFrame function can move it
if ( abs(event.normalizedValue) > 0.15 ) then
thisPlayer.isMovingX = event.normalizedValue
else
thisPlayer.isMovingX = 0
end
-- Draw the blue dot around the center to show how far you actually moved the stick
blueDot.x = display.contentCenterX + event.normalizedValue * 10
elseif ( axis[controller]["left_y"] and axis[controller]["left_y"] == thisAxis ) then
-- Just like X, now handle the Y axis
if ( abs(event.normalizedValue) > 0.15 ) then
thisPlayer.isMovingY = event.normalizedValue
else
thisPlayer.isMovingY = 0
end
-- Move the blue dot
blueDot.y = display.contentCenterY + event.normalizedValue * 10
elseif ( axis[controller]["right_x"] and axis[controller]["right_x"] == thisAxis ) then
-- We will use the right stick to rotate our player
thisPlayer.isRotatingX = event.normalizedValue
-- Use Pythagoras' Theorem to compute the distance the stick is moved from center
local a = math.abs( thisPlayer.isRotatingX * thisPlayer.isRotatingX )
local b = math.abs( thisPlayer.isRotatingY * thisPlayer.isRotatingY )
local d = math.sqrt( a + b )
-- If the distance isn't very far, set it to zero to account for
-- stick "slop" and not settling back to perfect center
if ( d < 0.15 ) then
thisPlayer.rotationDistance = 0
else
-- In the Runtime enterFrame listener we look at the current angle and the
-- last angle to determine which direction we need to rotate
thisPlayer.rotationDistance = d * 3
thisPlayer.lastAngle = thisPlayer.thisAngle
thisPlayer.thisAngle = math.floor( calculateAngle(thisPlayer.isRotatingX, thisPlayer.isRotatingY) )
end
elseif ( axis[controller]["right_y"] and axis[controller]["right_y"] == thisAxis ) then
-- Repeat for the Y axis on the right stick
thisPlayer.isRotatingY = event.normalizedValue
local a = math.abs( thisPlayer.isRotatingX * thisPlayer.isRotatingX )
local b = math.abs( thisPlayer.isRotatingY * thisPlayer.isRotatingY )
local d = math.sqrt( a + b )
if ( d < 0.15 ) then
thisPlayer.rotationDistance = 0
else
thisPlayer.rotationDistance = d * 3
thisPlayer.lastAngle = thisPlayer.thisAngle
thisPlayer.thisAngle = math.floor( calculateAngle(thisPlayer.isRotatingX, thisPlayer.isRotatingY) )
end
elseif ( axis[controller]["left_trigger"] or axis[controller]["right_trigger"] == thisAxis ) then
-- Use the analog triggers to gradually change the color of the player
-- No trigger pressure will be full brightness
-- The more you squeeze the trigger, the darker the square gets
local color = 1 * (1 - event.normalizedValue)
if ( color < 0.125 ) then
color = 0.125
elseif ( color >= 1 ) then
color = 1
end
if ( thisPlayer.color == "red" ) then
thisPlayer:setFillColor( color, 0, 0 )
else
thisPlayer:setFillColor( 0, color, 0 )
end
end
return true
end
Runtime:addEventListener( "axis", onAxisEvent )
view raw part5.lua hosted with ❤ by GitHub

Initialize the controllers

Finally we need to initialize everything, including mapping the controllers:

-- Fetch all input devices currently connected to the system
local inputDevices = system.getInputDevices()
-- Traverse all input devices
for deviceIndex = 1, #inputDevices do
-- Fetch the input device's axes
print( deviceIndex, "andoridDeviceid", inputDevices[deviceIndex].androidDeviceId )
print( deviceIndex, "canVibrate", inputDevices[deviceIndex].canVibrate )
print( deviceIndex, "connectionState", inputDevices[deviceIndex].connectionState )
print( deviceIndex, "descriptor", inputDevices[deviceIndex].descriptor )
print( deviceIndex, "displayName", inputDevices[deviceIndex].displayName )
print( deviceIndex, "isConnected", inputDevices[deviceIndex].isConnected )
print( deviceIndex, "permenantid", tostring(inputDevices[deviceIndex].permanentId) )
print( deviceIndex, "type", inputDevices[deviceIndex].type )
-- OUYA may append the controller name to the end of the display name in a future update
-- Future-proof this by looking at the first few characters and, if necessary, parse it
local displayName = inputDevices[deviceIndex].displayName
if ( string.sub(displayName,1,20) == "OUYA Game Controller" then
displayName = string.sub( displayName,1,20 )
end
local descriptor = inputDevices[deviceIndex].descriptor
local inputAxes = inputDevices[deviceIndex]:getAxes()
-- Only look for Joysticks at the moment and map the controllers
if ( inputDevices[deviceIndex].type == "joystick" ) then
print( "We have a joystick; let's find some analog inputs!" )
if ( #inputAxes > 0 ) then
local controller = 0
for axisIndex = 1, #inputAxes do
if ( axisMap[displayName] and axisMap[displayName][axisIndex] ) then
axis[descriptor][axisMap[displayName][axisIndex]] = axisIndex
print( "mapped axis[" .. axisMap[displayName][axisIndex] .. "] to ", axisIndex )
end
end
else
-- Device does not have any axes!
print( inputDevices[deviceIndex].descriptor .. ": No axes found." )
end
else
print( "Not a Joystick" )
end
end
-- Keys were handled in a previous blog post, but let's handle them to
-- demonstrate how some axis values map to key events
local function onKeyEvent( event )
local phase = event.phase
local keyName = event.keyName
print( event.phase, event.keyName )
local message = "Key '" .. event.keyName .. "' was pressed " .. event.phase
myKeyDisplayText.text = message
return false
end
Runtime:addEventListener( "key", onKeyEvent )
view raw part6.lua hosted with ❤ by GitHub

The above code loops over the list of axes returned by the inputDevices[deviceIndex]:getAxes() function. It’s very likely that we don’t need all of the axes returned. This is where we use the two data tables at the top of this tutorial to pick an axis that we find and store the axis number into the second table.

Caution

Note that game controllers may run out of battery power and just “drop out” without warning. It’s also possible that a player may want to change controllers in the middle of the game, and thus “Joystick 2” is actually player 1. It’s your responsibility to manage these events, using the inputDeviceStatus event:

local function onInputDeviceStatusChanged( event )
-- Handle the input device change
if ( event.connectionStateChanged ) then
print( event.device.displayName .. ": " .. event.device.connectionState, event.device.descriptor, event.device.type, event.device.canVibrate )
end
end
Runtime:addEventListener( "inputDeviceStatus", onInputDeviceStatusChanged )
view raw part7.lua hosted with ❤ by GitHub

In summary

This tutorial should get you up to speed on the axis type inputs on your controller and the basic mapping system.


Rob Miracle
rob@coronalabs.com

Rob is the Developer Relations Manager for Corona Labs. Besides being passionate about helping other developers make great games using Corona, he is also enjoys making games in his spare time. Rob has been coding games since 1979 from personal computers to mainframes. He has over 16 years professional experience in the gaming industry.

4 Comments
  • Anton
    Posted at 03:52h, 25 September

    When will add support for keyboard control for mac simulator? Now it is very inconvenient to develop an application for android, ouya.

    Back in iOS 7 appeared maintain joysticks, whether you plan to add support?

    • Rob Miracle
      Posted at 16:24h, 25 September

      Engineering is looking into when they can schedule it in. Our focus was to get support out for the OUYA and those features were not available in iOS at the time.

      • Rob Miracle
        Posted at 16:26h, 28 September

        Just an update, some key and mouse support went into a recent daily build for the Mac sim (not iOS), but this is just the computer’s keyboard.

        • Ernest Szoka
          Posted at 12:40h, 28 January

          Mouse and Keyboard support doesn’t work on PC sim for iOS devices, but does for Android!

The website is no longer being updated, and is for archival purposes only.

 

Corona SDK is now Solar2D

https://Solar2D.com/

×