Build a complete game with the Godot game engine

Button Smashing

Godot is prepared to read input from most sources. Keyboards and mice are obviously supported, but so are most game controllers and touchscreen buttons. Let's start simple, though, and link the turret's fire animation to when the player hits the space bar.

GDScript's Input object simplifies reading from peripherals and lets you do things like what you can see in Listing 3. This listing shows how to check whether a key pressed is a specific key (in this case a SPACE) and then trigger an action.

Listing 3

process() (Turret.gd)

01 func _process(delta):
02   if Input.is_key_pressed(KEY_SPACE):
03     $AnimatedSprite.play("fire", true)

GDScript's KEY_SPACE inbuilt constant is one of many supported by Godot. You can find a list of other constants and variables on the website [6]. These cover most of the keys you can find on keyboards and the buttons, triggers, and joystick positions you'll find on most controllers. Note that the true in the $AnimatedSprite's play() method (line 3) ensures the animation bounces back and the cannon does not stay retracted after the shot. When you run the scene, make sure that the AnimatedSprite's Playing and Loop properties are both off, and the animation will play only when you hit the space bar.

You would be right to feel chuffed already, but you can do one better. Apart from addressing specific keys and buttons, Godot has shortcuts for left, right, up, and down, so if you want to check if the player wants to move left, you can do

if Input.is_action_pressed("ui_left"):

and Godot will check for the left arrow button on a keyboard, but also the left button on the D-Pad on a game controller, and in a bunch of other places so you don't have to code them in explicitly.

What's great about this is that you can also create your own shortcuts. Although there is no preprogrammed shortcut for a "fire" button, you can make one easily. Visit the Project menu in the main menu (Figure 3, section 8) and select Project Settings… . A dialog will open with all the properties for your game. Select the Input Map tab, and you will see all the available shortcuts. Take a moment to review what's available. To add a new shortcut, fill in the name in the Action text box at the top of the dialog. Call the new action ui_fire. Click the Add button, and the action will appear at the bottom of the list.

To attach an input to the action, click on the + symbol to the right of your action and pick Key from the drop-down menu that appears. A pop-up dialog will appear urging you to press a key. Hit your space bar and the Ok button, and Space will appear under the ui_fire action. Click on the + again and pick Joy Button from the pop-up. Looking at the previously mentioned list [6], you see that JOY_R2 is the right trigger button on most controllers. Pick R2 from the drop-down and click Add.

Go back to your script and change the line:

if Input.is_key_pressed(KEY_SPACE):

to

if Input.is_action_pressed("ui_fire"):

Now the animation will play when you press space and when you hit the right trigger button on the game controller. While you're at it, let's add movement to the turret. It only needs to move left and right, so you can make do with something like what you can see in Listing 4.

Listing 4

Turret.gd (v2)

01 extends Area2D
02
03 var speed = 400
04 var screen_size
05
06 func _ready():
07   screen_size = get_viewport_rect().size
08   position = (Vector2(screen_size.x/2, screen_size.y-32))
09
10 func _process(delta):
11   var velocity = Vector2(0, 0)
12
13   if Input.is_action_pressed("ui_right"):
14     velocity.x += 1
15   if Input.is_action_pressed("ui_left"):
16     velocity.x -= 1
17   if velocity.length() > 0:
18     velocity = velocity.normalized() * speed
19   if Input.is_action_pressed("ui_fire"):
20     $AnimatedSprite.play("fire", true)
21
22   position += velocity * delta
23   position.x = clamp(position.x, 32, screen_size.x - 32)

There's a lot of interesting new stuff going on in Listing 4. On line 3, you set up a variable called speed that contains the speed at which the turret will move along the screen. The number is in pixels per second. The screen_size variable (line 4) will contain the width and height of the screen.

The _ready() function (lines 6 to 8) uses the inbuilt get_viewport_rect() GDScript function to get the details from the viewport and copy the size into the screen_size variable you declared earlier. The size attribute contains two values, x for the horizontal length of the playing field, and y for the vertical length. Use those values to position the turret halfway across the bottom of the screen (line 8). The position is of Area2D nodes.

