game development camp 2018 logo

Godot Shmup

Creating a new project

Godot is a lightweight, free and open-source game engine, that requires no installation. The engine works on Windows, Mac, or Linux, and can be downloaded directly from the Godot web site.

Creating the project directory

In Godot, projects are represented as a folder of files, where all files within the folder and its subfolders are considered part of the project. Therefore, we need to establish which folder our project will occupy.

Within the desired directory, such as the Desktop,

Create an empty folder named Godot Shmup

We will proceed to put all the necessary project files in this folder.

The project folder must be empty in order for the Godot Project Manager to establish a new project there.

Creating the project files with the Project Manager

When opening Godot, we are introduced with the Godot Project Manager. Let's do this now.

Locate the Godot file downloaded from the Godot web site,

Run the Godot application

The Godot Project Manager should open. This is where we see a list of all the Godot projects that have been opened by Godot on this computer. After we create our new project, it should show up in this list for us to be able to quickly open in the future.

This is also where we can craete a new project.

To start the new project process,

Click New Project

The Create New Project dialog window appears.

The first field, Project Name allows us to have a name for the project, as we will see it in the Godot Project Manager project list.

To give the project a name, within the Project Name field,

Type Godot Shmup

We now have to point Godot to the new folder we created for this new project. The easiest way to do this is to first copy the file path to the Godot Shmup folder we created.

Within the Godot Shmup directory, inside the address bar of the

Copy the Godot Shmup file path

>We will now supply this copied path to the Godot Project Manager.

In the Project Path field of the Create New Project window,

Paste the copied file path.

We are now ready to create the project.

In the Create New Project window,

Click Create & Edit

The Godot editor should now open with our new project.

Importing graphics into the new project

Now that we have the new project, let's bring the graphical assets we need to make our game. To do this, we simply have to copy the graphics into the project folder.

If you look inside the Godot Shmup folder we created, you'll see that it now has a few files that the Godot Project Manager placed in there. The most noteworthy file is the project.godot file. This file should always remain in the root directory of the project, and serves to remember all the settings of our project.

Let's now bring the graphics folder for our shmup game into the project folder.

Locate the graphics folder containing our shmup graphics,

Copy the graphics folder

This copied graphics should now be placed into the project folder.

Open the Godot Shmup project folder,

Paste in the copied graphics directory

The graphics are now ready to be used in the project.

Return to the Godot editor.

Upon returning to the Godot editor, you may see a quick importing progress bar that appears. Once this finishes, Godot has successfully automatically loaded the graphics into the project.

You can see all the project files, including the new graphics folder, in the FileSystem panel on the left of the Godot editor interface.

Setting the vewport size

The rectangle in the middle of the Godot editor scene editing panel describes the bounds of the viewport, which is also called the window of our game.

The default dimensions are 1024 pixels wide by 600 pixels tall. We can change this in the project settings.

In the Project menu,

Click Project Settings

The Project Settings dialog appears. As the name suggests, this is where project settings can be modified, including the viewport dimensions.

To find the viewport settings, scroll to the Display category on the left,

Click Window

We can modify the viewport size here to be 480 by 800.

In the Width field, Type 480.

Next, the height field.

In the Height field,

Type 800

We're done with the project settings for now.

To close the Project Settings window,

Click Close

We should now see the viewport rectangle should now reflect the new size in the editing panel.

It might take a few seconds for the editing window to refresh the viewport rectangle to reflect the new size.

Introducing nodes into the scene

In Godot, all objects in the scene are made up of what are called nodes. There are many types of nodes, ranging from graphical, physics, text, or any other variety that is needed for the project.

Creating the root node

Every scene in Godot must have exactly one root node. We have been using the term scene, a lot without truly understanding what a scene is. A scene in Godot is composed of nodes organized in a tree structure. Each node in the tree can have any number of child nodes descending from it, and each node has exactly one parent node

.

Our main scene requires a root node, so let's go ahead and create one.

To create a new node, in the Scene panel on the right,

Click new node button

>

The Create New Node dialog appears, prompting us to choose which type of node we want to create.

Though the root node can technically be of any node type, the Node2D type serves as a good generic type of node to organize all of our nodes in our 2D game.

To select Node2D from within the list,

Click Node2D, and click Create

The new node appears in the Scene panel on the right, and is given the name Node2D by default. Let's rename the node so that it better describes this node's purpose.

To rename the newly created Node2D node, in the Scene panel,

Double-click Node2D, type Main

Saving the main scene

Now that our scene has a root node, we can save the scene and not lose our progress.

In order to save the scene,

Press Ctrl+s

>

Let's keep the suggested name, Main.tscn. The file extension for scenes in Godot are tscn.

To finalize saving the file as Main.tscn, Click Save

The file should now appear in the root directory of our project folder, which you can see in the FileSystem panel.

Creating the background sprite graphic

