© Lee Stemkoski 2018
Lee StemkoskiJava Game Development with LibGDXhttps://doi.org/10.1007/978-1-4842-3324-5_13

13. Alternative Sources of User Input

Lee Stemkoski
(1)
DEPT OF MATH & CS, ADELPHI UNIVERSITY DEPT OF MATH & CS, Garden City, New York, USA
 
In previous chapters, your games have been controlled with traditional desktop computer hardware: a keyboard and a mouse. In this chapter, you’ll explore two alternative sources of user input: gamepad controllers and touchscreen controls. In particular, you will add these alternative sources of user input to the Starfish Collector game that has been featured in previous chapters. If you do not have access to a gamepad with a USB connector (as discussed later in this chapter), you can still follow along; the code will still compile, and you’ll leave keyboard controls as a fallback (a good practice to consider in general for the convenience of your game’s players). Similarly, even if you don’t have access to a device that’s touchscreen capable, learning about the associated design considerations is still worthwhile. Furthermore, touch events and mouse events are handled by the same methods in LibGDX; you can simulate single-touch input (but not multitouch input) with the mouse. On the other hand, if neither gamepad nor touch-based input is of interest to you, this entire chapter may be omitted without loss of continuity.

Gamepad Controllers

Gamepad controllers are specialized hardware devices that make it easier for the player to enter game-related input. They have been in existence as long as game consoles have, and have included various configurations of components such as joysticks, buttons, directional pads, dials, triggers, and touch pads. With the increase in console-style gaming available on desktop computers, many gamepads that can be connected via USB ports are now available. In this section, you’ll develop controls for an Xbox 360 gamepad, or one of the many alternative products that emulate it, such as the Logitech F310 gamepad , shown in Figure 13-1.
A352797_2_En_13_Fig1_HTML.jpg
Figure 13-1.
Xbox 360 and Logitech F310 gamepad controllers
Support for gamepad input is provided by the Controller and Controllers classes. These are not part of the core LibGDX libraries , and thus their code is contained in different JAR files, which must be included in your project. To begin, make a copy of your Starfish Collector project from Chapter 5 and rename the copied project folder to Starfish Collector Gamepad. Download the source code for this chapter and copy the contents of the downloaded gamepad project’s +libs folder into your project’s +libs folder (or into the BlueJ userlib directory if you chose to set it up at the beginning of the book). In particular, there are three new JAR files required to integrate gamepad controllers in desktop games: gdx-controllers.jar, gdx-controllers-desktop.jar, and gdx-controllers-desktop-natives.jar.
Recall that there are two types of input, continuous and discrete, which are processed using different techniques. For continuous input (corresponding to actions such as walking), you poll the state of the hardware device in the update method, which typically runs 60 times per second. Later, you will see that this process is analogous to polling for keyboard input : keyboard polling uses methods of the Gdx.input object, such as isKeyPressed, while gamepad polling uses methods of a Controller object, such as getAxis and getButton. For discrete input (corresponding to actions such as jumping), you previously implemented the methods in the InputProcessor interface. Those methods are automatically activated when certain inputs are received (such as when a key is initially pressed down). Similarly, you will implement the methods of an interface named ControllerListener, which are activated in response to discrete gamepad events, such as when a gamepad button is initially pressed down. You will write the code for both continuous and discrete gamepad input over the course of the next two sections.

Continuous Input

