Build a complete game with the Godot game engine

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).

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