Skip to main content 홈 크리에이터 johannesrabauer fxgl-skills fxgl-topdown
fxgl-topdown Build a top-down 2D game in FXGL — disable gravity for the physics world, implement 8-direction normalized movement, bind the camera to follow the player with world bounds, aim and fire projectiles toward the mouse cursor, spawn a melee attack hitbox in the facing direction, trigger NPC interactions on proximity, handle area/room transitions at screen edges, display a minimap with entity dots, and load Tiled TMX tile-based worlds. Use this skill when building a Zelda-style adventure, twin-stick shooter, top-down RPG, arena shooter, or any bird's-eye-view 2D game.
설치로 이동 Skills Marketplace 커뮤니티가 만든 AI 스킬을 발견하고 탐색하세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/JohannesRabauer/fxgl-skills --skill fxgl-topdown명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Zip 다운로드 다운로드 중... 이 저장소의 다른 Skills Add particle systems and visual effects to an FXGL game — configure ParticleEmitter with function-based setters (setVelocityFunction, setAccelerationFunction, setExpireFunction, setScaleFunction, setSpawnPointFunction); use built-in factory emitters (fire, explosion, smoke, rain); apply image-textured particles with multiplyColor / toColor; write custom per-particle physics via setControl; attach TrailParticleComponent for motion trails; apply SlowTimeEffect for bullet-time; apply WobbleEffect for screen-shake; stack multiple effects with EffectComponent; build fireworks displays. Use this skill when adding explosions, fire, smoke, rain, motion trails, impact bursts, bullet-time slow-motion, screen shake, or any particle-based visual polish.
Start a new FXGL project through a bounded Socratic discovery flow, then turn the answers into a docs-first starter specification with ADRs, an arc42-lite architecture summary, and Mermaid use-case diagrams before scaffolding the project itself. Continue using the same questioning approach for initialization choices, and require documentation updates plus automated tests for every implemented step.
Build a Breakout or Arkanoid style game in FXGL — control a paddle horizontally, keep a ball at constant speed, bounce it off walls, paddle, and bricks, load brick layouts from level data, angle the reflection based on paddle hit position, handle lives and bottom-drain loss, spawn falling power-ups, and finish the level when all breakable bricks are destroyed.
name fxgl-topdown description Build a top-down 2D game in FXGL — disable gravity for the physics world, implement 8-direction normalized movement, bind the camera to follow the player with world bounds, aim and fire projectiles toward the mouse cursor, spawn a melee attack hitbox in the facing direction, trigger NPC interactions on proximity, handle area/room transitions at screen edges, display a minimap with entity dots, and load Tiled TMX tile-based worlds. Use this skill when building a Zelda-style adventure, twin-stick shooter, top-down RPG, arena shooter, or any bird's-eye-view 2D game.
triggers ["top-down","bird's eye","8-direction","twin-stick","top-down RPG","Zelda-style","arena shooter","no gravity","top-down movement","aim at mouse"] compatibility Java 17+, FXGL 21.x
category fxgl/game-types tags ["fxgl","java","javafx","game-types","topdown"] metadata {"author":"fxgl-skills","version":"1.0","fxgl-version":"21.1"} allowed-tools ["Read","Write","Edit","Bash"]
FXGL Top-Down 2D
World Without Gravity
@Override
protected void initPhysics () {
getPhysicsWorld().setGravity(0 , 0 );
}
Player Entity (8-Direction)
@Spawns("player")
public Entity newPlayer (SpawnData data) {
PhysicsComponent physics = new PhysicsComponent ();
physics.setBodyType(BodyType.DYNAMIC);
physics.setFixedRotation(true );
FixtureDef fd = new FixtureDef ();
fd.setFriction(0.0f );
fd.setDensity(1.0f );
physics.addFixtureDef(fd);
return entityBuilder(data)
.type(EntityType.PLAYER)
.view("player.png" )
.bbox(BoundingShape.circle(14 ))
.with(physics)
.with(new CollidableComponent (true ))
.with(new TopDownPlayerComponent ())
.build();
}
TopDownPlayerComponent — 8-Direction Movement
public class TopDownPlayerComponent extends Component {
private PhysicsComponent physics;
private ;
{
, dy = ;
(getInput().isHeld(KeyCode.W) || getInput().isHeld(KeyCode.UP)) dy -= ;
(getInput().isHeld(KeyCode.S) || getInput().isHeld(KeyCode.DOWN)) dy += ;
(getInput().isHeld(KeyCode.A) || getInput().isHeld(KeyCode.LEFT)) dx -= ;
(getInput().isHeld(KeyCode.D) || getInput().isHeld(KeyCode.RIGHT)) dx += ;
(dx != && dy != ) {
dx *= ;
dy *= ;
}
physics.setVelocityX(dx * MOVE_SPEED);
physics.setVelocityY(dy * MOVE_SPEED);
(dx != || dy != ) {
entity.setRotation(Math.toDegrees(Math.atan2(dy, dx)));
}
}
}
static
final
double
MOVE_SPEED
=
180.0
@Override
public
void
onUpdate
(double tpf)
double
dx
=
0
0
if
1
if
1
if
1
if
1
if
0
0
0.7071
0.7071
if
0
0
Aim at Mouse Cursor
public void faceMouseCursor () {
Point2D mouseWorld = getInput().getMousePositionWorld();
Point2D playerPos = entity.getCenter();
double angle = Math.toDegrees(
Math.atan2(mouseWorld.getY() - playerPos.getY(),
mouseWorld.getX() - playerPos.getX()));
entity.setRotation(angle);
}
Ranged Attack Toward Mouse
private void fireProjectile () {
Point2D mousePos = getInput().getMousePositionWorld();
Point2D playerPos = player.getCenter();
Point2D direction = mousePos.subtract(playerPos).normalize();
spawn("bullet" , new SpawnData (playerPos.getX(), playerPos.getY())
.put("dir" , direction));
}
@Spawns("bullet")
public Entity newBullet (SpawnData data) {
Point2D dir = data.get("dir" );
return entityBuilder(data)
.type(EntityType.BULLET)
.view(new Circle (4 , Color.YELLOW))
.bbox(BoundingShape.circle(4 ))
.with(new CollidableComponent (true ))
.with(new ProjectileComponent (dir, 500 ))
.with(new ExpireCleanComponent (Duration.seconds(2 )))
.build();
}
Melee Attack Arc private void meleeAttack () {
double angle = player.getRotation();
double dist = 40.0 ;
double hitW = 60.0 ;
double hitH = 40.0 ;
double cx = player.getCenter().getX() + Math.cos(Math.toRadians(angle)) * dist;
double cy = player.getCenter().getY() + Math.sin(Math.toRadians(angle)) * dist;
entityBuilder()
.at(cx - hitW / 2 , cy - hitH / 2 )
.type(EntityType.PLAYER_HITBOX)
.bbox(BoundingShape.box(hitW, hitH))
.with(new CollidableComponent (true ))
.with(new ExpireCleanComponent (Duration.millis(120 )))
.buildAndAttach();
}
onCollisionBegin(EntityType.PLAYER_HITBOX, EntityType.ENEMY, (hitbox, enemy) -> {
enemy.getComponent(HPComponent.class).damage(25 );
play("sounds/hit.wav" );
});
Camera Follow with Bounds @Override
protected void initGame () {
setLevelFromMap("world.tmx" );
Viewport vp = getGameScene().getViewport();
vp.bindToEntity(player, getAppWidth() / 2.0 , getAppHeight() / 2.0 );
vp.setBounds(0 , 0 , WORLD_WIDTH * TILE, WORLD_HEIGHT * TILE);
vp.setLazy(true );
}
NPC Interaction on Proximity
onCollisionBegin(EntityType.PLAYER, EntityType.NPC, (player, npc) -> {
String npcName = npc.getString("name" );
showInteractPrompt(npcName);
currentNPC = npc;
});
onCollisionEnd(EntityType.PLAYER, EntityType.NPC, (player, npc) -> {
hideInteractPrompt();
currentNPC = null ;
});
onKeyDown(KeyCode.E, () -> {
if (currentNPC != null ) {
String dialogueFile = currentNPC.getString("dialogue" );
DialogueGraph graph = getAssetLoader().loadDialogueGraph(dialogueFile);
getCutsceneService().startDialogueScene(graph);
}
});
Room Transition at Screen Edge
onCollisionBegin(EntityType.PLAYER, EntityType.DOOR_TRIGGER, (player, door) -> {
String targetRoom = door.getString("targetRoom" );
String entryPoint = door.getString("entryPoint" );
transitionToRoom(targetRoom, entryPoint);
});
private void transitionToRoom (String roomFile, String entryPoint) {
animationBuilder()
.duration(Duration.seconds(0.3 ))
.fadeOut(getGameScene().getContentRoot())
.buildAndPlay();
runOnce(() -> {
setLevelFromMap(roomFile + ".tmx" );
Entity entry = getGameWorld().getEntitiesByType(EntityType.ENTRY_POINT)
.stream().filter(e -> e.getString("id" ).equals(entryPoint))
.findFirst().orElseThrow();
player.setPosition(entry.getPosition());
animationBuilder()
.duration(Duration.seconds(0.3 ))
.fadeIn(getGameScene().getContentRoot())
.buildAndPlay();
}, Duration.seconds(0.35 ));
}
Simple Minimap
private Canvas minimap;
private static final int MM_SCALE = 4 ;
@Override
protected void initUI () {
int mmW = WORLD_WIDTH * MM_SCALE;
int mmH = WORLD_HEIGHT * MM_SCALE;
minimap = new Canvas (mmW, mmH);
minimap.setTranslateX(getAppWidth() - mmW - 10 );
minimap.setTranslateY(10 );
addUINode(minimap);
}
@Override
protected void onUpdate (double tpf) {
GraphicsContext gc = minimap.getGraphicsContext2D();
gc.setFill(Color.color(0 , 0 , 0 , 0.6 ));
gc.fillRect(0 , 0 , minimap.getWidth(), minimap.getHeight());
gc.setFill(Color.GREEN);
gc.fillOval(player.getX() / TILE * MM_SCALE - 2 ,
player.getY() / TILE * MM_SCALE - 2 , 4 , 4 );
gc.setFill(Color.RED);
getGameWorld().getEntitiesByType(EntityType.ENEMY).forEach(e -> {
gc.fillOval(e.getX() / TILE * MM_SCALE - 1 ,
e.getY() / TILE * MM_SCALE - 1 , 3 , 3 );
});
}
Gotchas
setGravity(0, 0) in initPhysics, not initGame — initPhysics runs before initGame.
Setting gravity in initGame may still affect entities spawned in initPhysics.
setFixedRotation(true) on the physics body — without this, Box2D rotates the player
body when it slides along walls, causing the entity to visually spin unexpectedly.
Circle hitbox for smooth wall sliding — a box hitbox catches corners when the player
slides diagonally along a wall. Use BoundingShape.circle() for the player body.
Diagonal speed normalization is mandatory — without the 0.7071 factor, diagonal movement
is 41% faster than cardinal directions, making diagonal movement the optimal strategy.
getMousePositionWorld() returns world (game) coordinates, accounting for camera scroll.
getMousePositionUI() returns screen coordinates — use the former for in-game targeting.
ProjectileComponent entity type must be collidable — add CollidableComponent(true)
or the bullet won't trigger onCollisionBegin.
NPC interaction zone as sensor — if the NPC body is STATIC, it blocks player movement.
Make the NPC body a sensor (BodyType.STATIC + sensor fixture) or use a separate sensor trigger.