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.
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).
« Previous 1 2 3 4 Next »
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
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.
News
-
The Gnome Foundation Struggling to Stay Afloat
The foundation behind the Gnome desktop environment is having to go through some serious belt-tightening due to continued financial problems.
-
Thousands of Linux Servers Infected with Stealth Malware Since 2021
Perfctl is capable of remaining undetected, which makes it dangerous and hard to mitigate.
-
Halcyon Creates Anti-Ransomware Protection for Linux
As more Linux systems are targeted by ransomware, Halcyon is stepping up its protection.
-
Valve and Arch Linux Announce Collaboration
Valve and Arch have come together for two projects that will have a serious impact on the Linux distribution.
-
Hacker Successfully Runs Linux on a CPU from the Early ‘70s
From the office of "Look what I can do," Dmitry Grinberg was able to get Linux running on a processor that was created in 1971.
-
OSI and LPI Form Strategic Alliance
With a goal of strengthening Linux and open source communities, this new alliance aims to nurture the growth of more highly skilled professionals.
-
Fedora 41 Beta Available with Some Interesting Additions
If you're a Fedora fan, you'll be excited to hear the beta version of the latest release is now available for testing and includes plenty of updates.
-
AlmaLinux Unveils New Hardware Certification Process
The AlmaLinux Hardware Certification Program run by the Certification Special Interest Group (SIG) aims to ensure seamless compatibility between AlmaLinux and a wide range of hardware configurations.
-
Wind River Introduces eLxr Pro Linux Solution
eLxr Pro offers an end-to-end Linux solution backed by expert commercial support.
-
Juno Tab 3 Launches with Ubuntu 24.04
Anyone looking for a full-blown Linux tablet need look no further. Juno has released the Tab 3.