CS 276 // GAME SYSTEMS IN GODOT

Some Assembly Required.

Lecture W6T1: Components and Data-Driven Design

Lucas P. Cordova, Ph.D.

Willamette University  |  Tuesday, September 29, 2026

SCORE 000276HI 999999

Outline

  • Lab 3 pulse (2 minutes): where is your platformer?
  • The god object: where big scripts go to die.
  • Components: one part, one job, and why Godot already works this way.
  • Data-driven design: the numbers move out of the code.
  • Co-op: your game's parts list and tuning sheet, straight into the proposal.
QUEST LOG

Lab 3 is due Thursday, October 1, 9:59 PM. The proposal is due Thursday, October 8, 9:59 PM. Thursday's class: git for teams, lightning pitches, and Lab 4 goes out.

CS 276

Lab 3 pulse

CO-OP MODE

2 minutes with a neighbor:

  1. Which milestone are you on? (M1 body, M2 brain, M3 poses, M4 world, M5 twist)
  2. One thing the state machine made easier than Lab 2.
  3. What has to happen before Thursday night?
CS 276
LEVEL 01

The God Object

Where big scripts go to die

SCORE 000276HI 999999

Your player.gd, week 12

  • Week 7: movement and jump. Clean. Proud of it.
  • Week 8: health, damage flashes, a death animation.
  • Week 9: coins, a combo meter, the HUD wants updates.
  • Week 10: footstep audio, landing dust, a dash with a cooldown.
  • Week 11: dialog when near an NPC, checkpoint saves.
  • Week 12: 900 lines, and every feature you add has to tiptoe around every feature you already have.
WATCH OUT

Every new feature edits the same file, every bug hides in the same file, and your teammate is editing it right now too. Game Programming Patterns calls this class the god object.

CS 276

Inheritance will not save you

First instinct: make a class tree. One axis works fine, until an entity needs two.

THE TREE THAT COULD NOT
Entity ├─ MovingEntity │ ├─ Player │ └─ Rival ├─ ShootingEntity │ └─ Turret └─ FlyingShootingMoving...Entity ?!
WATCH OUT

A tree forces every entity onto one axis. A flying shooter needs two branches at once, and each new combination demands a new class. The hierarchy explodes.

CS 276
LEVEL 02

One Part, One Job

GPP: Component, the pattern engines are built from

SCORE 000276HI 999999

The component pattern

DEFINITION

A component isolates one domain (movement, health, audio, collision) in its own object. The entity stops being a giant class and becomes a container that composes parts.

Each part can be written alone, tested alone, and reused by any entity that needs that domain.

This is GPP: Component, this week's reading. It is also, quietly, the architecture of every engine you have heard of.

CS 276

You have been composing all along

RIVAL-BRAINS, SEEN AGAIN
Rival (CharacterBody2D) the brain ├─ Sprite2D the looks └─ CollisionShape2D the body

Every child node is a part. Drawing lives in Sprite2D, the physical outline lives in CollisionShape2D, and your script only ever wrote the brain.

PATTERN

In Godot, the scene tree is the component system. Adding a capability is adding a child node. The pattern is one sentence: a part is a child node.

CS 276

Build a part: health_component.gd

extends Node
class_name HealthComponent

signal died
signal changed(current: int)

@export var max_health: int = 3
var current: int

func _ready() -> void:
    current = max_health

func take_hit(amount: int = 1) -> void:
    current = clampi(current - amount, 0, max_health)
    changed.emit(current)
    if current == 0:
        died.emit()
  • Plain Node: no position, no sprite. Pure bookkeeping.
  • class_name registers the type engine-wide, so any scene can add one.
  • It announces (died, changed); it does no killing and no drawing.
  • One job: track hit points. That is the whole file.
CS 276

Bolt it onto anything

PLAYER.TSCN
Player (CharacterBody2D) ├─ Sprite2D ├─ CollisionShape2D └─ HealthComponent max_health = 3
RIVAL.TSCN
Rival (CharacterBody2D) ├─ Sprite2D ├─ CollisionShape2D └─ HealthComponent max_health = 1

Same part, added as a child in two scenes, tuned differently in the Inspector. The part never asks who owns it; it counts hits for anyone.

CHECKPOINT

Reuse without inheritance: the flying shooter is just a container holding a Fly part, a Shoot part, and a HealthComponent.

CS 276

Wiring: signals up, calls down

# player.gd
@onready var health: HealthComponent = $HealthComponent

func _ready() -> void:
    health.died.connect(_on_died)

func _on_spike_touched() -> void:
    health.take_hit()

func _on_died() -> void:
    respawn()
  • The owner calls down: health.take_hit().
  • The part signals up: died fires, whoever cares connects.
  • The part never reaches into its owner, so it works inside any owner.
PATTERN

Same shape as the rival's frighten() doorbell: public entrances, announced exits.

CS 276

Rules of parts

CHECKPOINT
  • One job per part. If you cannot name the job in three words, split it.
  • The part owns its data. Health lives in HealthComponent, nowhere else.
  • Signals up, calls down. Parts announce; owners decide.
  • The owner composes. An entity is a scene tree of parts plus one small brain script.

Lab 4, out Thursday, hands you a kit of parts to build and combine. This slide is its grading rubric in disguise.

CS 276
LEVEL 03

Numbers Belong in Data

The Inspector is the designer's control panel

SCORE 000276HI 999999

You already started

# rival.gd
@export var patrol_speed: float = 140.0
@export var chase_speed: float = 220.0
@export var sight_range: float = 260.0
@export var give_up_range: float = 360.0
  • Every @export moves a decision from code to the Inspector.
  • Run the game, feel the chase, nudge the number, run again. No code edit between tries.
  • That loop is tuning, and whoever owns it controls how the game feels.
DEFINITION

Data-driven design: behavior stays in code, values live in data that a person can change without touching the code. The @export was step one.

CS 276

Four rivals, one problem

RivalRolechasesightgive up
Linfield WildcatCHASER240320420
Puget Sound LoggerAMBUSHER200220300
George Fox BruinFLANKER220280360
Pacific BoxerWILDCARD180380380
WATCH OUT

Four copies of rival.gd with different numbers? The copies drift: you fix a bug in three of them. Hand-tune four node instances across scenes? The numbers hide in four places. Both roads rot.

CS 276

A file full of numbers: Resource

DEFINITION

A Resource is a data object saved as its own file (.tres). Give it a script with @exports and you have a custom data type the Inspector can create, edit, and swap.

# rival_stats.gd
extends Resource
class_name RivalStats

@export var patrol_speed: float = 140.0
@export var chase_speed: float = 220.0
@export var sight_range: float = 260.0
@export var give_up_range: float = 360.0
@export var flee_speed: float = 260.0
  • Scripts hold behavior; this holds settings.
  • Saved to disk as .tres files you can name, copy, and git-diff.
  • One class, as many files as you want personalities.
CS 276

One script, four temperaments

# rival.gd
@export var stats: RivalStats

    State.CHASE:
        velocity = to_player.normalized() * stats.chase_speed
        if to_player.length() > stats.give_up_range:
            _change_state(State.PATROL)
FOUR FILES, ZERO NEW CODE
rivals/ ├─ chaser.tres chase 240, sight 320 ├─ ambusher.tres chase 200, sight 220 ├─ flanker.tres chase 220, sight 280 └─ wildcard.tres chase 180, sight 380
  • The brain reads stats.chase_speed instead of its own copy.
  • Each rival instance gets a .tres dropped into its stats slot.
  • New difficulty mode? A new folder of .tres files. The code never hears about it.
CS 276

Why the numbers move out

  • Tuning without touching code: either teammate can balance the game, even mid-playtest.
  • Playtests become number edits: a tester says the chase feels unfair; you change 240 to 220 in a file and rerun.
  • Balance you can diff: .tres changes show up in git like any other change, so a bad tune is one revert away.
  • Content scales: more enemies, more berries, more mazes become more files, while the codebase stays the same size.
GATE CHECK

The beta gate (November 19) requires exactly this: playtest-informed balance living in data. Build it now and the requirement is free.

CS 276
LEVEL 04

Co-op Mode

Your game, in parts and numbers

SCORE 000276HI 999999

The parts list

CO-OP MODE

6 minutes, with your teammate, in your proposal doc or on paper:

  1. List your entities: player, enemies, pickups, hazards, UI.
  2. Circle every domain that appears twice or more (health, movement, hitboxes, drops, sounds). Each circle is a component you write once.
  3. Start the tuning sheet: every number a playtester might want changed (speeds, timers, ranges, counts, costs). That sheet is your first custom Resource.
  4. Fast pair? Sketch the scene tree for your player, parts and all.
CS 276
LEVEL 05

Game Over Screen

What you know now

SCORE 000276HI 999999

Checkpoint reached

CHECKPOINT
  • A god object grows one feature at a time until every change risks every feature. Inheritance trees explode; composition scales.
  • In Godot, a part is a child node: one job, its own data, signals up, calls down.
  • @export moves one entity's numbers to the Inspector; a custom Resource (.tres) moves shared numbers into files you can swap, diff, and revert.
  • Your tuning sheet from today belongs in the proposal, and balance-in-data is a beta-gate requirement.
QUEST LOG

Lab 3: due Thursday, October 1, 9:59 PM, pushed to main, Your Submission complete. Proposal: due Thursday, October 8, 9:59 PM. Thursday: git for teams, 60-second lightning pitches, and Lab 4 goes out. Reading: GPP Component.

CS 276

Sources

  • Nystrom, Game Programming Patterns: Component (gameprogrammingpatterns.com/component.html), this week's reading.
  • Godot docs: Resources (docs.godotengine.org, Scripting section).
  • Godot docs: Nodes and scene instances, the engine's own case for composition.
  • The rival-brains repo (github.com/LucasCordova/rival-brains): git checkout demo5 runs today's refactor: RivalStats plus four .tres temperaments to swap in the Inspector.
CS 276