Video Game Design and Coding

Week 25 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 25 — Enemies are state machines

## VGC-L121 — Plan readable enemy behavior

**Objective:** Describe an AI as explicit rules rather than human intelligence.

**Time:** 55 minutes

### Instruction
Game AI here means programmed behavior, not a model trained on personal data. Garden Patrol uses a guard that patrols, chases within a detection radius, and returns when the player escapes. Draw states and transitions before coding. Give the player a readable cue when the guard notices them. The guard does not need to be unbeatable or humanlike. Use an open room so direct steering is sufficient; maze navigation would require another system not included in this baseline.

### Worked example
A patrol guard follows two points. A separate manager handles damage, leaving steering reusable in another level with different round rules.

### Assignment
1. Draw Patrol, Chase and Return states with distance-based transitions.
2. Write one cue and one fairness constraint for each state.

### Acceptance checks
- Every state has a reachable transition.
- The design requires no network, training data or account.

**Submit:** Enemy-state diagram and fairness notes.

**Question:** Is this game AI the same as a generative chatbot?

**Support:** Use an open room and C-PATROL before C-GUARD-AI. Do not expect direct steering to navigate a maze. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Add a visible state label or icon and compare how it changes player understanding. 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-L122 — Implement a bounded patrol

**Objective:** Move between two points without overshoot jitter.

**Time:** 55 minutes

### Instruction
C-PATROL moves a CharacterBody2D toward one of two global endpoints. It reverses when close enough to the target, using a threshold related to speed times the physics step. Without a threshold, a guard can overshoot and oscillate around the point. Keep the path unobstructed: this recipe steers directly and does not search around walls. Test endpoints at least eighty pixels apart before adding detection. Use a visible guard shape distinct from the player.

### Worked example
A patrol guard follows two points. A separate manager handles damage, leaving steering reusable in another level with different round rules.

Scaffold C-PATROL — A two-point bounded patrol
Setup: CharacterBody2D root Guard with CollisionShape2D radius 12 and a visible Polygon2D or Sprite2D. Layer 4 mask 2 for walls, separate Area2D sensor if player detection is needed. Put under an unscaled Node2D level. Attach script. These endpoints are global coordinates; set points on one unobstructed walkway. This is direct steering, not pathfinding.
```gdscript
extends CharacterBody2D
@export var point_a: Vector2 = Vector2(200, 180)
@export var point_b: Vector2 = Vector2(440, 180)
@export var speed: float = 70.0
var toward_b: bool = true
func _physics_process(delta: float) -> void:
    var target := point_b if toward_b else point_a
    var offset := target - global_position
    if offset.length() <= maxf(3.0, speed * delta):
        toward_b = not toward_b
        velocity = Vector2.ZERO
    else:
        velocity = offset.normalized() * speed
    move_and_slide()
```

### Assignment
1. Build the patrol in an open test room.
2. Observe five reversals and test one deliberately obstructed route in a duplicate.

### Acceptance checks
- Unobstructed patrol reverses cleanly.
- The limitation at a wall is documented rather than called pathfinding.

**Submit:** Patrol source and five-reversal observations.

**Question:** Why can direct steering get stuck behind a wall?

**Support:** Use an open room and C-PATROL before C-GUARD-AI. Do not expect direct steering to navigate a maze. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Add a visible state label or icon and compare how it changes player understanding. 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-L123 — Add distance-based detection

**Objective:** Use world-space distance and a clear detection cue.

**Time:** 55 minutes

### Instruction
Distance compares positions in the same coordinate space. C-GUARD-AI uses global_position for both guard and target. Detection below 120 pixels enters Chase. Start by displaying the current state in a Label or changing a steady icon, not by adding damage immediately. Assign the Player to the exported target field. If the target is missing or freed, the guard stops safely. The detection radius is a game rule; this simple example can see through obstacles unless you add a separately tested visibility system.

### Worked example
A patrol guard follows two points. A separate manager handles damage, leaving steering reusable in another level with different round rules.

