Game Feel, the Lab 5 Launch, and Sprint 2
▶ PRESS START
Lucas P. Cordova, Ph.D.
Willamette University | Thursday, October 22, 2026
Lab 5 goes out today and is due Thursday, October 29, 9:59 PM. The Sprint 1 report is written in class today and due tonight, 9:59 PM, with the Sprint 2 board.

The quest log
| When | What | Where |
|---|---|---|
| Tonight, 9:59 PM | Sprint 1 report (written in class), and Sprint 2 begins | Repo: docs/sprint-1.md and FEEDBACK.md; URL on Canvas |
| Tonight, 9:59 PM | Peer evaluation for Sprint 1 | Canvas |
| Thu Oct 29, 9:59 PM | Lab 5: game feel (out today) | Push to main |
| Thu Oct 29, in class | Playtest day: the whole class plays every game | Bring a build |
| Thu Nov 5 | Alpha gate and Stakeholder Review 2 | All MUSTs in, or formally cut |
Today's report answers the review sheets you collected Tuesday: adopt, adapt, or decline, each with a reason.

The same game twice
Watch the Bearcat jump, land, and hit a spike. Then again.
M1. Run the project. Jump twice, touch the spike.M3. Accept the reload prompt. Run it. Same jumps, same spike.Speed, gravity, jump velocity, the level, the spikes: identical. Every number that decides where the Bearcat lands is the same in both builds.
Game feel is the real-time control of a virtual object, in a simulated space, with that interaction emphasized by polish [1].
Juice is the polish part: a lot of output for a little input. The player presses one key and the game answers with motion, sound, and light, all at once [2].
| Channel | What it moves | Today, in Godot |
|---|---|---|
| MOTION | The thing itself: squash, stretch, bounce | Tween on the sprite |
| TIME | The clock: hit stop, slow motion | Engine.time_scale |
| SCREEN | The camera: shake, zoom, flash | Camera2D.offset |
| PARTICLES | Debris: dust, sparks, confetti | CPUParticles2D, one shot |
| SOUND | A sound per event, never the same twice | AudioStreamPlayer plus pitch wobble |
| UI | The numbers react: a banner punches in, a label flashes | Tween on a Control |
One moment, several channels. The hit uses three; the jump uses two. That stacking is the whole trick.
@export you can turn while the game runs. You will overshoot, then settle.Juice does not fix a boring loop. It makes a good loop feel like one. Alpha first, feel second, and both before beta.
Tweens, shake, hit stop, particles, and sound, in Godot
var tween := create_tween() tween.tween_property(sprite, "scale", Vector2.ONE, 0.18) tween.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
tween_property(object, "property", target, seconds). Chain another call and it runs after; parallel() runs it alongside.EASE_OUT is fast then settling.TRANS_BACK with EASE_OUT overshoots the target a little before it lands. That overshoot reads as bounce.func _squash(to: Vector2) -> void: if squash_tween: squash_tween.kill() sprite.scale = to squash_tween = create_tween() squash_tween.tween_property( sprite, "scale", Vector2.ONE, squash_time ).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
sprite.scale = to is the snap: instant. The tween only brings it back to Vector2.ONE.kill() first. A landing that interrupts a jump would otherwise leave two tweens fighting over one property.sprite scales. The body and its collision shape never hear about it: rule one.jump_squash, land_squash, and squash_time are exports. Dials.func _change_state(new_state: State) -> void: var old_state := state state = new_state sprite.texture = TEXTURES[new_state] if old_state == State.AIR and new_state != State.AIR: _land() func _jump() -> void: velocity.y = jump_velocity _squash(jump_squash) _play(jump_sound) func _land() -> void: _squash(land_squash) dust.restart() _play(land_sound)
The door is the one place a state change happens, so it is the one place an entry effect belongs. Landing is "coming from AIR into anything else."
Three jump lines became one _jump(), so every jump (floor or air) gets the same squash and the same sound.
extends Camera2D ## Screen shake. One job: jitter the camera's offset, then settle. @export var decay: float = 60.0 # pixels of shake lost per second var strength: float = 0.0 func shake(amount: float) -> void: strength = amount func _process(delta: float) -> void: if strength <= 0.0: offset = Vector2.ZERO return offset = Vector2(randf_range(-strength, strength), randf_range(-strength, strength)) strength = move_toward(strength, 0.0, decay * delta)
offset nudges the view without moving the camera's position. It exists for this.strength.move_toward walks the strength to zero at decay pixels per second, then the offset snaps home.shake(amount). The brain calls it; the camera decides nothing.func hit_stop(scale: float, seconds: float) -> void: Engine.time_scale = scale await get_tree().create_timer(seconds, true, false, true).timeout Engine.time_scale = 1.0
Engine.time_scale = 0.05 slows every clock in the game, including this timer. A timer counting scaled seconds would take over two real seconds to fire, and the game would sit frozen. The fourth argument, ignore_time_scale = true, makes this one timer count real seconds.
await parks the function until the timer fires, then the last line runs. Nothing else waits.0.3 for half a second is slow motion. Two moods, one line of code.func respawn() -> void: if respawning: return respawning = true velocity = Vector2.ZERO died.emit() _flash() _play(hit_sound) await get_tree().create_timer( flash_time, true, false, true).timeout position = spawn_point respawning = false _change_state(State.AIR)
func _on_player_died() -> void: deaths += 1 hud.show_deaths(deaths) camera.shake(hit_shake) hit_stop(hit_stop_scale, hit_stop_time)
died.emit() fires first, so the brain freezes time and shakes the camera before the Bearcat moves.respawning guards the door: the spike can fire again while the body sits on it during the freeze.func _flash() -> void: sprite.modulate = flash_color var tween := create_tween() tween.tween_property(sprite, "modulate", Color.WHITE, flash_time) func _play(sound: AudioStreamPlayer) -> void: sound.pitch_scale = randf_range( 1.0 - pitch_wobble, 1.0 + pitch_wobble) sound.play()
CPUParticles2D with One Shot on and Emitting off; dust.restart() fires one burst. Set in the Inspector, fired from the door.AudioStreamPlayer per event, a child of the thing that makes the noise. The plain player is right for one screen; the 2D one fades with distance.The flash is a tween too: snap to magenta, ease back to white. It runs on the scaled clock, so it holds through the freeze and fades after.
A Label inside a VBoxContainer cannot be scaled by a tween. The container sets its children's position, size, rotation, and scale on every layout pass, and puts them straight back.
| Want to animate a Control's... | Do this |
|---|---|
modulate (color, alpha) | Safe anywhere. Containers never touch it. |
scale or rotation | Anchor it to the root directly, outside any container, and set its pivot offset to its center. |
position | Same: outside the container, or tween the container's own position. |
The CLEAR banner in Lab 5 sits outside the column for exactly this reason. It cost me twenty minutes; it will cost you zero.
Juice your platformer, or the kit
| Option A: the kit | Option B: your own Lab 3 | |
|---|---|---|
| Start from | A finished Lab 3 platformer (M4 plus double jump), in the Lab 5 repo | Your Lab 3 files, copied into the Lab 5 repo |
| Pick it if | Your Lab 3 stopped before M4, or you want a known good base | Your spikes respawn and your flag clears |
| Then | Build M1 to M5 from the guide | Build M1 to M5 from the guide, substituting your names |
| Grade | Identical | Identical |
Due Thursday, October 29, 9:59 PM. Solo. Accept the Classroom50 link on Canvas, clone, and open GUIDE.md. Sounds and a dust particle ship in assets/.
player.gd that get juiced.The feel rule, graded: every juice number is an @export, and none of them change a gameplay number.
| Criterion | Points |
|---|---|
| The shell: title, HUD, pause with Resume / Restart / Title, clear screen; title is the main scene | 25 |
| The jump: squash and stretch on jump and landing, a tween on the sprite only, landing dust | 15 |
| The hit: flash, camera shake from a camera script, hit stop that returns to 1.0, in the right order | 20 |
| Sound on four events, pitch variation on at least one | 10 |
| Your signature: one more moment, two channels, named in the README | 10 |
| Feel in dials: juice numbers are exports, gameplay numbers untouched, class code style | 5 |
| README's Your Submission section: screenshots, feel sheet, one dial, signature, surprise | 15 |
Close Sprint 1, plan Sprint 2
A ritual is a meeting or a document that happens at the same point of every sprint, so the team never has to decide whether to do it. Scrum calls them events [8].
| Ritual | When | Artifact |
|---|---|---|
| Sprint planning | First day of the sprint | The board: issues labeled, owned, ordered |
| The board | Every work session | Cards move; Done means it runs on main |
| Review gate | Mid-sprint Thursday | Two printed review sheets, a tagged build |
| Feedback log | The week after the gate | FEEDBACK.md: adopt, adapt, or decline, with a reason |
| Sprint report | Last day of the sprint | docs/sprint-N.md: shipped, slipped, learned, next |
| Peer evaluation | With the report | Canvas form; the commit history is the evidence |

