Build a complete game with the Godot game engine

Tutorial – Gaming with Godot

Author(s):

Creating a game requires a wide set of skills to combine graphics, animations, sound, double-clicks, and meticulous coding. The free and open source Godot game engine provides you with all the tools you need to get started.

Writing a game from scratch is hard, and that's why nobody does it anymore. Game creators instead use "engines" that combine a framework and a comprehensive set of tools that let you skip the drudgery and get to the creative parts right away. Godot [1] is one of the most popular free and open source game engines, and, after a couple of weeks playing with it, I can see why.

The Concept

One of Godot's creators, Juan "reduz" Linietsky, stated that the name "Godot" is a reference to the homonymous gentleman in Samuel Beckett's play. Godot, in the play, never arrives and, in a similar manner, Linietsky says the Godot game engine will never be entirely finished, as it can always be improved and expanded.

After six years of active development, Godot has grown to include a huge variety of tools. The best way to demonstrate Godot's capabilities is simply to build a game from beginning to end. So let's make a game of tactical interstellar warfare that … Who am I kidding? It's Space Invaders; we're making Space Invaders, people (Figure 1).

Figure 1: You will be making a Space Invaders clone with Godot.

Tower Defense

The documentation says Godot development is scene-based, and the first thing you have to grok is that a "scene," in Godot parlance, is a bunch of nodes (more about nodes in a minute) that work together.

Take the turret defending Earth in a Space Invaders video game. The image (or images, in case of an animation) of the turret, the collision map that lets you detect when it touches another game element, the sound it makes when it fires, etc., are all the different nodes that give life to the turret entity. This is what Godot calls a scene (Figure 2).

Figure 2: A "scene" is a bunch of related nodes that work together.

I'll start by building a turret that the player can move left or right across the bottom of the screen and that animates when the player hits the fire button.

Start up Godot and choose New Project from the Projects tab in the Projects Manager dialog. Give your game a name, select an empty folder to store all the stuff in it, and click Create and edit.

The first screen in the designer looks very exciting, with a 3D plane extending off into infinity. Unfortunately you won't be using that, as Spaced Marauders is a 2D game. Your workspace will look more like Figure 3. To get to that 2D editor, click on the 2D label in the top center of the screen (Figure 3, section 6).

Figure 3: Anatomy of Godot's editor: workspace (1), Scene dock(2), Inspector/Node tabs (3), Filesystem dock, (4) playback toolbar (5), editor modes (6), Animations pane (7), and main menu (8).

In the default layout, on the top left you have the Scene dock (Figure 3, section 2) that will contain the nodes for your current scene. Below that you have the FileSystem dock, where you can see your assets – that is, your files containing code, images, sounds, etc. (Figure 3, section 4). Center stage is the workspace (Figure 3, section 1) showing an image of the assets linked to the current node (if there are any) on the playing field. This view changes to a text editor when you need to start coding. On the right is the Inspector (Figure 3, section 3), which will show the properties of the currently selected node. Another tab behind Inspector called Nodes will show the signals you can leverage for the selected node and options for grouping similar nodes together. The usefulness of both will become clear later.

Go back to the Scene dock (Figure 3, section 2). If you haven't done anything yet, it will be showing several suggestions of nodes to add. Click on + Other Node to open a list of available nodes. There are a lot, but you can filter the options using the search function at the top of the dialog. Type area in the search box and chose Area2D from the list.

The node's names are pretty good descriptions of what they are; Area2D is an area that contains a 2D object. Usually, all the things that have to change when they "touch" each other (like the turret being hit by bullets or aliens) will be Area2D nodes. Once picked and added to your scene, you will see that Godot places a yellow warning icon next to the empty Area2D node. If you click on the warning icon, Godot tells you that an Area2D is not useful until it contains a collision shape. Double-click on the node's title of the node and edit the text to Turret. Press Ctrl+S to save the scene, and Godot will suggest Turret.tscn. That name is fine.

Before adding a collision shape, you will need to know what to base the shape on, namely the picture of the turret. You can use something like what you can see in Figure 4. Go to the directory where you are saving your Godot project and create a new subdirectory called art. Copy your image in there. If you would like to use the stuff I drew for this project, all art, code, and sounds are available online [2].

Figure 4: The turret is actually a sprite sheet containing six frames in one PNG image.

