Creating a game loop in JavaFX is incredibly simple. In fact, creating games with JavaFX in general is wonderfully easy. There’s extensive support for animation, rendering, and media, so a lot of the hard work is done for you.
What We’ll Achieve In This Tutorial
By the end of this tutorial, you’ll have:
And all in a prototype space shooter.

There are tonnes of different types of games, but we’ve chosen the top-down space shooter style because it’s an easy way to demonstrate the basics.
We’ve chosen this style because these games are still immensely popular. Check out Space Mayhem on Steam, or Hyperbolic Ignition by Heartship Games for the sorts of gameplay you can achieve with simple game dynamics and particle animations.

To produce games of this quality, you’ll need user interfaces, particle systems, custom sprite animations and enemy artificial intelligence. Obviously, we can’t do all of that today. But we can make a good start..
Creating our Game
The good news is that creating a game is really simple once you get the hang of the basics. We’re going to create our space shooter in 3 simple steps:
We’ll do each one in turn, starting from a simple JavaFX project with Maven support. We’re using Maven because it makes dependency management so much easier. If you’re not sold, read our article on it here!
Alright it’s time to dive in.
1. Creating the Game Loop with an Animation Timer
The core to any game is a smooth game loop. There is no system in the world that can guarantee a rendering rate of exactly the same frame rate for every frame. But there are tricks to making a game loop work the way you want it to.
A game loop can be created by extending the AnimationTimer class to operate on the length of the frame, rather than the time in nanoseconds. That’s going to ensure that we apply the correct movement to players and other moving components.. More importantly, it’s going to avoid ‘jittering’ – commonly encountered when calculations are needed like if we introduced a particle system!
Starting Code
If you’re not familiar with the JavaFX AnimationTimer
, check out our tutorial on pausing it, or testing its speed. Extending it is really simple, though. It’s an abstract class with the abstract method handle()
.
In our tutorial on pausing the animation timer, we extended the AnimationTimer
class to include the ability to track duration, pause and reset the timer to zero. Let’s nick that code as a starting point.
i. Change code to include a game canvas!
One exception is that we’ll change the FXML file to include a Canvas element in place of what we had before.
<AnchorPane fx:id="gameAnchor" prefHeight="450" prefWidth="750.0" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.edencoding.controllers.GameController"> <Canvas fx:id="gameCanvas" height="450.0" width="750.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"/> </AnchorPane>
We’ll also bind the width and height of the canvas to the size of the AnchorPane that encapsulates it to ensure we never have any blank area in our game window.
private void initialiseCanvas() { gameCanvas.widthProperty().bind(gameAnchor.widthProperty()); gameCanvas.heightProperty().bind(gameAnchor.heightProperty()); }
Of course your other option is to make the window not re-sizeable. Your choice!
ii. Change our code to track frame duration, not absolute time
First of all, let’s change the name of the class from PausableAnimationTimer
to GameLoop
. That’s clearer.
When you extend the AnimationTimer
class, you’re compelled by Java to override the handle method. We used that method to really good effect in defining the pause functionality, but it’s really important to remember it’s still the handle()
method that gets called by JavaFX’s animation pulse system.
To give our game app the ability to use the timer, we define a new abstract method and call it right at the end of the handle()
method in our GameLoop. That way, when the Animation Pulse comes through, it’ll trigger our code too.
We’ll call the new method tick()
.

Now, in the original PausableAnimationTimer
, we passed the tick()
method the raw nano time.
@Override public void handle(long now) { .... if (!isPaused) { long duration= now - animationStart; ... tick(duration); } ... }
This is practically useless in game loops. The time is completely arbitrary and what we really need is the time since the last frame. So let’s store the last frame time in nanos and use that to work out the duration of the frame in seconds:
@Override public void handle(long now) { float lastFrameTimeNanos; .... if (!isPaused) { ... float secondsSinceLastFrame = (float) ((now - lastFrameTimeNanos) / 1e9); lastFrameTimeNanos = now; tick(secondsSinceLastFrame); } ... }
That’s absolutely all we need for our animation timer.
2. Create a Player and handle user input
Here, we’re going to create a simple player – a spaceship we’ll be navigating about our scene. Then, we’ll make a handy Key manager so we always know which keys the User’s pressing. Finally in this section, we’ll combine the two to navigate our user about the screen.
a. Creating a Player
There are a number of ways we could create our player. Depending on the drawing style of our game, we could implement sprite animation, or render complex shapes rather than images. Each comes with its own requirements for rendering.
We’ll opt for a simple single-image entity, which we’ll rotate and move when the player changes position in the world. It might not have the compelling art style of isometric sprite animation, but for a top-down space game, it’ll do just fine.