main is worth everything.Rituals cost a team of two about an hour a sprint. Skipping them costs the last week of the semester.

Five minutes, with your teammate, on the board:
main from a fresh clone, so pull and check.Nothing gets deleted. Slipped cards are the first candidates for Sprint 2.

Sprint 2 runs October 22 to November 12. Its goal is the alpha: every MUST in the proposal is in the game, or formally cut. The alpha gate and Stakeholder Review 2 are Thursday, November 5; the Sprint 2 report is due November 12.
| The alpha gate checks | So the board needs |
|---|---|
| Every MUST in, or cut in writing | One card per MUST, or a line in the proposal saying it moved to WANT and why |
| A complete session: start, play, win or lose, restart | Cards for the title, the end state, and the restart, if any are missing |
FEEDBACK.md answering Review 1 | A card per adopted note; declined notes need no card, only a reason |
| Five minutes of play without a crash | A Stability pass card, owned, in the last week |
Tag alpha, both members in the history | Two cards: Tag alpha, and whichever teammate has fewer commits owns more |

FEEDBACK.md: adopt, adapt, or decline, with one reason. Adopted and adapted notes become issues.sprint-2, assign one owner per card.
Twenty minutes, both laptops on the team repo:
FEEDBACK.md from the two sheets while the other turns the proposal's MUSTs into issues.sprint-2, assign owners, order the Todo column: alpha rows on top.The Sprint 1 board stays as it is. Sprint 2 is a new label and a fresh Todo order on the same project.

