Lesson 02

Platformer Movement

Switch to a side view. Gravity pulls down constantly, and jumping only fights it for a moment.

Loading the game engine. This takes a moment the first time.

Platformer only

This is the first side-view lesson. Gravity is always pulling the player down. Jumping fights it for a moment, and then gravity wins again.

The floor, the two side walls and all three platforms are the same scene, block.tscn, dropped in six times. Build the piece once, use it everywhere. A block is two shapes cut from the same four corners: a Polygon2D you can see, and a CollisionPolygon2D you land on. Stretching a copy with Scale stretches both of them at once, so they can never come apart.

The gold platform in the middle is that same block again, with two settings changed on that one copy: a different colour, and One Way Collision ticked on its shape. Jump at it from underneath and you pass straight through, then land on top of it on the way down. That is how nearly every platformer lets you climb without trapping you under the floor above.

ONE WAY COLLISION

  1. In the Scene dock, click the CollisionPolygon2D INSIDE Platform2. Not Platform2 itself: the setting lives on the shape, not on the body.
  2. Untick "One Way Collision" in the Inspector on the right.
  3. Press F6 and try to jump up through the gold platform. It stops you now, exactly like the two grey ones. Tick it back on.

New code in this lesson

None of the earlier lessons use these. Each one shows the line where it appears here.

class_name

Gives a script a name the whole game knows, so another script can check if body is Player.

player.gd
class_name Player

delta

The seconds since the last frame, about 1/60. Multiply by it so things move at the same speed on a fast or slow computer.

player.gd
func _physics_process(delta: float) -> void:

not

Flips a condition: true becomes false and false becomes true.

player.gd
if not is_on_floor():

is_on_floor()

True when the body is standing on something. Checked after move_and_slide().

player.gd
if not is_on_floor():

+=

Adds to what a variable already holds, instead of replacing it.

player.gd
velocity.y += gravity * delta

and

Joins two conditions. Both must be true.

player.gd
if Input.is_action_just_pressed("jump") and is_on_floor():

Input.is_action_just_pressed()

True only on the one frame the key goes down. Holding it does nothing more.

player.gd
if Input.is_action_just_pressed("jump") and is_on_floor():

Input.get_axis()

Reads two opposite keys as one number: -1, 0 or 1. Enough for movement along one line.

player.gd
var direction: float = Input.get_axis("move_left", "move_right")

_ready()

Godot runs this once, as soon as the node is in the scene. Setting things up goes here.

goal.gd
func _ready() -> void:

.connect()

Hooks a signal up to a function, so the function runs every time the signal is sent. Usually done once, in _ready().

goal.gd
body_entered.connect(_on_body_entered)

body_entered

The signal an Area2D sends when a body moves into it. The body comes along, so you can check what it was.

goal.gd
body_entered.connect(_on_body_entered)

is

Checks what kind of thing a value is, like body is CharacterBody2D.

goal.gd
if body is Player:

.emit()

Sends a signal out, which runs every function connected to it.

goal.gd
GameEvents.goal_reached.emit()

GameEvents

A script Godot loads before any scene (an autoload), so every script can send and hear its signals without finding each other.

goal.gd
GameEvents.goal_reached.emit()

%Name

Grabs the node with that name anywhere in this scene. The node needs "Access as Unique Name" ticked.

hud.gd
@onready var message_label: Label = %MessageLabel

pressed

The signal a Button sends when it is clicked.

hud.gd
home_button.pressed.connect(_on_home_pressed)

get_tree()

The scene tree: the thing running the whole game. Ask it to change, reload or pause the scene.

hud.gd
get_tree().change_scene_to_file("res://shared/ui/lesson_select.tscn")

change_scene_to_file()

Swaps the whole game over to a different scene file.

hud.gd
get_tree().change_scene_to_file("res://shared/ui/lesson_select.tscn")

.text

The words a Label or Button shows. Set it to change them on screen.

hud.gd
message_label.text = "You reached the top!"

signal

Makes a signal: a message a node can send out. Any script can connect a function to it.

game_events.gd
signal goal_reached

The scenes

■ UI node · ■ 2D node · % unique name · .tscn instanced scene

game.tscn
  • Game Node2D
  • Player CharacterBody2D player.tscn
  • HUD CanvasLayer % hud.tscn
  • Floor StaticBody2D block.tscn
  • LeftWall StaticBody2D block.tscn
  • RightWall StaticBody2D block.tscn
  • Platform1 StaticBody2D block.tscn
  • Platform2 StaticBody2D block.tscn
  • Platform3 StaticBody2D block.tscn
  • Goal Area2D goal.tscn
goal.tscn
  • Goal Area2D goal.gd
  • Polygon2D
  • CollisionShape2D
hud.tscn
  • HUD CanvasLayer hud.gd
  • MarginContainer
  • MessageLabel Label %
player.tscn
  • Player CharacterBody2D player.gd
  • Sprite2D
  • CollisionShape2D
block.tscn
  • Block StaticBody2D
  • Polygon2D
  • CollisionPolygon2D

FileSystem

  • game.tscn
  • goal.tscn
  • goal.gd
  • hud.tscn
  • hud.gd
  • player.tscn
  • player.gd
  • block.tscn

game_events.gd

Switch it on once, under Project > Project Settings, on the Globals tab. Not made yet? Press Create Script to make it right there. Already made it? Press Select Script/Scene, pick it, and press Add. Its Name column should say GameEvents, which Godot fills in for you. That's the name your code uses.

The code

class_name Player
extends CharacterBody2D

## How fast the player runs left and right.
@export var speed: float = 200.0
## How hard the player jumps. This number is NEGATIVE on purpose. In Godot
## the top of the screen is 0 and the numbers grow going down, so up is
## the negative direction.
@export var jump_velocity: float = -650.0
## How hard gravity pulls down. A bigger number means you fall faster.
@export var gravity: float = 1800.0

@onready var sprite: Sprite2D = $Sprite2D

# This one function does four jobs. Here they are, one at a time.
func _physics_process(delta: float) -> void:
	# STEP 1: FALL. If our feet are not on the ground, gravity pulls us
	# down. Notice it is "+=" and not "=": we add a little more speed every
	# frame, so the longer you fall the faster you drop.
	if not is_on_floor():
		velocity.y += gravity * delta

	# STEP 2: JUMP. "is_action_just_pressed" is true only on the one frame
	# the key goes down. (If we used "is_action_pressed" it would be true
	# the whole time you hold the key, and you would jump over and over.)
	# The "is_on_floor()" check is what stops you jumping in mid-air.
	if Input.is_action_just_pressed("jump") and is_on_floor():
		velocity.y = jump_velocity

	# STEP 3: RUN. "get_axis()" hands back a single number:
	#   -1 for left, 1 for right, and 0 for neither.
	var direction: float = Input.get_axis("move_left", "move_right")
	velocity.x = direction * speed

	# STEP 4: MOVE. Steps 1 to 3 only DECIDED where to go.
	# "move_and_slide()" is the line that actually moves us.
	move_and_slide()

	# The warrior picture only faces right. Walking left mirrors it.
	if direction != 0.0:
		sprite.flip_h = direction < 0.0