Next up, edit the _process() function by defining a velocity variable (line 11). Note that velocity in this context is not the same as speed. While speed is scalar value, velocity is a vector. Indeed, Vector2 is a special kind of GDScript type that indicates the direction of the object. Usually, vector values vary between -1 and 1, so a vector with the values of, say, (1, 0) would point straight to the right; with a value of (1, -1), it would point up (lower numbers are higher up on the y axis in Godot) and to the right in a 45 degree angle; a value of (0.5, 1) would point down and to the right in a 63.4 degree angle.

Although the turret will be moving on a horizontal line (making vectors a bit of an overkill), it is a good habit to use vectors for movement and physical forces, as most inbuilt attributes use them. Either way, on line 11 velocity is set to (0, 0). On lines 13 to 16, we check the input and add and subtract from the velocity accordingly. If there has been movement, the length of velocity will be larger than zero (line 17), and we will normalize it and multiply it by the speed (line 18).

"Normalizing" entails figuring out the position of the node depending on the angle of the vector. Say the speed is 10 pixels per second. If the velocity is (1, 0), after one second, the sprite will have moved 10 pixels to the right from its prior position. If the velocity is (0, 1), the sprite will have moved 10 pixels down. But if the velocity is (1, 1), for example, it won't have moved 10 pixels to the right and 10 pixels left in one second, because then it would have traveled the square root of 200 (as per Pythagoras, the square root of 10 squared plus 10 squared) – that is, 14.1 pixels in one second. Godot's normalized() function figures out the correct values for the vector by dividing each component by the length of the vector. Normalizing is not strictly necessary for sprites that move perfectly horizontally or vertically, but it is good practice to include it.

Line 22 calculates the new position of your sprite by adding the velocity to the current position and multiplying by the time that has passed since the last time this function was run. Line 23 clamps the turret's position – that is, it limits it, in this case, to the left and right limits of the playing field. This stops the sprite from going over the edge and disappearing into gameland oblivion.

Taking Shape

The final piece the turret needs is its collision shape. You need a collision shape, because Godot doesn't know what bits of your image are meant to be solid.

Click on the + in the Scene dock to add a new node and look for CollisionPolygon2D. The moment you add it to your Turret node, the yellow warning sign disappears from the top node, but a new one appears next to your CollisionPolygon2D node. This is because the latter node is not complete without a defined shape. To add a shape, click on the CollisionPolygon2D node to select it, look at the Inspector dock on the right, and click on PoolVector2 Array (size 0). The zero indicates that there are no vertices in the shape yet.

Once you click PoolVector2 Array (size 0), the text will turn blue indicating it is in "edit" mode. In the workspace, use the Ctrl+mouse wheel to zoom in on the turret and click on one of its corners. A small dot will appear where you clicked. That is your first vertex. Move the cursor, and a red line between the first point and your cursor will appear. Follow the contour of the turret, clicking at every corner to set the vertices of the shape (Figure 6, left). To close the shape, move to the first vertex you set and click on it (Figure 6, right).

Figure 6: Drawing a collision shape (left) and the final shape covering your sprite (right).

Congratulations! No more warning icons. Your turret now has a shape that can collide and be collided with. Let's just give it something to collide with. The turret's enemies are the aliens you can see in Figure 7. To incorporate them into your game, click on the + symbol over the central workspace to create a new scene and add an Area2D node as the top node. Rename the node Enemy and press Ctrl+S to save everything as Enemy.tscn.

Figure 7: Your player's enemies: skully, cthulhy, and medussy (from top to bottom).

Add an AnimatedSprite node under Enemy. As all the enemies will behave in the same way, you can add all the animations from Figure 7 to the same node (Figure 8). To load in the skully frames, proceed like you did with the turret. Then, click on the New Animation button (Animations dock, top left) and add in cthulhy and then repeat the process for medussy. This time you do want the animation to loop, so make sure the Loop switch is on. The default FPS speed of 5 is fine.