Scaffold C-GUARD-AI — A readable patrol/chase/return state machine
Setup: Create CharacterBody2D Guard with visible circle or polygon and matching collider. Layer 4 mask 2 for walls. Attach script and assign target to your Player instance in Inspector. Level is one open rectangle; this is steering, not navigation around walls. Damage remains a separate sensor/manager event. Draw detection radii on a design diagram before tuning values.
```gdscript
extends CharacterBody2D
enum Mode { PATROL, CHASE, RETURN }
@export var target: Node2D
@export var speed: float = 70.0
var mode: Mode = Mode.PATROL
var home: Vector2
var patrol_target: Vector2
func _ready() -> void:
    home = global_position
    patrol_target = home + Vector2(80, 0)
func _physics_process(delta: float) -> void:
    if not is_instance_valid(target):
        velocity = Vector2.ZERO
        return
    var distance: float = global_position.distance_to(target.global_position)
    if mode != Mode.CHASE and distance < 120.0:
        mode = Mode.CHASE
    elif mode == Mode.CHASE and distance > 180.0:
        mode = Mode.RETURN
    var destination: Vector2
    if mode == Mode.CHASE:
        destination = target.global_position
    elif mode == Mode.RETURN:
        destination = home
        if global_position.distance_to(home) <= maxf(3.0, speed * delta):
            mode = Mode.PATROL
    else:
        destination = patrol_target
        if global_position.distance_to(patrol_target) <= maxf(3.0, speed * delta):
            patrol_target = home if patrol_target != home else home + Vector2(80, 0)
    var offset := destination - global_position
    velocity = offset.normalized() * minf(speed, offset.length() / maxf(delta, 0.001))
    move_and_slide()
```

### Assignment
1. Add detection using C-GUARD-AI and a visible state cue.
2. Test at distances clearly inside and outside 120 pixels.

### Acceptance checks
- Inside radius enters Chase.
- Missing target stops without invalid-instance errors.

**Submit:** Detection checkpoint and distance test table.

**Question:** Why compare both positions in global space?

**Support:** Use an open room and C-PATROL before C-GUARD-AI. Do not expect direct steering to navigate a maze. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Add a visible state label or icon and compare how it changes player understanding. 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-L124 — Use hysteresis to avoid flickering states

**Objective:** Choose different chase-entry and chase-exit distances.

**Time:** 55 minutes

### Instruction
If a guard enters and exits Chase at exactly the same radius, small position changes can make it flicker between states. Hysteresis uses different thresholds: enter below 120 pixels, exit above 180. Between them, the current chase state is preserved. C-GUARD-AI returns home after escape, then resumes patrol. Test the boundary band deliberately rather than only running far away. Hysteresis is a general technique for stable decisions near a threshold.

### Worked example
A patrol guard follows two points. A separate manager handles damage, leaving steering reusable in another level with different round rules.

Scaffold C-GUARD-AI — A readable patrol/chase/return state machine
Setup: Create CharacterBody2D Guard with visible circle or polygon and matching collider. Layer 4 mask 2 for walls. Attach script and assign target to your Player instance in Inspector. Level is one open rectangle; this is steering, not navigation around walls. Damage remains a separate sensor/manager event. Draw detection radii on a design diagram before tuning values.
```gdscript
extends CharacterBody2D
enum Mode { PATROL, CHASE, RETURN }
@export var target: Node2D
@export var speed: float = 70.0
var mode: Mode = Mode.PATROL
var home: Vector2
var patrol_target: Vector2
func _ready() -> void:
    home = global_position
    patrol_target = home + Vector2(80, 0)
func _physics_process(delta: float) -> void:
    if not is_instance_valid(target):
        velocity = Vector2.ZERO
        return
    var distance: float = global_position.distance_to(target.global_position)
    if mode != Mode.CHASE and distance < 120.0:
        mode = Mode.CHASE
    elif mode == Mode.CHASE and distance > 180.0:
        mode = Mode.RETURN
    var destination: Vector2
    if mode == Mode.CHASE:
        destination = target.global_position
    elif mode == Mode.RETURN:
        destination = home
        if global_position.distance_to(home) <= maxf(3.0, speed * delta):
            mode = Mode.PATROL
    else:
        destination = patrol_target
        if global_position.distance_to(patrol_target) <= maxf(3.0, speed * delta):
            patrol_target = home if patrol_target != home else home + Vector2(80, 0)
    var offset := destination - global_position
    velocity = offset.normalized() * minf(speed, offset.length() / maxf(delta, 0.001))
    move_and_slide()
```