Our background is really plain right now. Let's add a background graphic with a sprite node.

With the Main node selected,

Create a new node of type Sprite

By default, Godot assigns the node the name Sprite. Let's give it a better name.

Rename the new Sprite node to BackgroundSprite

A Sprite node doesn't have any appearance until its Texture property is set.

In the Texture field of the Inspector panel,

Click <null>>, click Load

The graphic we wanted is within the graphics folder we copied into the project folder earlier.

To select the desired background graphic, within the graphics folder,

Double-click bgGradient.png

We now see the graphic in the editing pane.

Modifying position and scale

Our background graphic, by design, is 1 pixel wide and 800 pixels tall. We want this to cover the entire viewport, so let's resize and reposition the sprite node.

Position, Rotation, and Scale properties can be found under the Transform group of the selected node's Inspector panel properties.

To modify the horizontal scale of the Sprite, within the Inspector panel, under the Transform drop-down,

Click in the Scale field, and type 480 for x

The graphic is still off-center, so let's fix that now. Sprite nodes are referenced by their center point by default, so we need to offset the node by half the viewport height and width.

To position the background to fit the viewport,

Click the Position field, and type 240 for x and 400 for y

Each type of node has its own set of properties relevant to it, as we can see separated into categories in the Inspector panel. For example, the Texture property can be found on Sprite nodes, but not Node2D nodes.

Locking the background node

As it stands, it is too easy to accidentally select and move the background sprite. Since we don't need to modify it anymore, we can lock it in editor so as to prevent any accidental modifications.

To modify the background sprite, with BackgroundSprite selected, in the editing pane toolbar,

Click lock node button

We can no longer select the background sprite from the editing pane.

Testing the game

We want to frequently test our game to make sure that our game still functions after the changes we make. Let's start testing our game now.

To test the game, at the top right corner of the interface,

Click test game button

Before we can test the game for the first time, we must first tell Godot which scene in our project should be the "main scene", i.e. the scene that loads first when the game is launched. Let's first clear this Please Confirm... dialog box.

To clear the Please Confirm... dialog box,

Click Select

To assign our Main.tscn file as our project's main scene, with Main.tscn selected,

Click Open

The main scene has been selected, and the testing game window appears. We no longer have to select the main scene each time we play our game to test it.

The game works, but is not all too exciting. Let's close the game for now.

Close the testing game window that just appeared.

Let's proceed to make our game more interesting.

Setting up the player nodes

Creating the player node

The player node will eventually have several nodes associated with it. To make managing these nodes a bit easier, we will contain all the nodes within a Node2D parent. We can think of the Node2D as somewhat of a keyring, where the keys of this keyring are the functional nodes (e.g. sprites, physics, etc.). The keyring just keeeps the keys together, so that when the keyring moves, all the keys move with it.

With the Main node selected,

Create a Node2D node

Make sure the Player node is a child of the Main node, and not the background sprite. If the Player node is a child of background sprite, positioning the player will not work as expected.

To rename the new Node2D to better describe that it is for the player object,

Rename the new Node2D to Player

Adding a player sprite graphic

With the Player node selected,

Create a Sprite node child of Player

We need to give it a Texture now in order to see it.

With the new child Sprite of Player selected,

Load into the Texture property the player.png file.

You should now see the player graphic.

Making child nodes not selectable

Nodes are positioned with respect to their parent node. For example, if a child node is positioned to 0 pixels on the x axis, and 0 pixels on the y axis, then that means the child node will be positioned exactly to the position of its parent.

Using the Node2D "keyring" analogy from before, we want to make sure that the keys (i.e. the Sprite in this case) to stay together with the keyring (i.e. Node2D Player node). We can do this by applying a setting to the Player node which locks its children to not be selectable on accident.

To make the Player node's children not selectable, with the Player node selected,

Click button to make child nodes not selectable

You should now see the same button icon appear next to the Player node in the Scene panel.

Modifying position and rotation

Let's move the Player node to the bottom middle of the viewport.

With the Player node selected,

Click and drag the Player node to the bottom middle of the viewport.

The Player node should be at a position of around 240, 700 now.

With the Player node selected, in the Rotation Degrees field,

Type 270

You can also rotate a node in the editor, by holding CTRL while dragging a control handle.

Introducing basic interactivity with scripts

Scripts are what make our games work. Scripts dynamically control the nodes in which they are embedded, as well as other nodes in the scene tree. Without scripts, nodes just sit lifeless, like they are now.

Creating a new godot script

With the Player node selected,

Click button to create new script

The Attach Node Script dialog box appears. Here you can choose in which programming language to write the script, what to name the script, and where to put it. We're just going to go with the defaults.

To finish creating the new script for the Player node,

Click Create

The script is created, as can be seen with the filename Player.gd in the FileSystem panel. The view has also shifted to the Script view.

