Week 7 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 07 — Timed objects
## VGC-L031 — Define the Meteor Mail loop
**Objective:** Specify an arcade game in a state-and-event model.
**Time:** 55 minutes
### Instruction
Meteor Mail is an original one-screen courier game. The player moves to avoid falling parcels marked as hazards and collects safe deliveries. Plan a thirty-second round with three starting lives and an explicit win when time expires with lives remaining. Loss occurs at zero lives. The early prototype uses plain shapes. Write a state table for Title, Play, Win and Lose, then list which events are legal in each. This prevents score continuing on a game-over screen.
### Worked example
A Timer with wait_time 2 creates one hazard per timeout. A hazard traveling 100 pixels/second moves 50 pixels during half a second and is removed after leaving the playfield.
### Assignment
1. Write a one-page design with controls, win/loss rules, entities and a four-state table.
2. Mark which events change score, lives and remaining time.
### Acceptance checks
- Each state has an entry or exit event.
- Win and loss are observable and do not contradict each other.
**Submit:** Meteor Mail design page and event table.
**Question:** Should a hazard collision reduce lives on the title screen?
**Support:** Use one manually placed falling hazard before enabling SpawnTimer. Keep the world unscaled and viewport 640 by 360. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
**Optional extension:** Compare two spawn intervals while keeping hazard speed fixed. 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-L032 — Animate a falling hazard
**Objective:** Use elapsed time and remove objects beyond the playfield.
**Time:** 55 minutes
### Instruction
C-HAZARD moves an Area2D down at a speed measured in pixels per second. Unlike CharacterBody2D move_and_slide, this direct position update needs delta. Start with a manually placed hazard at (320,-20). Its y coordinate grows until it passes 390, safely below the 360-pixel viewport. queue_free then removes it. Without cleanup, invisible objects accumulate and continue consuming processing time. The black cross distinguishes hazards from round gold deliveries without relying only on red versus gold.
### Worked example
A Timer with wait_time 2 creates one hazard per timeout. A hazard traveling 100 pixels/second moves 50 pixels during half a second and is removed after leaving the playfield.
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()
```
### Assignment
1. Create Hazard.tscn with C-HAZARD and test one falling instance.
2. Observe its scene-tree presence before and after it leaves.
### Acceptance checks
- The hazard travels downward at a steady rate.
- It disappears from the Remote tree after passing y390.
**Submit:** Hazard project and cleanup observation.
**Question:** Why does this recipe use delta when C-BODY does not?
**Support:** Use one manually placed falling hazard before enabling SpawnTimer. Keep the world unscaled and viewport 640 by 360. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
**Optional extension:** Compare two spawn intervals while keeping hazard speed fixed. 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-L033 — Spawn on a Timer event
**Objective:** Instantiate a PackedScene at a controlled interval.
**Time:** 55 minutes
### Instruction
A Timer emits timeout after its wait time. C-TIMER uses that signal to instantiate a PackedScene and add it to the Hazards container. Drag Hazard.tscn into the exported field; an empty field cannot create a scene. Randomize only x initially and spawn above the screen, so motion is predictable. Code uses instantiate in Godot 4; old Godot 3 examples using instance are not interchangeable. Count children to confirm each timeout creates one object rather than one per rendered frame.
### Worked example
A Timer with wait_time 2 creates one hazard per timeout. A hazard traveling 100 pixels/second moves 50 pixels during half a second and is removed after leaving the playfield.
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()
```
### Assignment
1. Build the C-TIMER scene and assign Hazard.tscn.
2. Record object counts at three moments during a short run.
### Acceptance checks
- One spawn occurs approximately every 1.2 seconds.
- No null-scene error appears when the exported field is assigned.
**Submit:** Spawner source and timed count log.
**Question:** Why should spawning not run unconditionally in _process?
**Support:** Use one manually placed falling hazard before enabling SpawnTimer. Keep the world unscaled and viewport 640 by 360. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
**Optional extension:** Compare two spawn intervals while keeping hazard speed fixed. 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-L034 — Cap resource use and test a long run
**Objective:** Bound spawning and prove that cleanup prevents accumulation.
**Time:** 55 minutes
### Instruction
A spawn cap is a safety limit, not a substitute for removing obsolete objects. C-TIMER refuses to add more when the Hazards container has spawn_limit children. Falling hazards should still free themselves off-screen. Test a ninety-second run and note counts every fifteen seconds. To verify the cap, temporarily disable motion in a duplicate test project so hazards remain; the count should stop at twelve. Restore the working version rather than leaving the artificial fault in your submission.
### Worked example
A Timer with wait_time 2 creates one hazard per timeout. A hazard traveling 100 pixels/second moves 50 pixels during half a second and is removed after leaving the playfield.
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()
```
### Assignment
1. Run the normal spawner for ninety seconds and record six counts.
2. In a duplicate, freeze hazards and verify the twelve-object cap.
### Acceptance checks
- Normal count does not grow without bound.
- Frozen test stops at twelve and the real project is restored.
**Submit:** Count table, cap-test evidence and restored project.
**Question:** Why keep both cleanup and a cap?
**Support:** Use one manually placed falling hazard before enabling SpawnTimer. Keep the world unscaled and viewport 640 by 360. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
**Optional extension:** Compare two spawn intervals while keeping hazard speed fixed. 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-L035 — Connect spawned hazard events
**Objective:** Wire dynamic objects to the manager without fragile paths.
**Time:** 55 minutes
### Instruction
A newly instantiated hazard does not automatically know the manager. In the spawner, connect hazard.hit_player to the World damage handler immediately after adding the hazard. Merge this into the existing _spawn function instead of creating a second function with the same name. Start with a handler that prints one message, then add lives next week. The hazard checks the player group and guards hit so a collision cannot notify twice. Static editor connections are insufficient for future runtime-created instances.
### Worked example
A Timer with wait_time 2 creates one hazard per timeout. A hazard traveling 100 pixels/second moves 50 pixels during half a second and is removed after leaving the playfield.
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()
```
### Assignment
1. Connect each spawned hazard’s hit_player signal to a single manager method.
2. Test a player overlap and a nonplayer overlap.
### Acceptance checks
- One hazard/player overlap prints once.
- Nonplayer bodies do not trigger damage notifications.
**Submit:** Dynamically connected spawner project and event log.
**Question:** Why is connecting only the first editor-placed hazard insufficient?
**Support:** Use one manually placed falling hazard before enabling SpawnTimer. Keep the world unscaled and viewport 640 by 360. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
**Optional extension:** Compare two spawn intervals while keeping hazard speed fixed. 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)
---
