Para desarrollar un juego en el que, por ejemplo, una serie de elementos se muevan por la pantalla, sin que el usuario sea el encargado de indicar su posición, se debe conseguir un bucle en el código que contenga las órdenes necesarias para colocar los elementos que se desean animar en la posición adecuada. Es lo que se denomina en inglés el game loop (bucle del juego).

AnimationTimer

En JavaFX se puede conseguir ese bucle utilizando la clase AnimationTimer de manera similar al código de la siguiente aplicación, que se encarga de mover una bola de lado a lado de manera indefinida:

import com.sun.javafx.perf.PerformanceTracker;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;

public class TestFrameRate extends Application {

    public static final double SCENE_WIDTH = 300;
    public static final double SCENE_HEIGHT = 250;
    public static final double BALL_RADIUS = 10;
    public static double ballSpeed = 1;

    @Override
    public void start(Stage primaryStage) {
        Group root = new Group();

        // Bola que se usará para la animación
        Circle ball = new Circle(BALL_RADIUS);
        ball.setTranslateX(SCENE_WIDTH * 0.5);
        ball.setTranslateY(SCENE_HEIGHT * 0.5);
        root.getChildren().addAll(ball);
        
        // Etiqueta que mostrará el valor de frames por segundo (FPS)
        Label label = new Label();
        label.setTranslateX(10);
        label.setTranslateY(10);
        root.getChildren().addAll(label);
        
        Scene scene = new Scene(root, SCENE_WIDTH, SCENE_HEIGHT);
        primaryStage.setScene(scene);
        primaryStage.show();

        // Game loop usando AnimationTimer
        AnimationTimer animationTimer = new AnimationTimer() {
            public void handle(long now) {
                // Mostrar la frecuencia de refresco FPS
                PerformanceTracker perfTracker = PerformanceTracker.getSceneTracker(scene);
                label.setText("FPS (AnimationTimer) = " + perfTracker.getInstantFPS());
                // Cambiar la dirección de la bola si llega a los extremos
                if(ball.getTranslateX() < 0 || ball.getTranslateX() > SCENE_WIDTH) {
                    ballSpeed *= -1;
                }                
                ball.setTranslateX(ball.getTranslateX() + ballSpeed);
            }
        };
        animationTimer.start();
    }

    public static void main(String[] args) {
        launch(args);
    }

}

El método handle que se implementa, se ejecuta unas 60 veces por segundo (60 FPS), como se puede ver en la ejecución de la aplicación, ya que se utiliza la clase PerformanceTracker para mostrar en la ventana la frecuencia de refresco.

En los siguientes GIF animados se puede ver el resultado de ejecutar la aplicación en el mismo equipo pero en distintos sistemas operativos (Windows, MacOS y Ubuntu). Como queda claro en las imágenes, en Ubuntu se ejecuta el AnimationTimer sin respetar los 60 FPS por lo que la velocidad de la animación es mucho mayor que en los otros casos.

TestFrameRateWindows 93267 TestFrameRate OSX 45ccf TestFrameRateUbuntu 3ba06 

Probando la misma aplicación en un dispositivo móvil Android, también se obtiene la misma tasa de refresco habitual de unos 60 FPS:

TestFrameRateAndroid 7ddad

Por tanto, hay que tener cuidado con este tipo de animación, ya que habría que controlar el tiempo que ha pasado entre una iteración y otra para que no se vea modificada la velocidad. Esto puede realizarse a partir del parámetro de tipo long que recibe el método handle, y que en el código anterior se ha llamado currentNanoTime.

Para ello, se puede crear una variable que almacene el tiempo correspondiente al último frame que se haya mostrado (tiempoFrameAnterior en el código mostrado a continuación). También se puede utilizar una constante que indique el tiempo de refresco deseado como frames por segundo.

    double tiempoFrameAnterior = System.nanoTime();
    final double FPS = 60;

A continuación, en el método handle se incluirá un código como el siguiente, donde se indica que sólo se ejecutará el código correspondiente a la animación si ha transcurrido el tiempo necesario para cumplir con la tasa de refresco deseada. Para ello se tendrá en cuenta el valor obtenido en el parámetro now que corresponde al momento en que se ejecuta. La tasa de refresco se divide de 1.000.000.000 ya que los tiempos vienen dados en nanosegundos.

        animationTimerCorrePersonaje = new AnimationTimer() {
            @Override
            public void handle(long now) {
                if(now - tiempoFrameAnterior >= 1_000_000_000.0 / FPS) {
                    tiempoFrameAnterior = now;
		    // Código de la animación
		}
	    }

Timeline

Otra alternativa es utilizar la clase Timeline en lugar de la clase AnimationTimer. Esta clase también permite crear bucles donde se ejecute un determinado código en segundo plano, permitiendo personalizar más aspectos que en la clase comentada anteriormente.

En el siguiente ejemplo de código fuente, se realiza las mismas acciones que en el caso anterior de la bola rebotando en los laterales. Observa que en la creación del objeto Timeline se pasa por parámetro un objeto KeyFrame al que se le indica cada cuánto tiempo se ejecutarán las acciones incluidas en el método handle. Se indica 0.017 para que se ejecute cada 0.017 segundos que equivale a unos 60 frames por segundo.

Observa también cómo al final de este código se indica que la repetición de este Timeline se hará de manera indefinida, y se inicia la ejecución con el método play().

        // Game loop usando Timeline
        Timeline timeline = new Timeline(
            // 0.017 ~= 60 FPS
            new KeyFrame(Duration.seconds(0.017), new EventHandler<ActionEvent>() {
                public void handle(ActionEvent ae) {
                    // Mostrar la frecuencia de refresco FPS
                    PerformanceTracker perfTracker = PerformanceTracker.getSceneTracker(scene);
                    label.setText("FPS (Timeline) = " + perfTracker.getInstantFPS());
                    // Cambiar la dirección de la bola si llega a los extremos                    
                    if(ball.getTranslateX() < 0 || ball.getTranslateX() > SCENE_WIDTH) {
                        ballSpeed *= -1;
                    }
                    ball.setTranslateX(ball.getTranslateX() + ballSpeed);
                }
            })                
        );
        timeline.setCycleCount(Timeline.INDEFINITE);
        timeline.play();        

Ten en cuenta que habrá que incluir las siguientes importaciones:

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;

Con esta manera de crear bucles para las animaciones se consigue mantener de manera más fiable la tasa de refresco, incluso en Ubuntu como se puede apreciar en la siguiente captura:

TestFrameRateUbuntuTimeLine 08109