You can shift between 2D view and Script view by clicking the tabs at the top middle of the interface.

Understanding functions and comments

Almost all gdscript code is contained within functions. Functions can be custom defined by the programmer, but several are built into the engine. When a script is created, both the _ready function and _process function are pre-written. These are the two most common functions used in scripting.

>The code in the _ready function runs the moment the node begins to exist in the game. The _process function runs every moement of the game.

Code that is indented underneath the function definition line is considered part of the code block, which is the code that runs when that function runs.

Any line of code that is preceded by # is a comment, which means that the game program will ignore it when interpreting the code. Comments are purely for humans to read.

This means that the _process function, as we can see in the default code given to us, is what is called commented out, which means that the code will not run, unless the # signs are removed.

In order to enable the _process function,

Remove the # characters which start each line

The _process function now looks something like this:

        func _process(delta):
            # Called every frame. Delta is time since last frame.
            # Update game logic here.
            pass
        

Basic debugging with the print function

In order to call a function, the syntax is to type the name of the function, followed by a pair of parentheses, where the value or comma separated values between the parentheses are what are "passed" into the function for the function to perform some kind operation associated with that function's purpose. The values put between the parentheses are called arguments. Using a function in this way is called calling the function.

The first built-in function we will be using is the print function. The print function takes the arguments in the function call and "prints" them to the output panel. This is useful to test to make sure code is working properly at that particular part of the program.

Let's make sure that the _process function is working properly.

Above the pass line within the _process function,

Type print("happens every frame")

Test the game.

You should see "happens every frame" being printed every frame in the Output log at the bottom of the interface.

To prevent the output log from continuously printing,

Comment out the print line

Setting the player position to the mouse

Properties of nodes can be accessed with the dot operator, which is represented as a period. The syntax is that the object name is typed, followed by the dot operator, followed by the property name. You can either get the property value or set the value with this technique.

To refer to the object on which the script is attached, we use the self keyword. For example, to get the node's position, we can type self.position.

To get the mouse position we can call a built-in function: get_global_mouse_position().

Above the pass line in the _process function,

Type self.position = get_global_mouse_position()

Test the game

We see the Player node following the mouse position. We want it to only follow the mouse's x coordinate.

To have the Player node follow just the mouse x position,

Add the code in bold self.position.x = get_global_mouse_position().x

Test the game.

The Player node now follows the mouse x position.

Introducing projectiles with separate scenes

Creating the projectile nodes

With the Main node selected,

Create a Node2D node

Rename the Node2D just created to Projectile
With the new Projectile node selected,

Create a Sprite node

To set the Texture> property of the new Sprite,

Load in projectile.png as the Texture property

Make the children of the Projectile node not selectable

Setting up the projectile script

Create a new script with default settings on the Projectile node

Uncomment the _process function

Making the projectile move

Type the code shown in bold:
func _process(delta):
    self.position.y = self.position.y - 5
            
Test the game
Modify the code as is shown in bold:
func _process(delta):
    self.position.y -= 5
            
Test the game

Making the projectile its own scene

We learned earlier that a scene is composed of a tree of nodes. Let's understand trees in a more fundamental way. Each node and its children nodes can be represented in a tree structure, regardless of where that node is in the larger tree. Indeed, if you consider real-life botanical trees, each branch of a tree itself looks just like a small version of the bigger tree.

In Godot, we can save any such sub-tree as its own scene, since again, a scene is just a container which holds a tree of nodes. When we create a new scene to encapsulate one of the branches of our Main scene, we can then make however many copies of that new scene inside of our Main scene. Each such copy is called an instance. Each instance of the same scene file shares the same default characteristics saved within the scene file.

To further grasp these concepts, let's dive into creating our own new scene for projectiles.

To access the Projectile node's context menu options,

Right-click the Projectile node

To convert our Projectile node into its own scene file,

Click Save Branch as Scene

We are now prompted to assign a file name for our new tscn file.

To accept the default name and file location suggestion,

Click Save

We should now notice a new icon next to our Projectile node in the Scene panel of the Main scene. This icon indicates that this Projectile node is an instance of the Projectile.tscn scene. Let's open the new scene file in the editor.

To open the newly created Projectile.tscn file, on the Projectile node,

Click button to open scene from which instance is created

Another way to open the Projectile scene file is by opening the Projectile.tscn file in the FileSystem panel.

We now have our Projectile scene open in another tab, alongside the Main scene. We are starting to see a deeper sense of Godot's nature of being an engine of scenes and nodes.

Creating scene instances within the editor

To switch back to editing the Main scene, at the top of the editing pane,

Click the tab labeled Main

We see the single instance of the Projectile instance in the Main scene. Let's create some more copies of the Projectile scene.

To duplicate a the Projectile node, with it selected,

Press CTRL + d

The new projectile instance appears directly over the existing projectile. We can see that the duplicate node shows up in the Scene panel.

