PLECK lesson 147 · suggested week 30, day 2
Implement the final player action
Objective. Make the core input-to-action path observable.
Godot Engine Standard desktop editor 4.7.2-stable, GDScript (Godot 4), Compatibility renderer. Headless Godot 4.7.2.stable.official.ed1daf0bf passed 106 assertions on the supplied scaffolds, including malformed JSON saves. Audio used the Dummy driver and a synthetic WAV, not a speaker. The visual editor, browser export, and signed-in submission were not tested. The desk still does not run a student's own upload.
About 55 minutes. The due date is chosen by a parent.
Project bootstrap, mechanics, richer system and first review
Suggested pace: Recall and predict 5 min; Read instruction and scaffold 12 min; Build or design the assigned artifact 25 min; Run acceptance tests 8 min; Explain and checkpoint 5 min.
Materials: Godot Standard 4.7.2 desktop editor (GDScript, Compatibility renderer), Local course folder and prior checkpoint, No paid assets or public account required.
Comes after VGC-L146.
Implement the action central to the final game using your architecture. For movement, verify both main and alternate keys and the collision footprint. For narrative, verify choice activation and focus. For a resource game, verify a request reaches validation before changing quantities. Use temporary diagnostics to show the event path, then keep player-facing feedback clear. Test three start positions or states so the mechanic is not accidentally tied to a single development setup.
Worked example
A vertical slice joins a player action, one challenge, progress feedback and an ending. It demonstrates real integration even while the art remains rectangles. Scaffold C-BODY — Reusable top-down player with collision Setup: Create CharacterBody2D root Player at (80,180); add CollisionShape2D with CircleShape2D radius 14. Attach this script to Player and save Player.tscn. Use C-FREE-MOVE input actions. Root collision layer 1, mask 2. Create a separate StaticBody2D Wall on layer 2 mask 1 with RectangleShape2D collision and a Polygon2D rectangle as visible child. Instance both under an unscaled Node2D World. Do not put a body inside another physics body. ```gdscript extends CharacterBody2D @export var speed: float = 160.0 func _draw() -> void: draw_circle(Vector2.ZERO, 14.0, Color.CORNFLOWER_BLUE) draw_line(Vector2(-8, 0), Vector2(8, 0), Color.WHITE, 3.0) func _physics_process(_delta: float) -> void: velocity = Input.get_vector("move_left", "move_right", "move_up", "move_down") * speed move_and_slide() ``` Scaffold C-PLATFORM — A gravity-and-jump platform player Setup: CharacterBody2D root Player, child CollisionShape2D RectangleShape2D size (24,32), no scale changes. Layer 1 mask 2. StaticBody2D floor layer 2 mask 1 with rectangular collider. Add Input Map move_left, move_right and jump bound to Space. Place player above floor, not intersecting it. Coordinates use positive y downward. Attach script to Player. ```gdscript extends CharacterBody2D @export var speed: float = 160.0 @export var gravity: float = 900.0 @export var jump_speed: float = 340.0 func _draw() -> void: draw_rect(Rect2(-12, -16, 24, 32), Color.SKY_BLUE) draw_line(Vector2(-8, -4), Vector2(8, -4), Color.BLACK, 2.0) func _physics_process(delta: float) -> void: velocity.x = Input.get_axis("move_left", "move_right") * speed if not is_on_floor(): velocity.y += gravity * delta if Input.is_action_just_pressed("jump") and is_on_floor(): velocity.y = -jump_speed move_and_slide() ``` Scaffold C-DIALOGUE — Data-driven branching dialogue without personal data Setup: Create Control root Dialogue with VBoxContainer Box containing Label Line and Buttons A and B. Set Box anchors to full rect; Line autowrap if needed. Attach script to Dialogue. Data keys are fictional scene IDs. All three screens can fit in 640x360. This is a self-contained example; replace story text in the assignment. ```gdscript extends Control var page: String = "start" var story: Dictionary = { "start": {"text": "Two paths enter the archive.", "choices": [["Ask the keeper", "keeper"], ["Read a sign", "sign"]]}, "keeper": {"text": "The keeper offers a map.", "choices": [["Return", "start"]]}, "sign": {"text": "Quiet room to the east.", "choices": [["Return", "start"]]} } func _ready() -> void: $Box/A.pressed.connect(_choose.bind(0)) $Box/B.pressed.connect(_choose.bind(1)) _show() func _show() -> void: var entry: Dictionary = story[page] $Box/Line.text = entry["text"] var buttons: Array[Button] = [$Box/A, $Box/B] for i in range(buttons.size()): buttons[i].visible = i < entry["choices"].size() if buttons[i].visible: buttons[i].text = entry["choices"][i][0] $Box/A.grab_focus() func _choose(index: int) -> void: var choices: Array = story[page]["choices"] if index < 0 or index >= choices.size(): return page = str(choices[index][1]) _show() ``` Scaffold C-INVENTORY — Dictionary inventory with guarded spending Setup: Attach to Node InventoryLab for standalone testing. In a real level put the same functions on its inventory node and update UI after successful calls. Item IDs are short fixed strings, never a player’s name. A key represents one item type and its integer value is the quantity. ```gdscript extends Node var items: Dictionary = {"seed": 0, "water": 0} func add_item(id: String, amount: int = 1) -> bool: if not items.has(id) or amount <= 0: return false items[id] = int(items[id]) + amount return true func spend(id: String, amount: int = 1) -> bool: if not items.has(id) or amount <= 0 or int(items[id]) < amount: return false items[id] = int(items[id]) - amount return true func _ready() -> void: assert(add_item("seed", 2)) assert(spend("seed", 1)) assert(not spend("seed", 2)) assert(items["seed"] == 1) ```
Reusable top-down player with collision
GDScript (Godot 4, not Scratch, Python, JavaScript or C#). Scaffold C-BODY. Copy starts from this text. The desk does not run it.
Scene and node setup
Create CharacterBody2D root Player at (80,180); add CollisionShape2D with CircleShape2D radius 14. Attach this script to Player and save Player.tscn. Use C-FREE-MOVE input actions. Root collision layer 1, mask 2. Create a separate StaticBody2D Wall on layer 2 mask 1 with RectangleShape2D collision and a Polygon2D rectangle as visible child. Instance both under an unscaled Node2D World. Do not put a body inside another physics body.
extends CharacterBody2D
@export var speed: float = 160.0
func _draw() -> void:
draw_circle(Vector2.ZERO, 14.0, Color.CORNFLOWER_BLUE)
draw_line(Vector2(-8, 0), Vector2(8, 0), Color.WHITE, 3.0)
func _physics_process(_delta: float) -> void:
velocity = Input.get_vector("move_left", "move_right", "move_up", "move_down") * speed
move_and_slide()
Checks to run in the editor
- Player stops at the wall rather than passing through.
- Diagonal speed matches horizontal speed.
- Do not multiply velocity by delta before move_and_slide.
Native Godot 4.7.2.stable.official.ed1daf0bf headless compile and representative node-tree/runtime tests passed; see native-qa-results.json. Visual UI, audible output, exports and website submission still need separate checks.
A gravity-and-jump platform player
GDScript (Godot 4, not Scratch, Python, JavaScript or C#). Scaffold C-PLATFORM. Copy starts from this text. The desk does not run it.
Scene and node setup
CharacterBody2D root Player, child CollisionShape2D RectangleShape2D size (24,32), no scale changes. Layer 1 mask 2. StaticBody2D floor layer 2 mask 1 with rectangular collider. Add Input Map move_left, move_right and jump bound to Space. Place player above floor, not intersecting it. Coordinates use positive y downward. Attach script to Player.
extends CharacterBody2D
@export var speed: float = 160.0
@export var gravity: float = 900.0
@export var jump_speed: float = 340.0
func _draw() -> void:
draw_rect(Rect2(-12, -16, 24, 32), Color.SKY_BLUE)
draw_line(Vector2(-8, -4), Vector2(8, -4), Color.BLACK, 2.0)
func _physics_process(delta: float) -> void:
velocity.x = Input.get_axis("move_left", "move_right") * speed
if not is_on_floor():
velocity.y += gravity * delta
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = -jump_speed
move_and_slide()
Checks to run in the editor
- Player lands without sinking.
- Holding jump does not repeatedly jump in air.
- Do not multiply the whole velocity by delta before move_and_slide.
Native Godot 4.7.2.stable.official.ed1daf0bf headless compile and representative node-tree/runtime tests passed; see native-qa-results.json. Visual UI, audible output, exports and website submission still need separate checks.
Data-driven branching dialogue without personal data
GDScript (Godot 4, not Scratch, Python, JavaScript or C#). Scaffold C-DIALOGUE. Copy starts from this text. The desk does not run it.
Scene and node setup
Create Control root Dialogue with VBoxContainer Box containing Label Line and Buttons A and B. Set Box anchors to full rect; Line autowrap if needed. Attach script to Dialogue. Data keys are fictional scene IDs. All three screens can fit in 640x360. This is a self-contained example; replace story text in the assignment.
extends Control
var page: String = "start"
var story: Dictionary = {
"start": {"text": "Two paths enter the archive.", "choices": [["Ask the keeper", "keeper"], ["Read a sign", "sign"]]},
"keeper": {"text": "The keeper offers a map.", "choices": [["Return", "start"]]},
"sign": {"text": "Quiet room to the east.", "choices": [["Return", "start"]]}
}
func _ready() -> void:
$Box/A.pressed.connect(_choose.bind(0))
$Box/B.pressed.connect(_choose.bind(1))
_show()
func _show() -> void:
var entry: Dictionary = story[page]
$Box/Line.text = entry["text"]
var buttons: Array[Button] = [$Box/A, $Box/B]
for i in range(buttons.size()):
buttons[i].visible = i < entry["choices"].size()
if buttons[i].visible:
buttons[i].text = entry["choices"][i][0]
$Box/A.grab_focus()
func _choose(index: int) -> void:
var choices: Array = story[page]["choices"]
if index < 0 or index >= choices.size():
return
page = str(choices[index][1])
_show()
Checks to run in the editor
- Both start choices work.
- Return works from each branch; hidden B cannot trigger a nonexistent choice.
Native Godot 4.7.2.stable.official.ed1daf0bf headless compile and representative node-tree/runtime tests passed; see native-qa-results.json. Visual UI, audible output, exports and website submission still need separate checks.
Dictionary inventory with guarded spending
GDScript (Godot 4, not Scratch, Python, JavaScript or C#). Scaffold C-INVENTORY. Copy starts from this text. The desk does not run it.
Scene and node setup
Attach to Node InventoryLab for standalone testing. In a real level put the same functions on its inventory node and update UI after successful calls. Item IDs are short fixed strings, never a player’s name. A key represents one item type and its integer value is the quantity.
extends Node
var items: Dictionary = {"seed": 0, "water": 0}
func add_item(id: String, amount: int = 1) -> bool:
if not items.has(id) or amount <= 0:
return false
items[id] = int(items[id]) + amount
return true
func spend(id: String, amount: int = 1) -> bool:
if not items.has(id) or amount <= 0 or int(items[id]) < amount:
return false
items[id] = int(items[id]) - amount
return true
func _ready() -> void:
assert(add_item("seed", 2))
assert(spend("seed", 1))
assert(not spend("seed", 2))
assert(items["seed"] == 1)
Checks to run in the editor
- Spending more than owned returns false and does not change inventory.
- Unknown IDs and nonpositive amounts are rejected.
Native Godot 4.7.2.stable.official.ed1daf0bf headless compile and representative node-tree/runtime tests passed; see native-qa-results.json. Visual UI, audible output, exports and website submission still need separate checks.
Implement the final player action — evidence task
- 1. Build the core action and three ordinary-case tests.
- 2. Trace one input to its state effect in a short note.
Deliverable. Final core-action checkpoint and test note.
Acceptance checks
- VGC-L147-T1. All three cases produce expected behavior.
- VGC-L147-T2. The input path matches the architecture diagram.
Understanding question. Why test more than the initial spawn state?
Include a downloadable project ZIP with project.godot, .tscn, .gd and original asset files when code changes. A private, teacher-accessible download link to that ZIP is equivalent. Screenshots or a playthrough do not replace editable source.
If this is still shaky. Use placeholder shapes and one room. Keep all twelve acceptance tests visible, with honest not-yet-implemented statuses. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
Optional extra. Add one optional visual improvement only after the slice is playable end to end. Document one changed parameter, your prediction, and the observed effect; optional, not required for completion.
Turn in typed notes and a JPEG, PNG, or WebP screenshot. One https address is stored as text and is not opened, extracted, or run. An MP3, WAV, M4A, MP4, or WebM file up to 4 MB can show a demo. The demo does not replace editable source. Zip and code files are refused. If there is no private link, keep the project on the family computer and review the code in person.
- Your first 2D game
Free public tutorial; no account; external asset download is optional, not used by this course.
· Opening this link does not submit the lesson.
This is a preview. A parent assigns the lesson and chooses the due date after the student is enrolled. The answer key is not on this page.
