Tutorial: Introduction to game controllers

Tutorial: Introduction to game controllers

It’s an exciting time to be a mobile app developer. Through the magic of mobile and Bluetooth support on devices, it’s now possible to use HID (Human Interface Device) controllers to play games, in addition to the standard touchscreen and accelerometer. While this isn’t yet “utopia,” you can do some really cool things with your wireless device and modern technology.

Key event framework

For HID controllers, we decided to build upon the existing key event framework. Thus, programming for controllers won’t be too much different — no plugins are required for HID-compliant controllers, nor do you need to require() anything special.

local function onKeyEvent( event )
local phase = event.phase
local keyName = event.keyName
if ( phase == "down" ) then
if ( keyName == "buttonA" ) then
return true
elseif ( keyName == "buttonB" ) then
return true
elseif ( keyName == "buttonX" ) then
return true
elseif ( keyName == "buttonY" ) then
return true
elseif ( keyName == "up" ) then  -- D-Pad up
return true
elseif ( keyName == "down" ) then  -- D-Pad down
return true
elseif ( keyName == "left" ) then  -- D-Pad left
return true
elseif ( keyName == "right" ) then  -- D-Pad right
return true
elseif ( keyName == "buttonSelect" ) then  -- Select/Back button
return true
elseif ( keyName == "buttonStart" ) then  -- Start/Home button
return true
elseif ( keyName == "buttonMode" ) then  -- Power on/off button
return true
elseif ( keyName == "leftShoulderButton1" ) then  -- Top-left button
return true
elseif ( keyName == "rightShoulderButton1" ) then  -- Top-right button
return true
elseif ( keyName == "leftShoulderButton2" ) then  -- Bottom-left button
return true
elseif ( keyName == "rightShoulderButton2" ) then  -- Bottom-right button
return true
elseif ( keyName == "leftJoyStickButton" ) then  -- Pressing down on the left joystick button
return true
elseif ( keyName == "rightJoystickButton" ) then  -- Pressing down on the right joystick button
return true
-- Now deal with Android standard buttons
elseif ( keyName == "back" ) then  -- Hardware back button
return true
elseif ( keyName == "volumeUp" ) then
return true
elseif ( keyName == "volumeDown" ) then
return true
end
print( "Done with keys" )
end
-- IMPORTANT! Return false to indicate that this app is NOT overriding the received key
-- This lets the operating system execute its default handling of the key
return false
end
Runtime:addEventListener( "key", onKeyEvent )

Of course, we haven’t provided any code other than returning true because it all depends on what you want to do. The button names may also vary from controller to controller — for example, they may not always be called "buttonA" or "buttonX", so if you are working with a new controller, it’s useful to check the value of the buttons that are pressed.

Button phases

Note that buttons/keys produce two phases, much like touch handlers. Those two phases are "up" and "down", where "down" means the user has pressed the button and "up" means they have released it. Like touch handlers, you do not “continuously” get events — once you press the button and get the "down" phase, you will not get another event until the button is released, thus dispatching the "up" phase.

Similar to touch events, it’s generally best practice to use the "down" state for time-sensitive actions like movement or firing weapons. However, for UI elements that typically respond like a mouse click, or touch events that trigger when the touch is complete, the "up" state may be warranted.

Putting it together

Let’s look at an example movePlayer() function using event-based buttons:

