CS 470 Game Development

Week 2, Lecture 6

Micro-Game 3: Fruit Frenzy

Collision Detection & Signals

Today's Goal

Fruits fall from the sky

Catch them with a basket. Score points!

Along the way: Area2D, CollisionShape2D, Signals, queue_free(), Timer

Agenda (~50 minutes)

  1. Quick Recap: Ricochet Concepts (4 min)
  2. New Node: Area2D (6 min)
  3. Building the Basket (8 min)
  4. Building the Fruit (7 min)
  5. Signals — Godot's Event System (8 min)
  6. Game Scene: Spawning + Score (10 min)
  7. Bonus: Fruit Variety (3 min)
  8. Summary + Exercises (4 min)

1. Quick Recap: Ricochet

What we built last time

What We Learned

ConceptWhat You Learned
VelocitySpeed + direction as a Vector2
Autonomous movementposition += velocity * delta
BouncingReverse velocity component at boundaries
preload() / instantiate()Load a scene, create copies at runtime
_input(event)Event-driven input (mouse clicks)

The Next Step

Ricochet spawns objects and moves them

Today we learn to detect when they touch

This is the missing piece for Pong — how does the ball know it hit a paddle?

2. New Node: Area2D

How do we know when two things touch?

What is Area2D?

  • A node type in Godot
  • Detects when other areas overlap with it
  • No physics — just detection
  • Think of it as an invisible sensor

Use cases:

  • Collectibles (coins, power-ups)
  • Damage zones
  • Trigger areas
  • Our basket & fruit!

CollisionShape2D

Area2D needs a shape to know its boundaries

Area2D = the sensor (detects overlaps)

CollisionShape2D = the shape (defines the hitbox)

Common shapes: RectangleShape2D, CircleShape2D, CapsuleShape2D

We'll use RectangleShape2D — it matches our sprites

Node2D vs Area2D

Node2DArea2D
Used inWanderer, RicochetFruit Frenzy
Can move?YesYes
Detects overlaps?NoYes
Needs collision shape?NoYes
Emits signals?Basic onlyarea_entered, area_exited

Rule of thumb: if it needs to detect contact with something, use Area2D

3. Part A: The Basket

The player-controlled catcher

Basket Scene Tree

Basket (Area2D) ├── Sprite2D └── CollisionShape2D

Compare to Wanderer:

Player (Node2D) └── Sprite2D

Same idea, but Area2D replaces Node2D and we add a CollisionShape2D

Step-by-Step Setup

  1. Create new scene → root node: Area2D
  2. Rename to Basket
  3. Add child: Sprite2D → assign basket texture
  4. Add child: CollisionShape2D
  5. In Inspector: Shape → New RectangleShape2D
  6. Resize rectangle to match sprite
  7. Save as basket.tscn
rect-shape-inspector.png
Screenshot: RectangleShape2D configured in Inspector, sized to match the basket sprite

The $ Shorthand

$NodeName = "get the child node called NodeName"

Basket (Area2D) ← our script lives here ├── Sprite2D ← $Sprite2D └── CollisionShape2D ← $CollisionShape2D

$Sprite2D is shorthand for get_node("Sprite2D")

Works for any child node — just use the name from the scene tree

Getting Sprite Size

How wide/tall is my sprite on screen?

# Raw texture size (pixels in the image file)
var tex_w = $Sprite2D.texture.get_width()
var tex_h = $Sprite2D.texture.get_height()

# Actual display size (accounts for scale!)
var display_w = tex_w * $Sprite2D.scale.x
var display_h = tex_h * $Sprite2D.scale.y

Why does this matter?

If your sprite is 128px wide but scaled to 0.5, it displays as 64px.

Always multiply by scale when you need the real on-screen size.

Basket Script

extends Area2D

var speed = 400

func _process(delta):
    if Input.is_action_pressed("ui_left"):
        position.x = position.x - speed * delta
    if Input.is_action_pressed("ui_right"):
        position.x = position.x + speed * delta

    var half_w = $Sprite2D.texture.get_width() * $Sprite2D.scale.x / 2   # display half-width
    var screen_w = get_viewport_rect().size.x                           # viewport width
    position.x = clamp(position.x, half_w, screen_w - half_w)          # keep edges on screen

Look familiar? This is the Wanderer pattern!

  • extends Area2D instead of extends Node2D
  • Same Input.is_action_pressed() polling
  • Same clamp() to stay on screen

Pattern Recognition

Code PatternFirst Seen InReused Here
Input.is_action_pressed()WandererBasket movement
position.x +/- speed * deltaWandererBasket movement
clamp()WandererKeep basket on screen

Learn once, use everywhere

4. Part B: The Fruit

