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.
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.
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 create a new project.
The
The first field,
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.
We will now supply this copied path to the Godot Project Manager.
We are now ready to create the project.
The Godot editor should now open with our new project.
Switching to the 2D view
Some engines only support 2D games, while others are primarily for 3D game development, but later came to support 2D games, despite its tools being mostly configured for 3D. Godot Engine natively supports 2D, as well as 3D games from the ground up. 2D and 3D mode are found in two entirely separate sandboxes.
Our new project by default opens to the 3D view. We are going to be making a 2D game, so let's switch to the 2D view.
We are now in the 2D view. The project should now open directly to the 2D view once we re-open our project in the future.
Importing graphics into the new project
Now that we have the new project, let's bring in 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
Let's now bring the graphics folder for our shmup game into the project folder.
This copied graphics should now be placed into the project folder.
The graphics are now ready to be used in the project.
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
Setting the viewport size
The rectangle in the middle of the Godot editor scene editing panel describes the
The default dimensions are 1024 pixels wide by 600 pixels tall. We can change this in the project settings.
The Project Settings dialog appears. As the name suggests, this is where project settings can be modified, including the viewport dimensions.
We can modify the viewport size here to be 480 by 800.
Next, the height field.
We're done with the project settings for now.
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
Creating the root node
Every scene in Godot must have exactly one
Our main scene requires a root node, so let's go ahead and create one.
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
The new node appears in the Scene dock, and is given the name Node2D by default. Let's rename the node so that it better describes this node's purpose.
Before creating our root Node2D node, the Scene dock offered a few buttons, one of which labeled 2D Scene. Clicking this also would have created a Node2D root node, as a shortcut for this common action when a new scene is created.
Saving the main scene
Now that our scene has a root node, we can save the scene and not lose our progress.
Let's keep the suggested name,
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.
By default, Godot assigns the node the name Sprite. Let's give it a better name.
A Sprite node doesn't have any appearance until its
The graphic we wanted is within the graphics folder we copied into the project folder earlier.
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
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.
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.
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.
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.
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.
Let's proceed to make our game more interesting.
Setting up the player nodes
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 child nodes (e.g. sprites, etc.). The keyring just keeps the keys together, so that when the keyring moves, all the keys move with it.
Creating the player 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.
Adding a player sprite graphic
We need to give it a Texture now in order to see it.
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.
You should now see the same button icon appear next to the Player node in the Scene panel.
Modifying position by dragging
Let's move the Player node to the bottom middle of the viewport.
The Player node should be at a position of around 240, 700 now.
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
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.
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
Any line of code that is preceded by #
is a
This means that the _process
function, as we can see in the default code given to us, is what is called #
signs are removed.
_process
function,#
characters which start each lineThe _process
function now looks something like this:
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
pass
Basic debugging with the print function
In order to use a
The first built-in function we will be using is the
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.
pass
line within the _process function
,Player.gd print("happens every frame")
You should see "happens every frame" being printed every frame in the Output log at the bottom of the interface.
print
lineSetting the player position to the mouse
Properties of nodes can be accessed with the
To refer to the object on which the script is attached, we use the
keyword. For example, to get the node's position, we can type self.position
.
To get the mouse position we can call the built-in function get_global_mouse_position()
.
pass
line in the _process
function,Player.gd self.position = get_global_mouse_position()
Some functions take no arguments in order to work, as we can see with the get_global_mouse_position
function. Even though the get_global_mouse_position
function takes no arguments, we still need to type an empty set of parentheses in order to tell the engine to call the function.
We see the Player node following the mouse position. We want it to only follow the mouse's x coordinate.
Player.gd self.position.x = get_global_mouse_position().x
The Player node now follows the mouse x position.
Introducing projectiles with separate scenes
What's a shmup without the ability to "shoot" projectiles? Unlike with the player node, we will potentially have many projectiles on the screen at once. We will handle that in this section.
Creating the projectile nodes
Let's create our basic projectile the same way we created the Player nodes.
Setting up the projectile script
_process
functionMaking the projectile move
The Player node moves by setting its x position to the mouse cursor's x position. Projectiles won't move like that. Instead, projectiles will move on their own, regardless of player input. Specifically, projectiles should travel a few pixels upward on the y axis every frame.
We can handle that in the _process
function.
Projectile.gd func _process(delta):
self.position.y = self.position.y - 5
The projectile travels upward by itself.
It may seem like a counter-intuitive circular reference to use self.position.y
on both sides of an equal sign, almost like using the word "the" in the dictionary to define the word "the." Rest assured that code like this works. The =
operator simply sets whatever is on the left-hand side of it to the value of whatever is on the right-hand side of it. If each side is valid, then the operation will work fine.
Adding to or subtracting from an existing value is so common that we have special operators to make these kinds of operations faster to type.
In our case, we will be using the -=
operator, which reduces the left-hand side of it by the amount of what is on the right-hand side of it.
Projectile.gd func _process(delta):
self.position.y -= 5
The projectile still travels up the screen as it had before using the -=
operator.
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
To further grasp these concepts, let's dive into creating our own new scene for projectiles.
We are now prompted to assign a file name for our new tscn file.
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.
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
We see the single instance of the Projectile instance in the Main scene. Let's create some more copies of the Projectile scene.
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.
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
Godot has all the base functionality we need to make a great game. However, a number of developers have done some additional 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
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.
We see a listing of pages of user-contributed assets. We are looking for an asset package called GoGodot.
We should see the GoGodot library in the list. The one we want is contributed by "oranjoose".
A pop-up with a description of the asset appears.
The files have been downloaded to a temporary directory, but the asset package hasn't been downloaded into our shmup project yet.
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.
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.
Just by opening this scene, it has injected the go.gd script into the project settings.
As of 1/14/2019, a Godot update broke the setup_gogodot.tscn file functionality. A fix has been uploaded but the AssetLib has not yet processed it. A temporary workaround is to go to Project Settings->Autoload tab->open the go.gd file->click Add. This should set up GoGodot.
Spawning projectiles with player input
We don't want our projectiles to appear until the player has hit a button. In this section, we will handle spawning instances of scenes in code using custom input actions.
Creating custom input actions in the input map
We can create custom actions in the Input Map, which is in the Project Settings.
Let's make a custom action for firing a projectile.
We have our custom input action now, but no actual physical input has been associated with it.
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
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.
Player.gd _process
function:Player.gd 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.
Player.gd 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.
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
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
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.
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.
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
We see under the Timer category a signal called "timeout". This signal is triggered or
When a signal is emitted, nothing happens automatically. If we want something to occur when a signal is emitted, we have to
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.
Let's now connect the signal to a function in our new script.
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.
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
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.
IncomerSpawnTimer.gd 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.
The Area2D controls the collision behavior, but it needs some kind of shape in order to detect the collision.
CollisionShape2D nodes allow an active region by which to check for overlapping areas, but we need to specify what kind of shape it is.
Now we can specify the exact dimensions of the circular collision shape.
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.
Handling collision with the area_entered 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 create a callback function for any signal that a node possesses by simply creating a function with the same name as the signal within a script attached to the node, or to the node's nearest ancestor node. By doing this, we usually won't have to manually connect signals like we did with the Timer node.
We can handle collisions in the code by using the Area2D's area_entered signal. We can make this work by defining an area_entered
function within the Area2D's script.
area_entered
function, with Incomer.gd func area_entered(otherArea):
print("collision occurred")
This area_entered 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
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 area_entered
function is within the incomer itself, we can refer to it by using the self
keyword.
area_entered
code as shown in bold:Incomer.gd func area_entered(otherArea):
go.destroy(self)
Incomer nodes now disappear when they collide with projectiles. However, projectiles keep on going. This might be intentional for your design, but let's say you want to balance your game such that projectiles are also removed when they collide with an incomer.
If your incomers appear to have started spawning more erratically, it's possible that your spawn interval is so short that two incomers are overlapping each other when they spawn. When two incomers overlap they destroy themselves, because of our new code. We will be learning how to tackle this kind of problem later.
Practice on your own
Repeat the steps above in order to make it so that projectiles are removed when they collide with an incomer.
The projectile and incomer should both be removed when they collide with each other.
Managing score with variables
Sure, we can destroy incomers, but it would be nice to offer some additional reward to the player for doing so. Let's learn how to make a score system.
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
First, we need to create a new script file, and then we can make it global.
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
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.
Our global.gd script should now be accessible from anywhere by using the object called global
You may have noticed there already was an AutoLoad script in the list. This was the GoGodot library that was installed when we opened the setup_gogodot.tscn file. This is how we are able to run GoGodot functions anywhere in our project.
Creating a global variable using the autoload script
In order for us to keep track of score, we need to tell the game to remember a number. Computer applications, games in our case, use
But before we do that, 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.
extends Node
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.
Let's make it so that the global score increases each time an incomer collides with a projectile.
func area_entered(otherArea):
go.destroy(self)
global.score += 10
print(global.score)
We see in the output log that the score is increasing by 10 for each collision.
If you typed the above code in your Projectile.gd file instead of the incomer script, then this would have worked too. However, in the future, Incomers might be destroyed by other objects, and sometimes might take multiple "hits" from projectiles, so writing this code in Incomer still might be the better option.
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
This new Label node appears in the top-left corner of our viewport, but it currently has no text.
Next, let's make it a bit bigger.
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.
A script can refer to any other node in the scene tree by using the $
. You can think of it like explaining to someone how you are related to someone in your family tree. However, since ScoreLabel is simply a child of Main, all we have to do is type "ScoreLabel" after $
. Let's modify ScoreLabel from within a script attached to Main.
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
func _process(delta):
$"ScoreLabel".text = "Score: 1000"
This makes the label display "Score: 1000" each frame, but that doesn't solve our problem.
Typing $ScoreLabel
without the quotes would work too in this case. However if you omit the quotes, it won't work in every situation, so we are sticking with using quotes.
Combining strings and numbers
We want to tack on the global score. We can achieve this with a concept called
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
function allows us to cast values as strings.
func _process(delta):
$"ScoreLabel".text = "Score: " + str(global.score)
The score now updates properly to display the player's current score. You may delete the print statement from the Incomer script since the score now displays in the game window.
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.
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.
You may also change the visibility of the node in the editor by clicking the eye icon to the right of the node's name in the Scene dock.
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
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
Let's create the global boolean variable now to control our game over state.
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
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.
Player now registers collisions.
The player isn't removed when it runs into Incomers. Let's set up the area_entered
function in the player script.
Player.gd func area_entered(otherArea):
go.destroy(self)
The player ship is removed when it collides with an incomer, but you may have noticed that the player ship and projectile are removed whenever you spawn a projectile. We'll fix that next.
Fixing collision issues by checking names
It is very common in game development that we have to differentiate what happens between different colliding objects. For example, in a game with checkpoints, we don't want an enemy object to trigger a checkpoint or cutscene. We can handle this issue by checking the names of the areas involved in the collision.
All this time we have been writing otherArea
between the parentheses in the area_entered
function definitions, but we haven't used that value yet. The value between the parentheses in a function definition is called a otherArea
parameter, it is used to pass to the function which area collided with the area associated with the script.
In the case of the player script, the otherArea
parameter will be an incomer's Area2D node if the player ship collided with an incomer, and otherArea
will be a projectile's Area2D node if the player ship collided with a projectile. The problem is that both the incomer's and the projectile's Area2D nodes have the name "Area2D". Let's rename those Area2D nodes so that we can tell them apart in the code.
Let's do the same for the projectile area.
Now we can go back to the player script and only remove the player ship if it collided with an incomer by checking the otherArea
node's name. We can do this by checking its name
property using the
operator. ==
, unlike =
, is a
Player.gd func area_entered(otherArea):
if otherArea.name == "IncomerArea":
go.destroy(self)
The player ship is no longer removed when colliding with the projectile, but the projectile is still removed. This is because the projectile's script still has it remove the projectile whenever it collides with any other Area2D node. Let's perform the same treatment on the projectile's script as we did for the player's script.
Practice on your own
Everything looks in order now. We can continue on to make the game over sprite appear when the player ship is removed.
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.
isGameOver
boolean, within the area_entered
function, type the code shown in bold:Player.gd if otherArea.name == "IncomerArea":
go.destroy(self)
print("Game Over")
global.isGameOver = true
"Game Over" printed to the output log when the player ship collides with an incomer, but the game over Sprite does not appear. We can use the modified global game over state to communicate to our game over Sprite that it is time to become visible.
At this point, we could either create a script for the GameOverSprite node, or we can have the Main node's script handle it. Either way is valid. We'll flip a coin and then choose to handle it in the Main node's script.
_process
function as shown in bold,Main.gd func _process(delta):
$ScoreLabel.text = "Score: " + str(global.score)
if global.isGameOver == true:
$"GameOverSprite".visible = true
The game over Sprite should now appear when the player is removed.
Restarting game with player input
Once the
Let's first set up the custom input action, as we did with allowing the player to fire projectiles.
Practice on your own
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.
We can restart the game with the go.restart_scene
function.
_process
function, add the code shown in bold:Main.gd $"ScoreLabel".text = "Score: " + str(global.score)
if global.isGameOver == true:
$"GameOverSprite".visible = true
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, sinced we actually want multiple conditions to be true, we can add another condition by using the and
and the condition on the right of and
must both evaluate to true
in order for the entire expression to evaluate to true
.
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:Main.gd if Input.is_action_just_pressed("restart_game") and global.isGameOver :
go.restart_scene()
When checking if a boolean is true, you can omit the ==
operator (as we did with global.isGameOver
just now) since the boolean already evaluates to true, and thus satisfies the condition. If it were still false, it would fail the condition.
We can now restart our game in the game over state.
Instead of using a logical operator like and
, you can use a 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.
Resetting global values
We are now able to restart our game, but you may have noticed that the score stays as it was before restarting the game, and the game over Sprite remains visible. It's not quite a full restart until we reset those thing back to the state they were in when the game first launched.
When we reset the scene, only the scene goes back to its original state. Values inside of an autoload script remain the same, since they are in a more global state. This can come in handy when you want the player character to remember how much health it had going between zones, or perhaps a High Score variable. In our case, we do want to reset score back to 0 and the game over state back to false. We can do that by simply changing those values in the Main script, right after calling the go.restart_scene
function.
Main.gd if Input.is_action_just_pressed("restart_game") and global.isGameOver:
go.restart_scene()
global.score = 0
global.isGameOver = false
The game now restarts as expected. All the issues are now fixed, so we can start looking at what else we might be able to add to make the game more interesting.
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, Godot possesses no "Crop" node,or 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 position
for examplebelongs to every node that derives from Node2D, but each such node can have a different value for the position
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.
health
property which belong to our Incomer nodes, add the code as shown in bold:Incomer.gd 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.
health
property by 1, upon collision, within the area_entered
function, add the following code in bold:Incomer.gd func area_entered(otherArea):
self.health -= 1
go.destroy(self)
global.score += 10
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.
health
property has reached 0, add and modify the code shown below in bold:Incomer.gd func area_entered(otherArea):
self.health -= 1
if self.health < 1:
go.destroy(self)
global.score += 10
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.
Incomer nodes now are removed after 3 collisions each.
This has been just the barebones of what you need to be able to get started making a highly complex game.
Author: Chabane Maidi