local player1 = display.newRect( 0, 0, 10, 30 )
player1.x = 30
player1.y = display.contentCenterY
player1:setFillColor( 1, 0, 0 )
player1.isMoving = false
player1.direction = "up"
local player2 = display.newRect( 0, 0, 10, 30 )
player2.x = display.contentWidth-30
player2.y = display.contentCenterY
player2:setFillColor( 0, 0, 1 )
player2.isMoving = false
player2.direction = "up"
local moveSpeed = 2
local function movePlayer( player, event )
if ( player == nil ) then
return
end
if ( player.isMoving ) then
if ( player.direction == "down" ) then
player.y = player.y + moveSpeed
if ( player.y > (display.contentHeight-20) ) then
player.y = display.contentHeight-20
end
else
player.y = player.y - moveSpeed
if ( player.y < 20 ) then
player.y = 20
end
end
end
end
local function onKeyEvent( event )
local thisPlayer
if ( event.device.descriptor == "Joystick 1" ) then
thisPlayer = player1
else
thisPlayer = player2
end
if ( event.keyName == "up" ) then
thisPlayer.direction = "up"
if ( event.phase == "down" ) then
thisPlayer.isMoving = true
else
thisPlayer.isMoving = false
end
return true
elseif ( event.keyName == "down" ) then
thisPlayer.direction = "down"
if ( event.phase == "down" ) then
thisPlayer.isMoving = true
else
thisPlayer.isMoving = false
end
return true
end
return false
end
Runtime:addEventListener( "key", onKeyEvent )
local function onFrame()
movePlayer( player1 )
movePlayer( player2 )
end
Runtime:addEventListener( "enterFrame", onFrame )

Detecting controllers

Most likely, you will also need to know if the controller is attached and also know when it's detached, or if a new controller comes into play. This is handled using the inputDevice type.

-- Fetch all input devices currently connected to the system
local inputDevices = system.getInputDevices()
local controllersActive = {}
-- Traverse all input devices
for deviceIndex = 1,#inputDevices do
    -- Fetch the input device's information
    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, "type", inputDevices[deviceIndex].type )
    print( deviceIndex, "permenantid", tostring(inputDevices[deviceIndex].permanentId) )
    print( deviceIndex, "andoridDeviceid", inputDevices[deviceIndex].androidDeviceId )
    local index
if ( event.device.descriptor == "Joystick 1" ) then
index = 1
elseif ( event.device.descriptor == "Joystick 2" ) then
index = 2
elseif ( event.device.descriptor == "Joystick 3" ) then
index = 3
elseif ( event.device.descriptor == "Joystick 4" ) then
index = 4
end
    controllersActive[index] = inputDevices[deviceIndex].isConnected
end

Here are a few notes regarding controller detection:

  • For multiplayer apps, the event.descriptor (i.e. "Joystick 1", "Joystick 2") can be used as a unique key that binds a particular controller to a player. This will scan for active devices and set up a table of devices that are currently connected.
  • You should watch for connections and disconnections during gameplay and handle "losing" a controller or adding a controller. You may also want to pause the game if a controller drops out.
  • You might want to hide any UI elements that are specific to one controller, or hide "touchscreen" features when there are controllers present.

And here's some sample code to handle this:

local function onInputDeviceStatusChanged( event )
-- Handle the input device change
if ( event.connectionStateChanged ) then
print( event.device.displayName .. ": " .. event.device.connectionState )
local index
if ( event.device.descriptor == "Joystick 1" ) then
index = 1
elseif ( event.device.descriptor == "Joystick 2" ) then
index = 2
elseif ( event.device.descriptor == "Joystick 3" ) then
index = 3
elseif ( event.device.descriptor == "Joystick 4" ) then
index = 4
end
controllersActive[index] = inputDevices[deviceIndex].isConnected
end
end
-- Add the input device status event listener
Runtime:addEventListener( "inputDeviceStatus", onInputDeviceStatusChanged )

Note that these status changes will not register if the app is suspended. It's your responsibility to poll the controllers if you're resuming back to gameplay state.

Conclusion

Most game controllers offer both digital inputs (buttons) and analog inputs called sticks or joysticks. Because the typical joystick moves both up and down and left and right, we measure the movement based on which "axis" it's moving on. An analog stick moving up and down will be measured along its Y axis, while side-to-side movement is measured on the X axis.

