Game AI skill for Codex

Enemies that read well and stay on budget. Codex starts from player-facing tells and tunable numbers, then picks the lightest structure that fits: FSM, hierarchical FSM, behaviour tree or utility scoring. It uses the engine's navmesh where one exists, writes admissible A* for grids, gives agents perception and memory instead of omniscience, staggers decisions to fit the frame budget and adds debug draws that show what each agent is thinking.

  • Game Development
  • $game-ai
  • 206 lines
  • 2 files
  • ~2,250 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- game-ai

Installs to ~/.agents/skills/game-ai/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $game-ai, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds NPC and enemy AI for games - finite state machines, hierarchical states, behavior trees with blackboards, utility AI scoring, A* pathfinding on grids and navmeshes with good heuristics, steering and avoidance, perception (sight cones, hearing, memory) and AI tick budgeting - using engine tools where they exist (Unity NavMesh, Godot NavigationAgent, Unreal Behavior Trees and EQS). Use when enemies need to patrol, chase, flee or take cover, pathfinding is slow or produces ugly paths, AI logic has become a tangle of booleans, or many agents tank the frame rate.

Use it when

  • Designing enemy or NPC decision making
  • Pathfinding is slow, produces ugly paths or agents get stuck
  • Enemy logic has grown into tangled booleans
  • Many agents push AI time over the frame budget

Use something else when

How to check the result

Debug draws show each agent's state, cone and path; scripted hide-and-seek scenarios behave as specified; path tests pass; AI time at max agents stays within its frame slice.

The mistake it prevents

Letting AI read the player's exact position every frame, which makes enemies omniscient and unfair instead of reacting to what they could perceive.

~/.agents/skills/game-ai/SKILL.md

--- name: game-ai description: Builds NPC and enemy AI for games - finite state machines, hierarchical states, behavior trees with blackboards, utility AI scoring, A* pathfinding… license: MIT ---

Done well means: AI is fun and readable to the player before it is smart, each agent’s decision structure matches its complexity (FSM for simple, behaviour tree or utility for rich), pathfinding uses the engine’s navigation when available, decisions run on a budget instead of every frame, and a debug view shows what every agent is thinking.

Before writing code

  1. Read AGENTS.md and check what the engine provides: Unity AI Navigation package (NavMeshAgent, NavMeshSurface), Godot NavigationRegion2D/3D + NavigationAgent2D/3D, Unreal Behavior Trees, Blackboards, EQS and AIController. Use them instead of hand-rolling.
  2. Write the behaviour spec as player-facing tells: “Guard patrols, hears footsteps within 8 m, investigates for 5 s, chases on sight, gives up after losing sight for 4 s, telegraphs attacks for 0.6 s.” Designers tune these numbers; expose them as data.

Choose a decision structure

Agent complexityStructure
2-6 clear modes (patrol, chase, attack, flee)Finite state machine
Modes with sub-modes (combat: approach, strafe, reload)Hierarchical FSM
Many conditional behaviours, reused across enemy types, designer-authoredBehaviour tree with blackboard
Many competing desires (hunger, threat, ammo, cover) choosing the bestUtility AI (score each action, pick highest)
Long plans with preconditions (crafting, colony sims)GOAP or HTN planner; expensive, use sparingly

Start with the simplest that fits. Most action-game enemies are an FSM plus good animation tells.

FSM with explicit transitions

type State = "patrol" | "investigate" | "chase" | "attack" | "return";

function think(a: Agent, w: WorldView, dt: number) {
  a.stateTime += dt;
  const sees = canSee(a, w.player);
  if (sees) a.lastSeen = { pos: w.player.pos.clone(), time: w.time };

  switch (a.state) {
    case "patrol":
      followPatrol(a);
      if (sees) return enter(a, "chase");
      if (a.heardNoise) return enter(a, "investigate");
      break;
    case "investigate":
      moveTo(a, a.heardNoise!.pos);
      if (sees) return enter(a, "chase");
      if (a.stateTime > a.cfg.investigateSeconds) return enter(a, "return");
      break;
    case "chase":
      moveTo(a, a.lastSeen!.pos);
      if (sees && dist(a.pos, w.player.pos) < a.cfg.attackRange) return enter(a, "attack");
      if (w.time - a.lastSeen!.time > a.cfg.loseSightSeconds) return enter(a, "investigate");
      break;
    case "attack":
      faceTowards(a, w.player.pos);
      if (a.stateTime > a.cfg.windupSeconds) performAttack(a);
      if (a.stateTime > a.cfg.windupSeconds + a.cfg.recoverSeconds) return enter(a, "chase");
      break;
    case "return":
      moveTo(a, a.patrolPoints[a.patrolIndex]);
      if (sees) return enter(a, "chase");
      if (arrived(a)) return enter(a, "patrol");
      break;
  }
}

function enter(a: Agent, s: State) { a.state = s; a.stateTime = 0; a.onEnter?.(s); }

Rules: transitions live in one place per state; entering a state resets its timers; add hysteresis (different enter and exit distances) so agents do not flicker between states.

Behaviour trees

  • Composites: Sequence (all succeed in order), Selector (first that succeeds), Parallel. Decorators: Inverter, Cooldown, Timeout, Repeat. Leaves: Conditions and Actions returning Success, Failure or Running.
  • Shared data goes on a blackboard (targetActor, lastKnownPosition, coverPoint), not in node fields, so subtrees are reusable.
  • Re-evaluate higher-priority branches while a lower one is running (Unreal: decorator “Observer aborts: Lower Priority”); otherwise an agent finishes a long patrol leg while being shot.
  • Keep trees shallow and readable; a 200-node tree is a design smell, split into subtrees.