In this section, you will modify the Turtle class so that the joystick of a gamepad can be used to move the turtle. In particular, this will allow you to precisely control both the speed and the angle of motion of the turtle, which is not possible when using keyboard controls alone. This process will require you to retrieve the instance of the active Controller object. The Controllers class provides the static utility method getControllers, which will help you with this process. Once the Controller has been obtained, you can poll for the state of joysticks, buttons, directional pads, and trigger buttons by using one of four provided get-style methods . Many of these require a single parameter: a constant value that corresponds to a component of the gamepad. These values are gamepad specific, and a particular gamepad might even have different values for different operating systems. The most robust method for determining these values is to allow the player to configure the gamepad mapping at runtime by looping through the different actions required by the game, asking the player to press the corresponding button, and storing the values for later use. For simplicity, you will assume that the player will use an Xbox 360–style controller (which includes those such as the Logitech F310 controller mentioned earlier), and you will create a class to store the constant values corresponding to the various gamepad components for this model. Open the Starfish Collector Gamepad project in BlueJ and create a new class named XBoxGamepad with the following code:
import com.badlogic.gdx.controllers.PovDirection;
public class XBoxGamepad
{
    /** button codes */
    public static final int BUTTON_A              = 0;
    public static final int BUTTON_B              = 1;
    public static final int BUTTON_X              = 2;
    public static final int BUTTON_Y              = 3;
    public static final int BUTTON_LEFT_SHOULDER  = 4;
    public static final int BUTTON_RIGHT_SHOULDER = 5;
    public static final int BUTTON_BACK           = 6;
    public static final int BUTTON_START          = 7;
    public static final int BUTTON_LEFT_STICK     = 8;
    public static final int BUTTON_RIGHT_STICK    = 9;
    /** directional pad codes */
    public static final PovDirection DPAD_UP    = PovDirection.north;
    public static final PovDirection DPAD_DOWN  = PovDirection.south;
    public static final PovDirection DPAD_RIGHT = PovDirection.east;
    public static final PovDirection DPAD_LEFT  = PovDirection.west;
    /** joystick axis codes */
    // X-axis: -1 = left, +1 = right
    // Y-axis: -1 = up  , +1 = down
    public static final int AXIS_LEFT_X  = 1;
    public static final int AXIS_LEFT_Y  = 0;
    public static final int AXIS_RIGHT_X = 3;
    public static final int AXIS_RIGHT_Y = 2;
    /** trigger codes */
    // Left & Right Trigger buttons treated as a single axis; same ID value
    // Values - Left trigger: 0 to +1.  Right trigger: 0 to -1.
    // Note: values are additive; they can cancel each other if both are pressed.
    public static final int AXIS_LEFT_TRIGGER  = 4;
    public static final int AXIS_RIGHT_TRIGGER = 4;
}
The following Controller class methods are available to poll the state of a gamepad component:
  • To poll the state of the joystick, use getAxis(code) , where code is an integer corresponding to either the left or right joystick, and either the x or y direction. The value returned is a float in the range from –1 to 1. On the x axis, –1 corresponds to left and +1 corresponds to right, while on the y axis, –1 corresponds to up and +1 corresponds to down. For example, consider the following line of code: float x = gamepad.getAxis(XBoxGamepad.AXIS_LEFT_X); If the value of x equals 0.5, then that means the left joystick of the gamepad is being pressed halfway to the right. It is important to remember that the orientation of the y axis used by most controllers (negative values correspond to the “up” direction) is the opposite orientation assumed by the LibGDX libraries (positive values correspond to the “up” direction). This will be important when processing input later.
  • To poll the state of the triggers, you also use getAxis(code) . On Xbox 360–style controllers, the left and right triggers are treated as a single axis. Pressing the left trigger generates the values in the range from 0 (not pressed) to +1 (fully pressed), while pressing the right trigger generates values in the range from 0 (not pressed) to –1 (fully pressed). If both triggers are pressed at once, the getAxis method will return the sum of their values; in particular, if both triggers are fully pressed, getAxis will return 0.
  • To check the state of the gamepad buttons, use getButton(code) , where code is an integer corresponding to a gamepad button. The value returned is a Boolean that indicates whether the corresponding button is currently being pressed down.
  • To determine which direction is being pressed on the directional pad,1 use getPov(num) , where num is the index of the directional pad (typically 0). Directional pads are interesting in that they yield return values more complex than a button (a Boolean value) but less complex than a joystick axis (a float value). This “middle ground” level of input is handled by returning an enumerated type (an enum) defined in the imported PovDirection class. However, for convenience, the XBoxGamepad class defines a set of alternative names that may be more familiar to modern gamers.