The falling collectible

Fruit Scene Tree

Fruit (Area2D) ├── Sprite2D └── CollisionShape2D

Same structure as the Basket — Area2D with a shape and sprite

Both need Area2D because both need to detect overlap with each other

Fruit Script

extends Area2D

var speed = 200                                                         # pixels per second

func _ready():
    add_to_group("fruits")                                              # tag for collision check

func _process(delta):
    position.y = position.y + speed * delta                              # fall downward

    var half_h = $Sprite2D.texture.get_height() * $Sprite2D.scale.y / 2 # display half-height
    var screen_h = get_viewport_rect().size.y                           # viewport height
    if position.y > screen_h + half_h:                                  # fully below screen?
        queue_free()                                                    # destroy this fruit

Two things happening:

  1. Fall downwardposition.y += speed * delta (like Ricochet, but only vertical)
  2. Self-destructqueue_free() when off screen

New Concept: queue_free()

queue_free() = "remove me from the game"

  • Removes the node from the scene tree
  • Frees the memory it was using
  • "Queue" = happens safely at end of frame (not mid-process)
  • Without this, off-screen fruits pile up forever → memory leak!

In Ricochet, logos bounced back. In Fruit Frenzy, missed fruits are destroyed.

5. Signals

Godot's Event System

What Are Signals?

Signals let one node notify another that something happened

Analogy: a doorbell

  • Someone presses the button → the bell rings
  • You hear the ring → you open the door
  • The person doesn't need to know HOW you'll respond

Signals decouple the sender (what happened) from the receiver (what to do about it)

Signal Flow: Fruit Meets Basket

1. Fruit moves into Basket's area

2. Basket emits area_entered signal

3. Signal calls _on_area_entered(area)

4. Handler destroys fruit + updates score

The area parameter is the other Area2D that entered — in our case, the Fruit

Connecting Signals in the Editor

  1. Select the Basket node in the scene
  2. Go to the Node panel (right side, next to Inspector)
  3. Click the Signals tab
  4. Double-click area_entered(area: Area2D)
  5. Connect to Basket → method: _on_area_entered
signals-connect.png
Screenshot: Node panel → Signals tab → connecting area_entered signal to Basket script

Groups: Tagging Nodes

A group is a tag you attach to a node so you can identify it later

add_to_group("fruits")       # in _ready(): tag this node as a fruit
area.is_in_group("fruits")   # later: ask "is this node a fruit?"

Why not check the node's name?

When you instantiate() multiple copies, Godot renames them:

"Fruit""@Fruit@2""@Fruit@3" → ...

Names change. Groups don't.

The Handler Function

func _on_area_entered(area):
    if area.is_in_group("fruits"):                                      # is it a fruit?
        area.queue_free()                                               # destroy it
        get_parent().add_score()                                        # tell Game we scored

Line 1: area = the other Area2D that overlapped (the Fruit)

Line 2: is_in_group("fruits") — check if the area belongs to the "fruits" group

Line 3: area.queue_free() — destroy the caught fruit

Line 4: get_parent().add_score() — tell the Game node to update the score

get_parent() Communication

Game (Node2D) ← get_parent() returns this ├── Basket (Area2D) ← we're here ├── ScoreLabel └── SpawnTimer

get_parent() = "give me the node above me in the tree"

Basket calls get_parent().add_score() → Game's add_score() runs

This works because the Basket knows its parent has an add_score() function

6. Game Scene: Spawning + Score

Putting it all together

Game Scene Tree

Game (Node2D) ├── Basket (basket.tscn, position: 400, 550) ├── ScoreLabel (Label, position: 10, 10, text: "Score: 0") └── SpawnTimer (Timer)
  1. New scene → root: Node2D → rename to Game
  2. Drag basket.tscn into scene → position at (400, 550)
  3. Add child: Label → rename to ScoreLabel → text: Score: 0
  4. Add child: Timer → rename to SpawnTimer
  5. Save as game.tscn

New Node: Timer

Timer = do something repeatedly, on a schedule

  • wait_time — seconds between each tick
  • start() — begin the countdown
  • timeout signal — emitted every time the timer fires

Timer with wait_time = 1.0:

1 sec → timeout → 1 sec → timeout → 1 sec → timeout → ...

Perfect for spawning a fruit every second!

Game Script

extends Node2D

var fruit_scene = preload("res://fruit.tscn")                           # load once at startup
var score = 0

func _ready():
    $SpawnTimer.wait_time = 1.0                                         # spawn every 1 second
    $SpawnTimer.start()                                                 # begin the countdown
    $SpawnTimer.timeout.connect(_on_spawn_timer_timeout)                # signal → function