There are some amazing free resources for games at opengameart.org. We used this spaceship from user scofanogd. We’ve made a few tweaks to make it fit with the background we’ll use later

Apart from an image, every entity in the game needs the same things so we can generalise the Player code and re-use it for every entity! The common features are:
And of course, it needs a method to update the location of the entity each frame. Check out the Entity code in the drop-down, or check out the project in full at the end.
Key things to remember for this class are:
public class Entity { Point2D position; float rotation; float scale = 1; double width; double height; Image entityImage; public Entity(Image entityImage) { this.entityImage = entityImage; this.width = entityImage.getWidth(); this.height = entityImage.getHeight(); } /* ********************************************************** * POSITION * ************************************************************ */ public Point2D getDrawPosition() { return position; } public void setDrawPosition(float x, float y) { this.position = new Point2D(x, y); } private void rotate(float rotation) { this.rotation += rotation; } private void move(Point2D vector) { this.position = this.position.add(vector); } public float getRotation() { return rotation; } public float getScale() { return scale; } public Point2D getCenter() { Point2D pos = getDrawPosition(); return new Point2D(pos.getX() + width / 2, pos.getY() + height / 2); } /* ********************************************************** * IMAGE * ************************************************************ */ public Image getImage() { return entityImage; } public double getWidth() { return this.width * getScale(); } public double getHeight() { return this.height * getScale(); } /* ************************************************************* * MOVEMENT * ************************************************************ */ private float MAX_SPEED = 5f; private Point2D currentThrustVector = new Point2D(0, 0); private float MAX_TORQUE = 5f; private float currentTorqueForce = 0; public void addTorque(float torqueForce) { float newTorque = currentTorqueForce + torqueForce; if (torqueForce > 0) { currentTorqueForce = Math.min(newTorque, MAX_TORQUE); } else { currentTorqueForce = Math.max(newTorque, -MAX_TORQUE); } } public void addThrust(double scalar) { addThrust(scalar, getRotation()); } private void addThrust(double scalar, double angle) { Point2D thrustVector = calculateNewThrustVector(scalar, Math.toRadians(-angle)); currentThrustVector = currentThrustVector.add(thrustVector); currentThrustVector = clampToMaxSpeed(currentThrustVector); } private Point2D calculateNewThrustVector(double scalar, double angle) { return new Point2D( (float) (Math.sin(angle) * scalar), (float) (Math.cos(angle) * scalar)); } private Point2D clampToMaxSpeed(Point2D thrustVector) { if (thrustVector.magnitude() > MAX_SPEED) { return currentThrustVector = thrustVector.normalize().multiply(MAX_SPEED); } else { return currentThrustVector = thrustVector; } } private void applyDrag() { float movementDrag = currentThrustVector.magnitude() < 0.5 ? 0.01f : 0.07f; float rotationDrag = currentTorqueForce < 0.2f ? 0.05f : 0.1f; currentThrustVector = new Point2D( reduceTowardsZero((float) currentThrustVector.getX(), movementDrag), reduceTowardsZero((float) currentThrustVector.getY(), movementDrag)); currentTorqueForce = reduceTowardsZero(currentTorqueForce, rotationDrag); } private float reduceTowardsZero(float value, float modifier) { float newValue = 0; if (value > modifier) { newValue = value - modifier; } else if (value < -modifier) { newValue = value + modifier; } return newValue; } public void update() { applyDrag(); move(currentThrustVector); rotate(currentTorqueForce); } }
b. Making a game-friendly key system
One of the many great things about JavaFX is its powerful events system. It can track clipboard, drag board, mouse and key events with absolute ease.
It’s powerful, but it’s based on events rather than polling. That means if we don’t catch it as it’s happening, we miss it.
In game-development systems like the Lightweight Java Game Library (LWJGL), it’s more common to give users the functionality to check when they want whether a chosen key is ‘down’.
To create that functionality, we’ll wrap the JavaFX event management in a polling-friendly wrapper.
To convert event handling to polling-based, we’ll have to set up a listener to run every time a key is pressed and keep track of the keys ourselves.
We have no need to maintain more than one set of keys, so we’ll make our KeyPolling
class a Singleton.
Key things to remember for this class are:
public class KeyPolling { private static Scene scene; private static final Set<KeyCode> keysCurrentlyDown = new HashSet<>(); private KeyPolling() { } public static KeyPolling getInstance() { return new KeyPolling(); } public void pollScene(Scene scene) { clearKeys(); removeCurrentKeyHandlers(); setScene(scene); } private void clearKeys() { keysCurrentlyDown.clear(); } private void removeCurrentKeyHandlers() { if (scene != null) { KeyPolling.scene.setOnKeyPressed(null); KeyPolling.scene.setOnKeyReleased(null); } } private void setScene(Scene scene) { KeyPolling.scene = scene; KeyPolling.scene.setOnKeyPressed((keyEvent -> { keysCurrentlyDown.add(keyEvent.getCode()); })); KeyPolling.scene.setOnKeyReleased((keyEvent -> { keysCurrentlyDown.remove(keyEvent.getCode()); })); } public boolean isDown(KeyCode keyCode) { return keysCurrentlyDown.contains(keyCode); } @Override public String toString() { StringBuilder keysDown = new StringBuilder("KeyPolling on scene (").append(scene).append(")"); for (KeyCode code : keysCurrentlyDown) { keysDown.append(code.getName()).append(" "); } return keysDown.toString(); } }
To set up our KeyPolling class, we just need to set the Scene that we want to keep track of in the main class (SpaceShooter.java)
KeyPolling.getInstance().pollScene(scene);
Because it’s static, we can easily access it in our GameController
, and because all the logic is properly encapsulated in the KeyPolling
class, all we need to worry about is asking whether our User Input keys are currently pressed!

