FXGL Third-Person 3D
Layering
Use fxgl-scene3d for the baseline 3D scene. This skill adds:
- orbit camera controls
- character-relative movement
- spring-arm smoothing
- lock-on behavior
- third-person combat / traversal structure
Orbit Camera
private Camera3D camera;
private double cameraYaw = 0;
private double cameraPitch = 20;
private double cameraRadius = 8;
private Point3D computeCameraOffset() {
double yawRad = Math.toRadians(cameraYaw);
double pitchRad = Math.toRadians(cameraPitch);
double x = cameraRadius * Math.sin(yawRad) * Math.cos(pitchRad);
double y = -cameraRadius * Math.sin(pitchRad);
double z = cameraRadius * Math.cos(yawRad) * Math.cos(pitchRad);
return new Point3D(x, y, z);
}
Update the camera every frame:
private void updateCamera(double tpf) {
Point3D target = player.getPosition3D().add(0, -1.5, 0);
Point3D desired = target.add(computeCameraOffset());
var transform = camera.getTransform();
transform.setX(lerp(transform.getX(), desired.getX(), tpf * 8));
transform.setY(lerp(transform.getY(), desired.getY(), tpf * 8));
transform.setZ(lerp(transform.getZ(), desired.getZ(), tpf * 8));
transform.lookAt(target);
}
Player Movement Relative to Camera
private void updatePlayerMovement(double tpf) {
Point3D camToPlayer = player.getPosition3D().subtract(camera.getTransform().getPosition3D());
Point3D forward = new Point3D(camToPlayer.getX(), 0, camToPlayer.getZ()).normalize();
Point3D right = forward.crossProduct(new Point3D(0, 1, 0)).normalize();
Point3D move = Point3D.ZERO;
if (getInput().isHeld(KeyCode.W)) move = move.add(forward);
if (getInput().isHeld(KeyCode.S)) move = move.subtract(forward);
if (getInput().isHeld(KeyCode.D)) move = move.add(right);
if (getInput().isHeld(KeyCode.A)) move = move.subtract(right);
if (move.magnitude() > 0) {
Point3D dir = move.normalize();
player.translate3D(dir.multiply(5 * tpf));
player.getTransformComponent().lookAt(player.getPosition3D().add(dir));
}
}
Jump and Grounding
private double yVelocity = 0;
private boolean grounded = true;
private void updateVerticalMotion(double tpf) {
yVelocity += 18 * tpf;
player.translateY(yVelocity * tpf);
double floorY = 0;
if (player.getY() > floorY) {
player.setY(floorY);
yVelocity = 0;
grounded = true;
}
}
Camera Clipping
For a prototype, shorten the camera arm when geometry blocks the segment from player to camera. Use a ray / pick test against known collidable geometry before committing the final camera position.
Lock-On Pattern
private Entity lockedTarget;
private void toggleLockOn() {
if (lockedTarget != null) {
lockedTarget = null;
return;
}
lockedTarget = findNearestEnemyInFront();
}
When locked on:
- keep camera aim centered between player and target
- rotate player to face target
- reinterpret movement as strafe / orbit
Animation State Suggestions
Typical 3D state set:
- idle
- walk
- run
- jump
- fall
- attack
- hurt
Drive this from movement magnitude, grounded state, and combat input rather than directly from keys.
Gotchas
- Orbit camera and player movement are separate systems — do not couple them so tightly that rotating the camera also rotates the player when standing still.
- Use
lookAt()on transform components, not guessed rotation math where avoidable — FXGL already provides a 3D look-at helper on transform components. - Spring-arm smoothing must run every frame — abrupt snapping feels bad immediately in third-person games.
- Camera clipping needs active handling — a smooth camera that goes through walls is still wrong.