The new instance is automatically named with a number tacked onto the end, because a node cannot have multiple child nodes with the same name.

Create a few more duplicates, and place them at different places of the scene.
Test the game.

All instances of the Projectile node travel upward off the screen. All instances of the Projectile scene share the same default node structure and scripts.

Anything we modify in the Projectile scene file is automatically adopted by all the instances of the Projectile scene. This is particularly useful for level design, so that the designer can compose a dynamic level full of interactive objects (e.g. doors, treasure chests, etc.), and feel confident that they will all behave consistently, and update to reflect whatever new behavior comes with any change of each dynamic object's original scene file. Projectiles though are typically not placed by the level designer, but rather spawned from the code at runtime.

Installing custom assets using AssetLib

Before we work on spawning projectiles from the code, we're going to take brief break to learn about Godot's repository of user-contributed assets called the AssetLib.

Godot has all the base functionality we need to make a great game. However, a number of developers have done some of the leg-work for us, and have put that work for free online for us to download. The author of this tutorial has created a collection of functions to make certain common operations in Godot a bit easier. Packages of code like this are commonly refered to as libraries.

Downloading the GoGodot library

We can access the AssetLib from the Godot website or even within the Godot editor directly. Let's go with the latter option.

To open the AssetLib, at the top of the Godot interface,

Click AssetLib

We see a listing of pages of user-contributed assets. We are looking for an asset package called GoGodot.

To find our desired asset package, in the Search field,

Type gogodot, and click Search

We should see the GoGodot library in the list. The one we want is contributed by "oranjoose".

To select the GoGodot library from the list,

Click GoGodot

A pop-up with a description of the asset appears.

To continue to download the GoGodot library, within the newly opened dialog box,

Click Install

The files have been downloaded to a temporary directory, but the asset package hasn't been downloaded into our shmup project yet.

To continue the downloading process, at the bottom of the AssetLib pane,

Click Install

A new window labeled Package Installer appears, showing exactly which files are going to be injected into the project folder and where. In this case, the primary files are go.gd and setup_gogodot.tscn, waiting to be put in the root directory of the project folder.

To finalize the file transfer, within the new Package Installer window,

Click Install

The new files now show up in the FileSystem panel.

Setting up GoGodot

For many assets in the AssetLib, they are ready to go just by putting the files in the project folder. For GoGodot, there is one thing we have to do to make the library work.

To enable the GoGodot library, within the FileSystem panel,

Double-click setup_gogodot.tscn

Just by opening this scene, it has injected the go.gd script into the project settings. The library is now active, but to get the project to fully recognize it, we should restart the Godot editor.

Restart the Godot editor.

Spawning projectiles with player input

Creating custom input actions in the input map

We can create custom actions in the Input Map.

To access the Input Map, we must first,

Open the Project Settings

At the top of the Project Settings window,

Click the tab labeled Input Map

Let's make a custom action for firing a projectile.

In the Action field at the top of the Input Map,

Type fire_projectile, and click Add

We have our custom input action now, but no actual physical input has been associated with it.

To reveal which type of human input device to use to associate with this custom input, to the right of the new fire_projectile action,

Click new action controller drop-down button

In the drop-down list that just appeared,

Click Mouse Button

To go with the default left mouse-click button,

Click Add

To close the Project Settings window,

Click Close

Checking action input with if structures

Much of programming is asking questions. Does the player have a particular power-up? Is the player's health below a certain threshold? And so on. We control these kinds of questions with logical structures typically called if structures.

An example of if structure can be seen here:

if 2 < 5:
    print("We can math!")
            

Note the colon at the end of the if line. Following that line, there's an indented block. Similar to function definitions, the code indented underneath it is associated with that structure. The code block of the if structure will only run if the condition evaluates to true.

Actually, the condition just needs to evaluate to what some programmers call "truthy", since the code block will run in a variety of other circumstances, such as a non-zero number, and a variety of other techniques we won't be discussing in these materials.

We are going to use an if structure to check each moment of the game if the fire_projectile action has occured. We can do this by using the is_action_just_pressed function, as part of the Input object.

Within the Player script, type the code shown in bold within the _process function:
self.position.x = get_global_mouse_position().x

if Input.is_action_just_pressed("fire_projectile"):
    print("projectile fired!")    
            

Spawning scene instances within the code

We are going to use a function within the GoGodot library to spawn instances of the Projectile.tscn scene.

To spawn a projectile instance at the player position, modify the code as shown in bold:
if Input.is_action_just_pressed("fire_projectile"):
    go.spawn_instance("Projectile", self.position.x, self.position.y)
            

In order to spawn a projectile at the player position without the GoGodot library, you could type something like this:

var newProjectile = load("res://Projectile.tscn").instance()
newProjectile.position = self.position
self.get_parent().add_child(newProjectile)
            