| On the board | What it means, and the fix |
|---|---|
| No card mentions a stakeholder note | The sheets were not read. Every adopted note is a card whose body quotes the note. |
| A MUST with no card and no written cut | It is neither in nor out. Decide now; the alpha rubric counts both. |
Twenty cards labeled sprint-2 | More than twelve sittings. Move the bottom of Todo to WANT and say so in the proposal. |
| Every card on one name | One teammate ships and one watches. Reassign until both own alpha rows. |
| No Stability pass, no Tag alpha | Add both. Five crash-free minutes is a rubric line. |
| Juice cards above alpha cards | Reorder. Feel is Lab 5's job this sprint; the alpha is the game's. |

Due tonight, 9:59 PM: docs/sprint-1.md in the team repo, with FEEDBACK.md at the root, and the repo URL on Canvas. Short. Pulled from the board you just built.
| Section | Where it comes from |
|---|---|
| Shipped | The Done column: closed issues, as a changelog. |
| Slipped | The cards you annotated in the closeout, with the one-line reasons. |
| Learned | Stakeholder Review 1 and Tuesday's playtests: what you saw people do, and what you will do differently. |
| Feedback log | A pointer to FEEDBACK.md: every note marked adopt, adapt, or decline, with a reason. |
| Next sprint | The Sprint 2 board: the top five cards, and any MUST that moved to WANT. |
Alongside the report, each of you files a peer evaluation of your teammate on Canvas. The commit history is the evidence.

What you know now
Tonight, 9:59 PM: Sprint 1 report, FEEDBACK.md, peer evaluation. Thu Oct 29: Lab 5 due 9:59 PM, and playtest day in class. Thu Nov 5: the alpha gate.

GUIDE.md, milestones M0 to M5.