Click on your Turret node to highlight it, and either click on the plus (+) sign in the upper left-hand corner of the dock or press Ctrl+A to add a new subnode. As you can see in Figure 4, the turret image is really a sequence of images (i.e., an animation containing six frames in one image). This is what Godot calls a sprite sheet. The idea is that every time you fire, the cannon will recoil before firing again.

If you start typing anima into Create New Node's search bar, the first option that pops up is AnimatedSprite and that is exactly what you need. Double-click on it to add it under your Turret node. The AnimatedSprite node comes with another yellow warning icon, this time telling you that you need to add an image before you will be able to see anything.

Cross over to the right of the screen to the Inspector (Figure 3, section 3) and notice how the Frames property says it is [empty]. Click on the arrow pointing down in the box and pick New SpriteFrames from the drop-down menu. A new horizontal panel, SpriteFrames, will open across the bottom of the workspace (Figure 3, section 7). Locate the Add Frames from Sprite Sheet button in the Animation Frames toolbar (second from left; it looks like a grid).

A file browser dialog opens. Navigate to the art directory you created earlier and pick the image containing the frames shown in Figure 4. It is important that all the frames are the same size and shape, as Godot will now ask you how you want to split the frames.

Change the value in the Horizontal text box to 6 (meaning there are 6 frames along the horizontal axis), change the value in the Vertical text box to 1 (meaning there is only one row of frames), and click Select/Clear All Frames to select all the frames. A set of six blue boxes will surround each frame. Click on Add 6 Frames at the bottom of the dialog to add them to your node. The frames will appear in the SpriteFrames pane at the bottom of the window and the sprite will take the shape of the first frame and show up in the upper left corner of the playing field (Figure 5). You can now rename your animation "fire," move frames around, or copy and paste frames into different positions.

Figure 5: Adding a sprite sheet shows the frames in the SpriteFrames panel and the first frame on the playing field in the upper left-hand corner.

Looping is fine for things like walk cycles or spinning wheels, but with your turret, you want the cannon to retract once and then stop the animation until the fire button is pressed again. So deactivate looping by switching off the Loop toggle button in the bottom left of the panel, and change the frames per second (FPS) to something like 40, so that it doesn't look like the cannon is recoiling in slow motion.

In the properties, changing the value of Frame will show the corresponding frame on your turret in the editor, and ticking the Playing checkbox will play the animation once (because I deactivated looping). If you need to change anything else about the animation, click on the arrow pointing down in the Frames drop-down and choose Edit from the list of options. Use the arrow pointing left at the top of the Inspector dock to get back to the AnimatedSprite's main property list when you're done editing the animation.

Before you continue, it is a good idea to lock all the graphical nodes in your scene together so you don't pick one up by mistake and move it separately from the others. Click on the top Turret node to select it, and then click on the tool that keeps the subnodes from being selected separately. It is the 12th icon from the left above the workspace (Figure 3, section 1) and looks like the icon shown in the green box in Figure 2.

Once you have locked all your nodes together, pick up the image of your turret by clicking and dragging on it in the workspace and move it somewhere near the center of the playing field so you can see clearly what happens next. The playing field is shown as a faint purple rectangle in the workspace.

One of Godot's tenets is that you should be able to test each scene separately, without having to run the complete project. This helps isolate problems later on, when you have put it all together. You have now reached a point in which you can "play" your first scene.

At the top right of the screen, there is a series of buttons (Figure 3, section 5). You can use the Play button (an icon with an arrow pointing to the right) or press F5 to play the whole project, but as we haven't defined the main scene, you can't do that just yet. Two icons to the right of the Play button is another one that looks like a movie clapper. Click it (or press F6), and it will play only the selected scene.

When you hit the Play Scene button, not much happens. Indeed, it looks like nothing happens: Your turret pops into existence on the playing field wherever you dragged it to and just sits there, no animation, no nothing. But that is fine, as you have not yet told Godot under which circumstances you want to animate the turret.

Getting Animated

This is where you need to start coding. Godot supports several programming languages, including a visual node-based one. But the best option is probably GDScript [3], a language similar to Python but designed specifically for Godot.

Although Godot does let you write scripts that are independent from the nodes, most will be associated with nodes. Such is the case with Turret, as you are going to link a script with its Area2D node. Select the node from the Scene dock by clicking on it and then click on the Attach Script icon in the toolbar (Figure 2, yellow box).

A dialog box pops up asking what language you want to use for the script (Godot has no problem letting you use different languages for different nodes), what it inherits (you usually won't want to change this), the template you want to use (Default is fine), whether the script will be built-in (don't choose that, otherwise you will not be able to edit the script with an external editor), and the path where you want to store the script in your resources directory.