Utility AI

const actions = [
  { name: "attack",  score: (a) => a.hasTarget ? curve(a.targetDistance, 0, a.cfg.range) * a.ammo01 : 0 },
  { name: "reload",  score: (a) => 1 - a.ammo01 },
  { name: "takeCover", score: (a) => a.threat01 * (1 - a.health01) },
  { name: "heal",    score: (a) => a.hasMedkit ? Math.pow(1 - a.health01, 2) : 0 },
];
const best = actions.reduce((x, y) => (y.score(a) > x.score(a) ? y : x));

Normalise every input to 0-1, shape it with response curves, add a small bonus for the current action (inertia) to avoid dithering, and log scores in the debug view.

Pathfinding

WorldApproach
Engine with navigationBake a navmesh; use NavMeshAgent / NavigationAgent / MoveTo
Tile gridA* on the grid, 8-directional with octile heuristic; smooth with line-of-sight string pulling
Large open grid, many agents to one targetFlow field (one Dijkstra from target, all agents follow gradient)
Huge mapsHierarchical A* (clusters/portals) or precomputed region graph

A* on a grid with a binary heap:

import heapq, math

def octile(a, b):
    dx, dy = abs(a[0] - b[0]), abs(a[1] - b[1])
    return (dx + dy) + (math.sqrt(2) - 2) * min(dx, dy)

def astar(start, goal, passable, cost=lambda a, b: 1.0):
    open_heap = [(octile(start, goal), 0.0, start)]
    came, g = {start: None}, {start: 0.0}
    while open_heap:
        _, gc, cur = heapq.heappop(open_heap)
        if cur == goal:
            path = []
            while cur is not None:
                path.append(cur); cur = came[cur]
            return path[::-1]
        if gc > g[cur]:
            continue                                   # stale heap entry
        x, y = cur
        for dx, dy in ((1,0),(-1,0),(0,1),(0,-1),(1,1),(1,-1),(-1,1),(-1,-1)):
            nxt = (x + dx, y + dy)
            if not passable(nxt):
                continue
            if dx and dy and not (passable((x + dx, y)) and passable((x, y + dy))):
                continue                               # no corner cutting
            step = (math.sqrt(2) if dx and dy else 1.0) * cost(cur, nxt)
            ng = gc + step
            if ng < g.get(nxt, math.inf):
                g[nxt], came[nxt] = ng, cur
                heapq.heappush(open_heap, (ng + octile(nxt, goal), ng, nxt))
    return None
  • The heuristic must never overestimate (admissible) for optimal paths; Manhattan on an 8-directional grid overestimates diagonals and produces odd paths.
  • Weighted A* (h * 1.2) is faster and slightly suboptimal; often a good trade for games.
  • Cap search nodes per request and return partial paths toward the goal when exceeded.

Steering and crowds

  • Path following: seek the next waypoint, advance when within a radius, and slow down on arrival (arrive behaviour) to avoid orbiting the target.
  • Avoidance: engine RVO (Unity NavMeshAgent obstacle avoidance, Godot avoidance_enabled) or separation forces between neighbours from a spatial hash.
  • Do not path every agent every frame. Repath when the target moved more than a threshold or on a timer (0.25-1 s), staggered across agents.

Perception

  • Sight: distance check, then cone (dot(forward, dirToTarget) > cos(halfAngle)), then a raycast for occlusion, in that order (cheapest first).
  • Hearing: gameplay noise events with radius, not audio playback.
  • Memory: last known position plus timestamp; agents search there instead of knowing where the player is. Omniscient AI feels unfair.

Budget and performance

  • Tick decisions at 5-10 Hz and stagger agents (agentIndex % 6 == frame % 6); movement and animation still update every frame.
  • Level of detail: off-screen or distant agents think less often or use simplified logic.
  • Batch raycasts and pathfinding requests; engines offer async path queries.

Gotchas

  • Perfect aim and reaction are not fun. Add reaction delay (200-400 ms), aim error that shrinks over time, and attack tokens so only 1-2 enemies attack at once.
  • Agents stuck on corners: navmesh agent radius smaller than the collider, or navmesh not rebaked after level changes.
  • AI reading the player’s exact position every frame bypasses perception; route through memory.

Verification

  • Debug draw: state label above each agent, sight cones, current path, blackboard values or utility scores.
  • Scripted scenarios: player hides behind a wall, agent investigates last known position and gives up after the configured time.
  • Pathfinding unit tests on small grids: blocked goal returns null, no corner cutting, path cost equals expected optimum.
  • Profile with max agent count: AI time under its slice of the frame budget (e.g. 2 ms).

Hand-off summary

**Guard AI:** FSM patrol/investigate/chase/attack/return; config in GuardConfig asset
**Perception:** 90 deg cone, 15 m sight, raycast LOS; hearing via noise events; memory 4 s
**Navigation:** Godot NavigationAgent3D, repath every 0.5 s staggered; avoidance on
**Fairness:** 300 ms reaction, 0.6 s attack windup, max 2 attackers via tokens
**Perf:** 40 guards, AI 1.1 ms/frame at 8 Hz decisions

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Game AI"
  short_description: "Enemies that read well and stay on budget"
  default_prompt: "$game-ai Make guards that patrol, investigate noises and chase the player on sight"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem