Coffee-Break Tutorials: Setting Up And Using Gamepads (GML)


Coffee-Break Tutorials: Setting Up And Using Gamepads (GML)

In this weeks coffee-break tutorial we'll be taking a look at gamepads and how to set them up and use them in your games. For this, we'll be using the following base GML project, which you'll need to download and import into GameMaker Studio 2:

Gamepads Twinstick Shooter Base

NOTE: This tutorial is for people that use GML. If you prefer to use DnD™ to make your games, we have a companion article for you here.

Once imported into GameMaker, take a moment to look over the objects and sprites etc... that it contains. In this article we will be using this base framework project to make a small prototype "twin-stick-shooter" game for up to four players using gamepads for controls. As such the project is prepared with a a number of simple sprites and some events so that we can concentrate on the important part - adding the controls.


THE SYSTEM EVENT

The first thing we are going to do is add an Asynchronous System Event to the controller object. We want our controller to handle who is playing and what gamepads are connected, and this can all be done quite simply from this event.

NOTE: The Asynchronous System Event is an event that has been added to GameMaker Studio 2 designed to trigger when certain system-level changes are detected, like the plugging in or removing of a gamepad.

So, open the object obj_Control and add the event by selecting Add Event >> Asynchronous >> Async - System:

The Async System Event

This event will always generate a DS map in the built-in variable async_load. This DS map will have an "event_type" key which tells us which type of system event has been triggered, and in this case we want to check for the following:

  • "gamepad discovered" - A gamepad has been plugged in
  • "gamepad lost" - A gamepad has been removed

If the event has been either of those types, then an additional key will also be present in the map:

  • "pad_index" - The index of the gamepad slot which has had the event

When a gamepad is plugged in to the device running the game, it is assigned a "slot" value which is its pad index. This index value is then used in all further gamepad functions to identify it, and on most platforms pads are indexed from 0, so the first pad connected will be in slot 0, and the second in slot 1 etc... However, this is not always the case, for example: on Android and iOS you may find that the first gamepad connected is actually placed in slot 1, as the OS reserves slot 0 for general bluetooth connections or other things, or on Windows it may be slot 4 because you are using a generic gamepad and not an Xbox gamepad. The important thing to take away from this is that regardless of the slot ID for the gamepad, it will be detected in the Async System Event and can be stored and used from that.

How does all this come together in our game? Well, thanks to this event, we have no need to code specific Step Event code to "listen" for gamepads, and can simply add some code to this System Event to catch any changes and assign variables etc... In this case, we are going to have it create a player if a gamepad is detected, and destroy it if there is not, so, in the Async System Event that we've just added, put the following code:

show_debug_message("Event = " + async_load[? "event_type"]);        // Debug code so you can see which event has been
show_debug_message("Pad = " + string(async_load[? "pad_index"]));   // triggered and the pad associated with it.

switch(async_load[? "event_type"])             // Parse the async_load map to see which event has been triggered
{
case "gamepad discovered":                     // A game pad has been discovered
    var pad = async_load[? "pad_index"];       // Get the pad index value from the async_load map
    gamepad_set_axis_deadzone(pad, 0.5);       // Set the "deadzone" for the axis
    gamepad_set_button_threshold(pad, 0.1);    // Set the "threshold" for the triggers
    if !(instance_exists(player[pad]))         // Check to see if an instance is associated with this pad index
        {
        // Create a player object and assign it a pad number
        player[pad] = instance_create_layer(64 + random(room_width - 128), 64 + random(room_height - 128), "Instances", obj_Player);
        with (player[pad])
            {
            image_index = instance_number(object_index);
            pad_num = pad;
            }
        }
    break;
case "gamepad lost":                           // Gamepad has been removed or otherwise disabled
    var pad = async_load[? "pad_index"];       // Get the pad index
    if (instance_exists(player[pad]))          // Check for a player instance associated with the pad and remove it
        {
        with (player[pad])
            {
            instance_destroy();
            }
        player[pad] = noone;                   // Set the controller array to "noone" so it detects a new pad being connected
        }
    break;
}

If you've looked at the Create Event for the controller object, you'll have seen that we initialise the array player[] to noone. This array holds the ID of a player instance, which is in turn mapped to a controller slot. In this way we can use the System Event to catch a controller being added or removed and assign the correct instance to the given controller. Also note that even though we are only going to have 4 players in the game, we have initialised the array to 12 places, since on Windows gamepads can be assigned to slots 0 to 11 depending on the type of gamepad that is being connected. On other platforms - as mentioned above - this is not quite the case and gamepads can be assigned just about any slot, so the array would be created with more values, or you could use something like a DS list instead.