The GoGodot library clearly simplifies this operation.

Test the game.

We should now be able to spawn projectiles by left-clicking the mouse.

Spawning incomers with a Timer node

Creating the incomer nodes

The "incomers" coming into the screen from the top of the viewport are going to functionally be like projectiles going the opposite direction, and spawned automatically, rather than by player input. As such, we are going to duplicate a lot of the code from before.

Practice on your own

Create the Incomer node as a Node2D, with a child Sprite node whose Texture property uses the incomer.png file.

Make sure to keep child nodes of Incomer from being selectable, as we did with Projectile.

Creating the incomer scene and script

Now that we have our Incomer node, let's go ahead and make it its own Incomer scene, with a script that makes it travel down a couple pixels per frame.

Practice on your own

Save the Incomer node as a scene with the name Incomer.tscn.
Attach a new script to the Incomer node, keeping the default settings.
Make it so that the Incomer travels down the screen 2 pixels each frame.
Test the game.

If all went well, the Incomer should appear to travel down the screen on its own.

Creating the Timer node

Incomers, unlike projectiles, are to be spawned automatically on a 1 second interval. We can do this with a Timer node.

To create a Timer node child of Main, with the Main node selected,

Create a Timer node

Rename the new Timer node as IncomerSpawnTimer

We made this a child of Main because this Timer affects the incomer spawning across the game. If we had made it a child of Incomer, then each time an Incomer spawned, it would start a new timer to spawn more Incomers, exponentially spawning Incomers out of control. If the timer was a child of Player, then the Incomers would stop spawning after the Player was removed, or it would double the Incomers if there was a second player.

To have the spawn timer start immediately, with IncomerSpawnTimer selected,

Set the Autostart field to On

We're going to keep the other properties as is, since we want the Timer to trigger on a one second interval and to loop indefinitely.

Using the timeout signal on the timer

We have been creating custom functionality with scripts. However, each type of node innately checks for certain events to occur, specific to that node type. These events are referred to as signals in Godot. We can think of them kind of like how each animal or plant may have a different kind of reflex or stimulus response they innately understand, like how a venus fly trap plant knows to close its "mouth" when an insect goes on the leaf.

To see the signals available to the IncomerSpawnTimer, with IncomerSpawnTimer selected,

Click the tab labeled Node

We see under the Timer category a signal called "timeout". This signal is triggered or emitted, as it is called in Godot, every time the time indicated by the Wait Time property has elapsed.

When a signal is emitted, nothing happens automatically. If we want something to occur when a signal is emitted, we have to connect the signal to a function within a script. This function is called a callback function.

We can connect the signal to any script within this scene tree, but we should try to choose a script that makes sense. Up to this point, we have subscribed to the mentality that each node handles itself with its own script. We can continue that logic by creating a script for the spawn timer.

To have a script for our timer's callback function,

Attach a new script to the IncomerSpawnTimer, choosing default settings

Let's now connect the signal to a function in our new script.

To start the process of connecting the timeout signal to a function,

Double-click timeout()

A dialog window appears asking which node possesses the script where we want to call the function when the signal is emitted, that is, when the timer's time runs out.

To select the IncomrSpawnTimer as the node which possesses the script,

Click IncomerSpawnTimer

To confirm the _on_IncomerSpawnTimer_timeout as the name of the function that will be created,

Click Connect

We should now see a new function defined at the bottom of the spawn timer script. Code inside that function's code block is executed when the timeout signal is emitted.

Practice on your own

Make an Incomer instance spawn at a position of 200, -50.

We should see a steady stream of Incomer instances spawning from the top of the screen in single-file.

In order to give the Incomer a random x position, we will be using the rand_range function, which takes two arguments, the first indicating the minimum value, and the second indicating the maximum value within the range of random values we can get.

To make it so the new instance spawns at an x position between 0 and 480, type the following code shown in bold:
func _on_IncomerSpawnTimer_timeout():
    go.spawn_instance("Incomer", rand_range(0, 480), -50)
            

Handling collisions

In games, it is common that some gameplay is centered around objects touching other objects, whether it is Mario touching goombas, or the hero triggering a cutscene. In our case, we want something to happen when projectiles touch incomers.

Enabling collision with the Area2D node

Currently, our Player, Incomer, and Projectile nodes share a similar structure, that is, a Node2D container and a Sprite child to give the node an appearance. However, neither of those node types support handling collision by default. We're going to add another "key" to the "keychain", to use the analogy we have been using throughout the tutorial. We will add collision handling with Area2D nodes.

Switch to the Projectile.tscn tab.
Select the Projectile root node,

Create a child node of type Area2D

The Area2D controls the collision behavior, but it needs some kind of shape in order to detect the collision.

With Area2D selected,

Create a child node of the type CollisionShape2D

CollisionShape2D nodes allow an active region by which to check for overlapping areas, but we need to specify what kind of shape it is.

