/******************************************************************************* * Copyright (c) 2013, Daniel Murphy All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted * provided that the following conditions are met: * Redistributions of source code must retain the * above copyright notice, this list of conditions and the following disclaimer. * Redistributions * in binary form must reproduce the above copyright notice, this list of conditions and the * following disclaimer in the documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ******************************************************************************/ package org.jbox2d.testbed.framework; import java.awt.event.KeyEvent; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.LinkedList; import org.jbox2d.common.IViewportTransform; import org.jbox2d.common.Vec2; import org.jbox2d.dynamics.World; import org.jbox2d.serialization.SerializationResult; import org.jbox2d.serialization.UnsupportedObjectException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Lists; /** * This class contains most control logic for the testbed and the update loop. It also watches the * model to switch tests and populates the model with some loop statistics. * * @author Daniel Murphy */ public abstract class AbstractTestbedController { private static final Logger log = LoggerFactory.getLogger(AbstractTestbedController.class); public static enum UpdateBehavior { UPDATE_CALLED, UPDATE_IGNORED } public static enum MouseBehavior { NORMAL, FORCE_Y_FLIP } public static final int DEFAULT_FPS = 60; private TestbedTest currTest = null; private TestbedTest nextTest = null; private long frameCount; private int targetFrameRate; private float frameRate = 0; private boolean animating = false; private final TestbedModel model; private boolean savePending, loadPending, resetPending = false; private final UpdateBehavior updateBehavior; private final MouseBehavior mouseBehavior; private final LinkedList<QueueItem> inputQueue; private final TestbedErrorHandler errorHandler; private float viewportHalfHeight; private float viewportHalfWidth; public AbstractTestbedController(TestbedModel argModel, UpdateBehavior behavior, MouseBehavior mouseBehavior, TestbedErrorHandler errorHandler) { model = argModel; inputQueue = Lists.newLinkedList(); setFrameRate(DEFAULT_FPS); updateBehavior = behavior; this.errorHandler = errorHandler; this.mouseBehavior = mouseBehavior; addListeners(); } private void addListeners() { // time for our controlling model.addTestChangeListener(new TestbedModel.TestChangedListener() { @Override public void testChanged(TestbedTest test, int index) { model.getPanel().grabFocus(); nextTest = test; } }); } public void load() { loadPending = true; } public void save() { savePending = true; } public void reset() { resetPending = true; } public void queueLaunchBomb() { synchronized (inputQueue) { inputQueue.add(new QueueItem()); } } public void queuePause() { synchronized (inputQueue) { inputQueue.add(new QueueItem(QueueItemType.Pause)); } } public void queueMouseUp(Vec2 screenPos, int button) { synchronized (inputQueue) { inputQueue.add(new QueueItem(QueueItemType.MouseUp, screenPos, button)); } } public void queueMouseDown(Vec2 screenPos, int button) { synchronized (inputQueue) { inputQueue.add(new QueueItem(QueueItemType.MouseDown, screenPos, button)); } } public void queueMouseMove(Vec2 screenPos) { synchronized (inputQueue) { inputQueue.add(new QueueItem(QueueItemType.MouseMove, screenPos, 0)); } } public void queueMouseDrag(Vec2 screenPos, int button) { synchronized (inputQueue) { inputQueue.add(new QueueItem(QueueItemType.MouseDrag, screenPos, button)); } } public void queueKeyPressed(char c, int code) { synchronized (inputQueue) { inputQueue.add(new QueueItem(QueueItemType.KeyPressed, c, code)); } } public void queueKeyReleased(char c, int code) { synchronized (inputQueue) { inputQueue.add(new QueueItem(QueueItemType.KeyReleased, c, code)); } } public void updateExtents(float halfWidth, float halfHeight) { viewportHalfHeight = halfHeight; viewportHalfWidth = halfWidth; if (currTest != null) { currTest.getCamera().getTransform().setExtents(halfWidth, halfHeight); } } protected void loopInit() { model.getPanel().grabFocus(); if (currTest != null) { currTest.init(model); } } private void initTest(TestbedTest test) { test.init(model); test.getCamera().getTransform().setExtents(viewportHalfWidth, viewportHalfHeight); model.getPanel().grabFocus(); } /** * Called by the main run loop. If the update behavior is set to * {@link UpdateBehavior#UPDATE_IGNORED}, then this needs to be called manually to update the * input and test. */ public void updateTest() { if (resetPending) { if (currTest != null) { currTest.init(model); } resetPending = false; model.getPanel().grabFocus(); } if (savePending) { if (currTest != null) { _save(); } savePending = false; model.getPanel().grabFocus(); } if (loadPending) { if (currTest != null) { _load(); } loadPending = false; model.getPanel().grabFocus(); } if (currTest == null) { synchronized (inputQueue) { inputQueue.clear(); return; } } IViewportTransform transform = currTest.getCamera().getTransform(); // process our input while (!inputQueue.isEmpty()) { QueueItem i = null; synchronized (inputQueue) { if (!inputQueue.isEmpty()) { i = inputQueue.pop(); } } if (i == null) { continue; } boolean oldFlip = transform.isYFlip(); if (mouseBehavior == MouseBehavior.FORCE_Y_FLIP) { transform.setYFlip(true); } currTest.getCamera().getTransform().getScreenToWorld(i.p, i.p); if (mouseBehavior == MouseBehavior.FORCE_Y_FLIP) { transform.setYFlip(oldFlip); } switch (i.type) { case KeyPressed: if (i.c != KeyEvent.CHAR_UNDEFINED) { model.getKeys()[i.c] = true; } model.getCodedKeys()[i.code] = true; currTest.keyPressed(i.c, i.code); break; case KeyReleased: if (i.c != KeyEvent.CHAR_UNDEFINED) { model.getKeys()[i.c] = false; } model.getCodedKeys()[i.code] = false; currTest.keyReleased(i.c, i.code); break; case MouseDown: currTest.mouseDown(i.p, i.button); break; case MouseMove: currTest.mouseMove(i.p); break; case MouseUp: currTest.mouseUp(i.p, i.button); break; case MouseDrag: currTest.mouseDrag(i.p, i.button); break; case LaunchBomb: currTest.lanchBomb(); break; case Pause: model.getSettings().pause = !model.getSettings().pause; break; } } if (currTest != null) { currTest.step(model.getSettings()); } } public void nextTest() { int index = model.getCurrTestIndex() + 1; index %= model.getTestsSize(); while (!model.isTestAt(index) && index < model.getTestsSize() - 1) { index++; } if (model.isTestAt(index)) { model.setCurrTestIndex(index); } } public void lastTest() { int index = model.getCurrTestIndex() - 1; while (index >= 0 && !model.isTestAt(index)) { if (index == 0) { index = model.getTestsSize() - 1; } else { index--; } } if (model.isTestAt(index)) { model.setCurrTestIndex(index); } } public void playTest(int argIndex) { if (argIndex == -1) { return; } while (!model.isTestAt(argIndex)) { if (argIndex + 1 < model.getTestsSize()) { argIndex++; } else { return; } } model.setCurrTestIndex(argIndex); } public void setFrameRate(int fps) { if (fps <= 0) { throw new IllegalArgumentException("Fps cannot be less than or equal to zero"); } targetFrameRate = fps; frameRate = fps; } public int getFrameRate() { return targetFrameRate; } public float getCalculatedFrameRate() { return frameRate; } public long getStartTime() { return startTime; } public long getFrameCount() { return frameCount; } public boolean isAnimating() { return animating; } public synchronized void start() { if (isAnimating() != true) { startAnimator(); } else { log.warn("Animation is already animating."); } } public synchronized void stop() { animating = false; stopAnimator(); } public void startAnimator() { animating = true; } public void stopAnimator() { animating = false; } protected long startTime, beforeTime, afterTime, updateTime, timeDiff, sleepTime, timeSpent; protected void stepAndRender() { float timeInSecs; if (nextTest != null) { initTest(nextTest); model.setRunningTest(nextTest); if (currTest != null) { currTest.exit(); } currTest = nextTest; nextTest = null; } timeSpent = beforeTime - updateTime; if (timeSpent > 0) { timeInSecs = timeSpent * 1.0f / 1000000000.0f; updateTime = System.nanoTime(); frameRate = (frameRate * 0.9f) + (1.0f / timeInSecs) * 0.1f; model.setCalculatedFps(frameRate); } else { updateTime = System.nanoTime(); } render(model.getPanel()); frameCount++; afterTime = System.nanoTime(); timeDiff = afterTime - beforeTime; sleepTime = (1000000000 / targetFrameRate - timeDiff) / 1000000; beforeTime = System.nanoTime(); } protected void render(TestbedPanel panel) { if (panel.render()) { if (currTest != null && updateBehavior == UpdateBehavior.UPDATE_CALLED) { updateTest(); } panel.paintScreen(); } } private void _save() { SerializationResult result; try { result = currTest.getSerializer().serialize(currTest.getWorld()); } catch (UnsupportedObjectException e1) { log.error("Error serializing world", e1); if (errorHandler != null) errorHandler.serializationError(e1, "Error serializing the object: " + e1.toString()); return; } try { FileOutputStream fos = new FileOutputStream(currTest.getFilename()); result.writeTo(fos); fos.flush(); fos.close(); } catch (FileNotFoundException e) { log.error("File not found exception while saving", e); if (errorHandler != null) errorHandler.serializationError(e, "File not found exception while saving: " + currTest.getFilename()); } catch (IOException e) { log.error("Exception while writing world", e); if (errorHandler != null) errorHandler.serializationError(e, "Error while writing world: " + e.toString()); } log.debug("Serialed world to " + currTest.getFilename()); } private void _load() { World w; try { FileInputStream fis = new FileInputStream(currTest.getFilename()); w = currTest.getDeserializer().deserializeWorld(fis); fis.close(); } catch (FileNotFoundException e) { log.error("File not found error while loading", e); if (errorHandler != null) errorHandler.serializationError(e, "File not found exception while loading: " + currTest.getFilename()); return; } catch (UnsupportedObjectException e) { log.error("Error deserializing object", e); if (errorHandler != null) errorHandler.serializationError(e, "Error deserializing the object: " + e.toString()); return; } catch (IOException e) { log.error("Exception while reading world", e); if (errorHandler != null) errorHandler.serializationError(e, "Error while reading world: " + e.toString()); return; } log.debug("Deserialized world from " + currTest.getFilename()); currTest.init(w, true); } } enum QueueItemType { MouseDown, MouseMove, MouseUp, MouseDrag, KeyPressed, KeyReleased, LaunchBomb, Pause } class QueueItem { public QueueItemType type; public Vec2 p = new Vec2();; public char c; public int button; public int code; public QueueItem() { type = QueueItemType.LaunchBomb; } public QueueItem(QueueItemType t) { type = t; } public QueueItem(QueueItemType t, Vec2 pt, int button) { type = t; p.set(pt); this.button = button; } public QueueItem(QueueItemType t, char cr, int cd) { type = t; c = cr; code = cd; } }