To begin incorporating this new functionality into your project, in the Turtle class , add the following import statements:
import com.badlogic.gdx.controllers.Controller;
import com.badlogic.gdx.controllers.Controllers;
import com.badlogic.gdx.math.Vector2;
Next, you will modify some of the code in the act method, which is where the continuous input is processed. In the following code, you check to see whether a controller is connected by testing whether the Array of controllers (retrieved by the getControllers method) contains at least one element; if not, the else block contains the keyboard controls you previously created as a fallback. If a single gamepad is connected, you retrieve it by getting the zeroth element of the array. Then, you determine the amount that the left analog joystick is being pressed in the x and y directions (remembering to negate the y value as mentioned previously), using the constants defined in the XBoxGamepad class you created earlier. Those values are then used to create a Vector2 object, which represents the direction in which the joystick is being pressed. You then need to check whether the joystick has moved passed a certain threshold (called the deadzone , used to compensate for controller sensitivity, typically set to a value between 10 and 20 percent), which can be determined by checking the length of the vector. If it passes this test, then you set the speed of the turtle to a fraction of its maximum speed (using the length of the direction vector as the percentage) and set the angle of motion of the turtle to the angle in which the direction vector is pointing. To accomplish these tasks, add the following code to the act method of the Turtle class , noting that the code corresponding to processing keyboard input should be moved into the else block:
if (Controllers.getControllers().size > 0)
{
    Controller gamepad = Controllers.getControllers().get(0);
    float xAxis =  gamepad.getAxis(XBoxGamepad.AXIS_LEFT_X);
    float yAxis = -gamepad.getAxis(XBoxGamepad.AXIS_LEFT_Y);
    Vector2 direction = new Vector2(xAxis, yAxis);
    float length = direction.len();
    float deadZone = 0.10f;
    if (length > deadZone)
    {
        setSpeed( length * 100 );
        setMotionAngle( direction.angle() );
    }
}
else
{
    if (Gdx.input.isKeyPressed(Keys.LEFT))
        accelerateAtAngle(180);
    if (Gdx.input.isKeyPressed(Keys.RIGHT))
        accelerateAtAngle(0);
    if (Gdx.input.isKeyPressed(Keys.UP))
        accelerateAtAngle(90);
    if (Gdx.input.isKeyPressed(Keys.DOWN))
        accelerateAtAngle(270);
}
If you have an XBox 360–style gamepad available, you can test the code at this point.

Discrete Input

Next, you will write the code necessary to process discrete gamepad input events. To avoid modifying your existing BaseScreen class , you will create an extension of this class that implements the ControllerListener interface with default versions of all the necessary methods, which can then be overridden as needed. (In practice, you will probably only make use of the buttonDown method.) Any class that uses gamepad input can then extend this new class instead of the original BaseScreen class. To begin, create a new class named BaseGamepadScreen with the following code:
import com.badlogic.gdx.controllers.ControllerListener;
import com.badlogic.gdx.controllers.Controller;
import com.badlogic.gdx.controllers.Controllers;
import com.badlogic.gdx.controllers.PovDirection;
import com.badlogic.gdx.math.Vector3;
public abstract class BaseGamepadScreen extends BaseScreen implements ControllerListener
{
    public BaseGamepadScreen()
    {
        super();
        Controllers.clearListeners();
        Controllers.addListener(this);
    }
    // methods required by ControllerListener interface
    //  enable discrete input processing
    public void connected(Controller controller)
    {  }
    public void disconnected(Controller controller)
    {  }
    public boolean xSliderMoved(Controller controller, int sliderCode, boolean value)
    {  return false;  }
    public boolean ySliderMoved(Controller controller, int sliderCode, boolean value)
    {  return false;  }
    public boolean accelerometerMoved(Controller controller, int accelerometerCode,
                                        Vector3 value)
    {  return false;  }
    public boolean povMoved(Controller controller, int povCode, PovDirection value)
    {  return false;  }
    public boolean axisMoved(Controller controller, int axisCode, float value)
    {  return false;  }
    public boolean buttonDown(Controller controller, int buttonCode)
    {  return false;  }
    public boolean buttonUp(Controller controller, int buttonCode)
    {  return false;  }
}
Note that this class must be declared as abstract because it does not implement the initialize or update methods from the BaseScreen class . Also note that the listener is “activated” by adding the currently active Screen to the set of listeners managed by the Controllers class. At the same time, you must also remove (via the clearListeners method) any previously added ControllerListener objects ; you don’t want other Screen objects that may be inactive (but still reside in memory) to respond to input, because this could cause unexpected problems. (For example, if pressing the Start button on a gamepad were used to begin a new game from the menu screen, after switching to the game screen you would no longer want this action to occur when pressing the gamepad Start button; therefore, you must stop the menu screen from “listening” and responding to these events.)
Next, you want to modify the LevelScreen class so that pressing the Back key on the gamepad will restart the level (similar to clicking on the Restart button in the top-right corner of the user interface). This requires three steps. First, in the LevelScreen class, add the following import statement:
import com.badlogic.gdx.controllers.Controller;
Next, change the class declaration so that it extends the BaseGamepadScreen class rather than the BaseScreen class, as follows:
public class LevelScreen extends BaseGamepadScreen
Finally, you need to add a buttonDown method to this class to process discrete gamepad button presses (analogous to how the keyDown method processes discrete keyboard key presses). Add the following method to the LevelScreen class:
public boolean buttonDown(Controller controller, int buttonCode)
{  
    if (buttonCode == XBoxGamepad.BUTTON_BACK)
        StarfishGame.setActiveScreen( new LevelScreen() );
    return false;
}
That’s all there is to it! Feel free to test out your project once again; move the turtle and collect some starfish, and then press the Back button on the gamepad to reset the level, thus enabling you to enjoy collecting the starfish all over again.

