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.
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
Instruções da origem · Visualização somente leitura
name
fxgl-ai-pathfinding
description
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.
RandomAStarMoveComponentwander=newRandomAStarMoveComponent(newAStarGridView(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
Waypoint Patrol
WaypointMoveComponentpatrol=newWaypointMoveComponent();
patrol.setSpeed(100);
patrol.setLooping(true);
patrol.addWaypoint(newPoint2D(100, 200));
patrol.addWaypoint(newPoint2D(500, 200));
patrol.addWaypoint(newPoint2D(500, 400));
patrol.addWaypoint(newPoint2D(100, 400));
entity.addComponent(patrol);
// Pause patrol on player detection, resume when player leaves
patrol.pause();
patrol.resume();
GOAP (Goal-Oriented Action Planning)
Define world state
// World state is a Map<String, Boolean>
Map<String, Boolean> worldState = newHashMap<>();
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 = newHashMap<>();
goal.put("enemyDead", true);
Define actions
// Each action: preconditions + effects + cost + perform logicpublicclassFindWeaponActionextendsGoapAction {
publicFindWeaponAction() {
// Preconditions: none (always possible if weapon exists in world)// Effects: hasWeapon = true
addEffect("hasWeapon", true);
setCost(2.0f);
}
@OverridepublicbooleancheckProceduralPrecondition(Entity agent) {
// Return true only if a weapon entity exists in the worldreturn !getGameWorld().getEntitiesByType(EntityType.WEAPON).isEmpty();
}
@Overridepublicbooleanperform(Entity agent) {
// Move toward nearest weapon and pick it upEntityweapon= getGameWorld().getClosestEntity(agent,
e -> e.isType(EntityType.WEAPON));
agent.getComponent(AStarMoveComponent.class).moveTo(weapon.getPosition());
if (agent.getPosition().distance(weapon.getPosition()) < 20) {
weapon.removeFromWorld();
returntrue; // action complete
}
returnfalse; // still in progress
}
}
publicclassAttackActionextendsGoapAction {
publicAttackAction() {
addPrecondition("hasWeapon", true);
addPrecondition("inRange", true);
addEffect("enemyDead", true);
addEffect("hasAmmo", false);
setCost(1.0f);
}
@Overridepublicbooleanperform(Entity agent) {
agent.getComponent(AttackComponent.class).attack();
returntrue;
}
}
publicclassMoveInRangeActionextendsGoapAction {
publicMoveInRangeAction() {
addPrecondition("hasWeapon", true);
addEffect("inRange", true);
setCost(1.5f);
}
@Overridepublicbooleanperform(Entity agent) {
Entityplayer= getGameWorld().getSingleton(e -> e.isType(EntityType.PLAYER));
if (agent.getPosition().distance(player.getPosition()) < 100) {
returntrue;
}
agent.getComponent(AStarMoveComponent.class).moveTo(player.getPosition());
returnfalse;
}
}
Run the planner
List<GoapAction> availableActions = List.of(
newFindWeaponAction(),
newMoveInRangeAction(),
newAttackAction()
);
Queue<GoapAction> plan = GoapPlanner.plan(agentEntity, availableActions, worldState, goal);
if (plan != null) {
// Execute plan in sequenceGoapActioncurrent= plan.poll();
// In onUpdate: execute current action, advance to next when complete
}
// ConfigDungeonConfigconfig=newDungeonConfig()
.gridWidth(40)
.gridHeight(40)
.minRoomSize(5)
.maxRoomSize(12)
.maxRooms(15);
// GenerateDungeonGeneratorgenerator=newDungeonGenerator(config);
Grid2D<DungeonCell> dungeon = generator.generate();
// Render
dungeon.forEach((cell, x, y) -> {
intworldX= x * TILE_SIZE;
intworldY= 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 floorcase DOOR -> spawn("door", worldX, worldY);
case BOSS_ROOM -> spawn("bossFloor",worldX, worldY);
}
});
// Player starts in first roomRoomstartRoom= generator.getRooms().get(0);
spawn("player", startRoom.getCenterX() * TILE_SIZE, startRoom.getCenterY() * TILE_SIZE);
// Boss in last roomRoombossRoom= generator.getRooms().get(generator.getRooms().size() - 1);
spawn("boss", bossRoom.getCenterX() * TILE_SIZE, bossRoom.getCenterY() * TILE_SIZE);
Maze Generation
MazeGeneratormazeGen=newMazeGenerator(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");
});
Gotchas
Rebuild 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.
Recalculate paths at intervals, not every frame — A* on a 40×40 grid costs ~0.5ms.
At 60fps with 10 enemies, that's 300ms/s wasted. Recalc every 0.5s max.
AStarGrid.fromWorld marks only STATIC physics bodies as NOT_WALKABLE — dynamic
entities (other enemies) are ignored. Handle entity-entity avoidance separately.
GOAP planner returns 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.
Dungeon generation is random — use DungeonConfig.seed(long) for reproducible layouts
(e.g., seeded from the current level number for consistent procedural content).
SenseAI field-of-view is from the entity's forward direction — make sure your enemy
entity faces the direction of travel (update entity.setRotation(angle) each frame).