With the new CollisionShape2D selected,

Choose New CircleShape2D for the Shape property

Now we can specify the exact dimensions of the circular collision shape.

With CollisionSHape2D selected,

Zoom in on the graphic in the editing pane

To resize the circular shape,

Click and drag on the control handle on the edge of the circle

We're all set for the Projectile's collision setup. Let's do the same for the Incomer.

Practice on your own

We are going to set up collisions for Incomer the same we did for Projectile.

Give Incomer an Area2D child, and CollionShape2D child of Area2D.
Assign a RectangleShape2D for the CollisionShape2D's Shape property.
Resize the rectangle to fit squarely with the Incomer Sprite dimensions.

Handling collision with the go_collision function

We have all the ingredients now for collisions to be detected between Incomers and Projectiles, but we now have to write some code for what happens when an collision occurs.

Using the GoGodot library, we can handle collisions in the code by defining the go_collision function, within the script who is nearest in ancestry to the Area2D node.

To set up the go_collision function, with Incomer.gd open, at the bottom of the script, type:
func go_collision(otherArea):
    print("collision occurred")
            

This go_collision function is executed when the Incomer area and another area overlap. The parameter in the function definition, otherArea is a reference to the area that collided with the incomer.

To handle collisions in the code without the GoGodot library, use the area_entered signal on the Area2D, and attach it to a script, much like how we did with the timeout signal on the spawn timer.

While testing the game,

Make a projectile collide with an incomer

We should see "collision occurred" appear in the output log. We will next make something more interesting happen when a collision occurs.

Removing nodes

Let's make it so that when a collision occurs, both the collided projectile and collided incomer are removed from the game. We can do this with go.destroy function from the GoGodot library.

Let's start with removing the collided incomer. Since the go_collision function is within the incomer itself, we can refer to it by using the self keyword.

To remove the collided incomer, modify the go_collision code as shown in bold:
func go_collision(otherArea):
    go.destroy(self)
            
Test the game.

Incomer nodes now disappear when they collide with projectiles.

Next let's remove the collided projectile. Since the go_collision function is in Incomer, we can assume that otherArea refers to the collided projectile.

To remove the collided area, modify the go_collision code as shown in bold:
func go_collision(otherArea):
    go.destroy(self)
    go.destroy(otherArea)
            
Test the game.

The projectiles don't look like they are being destroyed! Here we have to consider the structure of our Projectile node. otherArea refers to the Area2D node in of Projectile, which is just a child of the projectile's root. We should instead remove the Area2D's parent.

To remove the Projectile node at its root, modify the code as shown in bold:
func go_collision(otherArea):
    go.destroy(self)
    go.destroy(otherArea.get_parent())
            

Managing score with variables

Creating an autoload script

So far, all of our scripts have been attached to nodes in our scene. Sometimes, we want to have a script that can be accessed from anywhere, anytime. These are called autoload scripts.

First, we need to create a new script file, and then we can make it global.

To create a new blank script, in the top-left corner of the script editor,

Click File, and click New

We see the familiar new script window, but this time we want to manually type in a file name for it. The file name can be anything, but we will go with the name "global.gd"

In the Path field,

Type global.gd

We now need to tell the game engine that this new script we created is one we want to be able to access from all parts of our code. We do this from Project Settings.

Open the Project Settings window.
Within the Project Settings window,

Click the tab labeled AutoLoad

To add a script which is loaded as global, to the right of the Path field,

Click the button whose label is ..

In the new open dialog, select global.gd,

Click Open

To finalize the operation, and confirm that global will be the name of the object to use in the code,

Click Add

Close Project Settings.

Our global.gd script should now be accessible from anywhere by using the object called global

Creating a global variable using the autoload script

There's a lot of clutter in the new global.gd file. All we want to do with it is define a variable or two. Let's remove all unnecessary code.

Delete all code in the new file except for the line which says extends Node
To create a new global variable, type the code as shown in bold:
extends Node

var score = 0

We can now refer to this variable anywhere else in the code by typing global.score. Let's try it out in our collision function.

Switch to the Projectile.gd script.

Let's make it so that the global score increases each time an incomer collides with a projectile.

To increase the global score upon Incomer-Projectile collision, add the code shown in bold:
func go_collision(otherArea):
    go.destroy(self)
    go.destroy(otherArea.get_parent())

    global.score += 10
    print(global.score)
Test the game.

We see in the output log that the score is increasing by 10 for each collision.

Displaying text with a Label node

Only the developer sees what is printed to the output log. If we want our players to know what their current score is, we will have to create nodes for this.

Creating and modifying the score label

Godot has many nodes to build a rich user interface and menu system. For displaying score, we will just use the basic node to display text, which is the Label node.

Switch to editing Main.tscn.
With the Main node selected,

Create a child node of type Label

