The first lesson where the world is bigger than the screen. Limits, smoothing and a deadzone, each one switchable so you can feel what it does.
Loading the game engine. This takes a moment the first time.
Every lesson before this one fit on one screen, so the view never had to move. This world is 2400 x 1600. The screen is 1152 x 648. Most of the level is off-screen at any moment, and a camera is what decides which part you are looking at.
Open player.tscn. The Camera2D is a CHILD of the player. A child moves when its parent moves, so the camera follows with no code at all. That is the whole trick. Everything below is just polish.
limit_left / limit_top / limit_right / limit_bottom. Without them the camera walks straight off the edge of the world and you spend half the game looking at grey nothing. With them the view stops at the wall, like a real game.position_smoothing_enabled. Off, the camera is welded to the player and the whole world jitters when you move. On, the camera chases the player and eases into place.drag_horizontal_enabled / drag_vertical_enabled. This makes a box in the middle of the screen where you can move WITHOUT the camera moving at all. Only when you push past the edge of that box does the view start to travel. Almost every 2D game you have played does this.■ UI node · ■ 2D node · % unique name · .tscn instanced scene
extends Node2D
# How big the world is. The camera limits below are built from these, so
# changing the numbers here moves the walls AND the camera edges together.
const WORLD_WIDTH: int = 2400
const WORLD_HEIGHT: int = 1600
@onready var player: CharacterBody2D = %CameraPlayer
func _ready() -> void:
# The HUD owns the three buttons. It emits a plain signal saying which
# setting was flipped, and this file does the flipping. The HUD never
# touches the camera itself.
%HUD.setting_toggled.connect(_on_setting_toggled)
_apply_limits(true)
func _on_setting_toggled(setting: String, enabled: bool) -> void:
var camera: Camera2D = player.camera
match setting:
"limits":
_apply_limits(enabled)
"smoothing":
camera.position_smoothing_enabled = enabled
"drag":
camera.drag_horizontal_enabled = enabled
camera.drag_vertical_enabled = enabled
# Turning limits "off" does not delete them. Godot always has limit numbers.
# We push them far outside the world instead, which is the same as having none.
func _apply_limits(enabled: bool) -> void:
var camera: Camera2D = player.camera
if enabled:
camera.limit_left = 0
camera.limit_top = 0
camera.limit_right = WORLD_WIDTH
camera.limit_bottom = WORLD_HEIGHT
else:
camera.limit_left = -10000000
camera.limit_top = -10000000
camera.limit_right = 10000000
camera.limit_bottom = 10000000extends CanvasLayer
# Our own signal. "setting" is which button, "enabled" is its new state.
signal setting_toggled(setting: String, enabled: bool)
@onready var limits_button: Button = %LimitsButton
@onready var smoothing_button: Button = %SmoothingButton
@onready var drag_button: Button = %DragButton
# Each button starts in the state the camera starts in. See player.tscn.
var _state := {"limits": true, "smoothing": true, "drag": true}
func _ready() -> void:
# "bind" glues an extra argument onto the function. Every button runs the
# same "_on_toggled", and bind is what tells it WHICH button called.
limits_button.pressed.connect(_on_toggled.bind("limits"))
smoothing_button.pressed.connect(_on_toggled.bind("smoothing"))
drag_button.pressed.connect(_on_toggled.bind("drag"))
%HomeButton.pressed.connect(_on_home_pressed)
_refresh()
func _on_toggled(setting: String) -> void:
# "not" flips true to false and false to true.
_state[setting] = not _state[setting]
setting_toggled.emit(setting, _state[setting])
_refresh()
# Rewrites all three labels so they always match what the camera is doing.
func _refresh() -> void:
limits_button.text = "Limits: %s" % _word(_state["limits"])
smoothing_button.text = "Smoothing: %s" % _word(_state["smoothing"])
drag_button.text = "Deadzone: %s" % _word(_state["drag"])
func _word(on: bool) -> String:
return "ON" if on else "OFF"
# Loads the lesson list screen, which replaces this whole scene.
func _on_home_pressed() -> void:
get_tree().change_scene_to_file("res://shared/ui/lesson_select.tscn")extends CharacterBody2D
## How fast the player moves.
@export var speed: float = 260.0
# "%Camera" is the Camera2D child. We only grab it so the level script can
# switch its settings on and off while you watch.
@onready var camera: Camera2D = %Camera
func _physics_process(_delta: float) -> void:
var direction: Vector2 = Input.get_vector(
"move_left", "move_right", "move_up", "move_down"
)
velocity = direction * speed
move_and_slide()
# The warrior picture only faces right. Walking left mirrors it.
if direction.x != 0.0:
$Sprite2D.flip_h = direction.x < 0.0