Week 9 packet
Student instructions, tasks, and checks. The answer key is not in this packet. Links stay as text. This page does not embed a clip or run a file.
# Week 09 — Tune an arcade system
## VGC-L041 — Add a cooldown without hiding the rule
**Objective:** Prevent rapid repeated damage using a visible invulnerability interval.
**Time:** 55 minutes
### Instruction
Fairness can require a short recovery window after a hit. Add an invulnerable Boolean owned by the manager and a one-shot RecoveryTimer set to one second. A valid hit sets the flag, reduces lives, starts the timer and shows “Recovering” in the HUD. Timer timeout clears the flag. Do not use rapid flashing; a steady outline and text are sufficient. Decide whether a hazard touching during recovery disappears or continues, then document that consistent rule.
### Worked example
An incoming three-lane wave blocks left and center but leaves right clear. Randomly choosing among valid two-lane combinations creates variation while preserving an escape route.
Scaffold C-HAZARD — Falling hazard with player notification and cleanup
Setup: Area2D Hazard root on layer 4 (bit 8), mask layer 1 only, child CollisionShape2D circle radius 12. Attach this script. Player in group player. A Player damage method is deliberately not hardcoded: connect hit_player to a manager’s damage handler when instantiating. Parent Hazards must be unscaled and unrotated.
```gdscript
extends Area2D
signal hit_player
@export var fall_speed: float = 110.0
var hit: bool = false
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _draw() -> void:
draw_rect(Rect2(-12, -12, 24, 24), Color.TOMATO)
draw_line(Vector2(-8,-8),Vector2(8,8),Color.BLACK,2.0)
func _physics_process(delta: float) -> void:
position.y += fall_speed * delta
if position.y > 390.0:
queue_free()
func _on_body_entered(body: Node2D) -> void:
if hit or not body.is_in_group("player"):
return
hit = true
hit_player.emit()
queue_free()
```
Scaffold C-STATES — Explicit game states and gated scoring
Setup: Attach to Node root StateLab with child Label Status. This is a standalone demonstrator; call start_round, add_points, finish_round from buttons or Remote Inspector. In a real game the manager owns this state. Additional Player and spawner scripts must also honor the manager’s state, or be disabled when finishing.
```gdscript
extends Node
enum Phase { TITLE, PLAY, WIN, LOSE }
var phase: Phase = Phase.TITLE
var score: int = 0
func _ready() -> void:
_show()
func start_round() -> void:
score = 0
phase = Phase.PLAY
_show()
func add_points(amount: int) -> void:
if phase != Phase.PLAY:
return
score += maxi(amount, 0)
_show()
func finish_round(won: bool) -> void:
if phase != Phase.PLAY:
return
phase = Phase.WIN if won else Phase.LOSE
_show()
func _show() -> void:
$Status.text = "State: %s | Score: %d" % [Phase.keys()[phase], score]
```
### Assignment
1. Implement a one-second recovery window with nonflashing feedback.
2. Test two overlaps within half a second and one after recovery.
### Acceptance checks
- Two rapid overlaps cost only one life.
- A later valid overlap costs another life and feedback clears.
**Submit:** Cooldown code and timestamped test note.
**Question:** Why is merely changing the sprite color insufficient for invulnerability?
**Support:** Tune just hazard speed and spawn interval on a copied project. Use fixed test runs before adding more mechanics. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
**Optional extension:** Offer an optional challenge mode while retaining a gentle default. Document one changed parameter, your prediction, and the observed effect; optional, not required for completion.
**Optional free reading:** [Your first 2D game](https://docs.godotengine.org/en/stable/getting_started/first_2d_game/index.html)
---
## VGC-L042 — Constrain randomness for a fair challenge
**Objective:** Distinguish unpredictable variation from unavoidable failure.
**Time:** 55 minutes
### Instruction
Random x positions create variation but can accidentally block every escape route. Constrain hazards to a minimum horizontal separation or spawn one at a time with enough travel time to react. Estimate travel: at 110 pixels per second, a hazard from y-20 to y180 takes about 1.82 seconds. Compare that with player speed and route width. A random system needs invariants such as at least one navigable lane; “it usually works” is not enough for a fair rule.
### Worked example
An incoming three-lane wave blocks left and center but leaves right clear. Randomly choosing among valid two-lane combinations creates variation while preserving an escape route.
Scaffold C-TIMER — Timer-based spawning with a hard cap
Setup: World Node2D root contains Node2D Hazards and Timer SpawnTimer: wait_time 1.2, autostart On, one_shot Off. Hazard scene root Area2D has a CircleShape2D radius 12 and C-HAZARD. Attach script to World and drag Hazard.tscn into hazard_scene Inspector field. Set viewport 640x360. Name Hazards group not needed. This script is a separate lab; merge its _ready into an existing manager rather than declaring a second _ready.
```gdscript
extends Node2D
@export var hazard_scene: PackedScene
@export var spawn_limit: int = 12
func _ready() -> void:
$SpawnTimer.timeout.connect(_spawn)
func _spawn() -> void:
if hazard_scene == null or $Hazards.get_child_count() >= spawn_limit:
return
var hazard := hazard_scene.instantiate() as Area2D
$Hazards.add_child(hazard)
hazard.position = Vector2(randf_range(20.0, 620.0), -20.0)
```
### Assignment
1. Choose three 160-pixel-wide lanes and permit at most two occupied incoming lanes at one spawn wave.
2. Draw and test five possible wave layouts.
### Acceptance checks
- Every wave leaves at least one clear lane.
- Your test includes both adjacent and separated blocked lanes.
**Submit:** Lane rule, five layouts and revised spawner.
**Question:** Why is testing only one random run weak evidence?
**Support:** Tune just hazard speed and spawn interval on a copied project. Use fixed test runs before adding more mechanics. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
**Optional extension:** Offer an optional challenge mode while retaining a gentle default. Document one changed parameter, your prediction, and the observed effect; optional, not required for completion.
**Optional free reading:** [Your first 2D game](https://docs.godotengine.org/en/stable/getting_started/first_2d_game/index.html)
---
## VGC-L043 — Tune difficulty as data
**Objective:** Expose a small set of parameters and compare presets.
**Time:** 55 minutes
### Instruction
Difficulty becomes manageable when named parameters control it. Create Gentle and Standard presets for hazard speed, spawn interval and recovery duration. Keep the same core rules and give the player a clear choice before starting. A gentle mode should not shame the player or hide essential learning. Compare two thirty-second runs per preset using survival time and perceived control notes. Do not silently adapt difficulty based on personal information or collect telemetry from other players.
### Worked example
An incoming three-lane wave blocks left and center but leaves right clear. Randomly choosing among valid two-lane combinations creates variation while preserving an escape route.
Scaffold C-TIMER — Timer-based spawning with a hard cap
Setup: World Node2D root contains Node2D Hazards and Timer SpawnTimer: wait_time 1.2, autostart On, one_shot Off. Hazard scene root Area2D has a CircleShape2D radius 12 and C-HAZARD. Attach script to World and drag Hazard.tscn into hazard_scene Inspector field. Set viewport 640x360. Name Hazards group not needed. This script is a separate lab; merge its _ready into an existing manager rather than declaring a second _ready.
```gdscript
extends Node2D
@export var hazard_scene: PackedScene
@export var spawn_limit: int = 12
func _ready() -> void:
$SpawnTimer.timeout.connect(_spawn)
func _spawn() -> void:
if hazard_scene == null or $Hazards.get_child_count() >= spawn_limit:
return
var hazard := hazard_scene.instantiate() as Area2D
$Hazards.add_child(hazard)
hazard.position = Vector2(randf_range(20.0, 620.0), -20.0)
```
Scaffold C-STATES — Explicit game states and gated scoring
Setup: Attach to Node root StateLab with child Label Status. This is a standalone demonstrator; call start_round, add_points, finish_round from buttons or Remote Inspector. In a real game the manager owns this state. Additional Player and spawner scripts must also honor the manager’s state, or be disabled when finishing.
```gdscript
extends Node
enum Phase { TITLE, PLAY, WIN, LOSE }
var phase: Phase = Phase.TITLE
var score: int = 0
func _ready() -> void:
_show()
func start_round() -> void:
score = 0
phase = Phase.PLAY
_show()
func add_points(amount: int) -> void:
if phase != Phase.PLAY:
return
score += maxi(amount, 0)
_show()
func finish_round(won: bool) -> void:
if phase != Phase.PLAY:
return
phase = Phase.WIN if won else Phase.LOSE
_show()
func _show() -> void:
$Status.text = "State: %s | Score: %d" % [Phase.keys()[phase], score]
```
### Assignment
1. Implement two documented parameter presets selected at Title.
2. Run two trials of each and recommend the default.
### Acceptance checks
- Selected preset applies before the first hazard.
- Restart preserves or clearly reselects the documented mode.
**Submit:** Preset table, four trials and project checkpoint.
**Question:** Why should a preset apply before spawning starts?
**Support:** Tune just hazard speed and spawn interval on a copied project. Use fixed test runs before adding more mechanics. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
**Optional extension:** Offer an optional challenge mode while retaining a gentle default. Document one changed parameter, your prediction, and the observed effect; optional, not required for completion.
**Optional free reading:** [Your first 2D game](https://docs.godotengine.org/en/stable/getting_started/first_2d_game/index.html)
---
## VGC-L044 — Play without relying on one sense
**Objective:** Add non-color cues, pause and clear instructions.
**Time:** 55 minutes
### Instruction
Accessible design offers equivalent information through more than one channel. Hazards already use a cross while safe objects use a line-marked circle. Ensure the lives and recovery text are readable without sound. Implement C-PAUSE so the pause button continues processing while gameplay stops. Test using only keyboard focus and with audio muted. Accessibility is not a guarantee that one game serves every need; name the features you tested and the limitations that remain.
### Worked example
An incoming three-lane wave blocks left and center but leaves right clear. Randomly choosing among valid two-lane combinations creates variation while preserving an escape route.
Scaffold C-PAUSE — Pause with an always-processing resume control
Setup: Node2D World root has CanvasLayer HUD with Button Pause, plus normal gameplay children. Set Pause button Process > Mode to Always in Inspector; leave World and gameplay as Inherit/Pausable. Attach script to World; merge into its existing _ready if present. Signal callbacks still work from the always-processing button. Label text starts Pause.
```gdscript
extends Node2D
func _ready() -> void:
$HUD/Pause.pressed.connect(_toggle_pause)
func _toggle_pause() -> void:
get_tree().paused = not get_tree().paused
$HUD/Pause.text = "Resume" if get_tree().paused else "Pause"
```
### Assignment
1. Add pause/resume and visible control instructions.
2. Test muted play, non-color recognition and keyboard navigation of buttons.
### Acceptance checks
- Pause stops movement and spawning but Resume responds.
- Required information remains available with audio off.
**Submit:** Accessible arcade checkpoint and three-condition checklist.
**Question:** Why does a paused game need an always-processing resume control?
**Support:** Tune just hazard speed and spawn interval on a copied project. Use fixed test runs before adding more mechanics. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
**Optional extension:** Offer an optional challenge mode while retaining a gentle default. Document one changed parameter, your prediction, and the observed effect; optional, not required for completion.
**Optional free reading:** [Your first 2D game](https://docs.godotengine.org/en/stable/getting_started/first_2d_game/index.html)
---
## VGC-L045 — Explain and assess the arcade game
**Objective:** Demonstrate code ownership through prediction and reproducible tests.
**Time:** 55 minutes
### Instruction
A programming assessment combines the running artifact, source inspection and explanation. Before running, predict the result of a hit on Title, two rapid hits in Play and a hit after Lose. Then execute those tests. Explain how one spawned hazard reaches the manager’s damage function. Include one repaired bug with before/after evidence. You may present the explanation as text or private audio/video, but the editable project and event test log remain required. Keep optional polish separate from required functionality.
### Worked example
An incoming three-lane wave blocks left and center but leaves right clear. Randomly choosing among valid two-lane combinations creates variation while preserving an escape route.
Scaffold C-TIMER — Timer-based spawning with a hard cap
Setup: World Node2D root contains Node2D Hazards and Timer SpawnTimer: wait_time 1.2, autostart On, one_shot Off. Hazard scene root Area2D has a CircleShape2D radius 12 and C-HAZARD. Attach script to World and drag Hazard.tscn into hazard_scene Inspector field. Set viewport 640x360. Name Hazards group not needed. This script is a separate lab; merge its _ready into an existing manager rather than declaring a second _ready.
```gdscript
extends Node2D
@export var hazard_scene: PackedScene
@export var spawn_limit: int = 12
func _ready() -> void:
$SpawnTimer.timeout.connect(_spawn)
func _spawn() -> void:
if hazard_scene == null or $Hazards.get_child_count() >= spawn_limit:
return
var hazard := hazard_scene.instantiate() as Area2D
$Hazards.add_child(hazard)
hazard.position = Vector2(randf_range(20.0, 620.0), -20.0)
```
Scaffold C-HAZARD — Falling hazard with player notification and cleanup
Setup: Area2D Hazard root on layer 4 (bit 8), mask layer 1 only, child CollisionShape2D circle radius 12. Attach this script. Player in group player. A Player damage method is deliberately not hardcoded: connect hit_player to a manager’s damage handler when instantiating. Parent Hazards must be unscaled and unrotated.
```gdscript
extends Area2D
signal hit_player
@export var fall_speed: float = 110.0
var hit: bool = false
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _draw() -> void:
draw_rect(Rect2(-12, -12, 24, 24), Color.TOMATO)
draw_line(Vector2(-8,-8),Vector2(8,8),Color.BLACK,2.0)
func _physics_process(delta: float) -> void:
position.y += fall_speed * delta
if position.y > 390.0:
queue_free()
func _on_body_entered(body: Node2D) -> void:
if hit or not body.is_in_group("player"):
return
hit = true
hit_player.emit()
queue_free()
```
Scaffold C-STATES — Explicit game states and gated scoring
Setup: Attach to Node root StateLab with child Label Status. This is a standalone demonstrator; call start_round, add_points, finish_round from buttons or Remote Inspector. In a real game the manager owns this state. Additional Player and spawner scripts must also honor the manager’s state, or be disabled when finishing.
```gdscript
extends Node
enum Phase { TITLE, PLAY, WIN, LOSE }
var phase: Phase = Phase.TITLE
var score: int = 0
func _ready() -> void:
_show()
func start_round() -> void:
score = 0
phase = Phase.PLAY
_show()
func add_points(amount: int) -> void:
if phase != Phase.PLAY:
return
score += maxi(amount, 0)
_show()
func finish_round(won: bool) -> void:
if phase != Phase.PLAY:
return
phase = Phase.WIN if won else Phase.LOSE
_show()
func _show() -> void:
$Status.text = "State: %s | Score: %d" % [Phase.keys()[phase], score]
```
### Assignment
1. Submit the arcade source, readme and state/cooldown test log.
2. Explain signal flow and predict the three damage cases before testing.
### Acceptance checks
- Predictions are compared with actual results.
- Teacher can locate manager, spawner and hazard scripts in the artifact.
**Submit:** Meteor Mail assessment ZIP or private downloadable ZIP link, explanations and test log.
**Question:** What is the signal path for a runtime hazard hit?
**Support:** Tune just hazard speed and spawn interval on a copied project. Use fixed test runs before adding more mechanics. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
**Optional extension:** Offer an optional challenge mode while retaining a gentle default. Document one changed parameter, your prediction, and the observed effect; optional, not required for completion.
**Optional free reading:** [Your first 2D game](https://docs.godotengine.org/en/stable/getting_started/first_2d_game/index.html)
---
