Lecture W6R1: Carving Code, Git for Two, Lab 4
▶ PRESS START
Lucas P. Cordova, Ph.D.
Willamette University | Thursday, October 1, 2026
Lab 3 is due tonight, 9:59 PM. The proposal is due Thursday, October 8, 9:59 PM. Lab 4 goes out today and is due Tuesday, October 20, 9:59 PM.
Tuesday's parts get a name and a test
Rule one carries the most weight, and software engineering has a formal name for it.
Gather the things that change for the same reason. Separate the things that change for different reasons.
A module that follows the principle has one reason to change, and it answers to one kind of person asking for that change.
Robert C. Martin's formulation [1]. It is the S in SOLID, and it is the rule that tells you where to cut a god script into parts.
Component is the mechanism (a part is a child node). Single Responsibility is the judgment (which lines belong in which part).
| Test | Ask | Fails when |
|---|---|---|
| Three words | Can you name the job in three words or fewer? | The name needs an "and". |
| Who asks | Who would request a change to this code? | Designer, artist, and sound all would. |
| Lift out | Could this code move to another entity untouched? | It reaches into its owner or its neighbors. |
"Tracks hit points" passes all three. "Chases the player, flashes red, and updates the score" fails the first one out loud.
An enemy from a Blitz side game. It chases Blitz, hurts them on contact, takes three marionberries to defeat, and sometimes drops a fig. It works. It is also one file doing eight jobs.
1 # berry_bandit.gd (the whole enemy, one file) 2 extends CharacterBody2D 3 4 var hp: int = 3 5 6 func _physics_process(_delta: float) -> void: 7 var player = get_tree().get_first_node_in_group("player") 8 var to_player: Vector2 = player.global_position - global_position 9 if to_player.length() < 300.0: 10 velocity = to_player.normalized() * 180.0 11 else: 12 velocity = Vector2.ZERO 13 move_and_slide() 14 if to_player.length() < 24.0: 15 player.hp -= 1 16 player.get_node("Sprite2D").modulate = Color.RED 17 if player.hp <= 0: 18 get_tree().reload_current_scene()
20 func hit_by_berry() -> void: 21 hp -= 1 22 $Sprite2D.modulate = Color.RED 23 $HurtSound.pitch_scale = randf_range(0.9, 1.1) 24 $HurtSound.play() 25 await get_tree().create_timer(0.15).timeout 26 $Sprite2D.modulate = Color.WHITE 27 if hp <= 0: 28 Game.score += 50 29 get_node("/root/Main/HUD/ScoreLabel").text = "Score: " + str(Game.score) 30 if randf() < 0.25: 31 var fig := preload("res://fig.tscn").instantiate() 32 fig.global_position = global_position 33 get_parent().add_child(fig) 34 queue_free()
Count the reasons this file could change: the chase feels unfair, the flash is too long, the sound designer has a new clip, the HUD moves, figs drop too often. Five reasons, one file.
12 minutes, in pairs, on the handout:
BanditStats.4 minutes, two pairs together:
Designs differ. A cut you can defend with a test is a good cut.
One game, two keyboards, zero collisions
player.gd guarantees you and your teammate are in the same file all week.dash_component.gd, your teammate takes health_bar.gd.Single Responsibility is also a teamwork tool: one job per file means one owner per change.
| File | Commit it? | Why |
|---|---|---|
| *.gd, *.tscn, *.tres | Yes | Your game. All three are plain text, so git can diff them. |
| project.godot | Yes | Input map, layer names, main scene. |
| *.import, *.uid | Yes | Import settings and stable IDs. Without them your teammate's scenes lose their references. |
| art, audio | Yes | Keep sources small. Binary files cannot merge, so one owner per asset. |
| .godot/ | Never | The editor's cache, rebuilt on every machine. The lab .gitignore already excludes it. |
Starting your team repo from a lab template gives you the correct .gitignore for free [2].
[node name="Rival" type="CharacterBody2D"] collision_layer = 4 script = ExtResource("1_rival") [node name="HealthComponent" type="Node" parent="."] script = ExtResource("2_health") max_health = 3
.tscn is a list of nodes and their changed properties.One editor per scene at a time. Say it in your team chat: "I have main.tscn until tonight." Split big scenes into small instanced ones so there is more to go around.
git switch main git pull # start from the latest main git switch -c dash-component # one branch per feature # ...build, run, commit small... git add . git commit -m "Add dash component with cooldown" git push -u origin dash-component # on GitHub: open a pull request, teammate reviews, merge git switch main git pull # both of you, after every merge
After pulling a change to project.godot: Project menu, then Reload Current Project.
| Conflict in | What to do |
|---|---|
| A script (.gd) | Open the file, read both versions between the markers, keep what is right, delete the markers, run the game, commit. |
| A scene or resource (.tscn, .tres) | Pick one side whole (git checkout --theirs path or --ours), then redo the smaller change in the editor. |
| A binary (art, audio) | Pick one side whole. Ask the owner which one. |
Conflicts are a scheduling problem more than a git problem. Pull before you start, commit small, merge daily, and most weeks you will never see one.
4 minutes, with your teammate, straight into your proposal's branching plan:
feature/dash, fix/spawn).Build the parts once, bolt them onto two games
One Godot project, two tiny games (the lab calls them cabinets), and one shared folder of parts. The Berry Bandit you carved on paper is the first thing you build.
THE SHAPE OF THE LABlab4-component-kit/ ├─ components/ the kit: one copy of every part ├─ cabinet_a/ Bandit Brawl (Blitz, berries, bandits) ├─ cabinet_b/ a second game, different entities └─ stats/ Resource files: the tuning sheets
The kit rule: both cabinets use the same files in components/. A part that needs editing to work in the second cabinet has two jobs.
Solo lab. Due Tuesday, October 20, 9:59 PM.
| You build | The idea | |
|---|---|---|
| M0 | Accept, clone, run both cabinets. Things move; nothing can be hurt. | Starting line |
| M1 | HealthComponent, bolted onto Blitz and the bandit. | A part is a child node |
| M2 | HitboxComponent and HurtboxComponent: damage travels between parts. | Signals up, calls down |
| M3 | The second cabinet, using the kit with zero edits in components/. | Reuse |
| M4 | A stats Resource and at least three .tres files. Magic numbers leave the code. | Numbers live in data |
| M5 | Your own part, used by two different entities. | One job, yours |
M5 ideas: flash on hit, drop on death, knockback, a health bar, a lifetime timer. GUIDE.md has the full menu.
| Criterion | Points |
|---|---|
| The kit works in cabinet A: health, hitbox, hurtbox, defeat | 25 |
Reuse: cabinet B runs on the same, unedited components/ files | 20 |
| Rules of parts: one job each, parts own their data, signals up and calls down, no reaching into owners | 15 |
Numbers in data: a stats Resource, at least three .tres files, no magic numbers in scripts | 15 |
| Your own part (M5), used by two different entities | 10 |
| Your Submission in the README: parts list, wiring sketch, tuning sheet, reflection | 15 |
Your handout from today is a first draft of that README section, for a different enemy.
Until class ends, pick your lane:
Tonight: Lab 3, 9:59 PM. Thursday, October 8: proposal (PDF in Canvas and in your repo), 9:59 PM. Tuesday, October 20: Lab 4, 9:59 PM. Next week: UI systems on Tuesday, agile for game teams on Thursday.
What you know now