KIKI DAVID

Case Study — Technical Game Design · Systems

GPE205 — Dune Tank Game

For a Unity programming class I built finite-state-machine AI on top of the course's tank-game framework: a sandworm faction with a three-state machine, a Harkonnen faction with a seven-state machine, and a GameManager that runs the whole flow from title screen to game over.

The design is the interesting part: one shared decision loop, every faction layered on top of it, documented with the README's state diagrams — with an honest record of what's built, what's wired but stubbed, and what's still on the plan.

My Role Technical Game Designer · AI Programming
Tools Unity · C#
Project Academic Programming Project (GPE205)
Timeline 2024

The challenge. Turn the course's tank framework into a dangerous, Dune-like arena — AI factions that guard, hunt, flee, and lurk — driven by explicit state machines a reviewer can read top to bottom.

01

Project

The repo is a public Unity project built as a programming class assignment. The course supplied the tank framework — movement, shooting, spawning, map generation, and pickups. My work is the layer on top: the AI faction controllers and the game-flow state machine, plus the README that documents the AI. The point of the exercise was to be able to point at any enemy and say exactly what it's doing and why.

The AI follows a Dune-flavored fiction: a sandworm (Shai Hulud) that lurks by sound and consumes everything in its path; the Harkonnen, who mine spice at a post and defend it; and two more factions — Sardaukar and Smuggler — designed in the plan but not yet built. The README says so plainly: the project is in progress.

02

The FSM Architecture

Everything sits on one base AIController. It holds a 14-state enum (ChooseTarget, Seek, GuardDesert, WormAttack, WormRest, GuardSpice, Defend, Scan, Chase, Attack, Flee, BackToPost, Steal, Mine) and a MakeDecisions() loop — a switch/case over the current state, where each case runs the state's action and then checks its transitions. ChangeState() stamps the time of every transition; Seek() overloads rotate toward and advance the pawn; a RunAway() builds a flee vector away from the target, scaled by distance so the AI keeps a minimum gap; and distance helpers like isDistanceLessThan() power the transitions.

Shai Hulud — three states. GuardDesert (hidden, idle — the starting state), WormAttack (seek and consume, triggered by sound), WormRest (after reaching the sand's rim, a timer counts up, then back to GuardDesert). The README names them Asleep → Attack → Rest.

Harkonnen — seven states. GuardSpice (mining at the post — the starting state), Scan (rotate), Defend, Chase and Attack (pursue and assault Fremen; Attack doubles speed and rate of fire), Flee (pack up the mining rig and fly from the sandworm), and BackToPost (return to the post). The transition graph is written out explicitly: GuardSpice → Scan/Flee, Chase → Attack/Flee, Attack → Chase/Flee, Flee → BackToPost → GuardSpice.

03

Implemented, Wired & Still Planned

I want the record to be as exact as the code. Here's the honest split:

  • Implemented & documented. The FSM structure — enum, decision loop, ChangeState with transition timestamps, Seek overloads, RunAway's flee vector, distance helpers. The full transition graphs for Shai Hulud and Harkonnen. The GameManager's five-state flow, the keyboard-driven state tester, spawning (player + 8 AI units), and player-death → lives → respawn-or-game-over. The README with its state diagrams.
  • Wired but stubbed. Several per-state actions are log statements or empty placeholders (e.g. the worm's invisible/eat, the Harkonnen's MineSpice/Shoot/DoubleSpeed), and the perception predicates (IsFremenInAttackRange, DetectShaiHulud, IsAtPost) are placeholders that return false. canHear() — the sound-based "sense" behind the sandworm's ambush — is designed around a NoiseMaker but the integration is commented out, so it currently returns false.
  • Still planned. Sardaukar and Smuggler controllers are stubs with no FSM yet; the README lists them as coming in future versions, and ResetGameState only logs for now.

In practice this means the machines are fully authored — the states, the transitions, the timers — while the leaf behaviors and senses are the part left unfinished in the build you can read today.

04

Game Flow & Testability

A singleton GameManager drives the whole app as a five-state machine — Title, Main Menu, Settings, Play, and Game Over — each backed by a screen object. DeactivateAllStates() turns everything off, then an ActivateXScreen() method turns exactly one on, so two screens can never overlap. Activating Play generates the map and spawns the player plus eight AI units; player death costs a life, then respawns the player or calls the game over.

A companion GameStateTester lets me flip between all five screens from the keyboard (keys 1–5). It's a deliberate dev tool: I can exercise every state and its transitions without building UI first, which kept the loop for testing tight while the game flow was still being written.

05

Key Design Decisions

1 · One shared decision loop

The base controller owns the switch/case; each faction overrides MakeDecisions() to add its own states and transitions on top.

Why. Transition logic reads top-to-bottom like a spec, and extending means overriding one method instead of rewriting control flow.

Result. Both faction machines were wired without touching the base movement code.

2 · States as data, actions as methods

Each state runs an action method, then checks its transitions; ChangeState() records the time of every shift.

Why. Separating "what happens next" from "how the pawn moves" keeps states reviewable and testable in isolation.

Result. The whole graph could be designed before every behavior was built — which is exactly why the stubs are still honest.

3 · Perception is its own system

The sandworm is inert until it can hear. canHear() wraps a NoiseMaker so detection logic lives in one place for every AI.

Why. One shared "sense" keeps the read of danger consistent across factions.

Result. Designed and stubbed — the NoiseMaker integration is commented out and canHear() returns false in the current build.

4 · The framework is the sandbox, not the feat

Movement, shooting, spawning, and map generation came with the course project; the AI and the game flow are mine.

Why. Honest attribution — reviewers can compare my systems directly against the inherited baseline in the same repo.

Result. The README and this page mark exactly what's my layer versus what the course provided.

06

Evidence

The public source repo is the record — README with state diagrams, the AI controllers, the GameManager, and the course framework beside them.

Note. An in-progress class project. The FSM architecture, transition graphs, game-flow layer, and documentation are complete; several per-state behaviors and senses remain stubbed, and two of the four planned factions are unbuilt — exactly as the README states.

07

What I Learned

  • State machines make AI legible. A switch you can read top to bottom beats a tangle of booleans — for me, and for anyone reviewing the code later.
  • Wiring the graph before filling in the leaves works. I could design and review every transition before each behavior existed, so the skeleton was inspectable even mid-build.
  • Honesty about scope reads better than polish. The README names the unfinished factions and the stubbed behaviors; the project documents what it is at every step.
  • Attribution keeps a portfolio honest. Being able to point at the framework boundary — "here's what I wrote, here's what I was given" — makes both halves clearer.
What I Did Technical Game Design · FSM AI · Game Flow · Documentation
Tools Unity · C#
Evidence Public repo — README, AI controllers, GameManager

Note. This is a supporting portfolio piece: a systems-and-code sample from a programming course, documented as it stands — including what remains unfinished.