CS 470 Game Development

Week 2, Lecture 5

Micro-Game 2: Ricochet

Autonomous Movement & Bouncing Physics

Today's Goal

Build a logo that bounces off screen edges

Click to spawn more!

Along the way: Vector2, speed & direction, _ready(), mouse input, preload/instantiate

Agenda (~50 minutes)

  1. Quick Recap: Wanderer Foundations (4 min)
  2. Vector2 Deep Dive (7 min)
  3. Speed, Direction & Autonomous Movement (6 min)
  4. _ready() vs _process() (4 min)
  5. Building Ricochet — Live Demo (14 min)
  6. Mouse Input & Spawning (8 min)
  7. Bonus: Color Modulation (3 min)
  8. Summary + Exercises (4 min)

1. Quick Recap: Wanderer

What we built last time

What We Learned

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

What's Different Today?

Wanderer

  • YOU control it (keyboard)
  • Stays on screen (clamp)
  • Direction from input

Ricochet

  • It moves ITSELF (speed + direction)
  • Bounces off edges
  • Direction from _ready()

Wanderer = player-driven. Ricochet = autonomous.

This is how the Pong ball will work!

2. Vector2 Deep Dive

Two numbers, infinite power

What is 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

vector2-diagram.png
Visual showing Vector2 used as position (dot on grid) and direction (arrow)

Vector2 Operations

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 and Normalization

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

normalize-diagram.png
Arrow before normalization (length 5), arrow after normalization (length 1), then scaled by SPEED at movement time

Random Direction Generation

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!

3. Speed, Direction & Autonomous Movement

Keep them separate, combine at movement time

Speed + Direction (Separate Variables)

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

Autonomous Movement

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

The Bounce

bounce-diagram.png
Logo approaching right wall with direction arrow, then after bounce with flipped x-component. Only x-component flips.
# 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.

4. _ready() vs _process()

When your code runs matters

Two Lifecycle Functions

Function When How Often Ricochet Use
_ready() Node enters scene Once Set random direction
_process(delta) Every frame ~60/sec Move + bounce
ready-vs-process-timeline.png
Timeline showing _ready() firing once at start, then _process() firing repeatedly every frame (60 times per second)

Why _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()
  • Each logo gets a different random direction
  • If we set direction at declaration, ALL logos go the same way
  • _ready() runs once per instance — each spawned logo gets its own call

5. Building Ricochet

Step-by-step live demo

Step 1: Create the Player Scene

  1. New Project: Ricochet (or new scene in existing project)
  2. Create Node2D root, rename to Player
  3. Add child Sprite2D, drag icon.svg to Texture
  4. Save as prefabs/player.tscn
Player (Node2D)     ← script goes here
  └── Sprite2D      ← displays the image

Simpler than Wanderer — the Player IS the moving object, no separate container needed

Step 2: The Player Script

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

Code Walkthrough: Variables & _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.

Code Walkthrough: Movement & Bounce

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

Step 3: Create the Game Scene

  1. New scene: Node2D root, rename to Game
  2. Drag prefabs/player.tscn into the scene
  3. Position the Player at (400, 300)
  4. Save as game.tscn
Game (Node2D)                   ← will get spawning script later
  └── Player (player.tscn)     ← instance, position (400, 300)

Step 4: Run It!

  1. Press F5, select game.tscn as main scene
  2. The logo bounces endlessly!
ricochet-single-bounce.mp4
Single Godot icon bouncing off all four walls continuously, changing direction on each wall hit

You just built ball physics.

This is exactly how the Pong ball will move!

6. Mouse Input & Spawning

Click to spawn: a new input model

Two Ways to Handle Input

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 InputEventMouseButton

func _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!

The Game Script

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)
ricochet-chaos.mp4
Multiple logos spawned via mouse clicks, all bouncing independently around the screen — beautiful chaos!

Click everywhere. Each logo bounces with its own direction!

7. Bonus: Color on Bounce

A quick visual flourish

Modulate on Bounce

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 color
  • Color(randf(), randf(), randf()) — random RGB color on each bounce
ricochet-color.mp4
Multiple logos bouncing around, changing to random colors each time they hit a wall — colorful chaos

Summary: What You Learned

Core Concepts (1/2)

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

Core Concepts (2/2)

_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 vs Ricochet

Wanderer

  • Player-driven
  • Keyboard input
  • Clamp at boundaries
  • Direction from input

Ricochet

  • Autonomous
  • SPEED + direction
  • Bounce at boundaries
  • Direction from _ready()

Two different game patterns from the same building blocks

Exercises

Try these on your own!

Beginner Exercises

  1. Speed Experiment: Change SPEED to 500, then 100. What feels right for a bouncing logo?
  2. Gravity Pull: Add a small constant to direction.y each frame: direction.y += 0.5 * delta. What happens?
  3. Spawn Limit: Only allow 10 logos at once. Hint: check get_child_count() before adding a new one.

Intermediate Exercises

  1. Speed Increase: Each bounce makes the logo 10% faster. Hint: SPEED *= 1.1 inside the bounce block.
  2. Trail Effect: Every 0.1 seconds, spawn a fading copy at the logo's position. Hint: accumulate delta and use modulate.a for transparency.
  3. Screen Wrap: Instead of bouncing, make the logo wrap around to the opposite edge. Hint: use modulo or an if-else.

Advanced Exercise

  1. Logo Collision: Detect when two logos overlap and make them bounce off each other.
    • This needs Area2D — next lecture's topic!
    • Try planning the approach even if you can't implement it yet
    • What would you need? A collision shape? A signal?

Next Lecture: Fruit Frenzy

We'll add:

  • Area2D — detecting overlaps between objects
  • CollisionShape2D — defining hitboxes
  • Signals — Godot's event system
  • queue_free() — removing objects from the game

Ricochet spawns objects. Fruit Frenzy teaches you to destroy them!

Questions?

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]