package eu.lestard.snakefx.core;
import eu.lestard.snakefx.viewmodel.CentralViewModel;
import javafx.animation.Animation;
import javafx.animation.Animation.Status;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.util.Duration;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.List;
/**
* This class is the game loop of the game.
*
* @author manuel.mauky
*/
@Singleton
public class GameLoop {
private static final int ONE_SECOND = 1000;
private Timeline timeline;
private final List<Runnable> actions = new ArrayList<>();
private final CentralViewModel viewModel;
public GameLoop(final CentralViewModel viewModel) {
this.viewModel = viewModel;
viewModel.collision.addListener(new CollisionListener());
viewModel.speed.addListener(new SpeedChangeListener());
viewModel.gameloopStatus.addListener(new StatusChangedListener());
init();
}
/**
* Added Actions are called on every keyframe of the GameLoop. The order of invocation is not guaranteed.
*
* @param action the action that gets called.
*/
public void addAction(final Runnable action) {
this.actions.add(action);
}
/**
* Initialize the timeline instance.
*/
private void init() {
timeline = new Timeline(buildKeyFrame());
timeline.setCycleCount(Animation.INDEFINITE);
// in this place we can't use a direct binding as the ViewModel property
// can also be changed in other places.
timeline.statusProperty().addListener((observable, oldStatus, newStatus) ->
viewModel.gameloopStatus.set(newStatus));
}
/**
* This method creates a {@link KeyFrame} instance according to the configured framerate.
*/
private KeyFrame buildKeyFrame() {
final int fps = viewModel.speed.get().getFps();
final Duration duration = Duration.millis(ONE_SECOND / fps);
return new KeyFrame(duration, event ->
actions.forEach(Runnable::run));
}
/**
* This listener controls the timeline when there are external changes to the status property. This needs to be done
* because the {@link Timeline#statusProperty()} is readonly and can't be bound bidirectional.
*/
private final class StatusChangedListener implements ChangeListener<Status> {
@Override
public void changed(final ObservableValue<? extends Status> arg0, final Status oldStatus,
final Status newStatus) {
switch (newStatus) {
case PAUSED:
pause();
break;
case RUNNING:
play();
break;
case STOPPED:
stop();
break;
}
}
}
/**
* This listener controls the timeline when the desired speed has changed.
*/
private final class SpeedChangeListener implements ChangeListener<SpeedLevel> {
@Override
public void changed(final ObservableValue<? extends SpeedLevel> arg0, final SpeedLevel oldSpeed,
final SpeedLevel newSpeed) {
final Status oldStatus = timeline.getStatus();
if (Status.RUNNING.equals(oldStatus)) {
pause();
}
init();
if (Status.RUNNING.equals(oldStatus)) {
play();
}
}
}
/**
* This listener stops the timeline when an collision is detected.
*/
private final class CollisionListener implements ChangeListener<Boolean> {
@Override
public void changed(final ObservableValue<? extends Boolean> arg0, final Boolean oldValue,
final Boolean newCollision) {
if (newCollision) {
stop();
}
}
}
private void play() {
timeline.play();
}
private void pause() {
timeline.pause();
}
private void stop() {
timeline.stop();
}
}