CS 470 Game Development

Week 3, Lecture 7

Micro-Game 4: Pong

Putting It All Together

Today's Goal

Build a complete 2-player Pong game

Reuse everything from Wanderer, Ricochet, and Fruit Frenzy

Plus new concepts: @export, Editor Groups, Custom Signals, Sound

The Starter Project

Pre-Built (You Know This)

  • Paddle movement + clamp
  • Ball movement + wall bounce
  • Area2D + CollisionShape2D
  • Scene layout

Your Job (New Today)

  • @export for Player 2 controls
  • Groups (editor) for paddle identification
  • Custom signals for scoring
  • Paddle collision + angle
  • Sound effects

Open the starter project in Godot and follow along!

Agenda (~50 minutes)

  1. Starter Tour & Review (5 min)
  2. @export — Inspector-Editable Variables (6 min)
  3. Groups in the Editor (5 min)
  4. Custom Signals — Your Own Events (8 min)
  5. Ball-Paddle Collision (10 min)
  6. Game Assembly & Score Wiring (8 min)
  7. Sound Effects (4 min)
  8. Summary + Exercises (4 min)

1. Starter Tour

Let's look at what's already built

Game Scene Tree

Game (Node2D) ├── Background (ColorRect, 800×600) ├── Player1 (paddle.tscn, position: 50, 300) ├── Player2 (paddle.tscn, position: 750, 300) ├── Ball (ball.tscn, position: 400, 300) ├── Score1 (Label, position: 350, 20) └── Score2 (Label, position: 430, 20)

Two instances of the same paddle scene — like spawning logos in Ricochet!

Paddle Scene

paddle.tscn

Paddle (Area2D) ├── Sprite2D (or ColorRect, 20×100) └── CollisionShape2D (RectangleShape2D)

Same structure as Fruit Frenzy's Basket — Area2D + shape + visual

Used twice in the game scene: Player1 and Player2

Ball Scene

ball.tscn

Ball (Area2D) ├── Sprite2D (or ColorRect, 20×20) └── CollisionShape2D (RectangleShape2D)

Same structure as Fruit Frenzy's Fruit — Area2D for overlap detection

Both paddle and ball are Area2Ds — so Godot can detect when they overlap

Pre-Built Code: Paddle

paddle.gd

extends Area2D

var speed = 400
var move_up = "ui_up"      # ← Will become @export
var move_down = "ui_down"  # ← Will become @export

func _process(delta):
    if Input.is_action_pressed(move_up):
        position.y = position.y - speed * delta
    if Input.is_action_pressed(move_down):
        position.y = position.y + speed * delta
    position.y = clamp(position.y, 50, 550)

Wanderer pattern: input polling + delta + clamp

Problem: both paddles use the same keys!

Pre-Built Code: Ball

ball.gd

extends Area2D

var velocity = Vector2.ZERO
var speed = 400

func _ready():
    reset()

func reset():
    position = Vector2(400, 300)
    var direction = Vector2([-1, 1].pick_random(), randf_range(-0.5, 0.5))
    velocity = direction.normalized() * speed

func _process(delta):
    position = position + velocity * delta

    if position.y < 10 or position.y > 590:  # Top/bottom bounce
        velocity.y = -velocity.y

Ricochet pattern: velocity + autonomous movement + wall bounce

Missing: scoring, paddle collision, sound

2. @export

Inspector-Editable Variables

The Problem

Two paddles, same script, same controls

How do we give Player 2 different keys?

We could make two separate scripts...

But there's a better way!

The @export Keyword

In paddle.gd — change the variable declarations:

# Before (hardcoded):
var move_up = "ui_up"
var move_down = "ui_down"

# After (configurable):
@export var move_up = "ui_up"
@export var move_down = "ui_down"

@export makes a variable appear in the Inspector

Same script, different settings per instance

Player 1: ui_up / ui_down (arrow keys — default)

Player 2: w / s (set in Inspector)

Configure Player 2

  1. Add @export to move_up and move_down in paddle.gd
  2. Select Player2 node in the scene tree
  3. In Inspector: set move_up to w
  4. In Inspector: set move_down to s
  5. Run — each paddle has its own controls!
export-inspector.png
Screenshot: Inspector showing @export variables for Player 2 with "w" and "s" values

3. Groups in the Editor

A new way to assign groups

Groups Recap

In Fruit Frenzy, we used add_to_group() in code:

func _ready():
    add_to_group("fruits")    # tag this node as a fruit

Then checked membership in the collision handler:

if area.is_in_group("fruits"):
    # It's a fruit — catch it!

That's the code approach. There's also an editor approach!

Adding Groups via the Editor

  1. Open paddle.tscn
  2. Select the Paddle root node
  3. Go to Node panel → Groups tab
  4. Type paddle → click Add

Now every instance of paddle.tscn is in the "paddle" group

Player1 and Player2 both tagged automatically!

groups-panel.png
Screenshot: Node panel → Groups tab with "paddle" group added

Code vs Editor Groups

Code (add_to_group)Editor (Groups tab)
Used inFruit FrenzyPong
When to useSpawned at runtimePlaced in the editor
Checked withis_in_group() — same either way!

Spawned nodes → code groups. Editor-placed nodes → editor groups.

4. Custom Signals

Your Own Events

Signals Recap

SignalEmitterUsed In
area_enteredArea2DFruit Frenzy (basket catches fruit)
timeoutTimerFruit Frenzy (spawn fruits)

These are built-in signals — Godot defines them

Now: define your own signals!

Defining a Custom Signal

In ball.gd — add at the top of the script:

extends Area2D

signal scored(player)    # ← Define at the top of the script

var velocity = Vector2.ZERO
var speed = 400

signal scored(player)

  • signal — keyword to define a new signal
  • scored — the name (you choose it)
  • (player) — parameter: which player scored

This is like creating your own area_entered, but for scoring

Emitting the Signal

In ball.gd — add scoring detection to _process():

func _process(delta):
    position = position + velocity * delta

    if position.y < 10 or position.y > 590:
        velocity.y = -velocity.y

    # Ball passes left edge → Player 2 scores
    if position.x < 0:
        scored.emit(2)
        reset()

    # Ball passes right edge → Player 1 scores
    if position.x > 800:
        scored.emit(1)
        reset()

scored.emit(2) — fire the signal with player number

The ball says "someone scored" — it doesn't update the label itself

Connecting the Custom Signal

# In game.gd:
func _ready():
    $Ball.scored.connect(_on_ball_scored)
Built-in SignalCustom Signal
Defined byGodotYou
Examplearea_enteredscored
Emitted byGodot automaticallyYou call .emit()
Connected same way?Yes! .connect(handler)

Same pattern you already know — just applied to YOUR events

5. Ball-Paddle Collision

The heart of Pong

The Collision Handler

In ball.gd — complete the _on_area_entered() function:

func _on_area_entered(area):
    if area.is_in_group("paddle"):
        velocity.x = -velocity.x
        position.x = position.x + sign(velocity.x) * 10

Line 2: is_in_group("paddle") — only react to paddles

Line 3: Reverse horizontal direction (bounce!)

Line 4: Nudge ball away — prevents double-hit glitch

This is enough for basic Pong — but it's boring. Every bounce goes straight back.

Angle Calculation

Still in ball.gd, inside the collision handler:

Where the ball hits the paddle changes the bounce angle

Top of paddle → ball goes up

Center of paddle → ball goes straight

Bottom of paddle → ball goes down

# How far from paddle center did the ball hit?
var hit_offset = (position.y - area.position.y) / 50

# Convert to vertical velocity
velocity.y = hit_offset * speed * 0.75

# Keep total speed consistent
velocity = velocity.normalized() * speed

Complete Collision Code

ball.gd — full _on_area_entered():

func _on_area_entered(area):
    if area.is_in_group("paddle"):
        # 1. Horizontal bounce
        velocity.x = -velocity.x

        # 2. Prevent double-hit
        position.x = position.x + sign(velocity.x) * 10

        # 3. Angle based on hit position
        var hit_offset = (position.y - area.position.y) / 50
        velocity.y = hit_offset * speed * 0.75

        # 4. Maintain consistent speed
        velocity = velocity.normalized() * speed

4 steps: bounce → nudge → angle → normalize

This is what makes Pong feel like a skill game, not just random bouncing

6. Game Assembly & Scoring

Wiring it all up

Game Script

game.gd — connect the signal and handle scoring:

extends Node2D

var score1 = 0
var score2 = 0

func _ready():
    $Ball.scored.connect(_on_ball_scored)

func _on_ball_scored(player):
    if player == 1:
        score1 = score1 + 1
        $Score1.text = str(score1)
    else:
        score2 = score2 + 1
        $Score2.text = str(score2)

Signal Flow: Complete Picture

Ball passes left edge

scored.emit(2) — "Player 2 scored!"

Game receives signal via _on_ball_scored(2)

score2 += 1, update label

Ball calls reset() — back to center

Run It!