If you run the code you’ve got, you’ll just see grey – no spaceship and definitely no movement yet. That’s because although we’ve set up the Entity and we’ve set up the Key listener, we haven’t linked them into the animation timer. That’s the rendering pipeline.
3. Putting the rendering pipeline into the game loop
To link the input with the canvas, we’ll need a rendering pipeline that follows three simple steps:
- Prepare’s the canvas by clearing the last frame
- Updates the player’s movement based on the user’s input, and
- Prints the current frame’s content including the background and player
If you’re new to game development, this is probably the most confusing part. We don’t move pieces about like we would in the real world. Instead, we clear the entire contents of the screen and recreate the entire world every frame.
To make all of this easier, we’ll collect the rendering code into a class called Renderer
. Before the game loop, we’ll pass it the Canvas so it can draw on it each frame.
Renderer renderer = new Renderer(this.gameCanvas);
We’ll walk through how to get the Renderer going inside the game loop one step at a time. If you want the full code, it’s right here.
Key things to remember for this class are:
public class Renderer { Canvas canvas; GraphicsContext context; Image background; List<Entity> entities = new ArrayList<>(); public Renderer(Canvas canvas) { this.canvas = canvas; this.context = canvas.getGraphicsContext2D(); } public void addEntity(Entity entity) { entities.add(entity); } public void removeEntity(Entity entity) { entities.remove(entity); } public void clearEntities() { entities.clear(); } public void setBackground(Image background) { this.background = background; } public void render() { context.save(); if(background!=null){ context.drawImage(background, 0, 0); } for (Entity entity : entities) { transformContext(entity); Point2D pos = entity.getDrawPosition(); context.drawImage( entity.getImage(), pos.getX(), pos.getY(), entity.getWidth(), entity.getHeight() ); } context.restore(); } public void prepare(){ context.setFill( new Color(0.68, 0.68, 0.68, 1.0) ); context.fillRect(0,0, canvas.getWidth(),canvas.getHeight()); } private void transformContext(Entity entity){ Point2D centre = entity.getCenter(); Rotate r = new Rotate(entity.getRotation(), centre.getX(), centre.getY()); context.setTransform(r.getMxx(), r.getMyx(), r.getMxy(), r.getMyy(), r.getTx(), r.getTy()); } }
a. Clearing the last frame
Clearing the last frame is pretty simple. In other rendering pipelines, you’d have to clear buffer bits and swap frame buffers (complicated!). Luckily, with JavaFX, we just draw a giant rectangle the same shape of the canvas…
public void prepare(){ context.setFill( new Color(0.68, 0.68, 0.68, 1.0) ); context.fillRect(0,0, canvas.getWidth(),canvas.getHeight()); }
It’s not graceful, but it works..
Now the canvas is clear, we can draw the next frame’s background and player positions.
b. Calculating Player Movement
Every frame, the Renderer’s going to print all its entities to the canvas. We’ve only got one Entity for this game, but as you add enemies, resources and more, it’s useful to make a list. We then print each using it’s stored image, position, rotation and scale.
So, ahead of using the Renderer in the game loop, we need to pass the player Entity
to it.
renderer.addEntity(player);
The intricacies of how to print something to a canvas aren’t as relevant to the game loop as how we’re working out the player’s movement. If you’re interested, it’s all above, though.
Every frame, we’ll use our KeyPolling
class, which knows, which keys are currently down, to determine whether to add thrust, or turn our spaceship.
We’ll do that by defining a method updatePlayerMovement(secondsSinceLastFrame)
.
It’s going to go through the keys we define as important and – if pressed – apply the desired effect. In this case, we’re doing to control the spaceship with Up, Down, Left and Right. Although, we could also use W, A, S and D
In fact, a pretty common build on this type of game would be multiplayer with separate controls.
Finally, the method calls the player.update()
. With our newly calcualted values of thrust and torque (turn), the spaceship knows where to go next.
private void updatePlayerMovement(float frameDuration) { if (keys.isDown(KeyCode.UP)) { player.addThrust(20 * frameDuration); } else if (keys.isDown(KeyCode.DOWN)) { player.addThrust(-20 * frameDuration); } if (keys.isDown(KeyCode.RIGHT)) { player.addTorque(120f * frameDuration); } else if (keys.isDown(KeyCode.LEFT)) { player.addTorque(-120f * frameDuration); } player.update(); }
c. Drawing the Results To the Screen
Once we’ve updated the position of the player, we can draw the background and player in a single method:
public void render() { .... if(background!=null){ context.drawImage(background, 0, 0); } for (Entity entity : entities) { transformContext(entity); Point2D pos = entity.getDrawPosition(); context.drawImage( entity.getImage(), pos.getX(), pos.getY(), entity.getWidth(), entity.getHeight() ); } ... }
Finally, we sew it all together by defining the AnimationTimer’s tick() method to perform that rendering pipeline. We’ll do this right after we’ve defined the Renderer and passed it the player Entity
.
GameLoopTimer timer = new GameLoopTimer() { @Override public void tick(float secondsSinceLastFrame) { renderer.prepare(); updatePlayerMovement(secondsSinceLastFrame); renderer.render(); } }; timer.start();
You’ll find a link to the code for the whole project at the end, but it wouldn’t feel complete without including the GameController here for clarity. In case you want to see where the code above fits in.
Key things to remember for this class are:
public class GameController implements Initializable { public Canvas gameCanvas; public AnchorPane gameAnchor; KeyPolling keys = KeyPolling.getInstance(); private Entity player = new Entity(new Image(getClass().getResourceAsStream("/img/ship.png"))); @Override public void initialize(URL location, ResourceBundle resources) { initialiseCanvas(); player.setDrawPosition(350, 200); player.setScale(0.5f); Renderer renderer = new Renderer(this.gameCanvas); renderer.addEntity(player); renderer.setBackground(new Image(getClass().getResourceAsStream("/img/SpaceBackground.jpg"))); GameLoopTimer timer = new GameLoopTimer() { @Override public void tick(float secondsSinceLastFrame) { renderer.prepare(); updatePlayerMovement(secondsSinceLastFrame); renderer.render(); } }; timer.start(); } private void initialiseCanvas() { gameCanvas.widthProperty().bind(gameAnchor.widthProperty()); gameCanvas.heightProperty().bind(gameAnchor.heightProperty()); } private void updatePlayerMovement(float frameDuration) { if (keys.isDown(KeyCode.UP)) { player.addThrust(20 * frameDuration); } else if (keys.isDown(KeyCode.DOWN)) { player.addThrust(-20 * frameDuration); } if (keys.isDown(KeyCode.RIGHT)) { player.addTorque(120f * frameDuration); } else if (keys.isDown(KeyCode.LEFT)) { player.addTorque(-120f * frameDuration); } player.update(); } }
Conclusions
In three steps we’ve created an animation game loop for our game, established Entities
to draw to the screen and created a simple Rendering pipeline. It’s a pretty simple rendering pipeline,but that’s the beauty of using JavaFX.
We used JavaFX’s native 2D canvas and support for image loading to draw both the brackground and the player entities.
The whole code for the project can be found on my GitHub here in the SpaceShooter subdirectory.
