From fxgl-skills
Implement AI movement and pathfinding in FXGL — set up an AStarGrid, find paths using AStarPathfinder, attach AStarMoveComponent or RandomAStarMoveComponent to entities, implement GOAP (Goal-Oriented Action Planning) with world state and action preconditions, add SenseAI for vision and hearing, set up waypoint patrol routes, generate dungeons and mazes procedurally. Use this skill when making enemies chase the player, implementing patrol behaviours, building GOAP NPC AI, adding pathfinding to a tile-based game, or generating procedural levels.
How this skill is triggered — by the user, by Claude, or both
Slash command
/fxgl-skills:fxgl-ai-pathfindingThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
```java
// Create grid after level is loaded (in initGame, after setLevelFromMap)
private AStarGrid grid;
@Override
protected void initGame() {
setLevelFromMap("level1.tmx");
// Build grid: tile size must match Tiled tile width/height
grid = AStarGrid.fromWorld(getGameWorld(), 32, 32);
// Cells corresponding to WALL-type entities are automatically NOT_WALKABLE
// Mark additional non-walkable cells:
getGameWorld().getEntitiesByType(EntityType.OBSTACLE).forEach(e -> {
int cx = (int)(e.getX() / 32);
int cy = (int)(e.getY() / 32);
grid.getCell(cx, cy).setState(CellState.NOT_WALKABLE);
});
}
// Returns null if no path exists
List<AStarCell> path = grid.getAStarSearch().findPath(
(int)(startX / 32), // cell X of start
(int)(startY / 32), // cell Y of start
(int)(goalX / 32), // cell X of goal
(int)(goalY / 32) // cell Y of goal
);
if (path != null) {
path.forEach(cell -> {
double worldX = cell.getX() * 32;
double worldY = cell.getY() * 32;
System.out.println("Step: " + worldX + ", " + worldY);
});
}
// In enemy factory
@Spawns("enemy")
public Entity newEnemy(SpawnData data) {
AStarMoveComponent astar = new AStarMoveComponent(new AStarGridView(grid));
return entityBuilder(data)
.type(EntityType.ENEMY)
.view("enemy.png")
.bbox(BoundingShape.box(32, 32))
.with(astar)
.with(new EnemyAIComponent())
.build();
}
// In EnemyAIComponent.onUpdate — periodically recalculate path
public class EnemyAIComponent extends Component {
private AStarMoveComponent astar;
private double recalcTimer = 0;
@Override
public void onUpdate(double tpf) {
recalcTimer += tpf;
if (recalcTimer >= 0.5) { // recalc every 0.5s (not every frame — expensive)
recalcTimer = 0;
Entity player = getGameWorld().getSingleton(e -> e.isType(EntityType.PLAYER));
astar.moveToCell(
(int)(player.getX() / 32),
(int)(player.getY() / 32)
);
}
}
}
RandomAStarMoveComponent wander = new RandomAStarMoveComponent(new AStarGridView(grid));
wander.setMoveSpeed(120); // pixels per second
wander.setMinWanderDistance(3); // min distance in cells between waypoints
wander.setMaxWanderDistance(8); // max distance
entity.addComponent(wander);
// Entity wanders autonomously — no further code needed
WaypointMoveComponent patrol = new WaypointMoveComponent();
patrol.setSpeed(100);
patrol.setLooping(true);
patrol.addWaypoint(new Point2D(100, 200));
patrol.addWaypoint(new Point2D(500, 200));
patrol.addWaypoint(new Point2D(500, 400));
patrol.addWaypoint(new Point2D(100, 400));
entity.addComponent(patrol);
// Pause patrol on player detection, resume when player leaves
patrol.pause();
patrol.resume();
// World state is a Map<String, Boolean>
Map<String, Boolean> worldState = new HashMap<>();
worldState.put("hasAmmo", true);
worldState.put("hasWeapon", false);
worldState.put("enemyDead", false);
worldState.put("inRange", false);
// Goal: what we want to achieve
Map<String, Boolean> goal = new HashMap<>();
goal.put("enemyDead", true);
// Each action: preconditions + effects + cost + perform logic
public class FindWeaponAction extends GoapAction {
public FindWeaponAction() {
// Preconditions: none (always possible if weapon exists in world)
// Effects: hasWeapon = true
addEffect("hasWeapon", true);
setCost(2.0f);
}
@Override
public boolean checkProceduralPrecondition(Entity agent) {
// Return true only if a weapon entity exists in the world
return !getGameWorld().getEntitiesByType(EntityType.WEAPON).isEmpty();
}
@Override
public boolean perform(Entity agent) {
// Move toward nearest weapon and pick it up
Entity weapon = getGameWorld().getClosestEntity(agent,
e -> e.isType(EntityType.WEAPON));
agent.getComponent(AStarMoveComponent.class).moveTo(weapon.getPosition());
if (agent.getPosition().distance(weapon.getPosition()) < 20) {
weapon.removeFromWorld();
return true; // action complete
}
return false; // still in progress
}
}
public class AttackAction extends GoapAction {
public AttackAction() {
addPrecondition("hasWeapon", true);
addPrecondition("inRange", true);
addEffect("enemyDead", true);
addEffect("hasAmmo", false);
setCost(1.0f);
}
@Override
public boolean perform(Entity agent) {
agent.getComponent(AttackComponent.class).attack();
return true;
}
}
public class MoveInRangeAction extends GoapAction {
public MoveInRangeAction() {
addPrecondition("hasWeapon", true);
addEffect("inRange", true);
setCost(1.5f);
}
@Override
public boolean perform(Entity agent) {
Entity player = getGameWorld().getSingleton(e -> e.isType(EntityType.PLAYER));
if (agent.getPosition().distance(player.getPosition()) < 100) {
return true;
}
agent.getComponent(AStarMoveComponent.class).moveTo(player.getPosition());
return false;
}
}
List<GoapAction> availableActions = List.of(
new FindWeaponAction(),
new MoveInRangeAction(),
new AttackAction()
);
Queue<GoapAction> plan = GoapPlanner.plan(agentEntity, availableActions, worldState, goal);
if (plan != null) {
// Execute plan in sequence
GoapAction current = plan.poll();
// In onUpdate: execute current action, advance to next when complete
}
// Add SenseAIComponent to enemy entity
SenseComponent sense = new SenseComponent(250.0, 120.0); // range=250, fov=120 degrees
sense.setOnEntered(other -> {
if (other.isType(EntityType.PLAYER)) {
isPlayerDetected = true;
getComponent(AStarMoveComponent.class).moveTo(other.getPosition());
}
});
sense.setOnLeft(other -> {
if (other.isType(EntityType.PLAYER)) {
isPlayerDetected = false;
getComponent(WaypointMoveComponent.class).resume();
}
});
entity.addComponent(sense);
// Config
DungeonConfig config = new DungeonConfig()
.gridWidth(40)
.gridHeight(40)
.minRoomSize(5)
.maxRoomSize(12)
.maxRooms(15);
// Generate
DungeonGenerator generator = new DungeonGenerator(config);
Grid2D<DungeonCell> dungeon = generator.generate();
// Render
dungeon.forEach((cell, x, y) -> {
int worldX = x * TILE_SIZE;
int worldY = y * TILE_SIZE;
switch (cell.getType()) {
case FLOOR -> spawn("floor", worldX, worldY);
case WALL -> spawn("wall", worldX, worldY);
case CORRIDOR -> spawn("floor", worldX, worldY); // treat corridor as floor
case DOOR -> spawn("door", worldX, worldY);
case BOSS_ROOM -> spawn("bossFloor",worldX, worldY);
}
});
// Player starts in first room
Room startRoom = generator.getRooms().get(0);
spawn("player", startRoom.getCenterX() * TILE_SIZE, startRoom.getCenterY() * TILE_SIZE);
// Boss in last room
Room bossRoom = generator.getRooms().get(generator.getRooms().size() - 1);
spawn("boss", bossRoom.getCenterX() * TILE_SIZE, bossRoom.getCenterY() * TILE_SIZE);
MazeGenerator mazeGen = new MazeGenerator(20, 15); // width, height in cells
Grid2D<MazeCell> maze = mazeGen.generate();
maze.forEach((cell, x, y) -> {
if (cell.hasTopWall()) spawnWall(x, y, "top");
if (cell.hasLeftWall()) spawnWall(x, y, "left");
if (cell.hasRightWall()) spawnWall(x, y, "right");
if (cell.hasBottomWall()) spawnWall(x, y, "bottom");
});
AStarGrid after every level load — the grid caches cell states from the
world; stale grids cause enemies to walk through walls spawned in the new level.AStarGrid.fromWorld marks only STATIC physics bodies as NOT_WALKABLE — dynamic
entities (other enemies) are ignored. Handle entity-entity avoidance separately.null when no plan is possible with the given actions. Always
null-check and handle with a default behaviour (idle, wander, alert).WaypointMoveComponent requires exact world coordinates (pixels) not grid coordinates.
Multiply grid cell indices by tile size.DungeonConfig.seed(long) for reproducible layouts
(e.g., seeded from the current level number for consistent procedural content).entity.setRotation(angle) each frame).Provides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub johannesrabauer/fxgl-skills --plugin fxgl-skills