package edu.gatech.cs2340.trydent; import java.io.InputStream; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javafx.fxml.FXMLLoader; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.paint.Color; import edu.gatech.cs2340.trydent.internal.JavaFXManager; import edu.gatech.cs2340.trydent.internal.SwingManager; import edu.gatech.cs2340.trydent.log.Log; /** * Main class that kicks off the TrydentEngine. * * As a developer using the TrydentEngine, all you have to do is call * TrydentEngine.start() to get started. * * @author Garrett Malmquist * */ public class TrydentEngine { private static TrydentEngine instance; private final Object mainLock = new Object(); private volatile boolean doQuit = false; private volatile long frameNumber; private volatile boolean superCalledFlag = false; private Set<ContinuousEvent> continuousEvents, continuousEventsToAdd, continuousEventsToRemove; private Map<Runnable, ContinuousEvent> continuousRunnables; private JavaFXManager fxManager; private TrydentEngine() { continuousEvents = new HashSet<>(); continuousEventsToAdd = new HashSet<>(); continuousEventsToRemove = new HashSet<>(); continuousRunnables = new HashMap<>(); fxManager = new SwingManager(); } private void mainUpdate() { if (frameNumber == 0) Time.startTheDawnOfTime(); if (!doQuit) { try { updateContinuousEvents(); } catch (Exception ex) { Log.error(String.valueOf(ex)); ex.printStackTrace(); quit(); } Time.updateTime(); } else { cleanup(); } frameNumber++; } private void updateContinuousEvents() { continuousEvents.addAll(continuousEventsToAdd); continuousEventsToAdd.clear(); // Removing events is uglier than creating events, because // we need to make sure we handle the case where users // remove additional events in an events onStop() method. while (!continuousEventsToRemove.isEmpty()) { ContinuousEvent next = null; for (ContinuousEvent event : continuousEventsToRemove) { next = event; break; } next.doStop(); continuousEvents.remove(next); continuousEventsToRemove.remove(next); } for (ContinuousEvent event : continuousEvents) { clearSuperCalledFlag(); event.onPreUpdate(); if (!superCalledFlag) { throw new TrydentException("ContinuousEvent " + event + " overloaded onPreUpdate() but failed to call super method!"); } event.doUpdate(); } } private void cleanup() { // Stop any remaining events. ContinuousEvent[] toStop = new ContinuousEvent[continuousEvents.size()]; continuousEvents.toArray(toStop); for (ContinuousEvent event : toStop) { event.doStop(); } continuousEvents.clear(); continuousEventsToRemove.clear(); continuousEventsToAdd.clear(); fxManager.stopJavaFX(); } private static TrydentEngine getInstance() { if (instance == null) instance = new TrydentEngine(); return instance; } /** For internal engine use only. */ static void clearSuperCalledFlag() { getInstance().superCalledFlag = false; } /** For internal engine use only. */ static void setSuperCalledFlag() { getInstance().superCalledFlag = true; } /** * Adds a continuous event -- an event that the engine will invoke once * every frame. * * @param event * - A ContinuousEvent whose onUpdate() method will be invoked * every frame. */ static void addContinuousEvent(ContinuousEvent event) { getInstance().continuousEventsToAdd.add(event); } /** * Stops running the given continuous event. * * @param event */ static void removeContinuousEvent(ContinuousEvent event) { getInstance().continuousEventsToRemove.add(event); } static Group getRootNode() { return getInstance().fxManager.getBackground(); } /** * Replaces the current foreground layout node * * @param resource * the url of the new JavaFX node * @return the previous foreground JavaFX node, if none, returns null */ public static Node setForeground(InputStream resource) { Node previous = null; try { FXMLLoader myLoader = new FXMLLoader(); Node node = myLoader.load(resource); previous = getInstance().fxManager.setForeground(node); } catch (Exception e) { System.out.println(e.getMessage()); } return previous; } /** * Starts the TrydentEngine and JavaFX. */ public static void start() { final TrydentEngine engine = getInstance(); synchronized (engine.mainLock) { if (engine.fxManager.isRunning()) { Log.warn("Engine has already been started."); return; // Nothing to do. } engine.doQuit = false; engine.frameNumber = 0; engine.fxManager.setUpdateAction(new Runnable() { @Override public void run() { engine.mainUpdate(); } }); // Could pass command-line args here maybe // JavaFXFacade.main(args); engine.fxManager.startJavaFX(); } } public static void quit() { getInstance().doQuit = true; } /** * Hangs the current thread until the TrydentEngine stops running. * * @throws InterruptedException * if this thread is interrupted while waiting */ public static void waitUntilEngineStops() throws InterruptedException { // We don't have our own thread, because we rely on JavaFX // to invoke the engine's update method, so we have to busy-wait. while (getInstance().fxManager.isRunning()) { try { // Wait 100 ms and try again. Thread.sleep(100); } catch (InterruptedException ex) { } } } /** * Returns true if the TrydentEngine has been started, and hasn't been * stopped. * * @return whether the engine is running */ public static boolean isRunning() { return getInstance().fxManager.isRunning(); } /** * Stops the runnable that was previously executed with * {@link #runContinuously(Runnable)}. * <p> * Depending on when this method is called, the runnable might be run one * more time before stopping permanently. For more controlled behavior, use * the {@link edu.gatech.cs2340.trydent.ContinuousEvent} class instead of * {@link #runContinuously(Runnable)}. * * @param runnable * the runnable to stop executing every frame * @return true if the runnable was stopped, false if it was already stopped * or not run in the first place. */ public static boolean stopRunnable(Runnable runnable) { if (getInstance().continuousRunnables.containsKey(runnable)) { getInstance().continuousRunnables.get(runnable).stop(); return true; } return false; } /** * Tells the TrydentEngine to run the given runnable continuously (once per * frame), starting on the next frame. * <p> * For more sophisticated behavior, such as triggering actions when the * runnable starts or finishes, see * {@link edu.gatech.cs2340.trydent.ContinuousEvent}. * <p> * To stop the runnable, use {@link #stopRunnable(Runnable)}. * * @param runnable * action to run */ public static void runContinuously(final Runnable runnable) { getInstance().continuousRunnables.put(runnable, new ContinuousEvent() { @Override public void onUpdate() { runnable.run(); } }); } /** * Tells the TrydentEngine to run the given runnable on the next frame. * <p> * To run something every frame, use {@link #runContinuously(Runnable)} or * {@link edu.gatech.cs2340.trydent.ContinuousEvent} instead. * * @param runnable * action to run */ public static void runOnce(final Runnable runnable) { new ContinuousEvent() { @Override public void onStart() { runnable.run(); this.stop(); } @Override public void onUpdate() { } }; } /** * Sets the window title string for the TrydentEngine singleton. * @param title the title of the window. */ public static void setWindowTitle(String title) { getInstance().fxManager.setWindowTitle(title); } /** * Sets the window dimensions for the TrydentEngine singleton. * @param width the width of the window * @param height the height of the window */ public static void setWindowSize(int width, int height) { getInstance().fxManager.setWindowSize(width, height); } /** * Sets the window background color for the TrydentEngine singleton. * @param color the background color. */ public static void setBackgroundColor(Color color) { getInstance().fxManager.setBackgroundColor(color); } }