pong-demo.mp4
Video: 2-player Pong gameplay with paddle collision, angle control, and score updates

Two players, paddles, bouncing ball, score tracking

A complete game — built from concepts you already knew!

7. Sound Effects

Audio feedback

Where to Get Sound Effects

SourceTypeBest For
jsfxr — sfxr.me Generator (browser) Retro blips, hits, jumps, pickups
bfxr — bfxr.net Generator (desktop) Same style, more controls
freesound.org Library (free, CC) Realistic sounds, ambient, music
kenney.nl/assets Library (CC0) Game-ready packs, no attribution

For Pong: open jsfxr, click Hit/Hurt preset, tweak, export as .wav

Godot supports .wav and .ogg — drop the file into your project folder

AudioStreamPlayer

  1. Add AudioStreamPlayer child to Game → rename HitSound
  2. In Inspector: drag a sound file (.wav or .ogg) into the Stream property
  3. That's it — the node is ready to play

AudioStreamPlayer = a node that plays sound

Call .play() and it plays the assigned audio

Playing on Paddle Hit

Add to ball.gd, inside the paddle collision handler:

func _on_area_entered(area):
    if area.is_in_group("paddle"):
        velocity.x = -velocity.x
        position.x = position.x + sign(velocity.x) * 10

        var hit_offset = (position.y - area.position.y) / 50
        velocity.y = hit_offset * speed * 0.75
        velocity = velocity.normalized() * speed

        # Play hit sound
        get_parent().get_node("HitSound").play()

get_parent() — go up to Game node

.get_node("HitSound") — find the audio player

.play() — play the sound

Summary: What We Learned

New Concepts in Pong

ConceptWhat You Learned
@exportMake variables editable in the Inspector. One script, many configurations.
Groups (editor)Assign groups via the Node panel's Groups tab — no code needed for editor-placed nodes.
Custom signalsDefine your own events with signal. Emit with .emit().
AudioStreamPlayerPlay sound effects. Assign a stream, call .play().

The Full Journey

Wanderer — Input, movement, clamp

Ricochet — Velocity, autonomous movement, spawning

Fruit Frenzy — Area2D, collision, signals, groups, score

Pong — @export, editor groups, custom signals, sound

4 games. 16+ concepts. One foundation.

Concept Map

Wanderer

  • Nodes & Scenes
  • Sprite2D
  • Scripts
  • _process(delta)
  • Input polling
  • clamp()

Fruit Frenzy

  • Area2D
  • CollisionShape2D
  • Signals
  • Groups (code)
  • queue_free()
  • Timer

Ricochet

  • Vector2
  • Velocity
  • Bouncing
  • _ready()
  • _input(event)
  • preload/instantiate

Pong

  • @export
  • Groups (editor)
  • Custom signals
  • AudioStreamPlayer

Exercises

Try these on your own!

Beginner

  1. Speed Tuning: Experiment with ball speed (300, 400, 500) and paddle speed (300, 400, 500). What combination feels best?
  2. Center Line: Add a dashed center line using a ColorRect (thin, tall, centered). Pure visual — no code needed.
  3. Win Condition: First player to 5 points wins. Stop the ball, display "Player X Wins!" Hint: check scores in _on_ball_scored and call $Ball.set_process(false) to freeze the ball.

Intermediate

  1. Speed Increase: Make the ball 5% faster on each paddle hit. Add speed *= 1.05 in the collision handler. Cap at 800. What happens to gameplay?
  2. AI Paddle: Make Player 2 an AI. In _process(), move toward the ball's y position: position.y += (ball_y - position.y) * speed * delta * 0.02. Hint: pass ball reference with @export var ball_path: NodePath

Advanced

  1. Screen Shake: On paddle hit, briefly offset the camera by a random amount and return to center. Hint: add a Camera2D, offset it in the collision handler, use a Timer or Tween to reset.
  2. Multi-Ball: Every 20 seconds, spawn a second ball. Both balls score independently. Hint: preload and instantiate ball.tscn, connect each ball's scored signal.

Next Lecture: Flappy Bird

You've built 4 complete games — the foundation is set

Reused from Pong & earlier

  • Area2D + collision detection
  • Signals for game events
  • Score tracking with labels
  • Sound effects

New concepts

  • Gravity — constant downward acceleration
  • State machines — menu, playing, game over
  • Infinite scrolling — endless pipe generation

Same patterns, bigger games. Let's go!

Questions?

Try building Pong from scratch on your own

Experiment with the exercises

Challenge: add an AI opponent that's actually fun to play against!

Office hours: [Your time here]