In this final lesson, we’ll code the game’s win and lose condition. We’ll create a finish line the player needs to reach before the timer runs out.
We’ll also display a message when the game ends, whether the player won or lost.
Let’s start by adding the finish line.
The finish line is a place the player needs to touch to win. Once again, we can use an
node.Add an
node as a child of the Course node (not the ObstacleCourse) and call it FinishLine. Add a and a as its children.Select the finish.png
texture.
We want to place the finish line on the bottom-right island in the
level. Select the FinishLine node, and press W to
activate the move tool. Click and
drag the area over to the bottom-right of the map.
If you don’t have grid snapping on, activate it by pressing Shift+,G.
We want to stretch the line over the island to match the floor’s
width. If we were to resize the , we’d only change its Scale
property and stretch the texture, as shown below.
Instead, we want to preserve the texture’s original aspect ratio and repeat it over the sprite’s bounding box.
We can do that by modifying the sprite’s texture region.
In the Inspector, turn on Region -> Enabled.
Then, at the bottom of the editor, click the TextureRegion button to expand the texture region editor.
In this editor, you can manipulate the view like your game viewport. Press the middle mouse button to pan the view and use the mouse wheel to zoom in and out.
Also, you can left-click and drag to redefine a portion of the texture to draw using the
node. That’s your texture region. Notice how the region can be larger than the texture, and the image will repeat horizontally.To select our region precisely, in the TextureRegion editor, set the Snap Mode to Grid Snap.
Then set the grid Step to 64 x 64
pixels to
match our scene’s grid.
Finally, click and drag over the TextureRegion editor’s grid to extend the drawn region. You may need to move the sprite and resize the region a couple of times until the finish line perfectly fits the level’s floor.
Now we got the line looking fine, we need to add a fitting collision shape. Select the
node and give it a .Hold the Alt key, then click and drag on the shape’s horizontal resize handles to make the rectangle as wide as the finish line.
The shape doesn’t need to be as tall as the line. In general, areas don’t need to fit underlying sprites perfectly: it’s fine as long as the interactions feel fair in testing.
Sometimes, you’ll make collision shapes smaller than underlying sprites, like here. Other times, you’ll need them to be larger.
Before moving onto the code, we need one last change: the
FinishLine area node needs to detect the player. Select the
FinishLine node. In the Inspector, set its
Collision -> Mask to 2
. Turn off the
Collision -> Layer as well, as nothing needs to detect this
area.
We can now code the interaction between the player and the finish line.
Reopen the ObstacleCourse.gd
script, where we get the
FinishLine node and connect its body_entered
signal.
We call a new function from the signal callback function that we name
finish_game()
. In that function, we stop processing on the
ObstacleCourse and the Godot nodes to prevent things
from moving.
onready var finish_area := $Course/FinishLine
func _ready() -> void:
#...
connect("body_entered", self, "_on_Finish_body_entered")
finish_area.
func finish_game() -> void:
set_process(false)
set_physics_process(false)
godot.
# We can't directly connect the body_entered signal to the finish_game()
# function because the body_entered signal requires a callback with a `body`
# parameter.
#
# That's why we need this function.
func _on_Finish_body_entered(body: Node) -> void:
finish_game()
In the _ready()
function, we can also connect our
timer’s timeout
signal to the finish_game()
function.
func _ready() -> void:
#...
connect("timeout", self, "finish_game") timer.
That way, when the timer ends or the player touches the finish line,
we’ll run the code in finish_game()
and stop the player
from moving.
All that’s left is displaying a message when the player wins or loses using our Info label node.
Let’s head back to the ObstacleCourse script to complete the
finish_game()
function.
First, we reset our info label’s scale and lower its font size to
128
pixels to fit the final text on the screen.
onready var info_label := $CanvasLayer/Info
func finish_game() -> void:
#...
= Vector2.ONE
info_label.rect_scale get_font("font").size = 128
info_label.# We ensure that the label is visible.
show() info_label.
Then, we change the info label node’s text. By default, we set the text as if the player lost. We then check if they won, and if so, we change the text.
func finish_game() -> void:
#...
= "You lost!"
info_label.text # The variables below are there just to help you read the code. We'll
# sometimes use variables to put a name on an otherwise less explicit
# instruction.
var player_has_won: float = timer.time_left > 0.0
if player_has_won:
var player_time: float = timer.wait_time - timer.time_left
= "You won!\nTime: " + get_time_as_text(player_time) info_label.text
The last line needs some explanation. First, the text contains
strange characters: \n
.
When preceded by a backslash, some characters have a special meaning
for the computer. The characters \n
mean “line return.”
They’ll cause the text to appear on two lines.
Then, we add the output of get_time_as_text()
to the
string. Adding two values concatenates them: the second string
gets appended to the first one.
Lastly, we stop the timer. Otherwise, if the player wins the game and the timer runs out, the message will switch to the losing one.
func finish_game() -> void:
#...
stop() timer.
You should be able to play the game fully now, which brings this series to an end… almost! There’s one practice left for you.
Open the practice End game.
There, you’ll have to
This concludes the series.
With this obstacle course, you should better understand how we create and bring multiple mechanics together into a finished product.
It’s only the start: we’ll keep improving this obstacle later to flesh it out in preparation for the course’s final project.
In the next chapter, you’ll explore game AI by coding towers for a tower defense game.
Here’s the complete code for ObstacleCourse.gd
.
extends Node2D
onready var godot := $Course/Godot
onready var ui_remaining_time := $CanvasLayer/RemainingTime
onready var timer := $Timer
onready var finish_area := $Course/FinishLine
onready var info_label := $CanvasLayer/Info
func _ready() -> void:
set_physics_process(false)
godot.= get_time_as_text(timer.wait_time)
ui_remaining_time.text # This turns off calls to _process() every frame for this node.
set_process(false)
connect("body_entered", self, "_on_Finish_body_entered")
finish_area.connect("timeout", self, "finish_game")
timer.
func _process(delta: float) -> void:
# The Timer.time_left variable tells us the timer's remaining time.
= get_time_as_text(timer.time_left)
ui_remaining_time.text
func start() -> void:
set_physics_process(true)
godot.start()
timer.# Once we started the timer, we can reactivate calls to _process() to update
# the text every frame.
set_process(true)
func finish_game() -> void:
set_process(false)
set_physics_process(false)
godot.= Vector2.ONE
info_label.rect_scale get_font("font").size = 128
info_label.# We ensure that the label is visible.
show()
info_label.= "You lost!"
info_label.text # The variables below are there just to help you read the code. We'll
# sometimes use variables to put a name on an otherwise less explicit
# instruction.
var player_has_won: float = timer.time_left > 0.0
if player_has_won:
var player_time: float = timer.wait_time - timer.time_left
= "You won!\nTime: " + get_time_as_text(player_time)
info_label.text stop()
timer.
func get_time_as_text(time: float) -> String:
return str(time).pad_decimals(2).pad_zeros(2)
# We can't directly connect the body_entered signal to the finish_game()
# function because the body_entered signal requires a callback with a `body`
# parameter.
#
# That's why we need this function.
func _on_Finish_body_entered(body: Node) -> void:
finish_game()