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.
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.
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.
When opening Godot, we are introduced with the Godot Project Manager. Let's do this now.
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.
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.
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.
Copy the Godot Shmup file path
>We will now supply this copied path to the Godot Project Manager.
Paste the copied file path.
We are now ready to create the project.
Click Create & Edit
The Godot editor should now open with our 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.
Copy the graphics folder
This copied graphics should now be placed into the project folder.
Paste in the copied graphics directory
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 FileSystem panel on the left of the Godot editor interface.
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.
Click Project Settings
The Project Settings dialog appears. As the name suggests, this is where project settings can be modified, including the viewport dimensions.
Click Window
We can modify the viewport size here to be 480 by 800.
Next, the height field.
Type 800
We're done with the project settings for now.
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.
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.
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.
Click
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.
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.
Double-click Node2D, type Main
Now that our scene has a root node, we can save the scene and not lose our progress.
Press Ctrl+s
>Let's keep the suggested name, Main.tscn. The file extension for scenes in Godot are tscn.
The file should now appear in the root directory of our project folder, which you can see in the FileSystem panel.
Our background is really plain right now. Let's add a background graphic with a sprite node.
Create a new node of type Sprite
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 Texture property is set.
Click <null>>, click Load
The graphic we wanted is within the graphics folder we copied into the project folder earlier.
Double-click bgGradient.png
We now see the graphic in the editing pane.
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.
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.
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.
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.
Click
We can no longer select the background sprite from the editing pane.
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.
Click
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.
Click Select
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.
Let's proceed to make our game more interesting.
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.
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.
Rename the new Node2D to Player
Create a Sprite node child of Player
We need to give it a Texture now in order to see it.
Load into the Texture property the player.png file.
You should now see the player graphic.
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.
Click
You should now see the same button icon appear next to the Player node in the Scene panel.
Let's move the Player node to the bottom middle of the viewport.
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.
Type 270
You can also rotate a node in the editor, by holding CTRL while dragging a control handle.
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.
Click
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.
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.
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.
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
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.
Type print("happens every frame")
You should see "happens every frame" being printed every frame in the Output log at the bottom of the interface.
Comment out the print line
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().
Type self.position = get_global_mouse_position()
We see the Player node following the mouse position. We want it to only follow the mouse's x coordinate.
Add the code in bold self.position.x = get_global_mouse_position().x
The Player node now follows the mouse x position.
Create a Node2D node
Create a Sprite node
Load in projectile.png as the Texture property
Create a new script with default settings on the Projectile node
Uncomment the _process function
func _process(delta):
self.position.y = self.position.y - 5
func _process(delta):
self.position.y -= 5
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.
Right-click the Projectile node
Click Save Branch as Scene
We are now prompted to assign a file name for our new tscn file.
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.
Click
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.
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.
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.
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.
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.
We can access the AssetLib from the Godot website or even within the Godot editor directly. Let's go with the latter option.
Click AssetLib
We see a listing of pages of user-contributed assets. We are looking for an asset package called GoGodot.
Type gogodot, and click Search
We should see the GoGodot library in the list. The one we want is contributed by "oranjoose".
Click GoGodot
A pop-up with a description of the asset appears.
Click Install
The files have been downloaded to a temporary directory, but the asset package hasn't been downloaded into our shmup project yet.
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.
Click Install
The new files now show up in the FileSystem panel.
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.
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.
We can create custom actions in the Input Map.
Open the Project Settings
Click the tab labeled Input Map
Let's make a custom action for firing a projectile.
Type fire_projectile, and click Add
We have our custom input action now, but no actual physical input has been associated with it.
Click
Click Mouse Button
Click Add
Click Close
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.
self.position.x = get_global_mouse_position().x
if Input.is_action_just_pressed("fire_projectile"):
print("projectile fired!")
We are going to use a function within the GoGodot library to spawn instances of the Projectile.tscn scene.
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.
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.
Make sure to keep child nodes of Incomer from being selectable, as we did with Projectile.
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.
If all went well, the Incomer should appear to travel down the screen on its own.
Incomers, unlike projectiles, are to be spawned automatically on a 1 second interval. We can do this with a Timer node.
Create 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.
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.
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.
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.
Attach a new script to the IncomerSpawnTimer, choosing default settings
Let's now connect the signal to a function in our new script.
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.
Click IncomerSpawnTimer
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.
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.
func _on_IncomerSpawnTimer_timeout():
go.spawn_instance("Incomer", rand_range(0, 480), -50)
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.
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.
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.
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.
Choose New CircleShape2D for the Shape property
Now we can specify the exact dimensions of the circular collision shape.
Zoom in on the graphic in the editing pane
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.
We are going to set up collisions for Incomer the same we did for Projectile.
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.
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.
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.
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.
func go_collision(otherArea):
go.destroy(self)
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.
func go_collision(otherArea):
go.destroy(self)
go.destroy(otherArea)
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.
func go_collision(otherArea):
go.destroy(self)
go.destroy(otherArea.get_parent())
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.
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"
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.
Click the tab labeled AutoLoad
Click the button whose label is ..
Click Open
Click Add
Our global.gd script should now be accessible from anywhere by using the object called global
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
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 go_collision(otherArea):
go.destroy(self)
go.destroy(otherArea.get_parent())
global.score += 10
print(global.score)
We see in the output log that the score is increasing by 10 for each collision.
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.
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.
Create a child node of type Label
This new Label node appears in the top-left corner of our viewport, but it currently has no text.
Type Score: 0 for the Text property,
Next, let's make it a bit bigger.
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.
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.
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"
func _process(delta): $"ScoreLabel".text = "Score: 1000"
This makes the label display "Score: 1000" each frame, but that doesn't solve our problem.
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.
func _process(delta):
$"ScoreLabel".text = "Score: " + str(global.score)
The score now updates properly to display the player's current score.
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.
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.
Create a child node of type Sprite
Load gameOver.png as the value for GameOverSprite's Texture property
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.
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.
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.
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.
Let's finally make it so that the player can detect collisions by adding the necessary nodes.
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.
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.
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.
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.
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.
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.
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.
$"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.
if Input.is_action_just_pressed("restart_game") and global.isGameOver:
go.restart_scene()
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.
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.
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.
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.
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.
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.
Incomer nodes now are removed after 3 collisions each.
Author: Chabane Maidi