Rename the new Label node as ScoreLabel.

This new Label node appears in the top-left corner of our viewport, but it currently has no text.

To supply placeholder text for the new Label, with ScoreLabel selected,

Type Score: 0 for the Text property,

Next, let's make it a bit bigger.

To increase the size of the text, in the Rect group of ScoreLabel's properties,

Type 2 for Scale in both x and y fields

The text looks the way we want it to, but it doesn't update to actually display the score at runtime.

Updating child label text with Main node script

Up until now, we held true to the notion that "nodes can handle themselves," meaning that if a node can control its own behavior with its own script. For example, the Player, Incomer, and Projectile nodes control their own movement with their own respective scripts. It then stands to reason that we would have the ScoreLabel control its own text with its own script. This is an entirely valid approach, but we will do things slightly differently to illustrate how a node can refer to a different node from within the code.

Let's modify ScoreLabel from within a script attached to Main.

Attach a new script with default settings to the Main node.

To ensure that the player always sees the most up-to-date score, we can have the label update every frame. We can refer to ScoreLabel from Main.gd by typing $"ScoreLabel"

To have ScoreLabel update every frame, within Main.gd, type the following code:
func _process(delta):
    $"ScoreLabel".text = "Score: 1000"

This makes the label display "Score: 1000" each frame, but that doesn't solve our problem.

Combining strings and numbers

We want to tack on the global score. We can achieve this with a concept called string concatenation, which is the process of joining two strings. This is done with the + operator. An example of such an operation is "happy " + "birthday", which would produce the single string, "happy birthday".

However, we cannot simply type "Score: " + global.score, because the lefthand side of the operator is a string, and the righthand side is a number. Doing this would produce and error called a type mismatch. We need to first cast the score as a string properly concatenate the two values. The str function allows us to cast values as strings.

To update ScoreLabel's text to combine "Score: " and the global score, modify the code as shown in bold:
func _process(delta):
    $"ScoreLabel".text = "Score: " + str(global.score)
Test the game.

The score now updates properly to display the player's current score.

Creating a game over system

The player can gain score, but there's no end point, and no risk. Naturally, we'll make it so the player has to avoid the incomers.

Setting up the game over sprite

We'll have a graphic appear when the player collides with an incomer. We can start by putting a game over sprite in the screen.

With the Main node selected,

Create a child node of type Sprite

Rename the newly created Sprite to GameOverSprite
To assign the game over texture to the newly created sprite,

Load gameOver.png as the value for GameOverSprite's Texture property

Position GameOverSprite in the center of the viewport.

The game over graphic is now prepared for what the screen should look like when the player collides with an incomer, but we don't want it to appear when the game starts. We can make it invisible.

With GameOverSprite selected, within the Visibility category of the Inspector,

Set the Visible property to false (uncheck the box)

GameOverSprite is no longer be visible, but don't worry! We can set it back to visible at runtime using the code.

Creating the game over state in autoload script

Many aspects of games are controlled through a series of states. States can be used to describe behavior of the game at an overarching scope, such as the game state of being paused, or if it is the player's turn in a turn-based game. States can also be used to control behavior of individual objects, such as if an enemy is in the frozen state, idle state, patrol state, or attacking state.

To control the game over state, we will do so from the global script, since many nodes across our entire game depend on whether or not the game has reached game over. States are often controlled with variables that possess the boolean data type, whose only two possible values are true or false.

Let's create the global boolean variable now to control our game over state.

Switch to editing global.gd
To add a new global boolean variable named IsGameOver, type the following shown in bold:
var score = 0
var isGameOver = false
            

We assigned its default value to false because the game starts out not being in the game over state.

Custom variables like this don't do anything unless we set up our scripts to make use of them. We will do that later.

Setting up collisions for the player node

Let's finally make it so that the player can detect collisions by adding the necessary nodes.

Practice on your own

Add a child Area2D node to Player
>
Add a child CollisionShape2D node to the new Area2D node

When we chose for the Shape property in the past, it was a no-brainer to choose the CircleShape2D for Projectile, and RectangleShape2D for Incomer, but for our tringular Player, we find a notable lack of any obvious tringular shape options. The reason for this is that checking overlapping areas for rectangles and circles is less "expensive" in terms of processing cycles, than other shapes. We could create a tringular polygon shape with a different node, but a rectangular collision shape should suffice for player, despite its triangular appearance.

Choose RectangleShape2D for the Shape field on the new CollisionShape2D.
Line up the dimensions of the rectangle over the bottom half of the player's traingular appearance.

Player now registers collisions.

Test the game.

It may be surprising to notice that the player node already disappears when it collides with an incomer, without writing any additional code. However, looking back at the Incomer.gd script, we'll see that we are removing the other object in the collision, regardless of what type of node it was, projectile or player.

Displaying game over sprite

