Tutorial: Game controllers and axes

Share on Facebook0Share on Google+1Tweet about this on TwitterShare on LinkedIn0

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

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

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

    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

    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 )
        angleX = 90 - math.abs( angleX )

    return anglex

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
    if ( redPlayer.isMovingY ~= 0 ) then
        redPlayer.y = redPlayer.y + redPlayer.isMovingY

    -- Rotation code
    if ( redPlayer.rotationDistance > 0.1 ) then
        if ( redPlayer.thisAngle > redPlayer.lastAngle ) then
            redPlayer.rotation = redPlayer.rotation + redPlayer.rotationDistance
            redPlayer.rotation = redPlayer.rotation - redPlayer.rotationDistance

Runtime:addEventListener( "enterFrame", moveRedPlayer )

local function moveGreenPlayer()

    if ( greenPlayer.isMovingX ~= 0 ) then
        greenPlayer.x = greenPlayer.x + greenPlayer.isMovingX
    if ( greenPlayer.isMovingY ~= 0 ) then
        greenPlayer.y = greenPlayer.y + greenPlayer.isMovingY
    if ( greenPlayer.rotationDistance > 0.1 ) then
        if ( greenPlayer.thisAngle > greenPlayer.lastAngle ) then
            greenPlayer.rotation = greenPlayer.rotation + greenPlayer.rotationDistance
            greenPlayer.rotation = greenPlayer.rotation - greenPlayer.rotationDistance

Runtime:addEventListener( "enterFrame", moveGreenPlayer )

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

    -- 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
           thisPlayer.isMovingX = 0

       -- 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
           thisPlayer.isMovingY = 0

       -- 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

            -- 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) )

    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
            thisPlayer.rotationDistance = d * 3
            thisPlayer.lastAngle = thisPlayer.thisAngle
            thisPlayer.thisAngle = math.floor( calculateAngle(thisPlayer.isRotatingX, thisPlayer.isRotatingY) )

    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

        if ( thisPlayer.color == "red" ) then
            thisPlayer:setFillColor( color, 0, 0 )
            thisPlayer:setFillColor( 0, color, 0 )

    return true

Runtime:addEventListener( "axis", onAxisEvent )

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 )
    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 )
            -- Device does not have any axes!
            print( inputDevices[deviceIndex].descriptor .. ": No axes found." )

        print( "Not a Joystick" )

-- 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

Runtime:addEventListener( "key", onKeyEvent )

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.


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 )

Runtime:addEventListener( "inputDeviceStatus", onInputDeviceStatusChanged )

In summary

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

Share on Facebook0Share on Google+1Tweet about this on TwitterShare on LinkedIn0
Rob Miracle

Rob Miracle creates mobile apps for his own enjoyment and the amusement of others. He serves the Corona Community in the forums, on the blog, and at local events.

This entry has 4 replies

  1. Anton says:

    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 says:

      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 says:

        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 says:

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