Once you click Create, a scroll icon representing the script will appear to the left of the name of the node (Figure 2, red box). Click on the scroll icon and the script will open in the script editor, showing you the default template (Listing 1).

Listing 1

GDScript's Default Node Template

01 extends [Node Type]
02
03 # Declare member variables here. Examples:
04 # var a = 2
05 # var b = "text"
06
07 # Called when the node enters the scene tree for the first time.
08 func _ready():
09   pass # Replace with function body.
10
11 # Called every frame. 'delta' is the elapsed time since the previous frame.
12 #func _process(delta):
13 #  pass

Things to note:

  • When you associate a script with a node, GDScript treats the node like a class, and your script extends the class. Hence, line 1 in the script for our Turret scene will read extends Area2D. If you change the type of node later on, you will have to change line 1 by hand to the node's new type, or Godot will be unable to run the scene.
  • In GDScript, like in Python, indentation matters. When you create a function, start a loop, or establish a conditional structure, you must indent its contents.
  • The Godot project publishes a style guide [4] that you can ignore, but you would be advised to follow it to keep your code nice and organized.

The template provides you with two ready-made functions that are very common in many node-attached scripts: _ready() and _process().The _ready() function is called automatically when the object (node) is instantiated for the first time (i.e., when it is pulled in as part of the game). When an alien pops into existence at the beginning of a level, Godot looks for the alien's _ready() function first. You can use _ready() to set the node's properties when it starts. You could, for example, set the coordinates of an alien on the playing field.

The _process() function is called every game frame to update the node. If the node contains an animation, it will update the frame; if the node is moving, it will update its position.

The _process() function's delta variable provides what is called "game time," a way to calculate the state of things taking into account how long has passed since the node's _process() function was last called. For example, say a node's sprite is moving at 400 pixels per second horizontally across the screen, and the last time the node's _process() function was called was half a second ago (delta = 0.5). To find out where to paint the sprite next, you just have to multiply the sprite's speed by the delta (400 x 0.5), and Godot will show the sprite 200 pixels from its prior position.

In _process() is where you are going to do the first modification. Get rid of all the quoted lines and unquote line 12 (func _process(delta):) and change line 13 from pass to $AnimatedSprite.play("fire"). Note that this line has to be indented.

Using a dollar sign ($) plus the node's name is the way you refer to nodes in the node tree. In this case, you want to call $AnimatedSprite's play() method. How do you know $AnimatedSprite has a play() method? Because as soon as you type the point (.) after $AnimatedSprite, the GDScript editor pops up a useful list of the methods and attributes you can use with that particular node, and play() is on said list. The GDScript documentation also has a list of all the types of objects/nodes [5], and that list tells you what methods you can use with which node. Look for AnimatedSprite, and you will learn that the play() method plays the animation you pass it as a parameter. In this case, the fire animation you made earlier plays. The final script looks like what you can see in Listing 2.

Listing 2

Turret.gd (v1)

01 extends Area2D
02
03 func _ready():
04   pass
05
06 func _process(delta):
07   $AnimatedSprite.play("fire")

Now run the scene. The turret will animate, as if firing bullets out of its cannon. You can also see the scene working in the editor if you want. Add a line that says tool to the beginning of your script and save it, and the scene will play out as you edit. You only want the firing animation to play when the player hits the fire button. To do that, you have to be able to read input from the player.

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

Explosions

To show this explosion, you will add a new animation to your turret. As I am terrible at drawing animated explosions, I resort to the excellent Open Game Art [7] for these kind of things and, specifically, to an explosion designed by Ben Hickling (Figure 10) that he generously distributes under a CC0 license.

Figure 10: A cool, 50-frame explosion animation created by Ben Hickling and available from Open Game Art.

Set the animation not to loop and the FPS to 25. Go to your Main node again, select the Turret node from the list of instanced nodes, and go to the Node tab on the far right of the editor screen. Select the area_entered(…) signal again, click Connect, and connect it to the Turret node. That's right, there is no problem in connecting one signal to several nodes. As before, this will automatically create a function to handle the signal and open the editor to fill it in. Add the code shown in Listing 7.

Listing 7

_on_Turret_area_entered(area) (Turret.gd)