NOTE: We have no need to detect the controller in the Create Event, as the System Event will be fired on Game Start if there is already a controller connected. Therefore we can limit ourselves to placing all gamepad detection code in this event for our demo. In your own games, you might want to make the gamepad controller persistent and have it in the first room of your game, and then assign gamepads to variables which can be parsed when your game levels start.


DEADZONES AND THRESHOLDS

In the above code we have these two lines for when we detect a gamepad:

gamepad_set_axis_deadzone(pad, 0.5);       // Set the "deadzone" for the axis
gamepad_set_button_threshold(pad, 0.1);    // Set the "threshold" for the triggers

These functions do essentially the same thing, with the first working on the "stick" analogue controllers, and the second working on the "trigger" analogue buttons (beneath the shoulder bumpers).

The "deadzone" for the sticks is a value from 0 to 1 which will define at which point the game detects the stick as having moved. So, if the distance from the center to the full radius of the stick movement is 1, setting a deadzone of 0.5 will mean that your game won't detect any movement until the stick has been pushed halfway at least in any direction. This is an important setting as the default deadzone of 0 can give issues, since all gamepads are calibrated slightly differently and you may find that an instance moves even if the stick is not being touched due to the pad returning a distance axis value of 0.001 or something. In general, anything over 0.1 should be fine, but for our example we'll set it to 0.5. In your own games, you could have a "Calibrate" option where the user can set this manually.

The "threshold" for the triggers is the same. It is a value between 0 and 1 and setting a threshold value will mean that the press won't start being detected until it is over that value. In this case, we set the trigger threshold to 0.1 to ensure that when it's not being pressed nothing will happen.


DEBUGGING GAMEPADS

You can close the System Event code now, and you can add a Draw Event to our controller. This is not a required action, but we are adding it in to our prototype project to help debug the controller and also get a better idea of what is happening when we press a button or move.

So, add a Draw Event to the controller object now and give it the following code:

var _num = 0;
for (var i = 0; i < 12; i++;)
{
var xx = 32;
var yy = 32 + (160 * _num);
if gamepad_is_connected(i)
    {
    _num++;
    draw_text(xx, yy, "Gamepad Slot - " + string(i));
    draw_text(xx, yy + 20, "Gamepad Type - " + string(gamepad_get_description(i)));
    draw_text(xx, yy + 40, "Left H Axis - " + string(gamepad_axis_value(i, gp_axislh)));
    draw_text(xx, yy + 60, "Left V Axis - " + string(gamepad_axis_value(i, gp_axislv)));
    draw_text(xx, yy + 80, "Right H Axis - " + string(gamepad_axis_value(i, gp_axisrh)));
    draw_text(xx, yy + 100, "Right V Axis - " + string(gamepad_axis_value(i, gp_axisrv)));   
    draw_text(xx, yy + 120, "Fire Rate - " + string(gamepad_button_value(i, gp_shoulderrb)));
    }
}

This code is pretty self-explanatory and much of it will be covered later in this blog post, but it's worth noting the functions gamepad_is_connected() and gamepad_get_description(). The first can be used to detect if a gamepad is present at any time and takes the index of the gamepad slot to check (from 0 to 11) while the other will return a string that identifies the make and type of gamepad being used.

Gamepad Debug Test

You can go ahead and test your project now, and if you connect a gamepad (or already have gamepads connected) then a player instance should be created, and if you remove a gamepad then the corresponding player instance should be destroyed. You can also move the sticks and fire the right trigger to see the different values returned for those actions in the debug text.

