27 August 2013
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
-- 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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.
Kohan Ikin
Posted at 16:04h, 27 AugustThis 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 AugustSounds 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 AugustAlso curious about analog stick support. That would be awesome for the game I’m working on.
Rob Miracle
Posted at 16:43h, 28 AugustChris & 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 AugustThis 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 AugustActually the existing Ultimate config.lua should handle the OUYA and GameStick just fine as is.
Andrew
Posted at 19:22h, 05 SeptemberDownloaded 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 SeptemberKey/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 SeptemberThanks 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 SeptemberThe 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 SeptemberWhen 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 NovemberHow can i get this to work with the iCade (Core) ?
Rob Miracle
Posted at 17:08h, 20 NovemberAnton, 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 FebruaryDoes 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. 🙁