func _on_spawn_timer_timeout():
    var fruit = fruit_scene.instantiate()                               # create a new fruit
    var screen_w = get_viewport_rect().size.x                           # viewport width
    var margin = 50                                                     # edge padding
    fruit.position = Vector2(randf_range(margin, screen_w - margin), -20) # random x, above screen
    add_child(fruit)                                                    # add to scene, starts falling

func add_score():                                                       # called by Basket on catch
    score = score + 1
    $ScoreLabel.text = "Score: " + str(score)                           # update the UI

Code Walkthrough

preload("res://fruit.tscn") — load the fruit scene (from Ricochet!)

$SpawnTimer.wait_time = 1.0 — spawn every 1 second

$SpawnTimer.timeout.connect(...) — connect signal in code (alternative to editor)

fruit_scene.instantiate() — create a new fruit copy (from Ricochet!)

Vector2(randf_range(50, screen_w - 50), -20) — random x, just above screen

add_child(fruit) — add to scene tree, fruit starts falling

$ScoreLabel.text = "Score: " + str(score) — update the UI

Two Ways to Connect Signals

MethodHowWhen to Use
Editor Node panel → Signals tab → double-click Nodes already in the scene
Code .connect(function_name) Dynamic setup, or preference

We used the editor for Basket's area_entered

We used code for Timer's timeout

Both do the same thing — connect a signal to a function

Run It!

fruit-frenzy-demo.mp4
Video: Full Fruit Frenzy gameplay — fruits falling, basket catching, score updating

Fruits fall. Catch them. Score goes up.

A complete game in ~30 lines of code!

7. Bonus: Fruit Variety

Adding visual flair

Random Colors & Speeds

Add to the Fruit script's _ready():

func _ready():
    add_to_group("fruits")                                              # tag for collision check
    $Sprite2D.modulate = Color(randf(), randf(), randf())               # random color tint
    speed = randf_range(150, 350)                                       # random fall speed
  • $Sprite2D.modulate — same trick from Ricochet's color bonus!
  • randf_range(150, 350) — each fruit falls at a different speed
  • Each instantiate() calls _ready() → unique fruit every time

Summary: What We Learned

New Concepts

ConceptWhat You Learned
Area2DA node that detects when other areas overlap with it
CollisionShape2DDefines the hitbox shape for an Area2D
SignalsNodes emit events; other nodes respond. Godot's event system.
area_enteredSignal emitted when another Area2D overlaps
queue_free()Remove a node from the game (destroy it safely)
TimerEmits timeout signal at regular intervals
.connect()Connect a signal to a function in code
get_parent()Access the parent node (for upward communication)

The Journey So Far

Wanderer

Input, movement, clamp

Ricochet

Velocity, autonomous movement, spawning

Fruit Frenzy

Area2D, collision detection, signals, score

Pong

All of the above, combined!

Exercises

Try these on your own!

Beginner

  1. Speed Experiment: Change the fruit's fall speed from 200 to 400, then 100. What makes the game fun? What about the basket speed?
  2. Miss Counter: Add a misses variable to the Game script. When a fruit calls queue_free() because it went off screen (not caught), tell the game. Display "Missed: X" in a second label.
  3. Spawn Rate: Make fruits spawn faster over time. Every 10 seconds, reduce $SpawnTimer.wait_time by 0.1 (minimum 0.3). Hint: use a second Timer.

Intermediate

  1. Fruit Types: Create 3 different fruit scenes with different colors and point values. Red = 1 point, gold = 3 points, rainbow = 5 points. Randomly pick which to spawn.
  2. Power-Up: Every 15 seconds, spawn a special "growth" item. When caught, the basket's CollisionShape2D gets 50% wider for 5 seconds. Hint: use $CollisionShape2D.shape.size

Advanced

  1. Falling Bombs: Spawn red "bomb" Area2Ds alongside fruits. Catching a bomb subtracts 3 points. Hint: add bombs to a "bombs" group and check area.is_in_group("bombs")
  2. Game Over: Add 3 lives. Missing a fruit loses a life. At 0 lives, stop the spawn timer and show "Game Over" text. Add a restart button using _input(event).

Next Lecture: Pong

We'll combine everything:

  • Paddles — Wanderer's input pattern
  • Ball — Ricochet's bounce physics
  • Scoring — Fruit Frenzy's signals & collision

Plus new concepts:

  • @export — customize values from the Inspector
  • Groups — organize nodes by type
  • Custom signals — your own events
  • Sound — audio feedback

Three micro-games → one real game

Questions?

Try building Fruit Frenzy on your own

Experiment with the exercises

Challenge: add a lives system — 3 missed fruits and game over!

Office hours: [Your time here]