Build a side-scrolling platformer in FXGL — set up physics with gravity for a player entity, implement jump with coyote time and jump buffer, handle one-way pass-through platforms, attach moving kinematic platforms the player can ride, bind the camera viewport to the player with world bounds, add parallax scrolling background layers, load Tiled TMX levels with solid tile collision, detect pit/fall-out-of-bounds deaths, stomp enemies from above, and implement a checkpoint and respawn system. Use this skill when building a Mario-style, precision platformer, run-and-jump, or any side-scrolling game with gravity-driven movement.
Build a side-scrolling platformer in FXGL — set up physics with gravity for a player entity, implement jump with coyote time and jump buffer, handle one-way pass-through platforms, attach moving kinematic platforms the player can ride, bind the camera viewport to the player with world bounds, add parallax scrolling background layers, load Tiled TMX levels with solid tile collision, detect pit/fall-out-of-bounds deaths, stomp enemies from above, and implement a checkpoint and respawn system. Use this skill when building a Mario-style, precision platformer, run-and-jump, or any side-scrolling game with gravity-driven movement.
// Add a thin sensor below player feet in factory:
entityBuilder(data)
// ... other setup
.with(physics)
.buildAndAttach();
// Use a separate sensor entity, or detect via collision callbacks:@OverrideprotectedvoidinitPhysics() {
// Player landing on any PLATFORM or GROUND entity
onCollisionBegin(EntityType.PLAYER, EntityType.GROUND, (player, ground) -> {
doubleplayerBottom= player.getY() + player.getHeight();
doublegroundTop= ground.getY();
if (playerBottom <= groundTop + 8) { // small tolerance
player.getComponent(PlayerComponent.class).setGrounded(true);
}
});
onCollisionEnd(EntityType.PLAYER, EntityType.GROUND, (player, ground) -> {
// don't reset grounded here — let PlayerComponent.onUpdate handle it
});
}
One-Way Platforms
// In EntityFactory — a platform you can jump through from below@Spawns("oneWayPlatform")public Entity newOneWayPlatform(SpawnData data) {
PhysicsComponentphysics=newPhysicsComponent();
physics.setBodyType(BodyType.STATIC);
// One-way: only collide from aboveFixtureDeffd=newFixtureDef();
fd.restitution = 0.0f;
physics.addFixtureDef(fd);
return entityBuilder(data)
.type(EntityType.ONE_WAY_PLATFORM)
.view(newRectangle(data.<Integer>get("width"), 12, Color.BROWN))
.bbox(BoundingShape.box(data.get("width"), 12))
.with(physics)
.build();
}
// In initPhysics — use pre-solve to cancel collision from below// (requires PhysicsContactListener override — see FXGL physics collision skill)
@OverrideprotectedvoidinitGame() {
// After loading the level from Tiled:
setLevelFromMap("level1.tmx");
// Bind camera to player — will follow player continuouslyViewportviewport= getGameScene().getViewport();
viewport.bindToEntity(player, getAppWidth() / 2.0, getAppHeight() / 2.0);
// Clamp camera to level bounds (world size from Tiled)
viewport.setBounds(-64, 0, LEVEL_WIDTH * 32, LEVEL_HEIGHT * 32);
}
Parallax Background
// Create background layers that scroll at different speedsprivatevoidinitParallax() {
// Far background (slowest)Entitybg0= entityBuilder()
.at(-100, 0)
.view(newImageView(image("bg_far.png")))
.zIndex(-3)
.buildAndAttach();
// Mid backgroundEntitybg1= entityBuilder()
.at(0, 0)
.view(newImageView(image("bg_mid.png")))
.zIndex(-2)
.buildAndAttach();
// Update parallax in onUpdate
}
@OverrideprotectedvoidonUpdate(double tpf) {
doublecamX= getGameScene().getViewport().getX();
bg0.setX(camX * 0.1); // 10% of camera speed
bg1.setX(camX * 0.4); // 40% of camera speed
}
Pit Detection
// Sensor at the bottom of the world to detect falling out@Spawns("pitSensor")public Entity newPitSensor(SpawnData data) {
return entityBuilder(data)
.type(EntityType.PIT)
.bbox(BoundingShape.box(LEVEL_WIDTH * 32, 64))
.with(newPhysicsComponent()) // sensor
.build();
}
// In initPhysics:
onCollisionBegin(EntityType.PLAYER, EntityType.PIT, (player, pit) -> {
onPlayerDeath();
});
Enemy Stomp Pattern
// In initPhysics:
onCollisionBegin(EntityType.PLAYER, EntityType.ENEMY, (player, enemy) -> {
PlayerComponentpc= player.getComponent(PlayerComponent.class);
doubleplayerBottomY= player.getY() + player.getHeight();
doubleenemyTopY= enemy.getY();
// Stomp: player is falling (velocity > 0) and above the enemyif (player.getComponent(PhysicsComponent.class).getVelocityY() > 0
&& playerBottomY <= enemyTopY + 12) {
enemy.removeFromWorld();
inc("score", 100);
// Bounce player up
player.getComponent(PhysicsComponent.class).setVelocityY(-8);
play("sounds/stomp.wav");
} else {
onPlayerHurt(player);
}
});
Zero friction on player is critical — without fd.setFriction(0), the player sticks
to walls and slides along them due to Box2D's default friction. Always set friction=0 on
the player body fixture.
setVelocityX not applyForce for horizontal movement — force-based movement feels
sluggish and overshoots. Velocity setting gives tight, predictable control.
Coyote time and jump buffer must both be implemented — without them, jumps at ledge
edges feel unresponsive and players feel cheated. Both are industry-standard patterns.
isGrounded reset each frame — reset isGrounded = false in onUpdate, then set
it to true only when collision confirms it. Do NOT set it in onCollisionEnd because
that fires when one platform ends while another begins.
Moving platforms: player must be a DYNAMIC body — kinematic-on-kinematic collision
doesn't work in Box2D. Player must be DYNAMIC for moving platform physics to transfer.
Viewport bindToEntity centers on entity origin, not center — pass appWidth/2, appHeight/2 as offset to center the player on screen, or adjust for different screen regions.
Parallax layers need to be at a low zIndex — use zIndex(-3) or lower so game
entities render in front. FXGL defaults entities to zIndex 0.