Figure 8: As all the enemies behave in the same way, you can load all three animations into the same AnimatedSprite node.

In the Inspector dock, you can choose which animation to preview in the Animation drop down. Clicking the Playing checkbox will play the animation on a loop so you can check that everything is working correctly.

Add a CollisionShape2D to the Enemy node. This is simpler than the CollisionPolygon2D we used for the turret, because you can pick a fixed Shape in the Inspector, and you don't have to faff around with vertices and segments. I picked a circle, and that works just fine. Listing 5 shows how you could move a medussy alien from left to right across the bottom of the screen and have it animated to boot.

Listing 5

Enemy.gd

01 extends Area2D
02
03 var speed = 80
04 var screen_size
05 var direction = 1
06
07 func _ready():
08   screen_size = get_viewport_rect().size
09   position = Vector2(32, screen_size.y - 32)
10
11 func _process(delta):
12   var velocity = direction * speed
13   position.x += velocity * delta
14
15   $AnimatedSprite.play("medussy")

Collisions

What we need now is to combine both the turret and enemy scenes so that both elements are on screen at the same time. To do that, create a new scene by clicking on the + sign over the main workspace area, choose Other Nodes from the options in the Scene dock on the left, and pick a plain and simple Node from the list.

Change the name Node to Main and click on the icon showing three connected chain links (Instance a Scene) in the Scene dock toolbar located directly above the node you just created. This will open a dialog with the available scenes, namely Turret and Enemy. Select both and click the Open button.

The Turret and Enemy scenes will now appear as nodes of Main. If you run Main, both scenes will run as one (Figure 9). However, when the enemy and turret meet, nothing happens: The alien drifts over the turret as if it wasn't there. In fact, a collision is happening; it is just that you are not doing anything with it.

Figure 9: By instancing scenes Turret and Enemy under an umbrella scene called Main, you can make them both run as one.

To solve this, click on the Turret node in Main to select it; over on the right of the Godot editor, click on the Node tab (located next to the Inspector tab). This will show all the signals/events available to the currently selected node, the Area2D Turret in this case.

The first one reads area_entered(area: Area2D), and it is a signal that is triggered when another body with a collision shape hits the current Area2D node. This is exactly what we need now. Click on it to select it and click the Connect button at the bottom of the dock.

A dialog will open with a list of nodes under Main. What Godot is asking you here is which node the signal is going to affect. As an experiment, let's just make the alien stop in its tracks when the turret hits it. As the node affected by the signal will be the alien, pick the Enemy node from the list.

In the text box at the bottom, Godot suggests _on_Turret_area_entered. This is the name of the function/method that will run when the signal is triggered. You could change it or make it point to functions you have already written to manage the signal, but in this case you can just click Connect.

Godot opens the scripting editor and provides you with an empty template for the _on_Turret_area_entered() function. Edit the function so it looks like what you can see in Listing 6. Save your work, run Main, and when the alien hits the turret, it will stop in its tracks. You can also do something more exciting and make your turret explode.

Listing 6

_on_Turret_area_entered() (Enemy.gd)

01 func _on_Turret_area_entered(area):
02   speed = 0

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Introduction

    This month in Linux Voice.

  • FOSSPicks

    This month Graham looks at Godot 4, PostRunner, LeanCreator, lurk, Cubic, SuperStarfighter, and more!

  • FOSSPicks

    This month Graham reviews PeaZip, LibreSprite, NeoChat, Beaker, Giada, Thrive, Kurve, and much more!

  • Animation with OpenToonz

    OpenToonz is a professional animation tool for comic and manga artists.

  • Tutorials – Natron

    Natron gives you the power to apply sophisticated effects to your videos, but its node-based interface can be a bit confusing. This tutorial will help you get a grasp on the basics.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News