01 func _on_Turret_area_entered(area):
02   speed = 0
03   position.y -= 20
04   $AnimatedSprite.play("explosion")
05   yield($AnimatedSprite, "animation_finished")
06   hide()
07   set_deferred("disabled", true)
08   queue_free()

In Listing 7, the first thing you do is that, when the Turret collides with an alien, you stop it in its tracks (line 2) and then move it upwards 20 pixels (line 3). I found that the explosion was a bit bigger than the image of the turret; if it is not moved up, a lot of the explosion happens off the bottom of the playing field. Once in place, run the animation proper on line 4.

Godot's inbuilt yield() function (line 5) stops all the action on the node until something occurs (i.e., a signal is triggered). It takes two parameters: the node to watch and the event (signal) to watch for. In this case, Turret's AnimatedSprite has a signal that indicates that the animation has finished (you can check it by selecting the AnimatedSprite node under Turret and then looking up the animation_finished() signal in the Node dock). That is what you tell Godot to wait for. If it didn't wait, Godot would quickly go on to the next step in the program, and you would probably not see the explosion at all, because the next step is to hide() the node (line 6) and then disable it (line 7).

You use Godot's set_deferred() function to set a property of a node to a certain value. The difference between using set_deferred() and just doing property = value is that set_deferred() waits until the current game frame ends and then updates the property before the next frame starts.

Finally, on line 8, the GDScript's queue_free() function releases and removes the node from the node tree, effectively purging it from the game. Run Main and watch how the turret explodes in a ball of fire when the alien touches it. Yay!

Lining Up the Alien Invasion

In the traditional game of Space Invaders, aliens start at the top of the screen, march right, reach the right edge of the screen, move down a certain number of pixels, and then start marching left. When they reach the left side of the screen, they again shuffle down, change direction, and start marching right again.

You may think that the way to do that is to check the position of an alien every time it moves. I guess that would be fine if we were talking about one alien, but what about 60, 70, or 100? Checking every frame for every alien is a massive waste of computing resources.