NOTE: The above is checking 12 gamepad slots which is the maximum number permitted on Windows. However, other platforms may vary, and the first gamepad slot that is occupied could be 5, or even 20 (on Android, for example, bluetooth gamepads are assigned a slot when they are detected, and that slot is then reserved for that gamepad whether it is connected again in the future.


MOVEMENT

As we said at the start, this is to be a "twin-stick-shooter", so obviously the player movement should be controlled by the left stick of the controller. Gamepad sticks are analogue, meaning they will not return a simple on/off or press/release value, but rather a constant stream of different values from -1 to 1 depending on what direction they are being pushed in. Since the sticks can move in a circle, the exact position is calculated as a vector of the x axis and the y axis position, with - for example - a value of -1 indicating to the left and a value of 1 indicating to the right along the x (horizontal) axis.

As you saw in our debug code, you can get these axis values by using the function gamepad_axis_value() along with the index of the pad to check and the constant for the stick axis. These constants are as follows:

  • gp_axislh - Left stick horizontal (x) axis (analogue)
  • gp_axislv - Left stick vertical (y) axis (analogue)
  • gp_axisrh - Right stick horizontal (x) axis (analogue)
  • gp_axisrv - Right stick vertical (y) axis (analogue)

How do we turn these values into movement? There are many ways this can be done, but in this example we have chosen the simplest to get you started. So, open up the object obj_Player and add a Step Event with the following:

var h_move = gamepad_axis_value(pad_num, gp_axislh);
var v_move = gamepad_axis_value(pad_num, gp_axislv);

if ((h_move != 0) || (v_move != 0))
    {
    x += h_move * 4;
    y += v_move * 4;
    }

Here we are simply checking the two left stick axis to see if they are anything other than 0, and if they are then we use the value multiplied by 4 to move. If we had not set the deadzone for the stick axis, then the above code may not work as intended (try removing the function from the controller and see what happens), but with the deadzone set we can be sure that the h_move and v_move values will go to 0 when not being touched. We also multiply the result by four as that is the maximum speed we want the instance to move. This will actually give you variable speeds for the player, from anywhere between 0 to 4 pixels per step, since - for example - an axis value of -0.5 (to the left) would be -0.5 * 4 = -2px per step.

You can also use the d-pad to move your player, which works just the same as you would expect for a key press. Although we don't want this in our demo project, we'll post the code to illustrate how it would be done:

var h_move = gamepad_button_check(pad_num, gp_padr) - gamepad_button_check(pad_num, gp_padl);
var v_move = gamepad_button_check(pad_num, gp_padd) - gamepad_button_check(pad_num, gp_padu);

if ((h_move != 0) || (v_move != 0))
    {
    x += h_move * 4;
    y += v_move * 4;
    }

TURNING

With the movement done, we now need to make the player instances turn to face the direction that the right stick is pointing in. This will be done in a similar way to the movement, only the returned axis values will be used to set a direction which will in turn be used to change the image_angle of the instance.

To turn the player, add the following code to the Step Event after the code for movement:

var h_point = gamepad_axis_value(pad_num, gp_axisrh);
var v_point = gamepad_axis_value(pad_num, gp_axisrv);

if ((h_point != 0) || (v_point != 0))
    {
    var pdir = point_direction(0, 0, h_point, v_point);
    var dif = angle_difference(pdir, image_angle);
    image_angle += median(-20, dif, 20);
    }

Again, we poll the axis values for the right stick, then if they are not equal to 0, we use them to get a direction vector using the point_direction() function. We could simply set the image_angle to this value, but to make things nicer and "feel" better to the player, we use the angle_difference() function to rotate the player instance towards the given point rather than turn directly.


SHOOTING

We have the "twin-stick" part of our prototype, but we are missing the "shooter" part! For shooting, we are going to use the right trigger and we are also going to make it so that the more the trigger is pressed, the more we shoot. This can be done because, as we explained earlier, the trigger buttons are analogue and will return a value from 0 to 1, which can then be used to determine the rate of fire.

Once again in the step event add the following after all the rest of the code:

var r_trig = gamepad_button_value(pad_num, gp_shoulderrb);
var rate = 1 - r_trig;
if can_shoot && rate < 1
    {
    with (instance_create_layer(x, y, "Instances", obj_Bullet))
        {
        speed = 10;
        direction = other.image_angle;
        image_angle = direction;
        }
    can_shoot = false;
    alarm[0] = max(5, (room_speed * rate));
    }

Here we get the value of the right trigger and then reverse it (so that if the trigger is 0.2, the rate is 0.8, and a rate of 1 means it's not being pressed), then we create the bullet instance and set an alarm using the rate value. We clamp this to a minimum value of 5 to make sure that the bullets don't shoot too fast.

Finally, we are also going to use one of the face buttons (A, B, X, Y on an X-Box controller) to fire a grenade object with the following code:

if gamepad_button_check_pressed(pad_num, gp_face1)
    {
    with (instance_create_layer(x, y, "Instances", obj_Grenade))
        {
        direction = other.image_angle;
        image_angle = direction;
        }
    }

As you can see, getting the appropriate button is a case of checking the pad index and then the button constant that we want to use (you can find a list of all button constants here), just like you would a keyboard check or a mouse button check.


SUMMARY

Our basic twin-stick-shooter prototype is now ready to play! When you test it you should see that player instances will be created automatically for the gamepads detected, and that adding or removing gamepads will also add or remove player instances from the game. The left stick should move each player, and the right stick should turn them, with the right trigger shooting and the "A" button (gp_face1) should create a grenade instance.

That's the basics of using a gamepad for controls, and as you can see it's pretty simple to set up and use. Obviously in a real game you will want to have both keyboard and mouse and gamepad (or a combination of them) support, but that's just a case of adding in extra checks for those input mechanisms, and maybe having a controller variable and a switch()statement to check which controls to use. Maybe try adding this into the prototype now that you've got it working with gamepads? Or expand the controls to include a "dash" button, or extra weapons on the different buttons available?

Whatever you do, we hope that this little tutorial has given you a good insight into gamepads and controls in general!

Happy GameMaking!