Posted on by

hid-featIn our previous tutorial about HID controllers, we showed you how to use Corona SDK to read button inputs from game 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 the Sony PLAYSTATION 3 Dual Shock 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. Currently, many OUYA games support either the native controller, the Playstation(R)3, and the XBox 360 (wired-only) controllers.

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["PLAYSTATION(R)3 Controller"] = {}
axisMap["PLAYSTATION(R)3 Controller"][1] = "left_x"
axisMap["PLAYSTATION(R)3 Controller"][2] = "left_y"
axisMap["PLAYSTATION(R)3 Controller"][13] = "left_trigger"
axisMap["PLAYSTATION(R)3 Controller"][3] = "right_x"
axisMap["PLAYSTATION(R)3 Controller"][4] = "right_y"
axisMap["PLAYSTATION(R)3 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 PS3 Dual Shock 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 very 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( 255,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,255,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"

--Object "blueDot" isn't a player, but simply a visual to show the physical stick movement.

local blueDot = display.newCircle( display.contentCenterX, display.contentCenterY, 7 )
blueDot:setFillColor( 0,0,255 )

--Object "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 you should compensate for.

local whiteDot = display.newCircle( display.contentCenterX, display.contentCenterY, 2 )
whiteDot:setFillColor( 255,255,255 )

--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 quadrent based on if
--the values for the two axis are positive or negative. Negative Y, positive X is in
--the top-right area. Positive X, Positive Y is bottom right. Negative X, positive Y
--is bottom left and negative x, negative y is top left.

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

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

local function moveRedPlayer()

   --Set the .isMovingX and .isMovingY values in our event handler and, as long as
   --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 )

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 = 255 * (1 - event.normalizedValue)
      if ( color < 32 ) then color = 32
      elseif ( color > 255 ) then color = 255
      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 )

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 )

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.

Gotchas…

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 )

In Summary

This tutorial should get you up to speed on the axis type inputs on your controller and the basic mapping system. To start integrating axis control into your Corona-based apps, please download the sample project from DropBox.


Posted by . Thanks for reading...

4 Responses to “Tutorial: Game Controllers and Axes”

  1. Anton

    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?

    Reply
    • Rob Miracle

      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.

      Reply
      • Rob Miracle

        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.

        Reply

Leave a Reply

  • (Will Not Be Published)