Touchscreen Controls

In this section, you’ll learn how to implement gamepad-inspired onscreen touch controls. Again, as mentioned in the beginning of this chapter, access to a touchscreen device is not needed to test the code for this section, as LibGDX handles mouse events and touch events with the same methods; single-touch input is simulated by the mouse. Since you already learned about the Button class in Chapter 5, you are well on your way. In what follows, you’ll learn about another user-interface control provided by the LibGDX library, the Touchpad class, which was created to simulate a traditional arcade joystick. Figure 13-2 shows an example of a traditional arcade-style joystick and a touchpad control that can be created with LibGDX , which is rendered in a top-down perspective of the arcade-style joystick.
A352797_2_En_13_Fig2_HTML.jpg
Figure 13-2.
A traditional arcade-style joystick and a touchpad control created in LibGDX
The biggest challenge to successfully using these controls is not the creation of the object, but rather a design challenge: how should these elements be arranged and placed on the screen? One option is to overlay the elements on top of the game world itself, as you have with various Label objects in previous chapters. However, you rapidly discover the problem that the controls—which must typically be much larger than labels, for easy operation—can obscure the game world to the extent that it interferes with game play. If poorly placed , a touchpad could completely obscure the main character. Figure 13-3 illustrates this possible situation by placing the touchpad in the lower-left corner of the game screen. Notice how it could cover the turtle partially, or even completely!
A352797_2_En_13_Fig3_HTML.jpg
Figure 13-3.
A poorly placed touchpad control obscuring the turtle
Some games attempt to address this issue by making the controls on the user interface translucent, yet the core difficulty remains because the player’s fingers will often be positioned over the region where the controls are, thus still obscuring the view of the game world. An alternative approach that you will implement in this section is to reserve a particular region of the screen for the controls and render the game world in the remaining area, as illustrated in Figure 13-4.
A352797_2_En_13_Fig4_HTML.jpg
Figure 13-4.
Placing the game controls in a dedicated region below the game world

Redesigning the Window Layout

