Autonomous Movement & Bouncing Physics
Build a logo that bounces off screen edges
Click to spawn more!
Along the way: Vector2, speed & direction, _ready(), mouse input, preload/instantiate
_ready() vs _process() (4 min)What we built last time
| Nodes & Scenes | Game objects in a tree, saved as .tscn |
_process(delta) |
Runs every frame, delta = time since last frame |
| Input Polling | Input.is_action_pressed() — non-blocking |
| Clamping | clamp() keeps objects inside the screen |
clamp)_ready()Wanderer = player-driven. Ricochet = autonomous.
This is how the Pong ball will work!
Two numbers, infinite power
Vector2?A pair of numbers (x, y) bundled together
var pos = Vector2(400, 300) # a position
var dir = Vector2(1, 0) # a direction (right)
var spd = Vector2(200, 150) # speed in each axis
Same type, different meanings — context matters
| Operation | Example | Used For |
|---|---|---|
| Addition | (2, 3) + (1, -1) = (3, 2) |
Combining movements |
| Subtraction | (5, 3) - (2, 1) = (3, 2) |
Direction between points |
| Scalar multiply | (1, 0) * 200 = (200, 0) |
Scaling (speed) |
| Negation | -(3, 4) = (-3, -4) |
Reverse direction |
Every one of these operations appears in Ricochet
Length: |(3, 4)| = 5
Normalized: (3, 4).normalized() = (0.6, 0.8)
Normalized = same direction, length 1
Then multiply by speed to get movement per second
A 2-step pattern we'll use in Ricochet:
# Step 1: Random x and y between -1 and 1
var random_vec = Vector2(randf_range(-1, 1), randf_range(-1, 1))
# Step 2: Normalize to length 1
var direction = random_vec.normalized()
Speed (SPEED) is a separate constant — applied during movement
This is exactly what Ricochet's _ready() does!
Keep them separate, combine at movement time
SPEED
How fast (a number)
e.g., 300 pixels/sec
direction
Which way (a normalized Vector2)
e.g., (0.6, 0.8)
var SPEED = 300 # pixels per second
var direction = Vector2(0, 0) # set in _ready()
Movement = direction * SPEED * delta
In Wanderer, direction came from keyboard input
In Ricochet, direction is stored in a variable, set once in _ready()
# Every frame: move by direction * SPEED * delta
position += direction * SPEED * delta
No Input.is_action_pressed()
The object moves itself
direction * SPEED * delta = frame-rate independent, just like Wanderer
# Hitting right or left wall:
direction.x = -direction.x # reverse horizontal
# Hitting top or bottom wall:
direction.y = -direction.y # reverse vertical
Negate the direction component perpendicular to the wall. That's ALL bounce physics is.
_ready() vs _process()When your code runs matters
| Function | When | How Often | Ricochet Use |
|---|---|---|---|
_ready() |
Node enters scene | Once | Set random direction |
_process(delta) |
Every frame | ~60/sec | Move + bounce |
_ready() for Direction?var SPEED = 300
var direction = Vector2(0, 0) # overwritten in _ready()
func _ready():
direction = Vector2(randf_range(-1, 1), randf_range(-1, 1)).normalized()
_ready() runs once per instance — each spawned logo gets its own callStep-by-step live demo
Ricochet (or new scene in existing project)Playericon.svg to Textureprefabs/player.tscnPlayer (Node2D) ← script goes here
└── Sprite2D ← displays the image
Simpler than Wanderer — the Player IS the moving object, no separate container needed
Select Player → Attach Script → Create
extends Node2D
var SPEED = 300 # pixels per second
var direction = Vector2(0, 0) # random direction to begin with
func _ready():
direction = Vector2(randf_range(-1, 1), randf_range(-1, 1)).normalized()
func _process(delta):
var screen_size = get_viewport_rect().size
var sprite_width = 128
var sprite_height = 128
position += direction * SPEED * delta
# Bounce off walls
var bounced = false
if position.x > screen_size.x - sprite_width/2 or position.x < sprite_width/2:
direction.x = -direction.x
bounced = true
if position.y > screen_size.y - sprite_height/2 or position.y < sprite_height/2:
bounced = true
if bounced:
$Sprite2D.modulate = Color(randf(), randf(), randf())
direction.y = -direction.y
_ready()var SPEED = 300 # pixels per second
var direction = Vector2(0, 0) # random direction to begin with
SPEED is a constant (uppercase). direction is a Vector2 — set in _ready()
func _ready():
direction = Vector2(randf_range(-1, 1), randf_range(-1, 1)).normalized()
Creates a random unit direction — normalized to length 1
Remember the 2-step pattern: random → normalize. Speed is applied at movement time.
position += direction * SPEED * delta
Move by direction * SPEED each frame — autonomous, no input needed
if position.x > screen_size.x - sprite_width/2 or position.x < sprite_width/2:
direction.x = -direction.x
Past left or right edge? Flip horizontal direction
if position.y > screen_size.y - sprite_height/2 or position.y < sprite_height/2:
direction.y = -direction.y
Past top or bottom edge? Flip vertical direction
Compare to Wanderer's clamp — clamp stops the object, bounce reverses it
Gameprefabs/player.tscn into the scene(400, 300)game.tscnGame (Node2D) ← will get spawning script later
└── Player (player.tscn) ← instance, position (400, 300)
game.tscn as main sceneYou just built ball physics.
This is exactly how the Pong ball will move!
Click to spawn: a new input model
Polling (Wanderer)
func _process(delta):
if Input.is_action_pressed("ui_right"):
# held this frame
Check every frame: is key held?
Good for: continuous movement
Events (Ricochet)
func _input(event):
if event is InputEventMouseButton:
# just happened
Called WHEN something happens
Good for: single clicks, key taps
Use polling for held keys, events for one-shot actions
_input() and InputEventMouseButtonfunc _input(event):
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
# Mouse was left-clicked!
print("Clicked at: ", event.position)
event is InputEventMouseButton — Is this a mouse event?event.button_index == MOUSE_BUTTON_LEFT — Left button?event.pressed — A press (not release)?event.position — Where on screen? (a Vector2)preload() and instantiate()var logo_scene = preload("res://prefabs/player.tscn")
preload() loads the scene file once, at script load time
var new_logo = logo_scene.instantiate()
instantiate() creates a new node tree from the scene
new_logo.position = event.position
add_child(new_logo)
add_child() puts it in the scene tree — it starts living!
Each instantiated logo calls its OWN _ready() — getting its own random direction!
Attach this script to the Game node:
extends Node2D
var logo_scene = preload("res://prefabs/player.tscn")
func _ready():
pass
func _process(delta):
pass
func _input(event):
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
var new_logo = logo_scene.instantiate()
print(event.position)
new_logo.position = event.position
add_child(new_logo)
Click everywhere. Each logo bounces with its own direction!
A quick visual flourish
We use a bounced flag to change color on any wall hit:
var bounced = false
if position.x > screen_size.x - sprite_width/2 or position.x < sprite_width/2:
direction.x = -direction.x
bounced = true
if position.y > screen_size.y - sprite_height/2 or position.y < sprite_height/2:
bounced = true
if bounced:
$Sprite2D.modulate = Color(randf(), randf(), randf())
direction.y = -direction.y
$Sprite2D — shorthand for get_node("Sprite2D") — accesses child node.modulate — tints the sprite's colorColor(randf(), randf(), randf()) — random RGB color on each bounce| Vector2 | Two numbers (x, y) stored together |
| SPEED + direction | Separate speed (number) and direction (Vector2) |
| Autonomous movement | Object moves itself — no keyboard input |
| Boundary checking | Compare position to screen edges |
| Bouncing | Reverse direction component on wall hit |
_ready() |
Runs once when node enters the scene |
_input(event) |
Called on input events (mouse, key taps) |
preload() |
Load a scene file at compile time |
instantiate() |
Create a new copy of a scene |
add_child() |
Add a node to the scene tree at runtime |
$NodeName |
Shorthand for get_node("NodeName") |
11 new concepts — each one builds on the last
Wanderer
Ricochet
_ready()Two different game patterns from the same building blocks
Try these on your own!
SPEED to 500, then 100. What feels right for a bouncing logo?direction.y each frame: direction.y += 0.5 * delta. What happens?get_child_count() before adding a new one.SPEED *= 1.1 inside the bounce block.modulate.a for transparency.Area2D — next lecture's topic!We'll add:
Area2D — detecting overlaps between objectsCollisionShape2D — defining hitboxesqueue_free() — removing objects from the gameRicochet spawns objects. Fruit Frenzy teaches you to destroy them!
Try building Ricochet on your own
Experiment with the exercises
Challenge: make the logos bounce off each other, not just walls
Office hours: [Your time here]