Turns out collision shapes are useful here too. The trick consists of creating a new scene (let's call it Limits) that contains two CollisionShape2D nodes, each of which is a segment. Then you create a script for Limits that extends the segment along the left and right border of the playing field from top to bottom. Listing 8 shows how this would work.

Listing 8

Limits.gd

01 extends Area2D
02
03 func _ready():
04   var screen_size = get_viewport_rect().size
05   $Left.shape.a = Vector2 (0, 0)
06   $Left.shape.b = Vector2 (0, screen_size.y)
07
08   $Right.shape.a = Vector2 (screen_size.x, 0)
09   $Right.shape.b = Vector2 (screen_size.x, screen_size.y)

Next instance Limits into the Main node so you can connect Limits's area_entered signal to an on_Limits_area_entered() function in Enemy.gd (Listing 9). Find the line in Enemy.gd that says

position = Vector2(32, screen_size.y - 32)

Listing 9

on_Limits_area_entered() (Enemy.gd)

01 func _on_Limits_area_entered(area):
02   direction = -direction
03   position.y += 10

and change it to

position = Vector2(32, 32)

so that the alien starts marching at the top of the playing field and run Main. Your alien will now march along the top of the playing field and move downwards and switch direction when it reaches an edge. But one alien an invasion does not make, so the next step would be to create many aliens. To do this you could try something like what is shown in Listing 10.

Listing 10

_ready() (Main.gd)

01 func _ready():
02   var enemy_types = ["skully", "cthulhy", "medussy"]
03   var row_y_location = 0
04
05   for alien in enemy_types:
06     for _j in range (2):
07       for i in range(10):
08         var enemy = preload("res://Enemy.tscn").instance()
09         add_child(enemy)
10         enemy.start(Vector2((i * 64) + 50, row_y_location + 50), alien)
11       row_y_location += 64

Using Main.gd's _ready() method, set up an array with the different animations of the aliens (line 2) and then loop over the array and make two lines of 10 aliens each in formation, similar to what you can see in Figure 1. On line 8, GDScript's preload() function loads data from a resource on disk, in this case the Enemy scene, and puts a pointer to its instance into the enemy variable. GDScript's addchild() function then adds each instance to the Main scene. Finally, on line 10, call start(), a new function you create in Enemy.gd (Listing 11) for each enemy. The start() function actually places the alien on the playing field. Run Main and you will see a bunch of critters a-crawling across the playing field.

Listing 11

start() (Enemy.gd)

01 func start(start_position, alien):
02   position = start_position
03   animation = alien

This looks like we're halfway there, but there are still problems. One of them is that you already instantiated Enemy once so you could pass the signal from Limits on to it. This means that one random alien that doesn't belong to the legion pops up in the upper left-hand corner and behaves strangely. Another problem is that when the first column of aliens hits the right side of the playing field, there is a confusing cascade of signals that make deciding what each alien should do next very hard.

It is much easier to treat the invading army as a unit for some things and as individuals for others; you also want to tell Godot to wait until the aliens clear the limits before checking to see if the signal has fired again. To fix these problems, first remove Enemy from the list of instantiated objects in Main, open the Enemy scene, and click on the Node tab in the dock on the right side of the editor. Note that, apart from Signals, there is another set of options under a heading that says Groups. Add a new group by typing enemies in the text box and clicking the Add button. Now, every time a new alien is created, like when the legion of invaders is generated at the beginning of each level, each critter will be added to the enemies group. Re-write the code for Enemy.gd so it looks like Listing 12.

Listing 12

Enemy.gd

01 extends Area2D
02
03 var speed = 80
04 var direction = 1
05 var animation = "medussy"
06
07 func _ready():
08   position = Vector2(50, 50)
09
10 func start(start_position, alien):
11   position = start_position
12   animation = alien
13
14 func _process(delta):
15   position.x += direction * (speed * delta)
16   if speed != 0:
17     $AnimatedSprite.play(animation)
18
19 func switch_direction():
20   direction = -direction
21   position.y += 10
22
23 func stop():
24   speed = 0
25   $AnimatedSprite.stop()

Aliens Advance

Now you need to create a scene the sole purpose of which is to act as a container for all those aliens and manage their movement. Create a new scene and add a plain Node node to it. Rename the node Swarm and save the scene as Swarm.tscn.

To solve the problem of the aliens still touching the limits for several consecutive frames, Godot provides Timers; so under the top Swarm node, add a Timer node (look for "timer" in the Create New Node dialog). Rename your timer CollisionTimer and view its properties in the Inspector dock. Set its Wait time to 0.25 seconds and check the One shot checkbox.

One shot timers start when you tell them, count down the time you tell them, and then stop until the next time you need to start them. Non-one shot timers count down the time you tell them and then immediately start again until you tell them to stop looping. As you want a timer that only starts when the first alien hits a limit on the edge of the playing field, one shot is the way to go. A quarter of a second is plenty of time to clear the limit when the invaders change direction. Add the script in Listing 13 to the Swarm node.

Listing 13

Swarm.gd

01 extends Node
02
03 func new_level():
04   var enemy_types = ["skully", "cthulhy", "medussy"]
05   var row_y_location = 0
06
07   for alien in enemy_types:
08     for _j in range (2):
09       for i in range(10):
10         var enemy = preload("res://Enemy.tscn").instance()
11         add_child(enemy)
12         enemy.start(Vector2((i * 64) + 50, row_y_location + 50), alien)
13       row_y_location += 64
14
15 func _on_Limits_area_entered(area):
16   if $CollisionTimer.is_stopped():
17     $CollisionTimer.start()
18     get_tree().call_group("enemies", "switch_direction")
19
20 func _on_Turret_area_entered(area):
21   get_tree().call_group("enemies", "stop")

The new_level() function (lines 3 to 13), which you will call from Main.gd, fills in the rows of aliens. More interesting are the _on_Limits_area_entered(area): and _on_Turret_area_entered(area) functions. The first manages what happens when an alien hits the limit. It checks to see if the timer is running. If not, it means it's the first alien to hit a limit in awhile, so it proceeds to start the timer and calls Enemy.gd's switch_direction() function to force all the aliens in the enemies group (i.e., all of them) to change direction.

On the other hand, if the signal is fired and the timer is already running, it means another alien has recently hit the limit, which in turn means all of the aliens are already moving in the new direction, so no changes are made. When an alien brushes the turret, the _on_Turret_area_entered(area) function runs, calling Enemy.gd's stop function for all aliens. Tying together, add the Swarm scene to Main and change the content of Main.gd to is shown in Listing 14.

Listing 14

Main.gd

01 extends Node
02
03 func _ready():
04   $Swarm.new_level()

Now is a good time to make Main the main scene of your project. Go to Project | Project Settings… in the menus and click on Run in the left sidebar of the settings dialog. Click on the folder icon in the Main Scene field and pick Main.tscn from the list of available scenes. Click Open. Now you can run your whole game when you click the Play button in the toolbar above the Inspector dock (or just hit F5).

Firing Bolts

So far you have a bunch of aliens invading but no way to fight back. The next step, then, is to enhance the turret's defense system and actually make it shoot something when the player hits fire.

Create another Area2D-based scene and call it Bolt. Give it an AnimatedSprite node and add two animations: a default animation with an image of a bullet (mine is an orangey oblong). Call this animation bolt. The second animation is another "explosion" from Open Game Art.

When you fire the bolt, it will stop in its tracks and shift to the animation of the explosion when it hits an alien in the same way the turret explodes when it hits an alien. Lines 14 to 21 in Listing 15 show what that would look like.

Listing 15

Bolt.gd

01 extends Area2D
02
03 var speed = 400
04
05 func _process(delta):
06   position.y -= speed * delta
07
08 func start (bolt_position):
09   position = bolt_position
10
11 func _on_VisibilityNotifier2D_screen_exited():
12   queue_free()
13
14 func _on_Bolt_area_shape_entered(area_id, area, area_shape, self_shape):
15   area.destroy()
16   speed = 0
17   $CollisionShape2D.set_deferred("disabled", true)
18   $AnimatedSprite.play("explosion")
19   yield($AnimatedSprite, "animation_finished")
20   hide()
21   queue_free()

Use Bolt's area_shape_entered() signal (line 14) to determine which alien the bolt collided with, as it delivers the information you need through its area parameter. You can then call a new method, destroy(), in Enemy.gd that removes the blasted enemy from the node tree.

The collision layer the turret is on must be different from the one the bolt is on; otherwise the bolt will collide with the turret. Open the turret scene and select the Turret node. Click to unfold the Collision section in the node's properties in the Inspector dock on the right, and take a look at the Layer grid. There are 20 layers that Area2D nodes can be on, and by default new Area2D nodes start on layer 1. For the turret that is fine, but now do the same for Bolt and deactivate layer 1 by clicking on it and activate layer 2. Like that, collisions for bolt only happen with other nodes on layer 2.

Your enemies will also be on layer 1. Select the Enemy scene and the top Enemy node and again open Collision. As you need to be able to collide with Turret on layer 1 and Bolt on layer 2, leave layer 1 as active, but also activate layer 2.

To instantiate a Bolt scene when a player hits fire, create a custom signal in Turret.gd by adding the line signal fire to the top of the script and then emit the signal with the line emit_signal("fire") underneath the line that checks for the input from the "fire" button (Listing 4, line 19). While you are at it, you may want to add a timer to Turret so that there is a short delay (half a second works well) between each shot (Figure 11).

Figure 11: Unless you add a timer that forces a delay between shots, this game will be very easy.

Back to the fire signal, open the Main scene and connect the fire signal from Turret to Main. You must write the body of the _on_Turret_fire() function Godot creates in Main.gd, making it do the following:

  1. Preload the Bolt scene.
  2. Add it as a child instanced node to Main.
  3. Call the start() method in Bolt.gd (Listing 15, line 8).

Listing 16 will do the trick.

Listing 16

_on_Turret_fire() (Main.gd)

01 func _on_Turret_fire():
02   var bolt = preload("res://Bolt.tscn").instance()
03   add_child(bolt)
04   bolt.start(Vector2($Turret.position.x, $Turret.position.y - 20))

When a player hits fire, a bolt node is created and placed on the tip of the turret's cannon and starts moving upwards (Listing 15, lines 5 and 6) at 400 pixels per second. If it flies off the screen without hitting anything, it is removed in Listing 15 lines 11 and 12. If it hits an alien, you call Enemy.gd's destroy() function to remove the alien (line 15), stop the bolt (line 16), and remove the bolt's capacity to collide with anything else (line 17). Then you play the explosion's animation and pause the script until it's finished (lines 18 and 19). Finally, you remove the bolt from the game (lines 20 and 21).

Conclusion

Godot has many other options that this article has not covered. We have not mentioned sound design, exporting your game to work natively on different platforms (Godot supports Linux, Windows, macOS, Android, iOS, and HTML5), or 3D game design.

If you need more ideas, try increasing the difficulty of the Spaced Marauders game by having the aliens fire back and moving quicker as you pick them off. You can also create new levels in which the aliens start lower down, move faster, or fire more. Or you could try adding a scoreboard. If you get stuck, know that I will gradually add all these changes and more to the game, and you can find all the code and assets under a GPL license online [2].