### Assignment
1. Test approach at 130 then 110 pixels, retreat to 150 then 190.
2. Record state after each step.

### Acceptance checks
- The sequence is Patrol/Return as appropriate, Chase, Chase, Return.
- State does not rapidly toggle near 120 after chase begins.

**Submit:** Boundary-band state trace and AI checkpoint.

**Question:** Why should a chasing guard remain in Chase at distance 150?

**Support:** Use an open room and C-PATROL before C-GUARD-AI. Do not expect direct steering to navigate a maze. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Add a visible state label or icon and compare how it changes player understanding. 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-L125 — Integrate a fair guard challenge

**Objective:** Separate steering, damage sensing and round rules.

**Time:** 55 minutes

### Instruction
A guard’s motion script should not own the entire game. Add a separate Area2D contact sensor that notifies the manager when the player is touched. Reuse the recovery window or safe checkpoint policy from earlier games. Test whether the guard’s speed leaves a possible escape route. Show its detection cue without requiring sound. A guard that instantly catches the player at spawn is a layout problem even if its movement code is correct.

### Worked example
A patrol guard follows two points. A separate manager handles damage, leaving steering reusable in another level with different round rules.

Scaffold C-GUARD-AI — A readable patrol/chase/return state machine
Setup: Create CharacterBody2D Guard with visible circle or polygon and matching collider. Layer 4 mask 2 for walls. Attach script and assign target to your Player instance in Inspector. Level is one open rectangle; this is steering, not navigation around walls. Damage remains a separate sensor/manager event. Draw detection radii on a design diagram before tuning values.
```gdscript
extends CharacterBody2D
enum Mode { PATROL, CHASE, RETURN }
@export var target: Node2D
@export var speed: float = 70.0
var mode: Mode = Mode.PATROL
var home: Vector2
var patrol_target: Vector2
func _ready() -> void:
    home = global_position
    patrol_target = home + Vector2(80, 0)
func _physics_process(delta: float) -> void:
    if not is_instance_valid(target):
        velocity = Vector2.ZERO
        return
    var distance: float = global_position.distance_to(target.global_position)
    if mode != Mode.CHASE and distance < 120.0:
        mode = Mode.CHASE
    elif mode == Mode.CHASE and distance > 180.0:
        mode = Mode.RETURN
    var destination: Vector2
    if mode == Mode.CHASE:
        destination = target.global_position
    elif mode == Mode.RETURN:
        destination = home
        if global_position.distance_to(home) <= maxf(3.0, speed * delta):
            mode = Mode.PATROL
    else:
        destination = patrol_target
        if global_position.distance_to(patrol_target) <= maxf(3.0, speed * delta):
            patrol_target = home if patrol_target != home else home + Vector2(80, 0)
    var offset := destination - global_position
    velocity = offset.normalized() * minf(speed, offset.length() / maxf(delta, 0.001))
    move_and_slide()
```

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. Integrate one guard into an open Garden Patrol room with a collectible goal.
2. Test spawn safety, escape, contact recovery and completion.

### Acceptance checks
- Player can leave the start without immediate unavoidable damage.
- Contact and completion respect the manager’s phase.

**Submit:** Guard challenge checkpoint and four-test log.

**Question:** Why keep guard steering separate from the manager’s lives or win state?

**Support:** Use an open room and C-PATROL before C-GUARD-AI. Do not expect direct steering to navigate a maze. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Add a visible state label or icon and compare how it changes player understanding. 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)

---