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 DnD™ project, which you'll need to download and import into GameMaker Studio 2:
NOTE: This tutorial is for people that use DnD™. If you prefer to use GML 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 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:
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 actions:
So, here we are getting the event that has spawned the Async System event, as well as the slit ID of the pad, then we output this to the console for debugging, and finally we check a
Switch to see which type of gamepad event has been triggered. Let's fill in those Switch cases now, starting with the "
gamepad discovered" event:
In this event we are checking to see which event has been called, and if it's a "discover" event, we get the slot ID for the gamepad and create an instance of the player object, assigning this slot ID to the instance. We also store the instance ID in an array on the controller variable so we can track the players that have been created and the gamepad slot they've been assigned (if you've looked at the Create Event for the controller object, you'll have seen that we initialise the array
noone, which we've initialised with 12 entries, since technically we could connect up to 12 gamepads, although this prototype will repeat sprites after 4 have been added). On other platforms this is not quite the case - as mentioned above - 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.
And now the "
gamepad lost" case:
These actions detect a gamepad being disconnected and will destroy the instance that corresponds to the gamepad slot ID, as well as set the array with the instance ID to
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.
In the above code we have these two lines for when we detect a gamepad:
These actions 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.
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 actons:
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_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.
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.
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 actions, you can get these axis values by using the action
Get Gamepad Axis() 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:
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
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.
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:
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.
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 actions already there:
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 actions (this goes under the
If Expression node, as shown in the image below):
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.
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!