Axis input will be covered in a future blog post, but it's important to know that many game controllers will map the left analog stick to the D-Pad. As such, moving the left stick will frequently generate "up", "down", "left" and "right" events. Also, the bottom shoulder buttons are considered "analog" inputs, but they will generate "button" events when they are squeezed past a certain threshold. If your app needs to know precisely what value those triggers are producing, you would use the axis method of measuring things, for fine-tuned control.


Rob Miracle
rob@coronalabs.com

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.

14 Comments
  • Kohan Ikin
    Posted at 16:04h, 27 August

    This is awesome news, but will HID controller support also come to the Simulator as well soon? (ie So we can plug a gamepad into our Mac and use it to test our game in the Simulator, without having to build for the device every time we make a change?)

  • Chris
    Posted at 07:33h, 28 August

    Sounds fantastic.
    What about Analog Sticks?
    Do we have a way to return the x/y (so we can calculate the angle that it’s facing)?

    • Dave Cheng
      Posted at 10:00h, 28 August

      Also curious about analog stick support. That would be awesome for the game I’m working on.

      • Rob Miracle
        Posted at 16:43h, 28 August

        Chris & Dave as we mentioned above, we will be covering axis support (analog stick, x, y, etc.) in a future post. This was was rather long as it was and I’ve just had my OUYA for a few days, so I’ve got to get a few more ducks in a row before I can write that up. The features are there in the Daily Build API docs

        http://docs.coronalabs.com/daily/api/

        These features should be rolling into the public build pretty soon as such the regular docs will be updated with the info. If you want to take a peek before we blog about it, you can look for “axis” under the Event’s section.

  • Andrew
    Posted at 05:53h, 30 August

    This is FANTASTIC NEWS! Been looking forward to when Corona can build out to OUYA. Rob – this does mean that we’ll be looking forward to an update to your ultimate config and device detection code to include the configs/etc for OUYA. Do you happen to know when the next public release will be?

    • Rob Miracle
      Posted at 19:11h, 30 August

      Actually the existing Ultimate config.lua should handle the OUYA and GameStick just fine as is.

  • Andrew
    Posted at 19:22h, 05 September

    Downloaded the Stable build today, and tried connecting my PS3 controller to my Mac to test this out, seemed like I had my controller connected to the Mac successfully, but couldn’t figure out how to get it working with Corona. Has anyone else done this successfully? If so how did you get it to work?

    • Rob Miracle
      Posted at 20:31h, 05 September

      Key/Axis support is unavaialble on the Mac simulator as well as iOS. You have to test it on an Android device that supports the controllers or from the Windows simulator.

      http://docs.coronalabs.com/api/event/key/index.html

      • Andrew
        Posted at 05:41h, 06 September

        Thanks Rob! Somewhat dampens the excitement of being able to test with a controller in the Simulator in Mac/Windows. Do you know if this is in the plan to support this in a future release? If not can you direct me to the feature request page, and I’ll submit the request.

        • Rob Miracle
          Posted at 14:18h, 06 September

          The Windows simulator does support it. This is not something supported in iOS and I’m unsure of why Mac support isn’t there. I’m pretty ignorant of the Mac OS-X SDK and what is or is not possible with it or how difficult it is to add in.

  • Anton
    Posted at 03:51h, 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?

  • Philip Dijkstra
    Posted at 05:03h, 20 November

    How can i get this to work with the iCade (Core) ?

    • Rob Miracle
      Posted at 17:08h, 20 November

      Anton, we do support limited keyboard functionality on the OS-X simulator now. In fact there was a blog post last Tuesday about a way to use a Mac App to convert HID controller input to keystrokes that were compatible with code you might write for an OUYA.

      Philip, I looked up the iCade and it appears to just need Keyboard control. I don’t know if we have keyboard support for iOS yet or not.

      Rob

  • Nick
    Posted at 20:59h, 18 February

    Does anyone else experience really bad frame rate drops when using OnKeyEvent heavily? I’m building an OUYA game and everytime a button is pressed, even if it doesn’t do anything, framerate drops to 10 or so. 🙁