10.adding-the-finish-area

Winning or losing the race

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 area

The finish line is a place the player needs to touch to win. Once again, we can use an Area2D node.

Add an Area2D node as a child of the Course node (not the ObstacleCourse) and call it FinishLine. Add a Sprite and a CollisionShape2D as its children.

Select the Sprite, right-click on its Texture slot, and select Quick Load to load 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 Sprite using the select tool , 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.

Drawing the finish line

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 Sprite 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.

Making the finish line detect the player

Now we got the line looking fine, we need to add a fitting collision shape. Select the CollisionShape2D node and give it a RectangleShape2D.

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.

Coding 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:
    #...
    finish_area.connect("body_entered", self, "_on_Finish_body_entered")

func finish_game() -> void:
    set_process(false)
    godot.set_physics_process(false)

# 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:
    #...
    timer.connect("timeout", self, "finish_game")

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.

Displaying the win or lose message

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:
    #...
    info_label.rect_scale = Vector2.ONE
    info_label.get_font("font").size = 128
    # We ensure that the label is visible.
    info_label.show()

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:
    #...
    info_label.text = "You lost!"
    # 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
        info_label.text = "You won!\nTime: " + get_time_as_text(player_time)

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 String 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:
    #...
    timer.stop()

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.

Practice: End game

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.

The code

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:
    godot.set_physics_process(false)
    ui_remaining_time.text = get_time_as_text(timer.wait_time)
    # This turns off calls to _process() every frame for this node.
    set_process(false)

    finish_area.connect("body_entered", self, "_on_Finish_body_entered")
    timer.connect("timeout", self, "finish_game")


func _process(delta: float) -> void:
    # The Timer.time_left variable tells us the timer's remaining time.
    ui_remaining_time.text = get_time_as_text(timer.time_left)


func start() -> void:
    godot.set_physics_process(true)
    timer.start()
    # 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)
    godot.set_physics_process(false)
    info_label.rect_scale = Vector2.ONE
    info_label.get_font("font").size = 128
    # We ensure that the label is visible.
    info_label.show()
    info_label.text = "You lost!"
    # 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
        info_label.text = "You won!\nTime: " + get_time_as_text(player_time)
    timer.stop()


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()