Tech

How To Setup And Use GamePads

Posted by Mark Alexander on 14 November 2014

With the addition of console support, as well as the release of the new GameMaker: Player, it's more important than ever to have gamepad support in your projects. To many users, this can seem a complicated process, but hopefully in this tech-blog we can show how simple it can really be and you'll be able to integrate gamepad support in your games in no time!

This article is based on a pre-made project framework which you can get from the link below:

Import this into GameMaker: Studio now and look over the objects and sprites etc... that it contains. In this article we will be using this framework project to make a small demo "twin-stick-shooter" game for up to four players using gamepads for controls, as such it is prepared with a anumber of simple sprites and some events so that we can concentrate on the important part - adding 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 a new event that has been added to GameMaker: Studio in recent Early Access updates. It is 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 >> 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 un
  • "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... Be aware that on Android you may find that the first gamepad connected is actually placed in slot 1, as the OS reserves slot 0 for general bluetooth connections.

How does all this come together in out game? Well, thanks to this event, we 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 add a code action with the following:

show_debug_message("Event = " + async_load[? "event_type"]);        // Debug cocde 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(64 + random(room_width - 128), 64 + random(room_height - 128), obj_Player); with (player[pad]) { image_index = pad; 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.

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 demo project to help debug the controller and also get a better idea of what is happening when we press a button or move.

Add a code action to the Draw Event now with the following code:

for (var i = 0; i < 4; i++;)
{
var xx = 32;
var yy = 32 + (160 * i);
if gamepad_is_connected(i)
    {
    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)));
    }
else
    {
    draw_text(xx, yy, "Gamepad Slot - " + string(i));
    draw_text(xx, yy + 20, "Gamepad not connected" + string(gamepad_get_description(i)));
    }
}

This code is pretty self-explanitory and much of it will be covered later in this tech blog, 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 3 in this case) while the other will return a string that identifies the make and type of gamepad being used.

You can go ahead and test your project now, and if you connect a gamepad then a player instance should be created, and if you remove a gamepad then a player instance should be removed. you can also move the sticks and fire the right trigger to see the different values returned for those actions.

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. Since the sticks are analogue, 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 it is being pushed in. Since the stick 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 in the Step Event add 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 contoller 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 simlar 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 demo, 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(x, y, 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.

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

var button = gamepad_button_check_pressed(pad_num, gp_face1);
if button 
{
with (instance_create(x, y, obj_Granade))
    {
    direction = other.image_angle;
    image_angle = direction;
    }
}

As you can see, getting the approipriate 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 constnts here), just like you would a keyboard check or a mouse button check.

Summary

Our basic twin-stick-shooter demo 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 granade 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 keybord 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. You can find a full version of the demo from the link below.

Back to Top