To begin, make another copy of your Starfish Collector project from Chapter 5 and rename this one to Starfish Collector Touchscreen. Download the source code for this project from the book’s website and copy the contents of the downloaded project’s assets folder into your new project’s assets folder. In particular, there are three new images, corresponding to the additions seen in Figure 13-4. (You do not need to copy the contents of the +libs folder; unlike the gamepad project from the first half of this chapter, there are no new JAR files required.)
Your first goal will be to reconfigure where the stages will be displayed on the screen and to add a new stage and table to contain the touch controls. You will need to resize the LevelScreen window to 800 by 800 pixels, reserving the lower 800 by 200 pixels for the touch controls, but to minimize the number of changes you need to make, you will keep the other screens at their original size. In addition, you will want to render the game world in an 800 by 600 region, with its lower-left corner starting at the point (0, 200); this rendering area can be set using a method called glViewport, as you will see later. As a first step, in the BaseScreen class, you need to set the size of the main and user interface stages using a FitViewport object, for otherwise they will default to the size of the entire window (which has been acceptable for previous projects, but will not be for this project). In the BaseScreen class, add the following import statement:
import com.badlogic.gdx.utils.viewport.FitViewport;
In the BaseScreen constructor, change the lines of code that initialize the mainStage and uiStage objects to the following:
mainStage = new Stage( new FitViewport(800,600) );
uiStage = new Stage( new FitViewport(800,600) );
Next, you need to create the stage and table for the controls and determine where everything will be rendered. As in the previous project, you will create an extension of the BaseScreen class, called BaseTouchScreen, which incorporates these new elements and overrides the render method. Unlike the BaseScreen class, however, you will not initialize the stage and table in the BaseTouchScreen constructor method. This is because the BaseTouchScreen constructor immediately and automatically calls the BaseScreen constructor, which in turn calls the initialize method (which is where you will add components to the stage and table), and only after this does the program flow return to execute the rest of the code in the BaseTouchScreen constructor. Due to this unavoidable order of execution, you will create a separate method called initializeControlArea that sets up the stage and table that will contain the controls, and you will call this method from the initialize method in the LevelScreen class. You also need to make sure that the new stage is able to process discrete input by adding it to and removing it from the game’s InputMultiplexer at the appropriate times, which takes place in the show and hide methods. In addition, to avoid the complexity of translating touch/mouse coordinates to viewport coordinates, the stage containing the controls will be the same height as the window, even though only the lower 200 pixels will be used. To implement these features, create a new class called BaseTouchScreen with the following code:
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.InputMultiplexer;
import com.badlogic.gdx.utils.viewport.FitViewport;
public abstract class BaseTouchScreen extends BaseScreen
{
    protected Stage controlStage;
    protected Table controlTable;
    public BaseTouchScreen()
    {
        super();
    }
    // run this method during initialize
    public void initializeControlArea()
    {
        controlStage = new Stage( new FitViewport(800,800) );
        controlTable = new Table();
        controlTable.setFillParent(true);
        controlStage.addActor(controlTable);
    }
    public void show()    
    {  
        super.show();
        InputMultiplexer im = (InputMultiplexer)Gdx.input.getInputProcessor();
        im.addProcessor(controlStage);
    }
    public void hide()    
    {  
        super.hide();
        InputMultiplexer im = (InputMultiplexer)Gdx.input.getInputProcessor();
        im.removeProcessor(controlStage);
    }
    public void render(float dt)
    {
        // act methods
        uiStage.act(dt);
        mainStage.act(dt);
        controlStage.act(dt);
        // defined by user
        update(dt);
        // clear the screen
        Gdx.gl.glClearColor(0,0,0,1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        // set the drawing regions and draw the graphics
        Gdx.gl.glViewport(0,200, 800,600);      
        mainStage.draw();
        uiStage.draw();   
        Gdx.gl.glViewport(0,0, 800,800);    
        controlStage.draw();
    }
}
The next steps are to add a joystick-like control, represented by the Touchpad class, and move the Reset button to controlStage, which is covered in the following section.

Working with a Touchpad

Touchpad objects are rendered using two images : one representing the background, and the other representing the knob. The user can touch (or click) the knob and drag it off-center; its movement is constrained to a circular area contained within the rectangular region defined by the background image.
These objects require two parameters to be initialized. First, you supply a value for the deadzone radius —the minimal distance (in pixels) the knob must be dragged in order for any change to register. (This is the same deadzone concept as was discussed earlier in the gamepad section of this chapter.) This is useful for situations when the player wants to leave a finger on the touchpad but also wants the character to remain still. Without a deadzone setting , the controls would be too sensitive for this to be possible. It is unlikely that the average player would have pixel-perfect finger positioning to keep the knob exactly centered, and the result would be unwanted (and possibly player-frustrating) drift of the character being controlled.
Second, the images used in a Touchpad object are stored in a TouchpadStyle object, which contains two images, both stored as Drawable objects , as is standard for UI elements in LibGDX; you encountered this situation before when creating the image for the Restart button object. As before, you will load each image into a Texture object then convert it to a TextureRegion and then a TextureRegionDrawable.
To begin these additions, in the LevelScreen class, change the class declaration so that it extends your newly created BaseTouchScreen class instead of BaseScreen as follows:
public class LevelScreen extends BaseTouchScreen
Then, add the following import statements:
import com.badlogic.gdx.scenes.scene2d.ui.Touchpad;
import com.badlogic.gdx.scenes.scene2d.ui.Touchpad.TouchpadStyle;
import com.badlogic.gdx.math.Vector2;
Also, add the following variable declaration:
private Touchpad touchpad;
Then, in the initialize method , add the following code to change the size of the window, activate the function that sets up the control stage and table, and add a background image :
Gdx.graphics.setWindowedMode(800,800);
initializeControlArea();
BaseActor controlBackground = new BaseActor(0,0, controlStage);
controlBackground.loadTexture("assets/pixels.jpg");
After this, add the following code to set up the Touchpad object, with a deadzone radius of 5 pixels:
TouchpadStyle touchStyle = new TouchpadStyle();
Texture padKnobTex = new Texture(Gdx.files.internal("assets/joystick-knob.png"));
TextureRegion padKnobReg = new TextureRegion(padKnobTex);
touchStyle.knob = new TextureRegionDrawable(padKnobReg);
Texture padBackTex = new Texture(Gdx.files.internal("assets/joystick-background.png"));
TextureRegion padBackReg = new TextureRegion(padBackTex);
touchStyle.background = new TextureRegionDrawable(padBackReg);
touchpad = new Touchpad(5, touchStyle);
Next, add the touchpad and the Restart button to controlTable as follows. Note the addition of an empty row with height 600, which is used to keep the controls in the lower 200 pixels of the window (which has height 800).
controlTable.toFront();
controlTable.pad(50);
controlTable.add().colspan(3).height(600);
controlTable.row();
controlTable.add(touchpad);
controlTable.add().expandX();
controlTable.add(restartButton);
Now, in order to make use of the touchpad data, in the update method, add the following code, which stores the touchpad knob displacement data in a vector, using the length of the vector to set the turtle’s speed to a percentage of its maximum speed (100 pixels/second) and the angle of the vector to set the turtle’s direction of motion. This code is similar to the code from the earlier section on gamepad controllers, except that the Touchpad class itself handles the deadzone calculation, so it does not appear in the comparison here:
Vector2 direction = new Vector2( touchpad.getKnobPercentX(), touchpad.getKnobPercentY() );
float length = direction.len();
if ( length > 0 )
{
    turtle.setSpeed( 100 * length );
    turtle.setMotionAngle( direction.angle() );
}
Finally, since you are using the touchpad to control the turtle’s motion, you can delete the lines of code in the Turtle class act method that check for keyboard input and set the acceleration of the turtle. At this point, the Starfish Collector game should now render as shown in Figure 13-4, and you are ready to test your program, using the mouse to control the touchpad that now moves the turtle.

Summary and Next Steps

In this chapter, you added two new ways for the player to interact with the Starfish Collector game. First, you added gamepad-controller support to the base game by using the controller extensions for the LibGDX libraries. This required the inclusion of some new JAR files in your project, as well as the creation of an extension of the BaseScreen class and a class dedicated to storing the values corresponding to each of the joysticks, buttons, directional pads, and triggers on your particular gamepad. You learned how to poll for continuous input, as well as how to implement an interface that responds to discrete input. Following this, you learned how to add touchscreen-style support, creating another extension of the BaseScreen class and using a Touchpad object. This chapter also discussed the design issues that arise when adding onscreen controls and showed one way to alleviate these issues—by repositioning the rendering locations of the stages using the glViewport method.
At this point, to practice and refine your newfound skills, you can return to earlier game projects and implement gamepad or touchscreen controls for them as well. When you are satisfied with your progress, the next chapter will introduce another advanced topic: procedural content generation in the context of creating maze-based games.
Footnotes
1
The control element typically referred to as a directional pad was referred to as a point-of-view control in traditional flight simulators, which explains the use of the POV acronym in the LibGDX source code.
 
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset