Some Assembly Required.
Lecture W6T1: Components and Data-Driven Design
▶ PRESS START
Lucas P. Cordova, Ph.D.
Willamette University | Tuesday, September 29, 2026
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.
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.

Lab 3 pulse
2 minutes with a neighbor:
- Which milestone are you on? (M1 body, M2 brain, M3 poses, M4 world, M5 twist)
- One thing the state machine made easier than Lab 2.
- What has to happen before Thursday night?

The God Object
Where big scripts go to die
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.
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.

Inheritance will not save you
First instinct: make a class tree. One axis works fine, until an entity needs two.
THE TREE THAT COULD NOTEntity ├─ MovingEntity │ ├─ Player │ └─ Rival ├─ ShootingEntity │ └─ Turret └─ FlyingShootingMoving...Entity ?!
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.

One Part, One Job
GPP: Component, the pattern engines are built from
The component pattern
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.

You have been composing all along
RIVAL-BRAINS, SEEN AGAINRival (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.
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.

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_nameregisters 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.

Bolt it onto anything
PLAYER.TSCNPlayer (CharacterBody2D) ├─ Sprite2D ├─ CollisionShape2D └─ HealthComponent max_health = 3
RIVAL.TSCNRival (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.
Reuse without inheritance: the flying shooter is just a container holding a Fly part, a Shoot part, and a HealthComponent.

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:
diedfires, whoever cares connects. - The part never reaches into its owner, so it works inside any owner.
Same shape as the rival's frighten() doorbell: public entrances, announced exits.

Rules of parts
- 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.

Numbers Belong in Data
The Inspector is the designer's control panel
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
@exportmoves 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.
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.

Four rivals, one problem
| Rival | Role | chase | sight | give up |
|---|---|---|---|---|
| Linfield Wildcat | CHASER | 240 | 320 | 420 |
| Puget Sound Logger | AMBUSHER | 200 | 220 | 300 |
| George Fox Bruin | FLANKER | 220 | 280 | 360 |
| Pacific Boxer | WILDCARD | 180 | 380 | 380 |
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.

A file full of numbers: Resource
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
.tresfiles you can name, copy, and git-diff. - One class, as many files as you want personalities.

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 CODErivals/ ├─ 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_speedinstead of its own copy. - Each rival instance gets a
.tresdropped into its stats slot. - New difficulty mode? A new folder of .tres files. The code never hears about it.

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.
The beta gate (November 19) requires exactly this: playtest-informed balance living in data. Build it now and the requirement is free.

Co-op Mode
Your game, in parts and numbers
The parts list
6 minutes, with your teammate, in your proposal doc or on paper:
- List your entities: player, enemies, pickups, hazards, UI.
- Circle every domain that appears twice or more (health, movement, hitboxes, drops, sounds). Each circle is a component you write once.
- Start the tuning sheet: every number a playtester might want changed (speeds, timers, ranges, counts, costs). That sheet is your first custom Resource.
- Fast pair? Sketch the scene tree for your player, parts and all.

Game Over Screen
What you know now
Checkpoint reached
- 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.
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.

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 demo5runs today's refactor: RivalStats plus four .tres temperaments to swap in the Inspector.

CS 276: Components and Data-Driven Design
By Lucas Cordova
CS 276: Components and Data-Driven Design
W6T1 "Some Assembly Required": the component pattern and data-driven design, in the Insert Coin arcade theme.