The player is removed upon collision with an incomer, but we also want the game over Sprite to appear. In fact, when a game over occurs in a game, typically many things change, and so it is useful to enable the game over state we prepared with the global boolean we created earlier.

Switch to editing Incomer.gd
To set true our custom isGameOver> boolean, within the on_collision function, type the code shown in bold:
go.destroy(self)
go.destroy(otherArea.get_parent())

global.score += 10

print("Game Over")
global.isGameOver = true
            

At this point, it will print "Game Over" to the output log even when an incomer collides with a projectile, since on_collision will run no matter what Area2D node the incomer touches. We need to verify that the Area2D is a child of player first before setting the game over state. We can check this by getting the node's name property.

To ensure the game over state is set only if the player was hit, modify the code as shown in bold:
go.destroy(self)
go.destroy(otherArea.get_parent())

global.score += 10

if otherArea.get_parent().name == "Player":
    print("Game Over")
    global.isGameOver = true
            

Remember, otherArea refers to the Area2D node, which isn't the node whose name is "Player". If you look at the Scene panel, that node is actually the player Area2D's parent node.

Our custom game over state is only intitiated if it is the player the incomer touches.

Restarting game with player input

Once the Player node is removed, it would be nice if the player had a way to restart the game.

Let's first set up the custom input action, as we did with allowing the player to fire projectiles.

Practice on your own

Create a new input action named restart_game.
Assign the Enter key to the new restart_game action that was just created.

We can restart the game from any script. However, there are several bad choices of scripts for this code, such as the player script, since that script won't be available when the player is removed. Let's just use the Main script.

Switch to editing Main.gd

We can restart the game with the go.restart_scene function.

To restart the Main scene when the custom restart_game action is pressed, within the _process function, add the code shown in bold:
$"ScoreLabel".text = "Score: " + str(global.score)

if Input.is_action_just_pressed("restart_game"):
    go.restart_scene()
            

This would allow the player to restart the game, even while not in the game over state. For this action, we actually want multiple conditions to be true, we can add another condition by using the and logical operator. The condition on the left of and and the condition on the right of and must both evaluate to true in order for the entire expression to evaluate to true.

To add to the if condition such that the game must also be in the game over state in order to restart the scene, add the code shown in bold:
if Input.is_action_just_pressed("restart_game") and global.isGameOver:
    go.restart_scene()
            
Test the game.

We can now restart our game in the game over state.

Instead of using a logical operator like and, you can use a nested if statement, which is an if> structure within the block of an if> structure. The inner block will only execute if both the outer and inner if conditions are true.

Creating a health system with custom properties

We have been using many properties so far, such as position on Node2D nodes, text on our Label node, and so on. These properties are all built into the nodes as part of the engine. However, when making a game, we usually need much more than those basic properties Godot gives us by default. For example, if we wanted to make a farming game, there's no "crop" node, with properties like "harvest time", "water needed", "yield", or anything like that. We'd have to create that all ourselves. Fortunately, Godot, like other game engines, provide us the ability to create custom properties.

A property or member variable, is a variable written in a script that belongs to the node itself, where each node that posseses that script automatically gets that same property, though each instance can possess a different value for the same property.

Giving incomers a health property

So far, incomers are removed after just one collision. To make our game more interesting, offering up more depth of play, we can allow incomers to take multiple hits before being removed. We will do this by giving Incomer a health property.

To create a property, we simply define it like any other variable, but we do so outside of any function block, preferably at the top of the script.

Switch to editing Incomer.gd.
To create a health property which belong to our Incomer nodes, add the code as shown in bold:
extends Node2D

var health = 3
            

We've created the health property, which can now be accessed by the dot operator either within this script by typing self.health, or within another script.

The default value we've assigned the health property is 3. This means that each instance will spawn with a health value of 3, though this number can be changed immediately after the incomer is spawned, or after the incomer is "damaged." In fact, let's reduce the incomer's health upon collision.

To reduce the incomer's health property by 1, upon collision, within the go_collision function, add the following code in bold:
func go_collision(otherArea):
    self.health -= 1
    go.destroy(self)
    go.destroy(otherArea.get_parent())
            

This code, as it stands, will still eliminate the incomer upon collision, because the go.destroy(self) line runs no matter what. Let's put that line within an if block whose condition checks if the incomer's health has reached zero.

To destroy the incomer only if its health property has reached 0, add and modify the code shown below in bold:
func go_collision(otherArea):
    self.health -= 1
    if self.health < 1:
        go.destroy(self)
    go.destroy(otherArea.get_parent())
            

You may have noticed that we aren't checking if health is equal to exactly 0, but rather at most 0. The reason for this is that you might in the future have a projectile reduce more than 1 health from the incomer, and you wouldn't want to accidentally let an incomer have negative health and not be destroyed.

Test the game.

Incomer nodes now are removed after 3 collisions each.

Author: Chabane Maidi