Hi folks,
I've been playing around with jme for some months now and from the very beginning I wished to use the StandarGame and GameStateManager in order to render to a Canvas.
I'm not a "3D guy", I'm a total newbie and I'm not (yet) a game developer. At this time, I'm interested in deploying swing applications for 3D simulation and system test (robotics, physics, etc.). After a lot of nights without sleeping and a lot of fighting I achieved a very basic StandarGame implementation that is capable to render in a Canvas as it was supposed to StandardGame to do. I create a brand new class inspite of messing with StandardGame for some reasons…I give a couple:
1 - StandardGame is final and this new class is not. Further implementations and customizations are imperial for canvas games.
2 - StandardGame has to many issues with the Canvas mode and it doesn't work at all. I tryed to mess with the StandardGame but this implementation is such different that I found that would be a good decision not to mess with the StandardGame class (to much code for such a powerful and complex class)
This is a 0.1 version. It's far away from being stable and there are some bugs/issues that I want to correct in the future:
1 - Once the canvas is On, the displaysystem and sounds can't be reinitialized. I don't really understand about Java's Concurrency and I don't know very well how to call a function/procedure inside GL thread.
2 - (1) Again! locks and unlocks and calling for example, my destroy() will result in a exception because isn't being called withing GL thread…hope to learn much more about this in the future :?
3 - At this moment I can only use InputSystem.INPUT_SYSTEM_AWT provider…I don't know if it is possible to switch between other providers…can I? or when using a swing/awt application I'm limited to the hardware providers?
4 - JMEDesktop and JMEDesktopStates seem to have problems with swing applications and I don't expect them to work…
In spite of those issues, StandarCanvasGame works very much like StandardGame…lol, at least I hope.
So, the code:
package jmecs.app;
import java.awt.Canvas;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import javax.swing.SwingUtilities;
import com.jme.app.AbstractGame;
import com.jme.input.joystick.JoystickInput;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.scene.Node;
import com.jme.system.DisplaySystem;
import com.jme.system.GameSettings;
import com.jme.system.PreferencesGameSettings;
import com.jme.system.canvas.JMECanvasImplementor;
import com.jme.system.jogl.JOGLDisplaySystem;
import com.jme.system.lwjgl.LWJGLDisplaySystem;
import com.jme.util.GameTaskQueue;
import com.jme.util.GameTaskQueueManager;
import com.jme.util.TextureManager;
import com.jme.util.Timer;
import com.jmex.audio.AudioSystem;
import com.jmex.game.state.BasicGameState;
import com.jmex.game.state.GameStateManager;
/**
* @author jecs
* @version 0.2
*
* Just works with LWJGL
*/
public class StandardCanvasGame extends AbstractGame
{
private static final Logger logger = Logger.getLogger(StandardCanvasGame.class.getName());
// #region Variables
protected Renderer renderer;
protected Camera camera;
protected GameStateManager stateManager;
protected Timer timer;
protected Float tpf;
protected BasicGameState rootState;
protected Node rootNode;
protected Canvas canvas;
protected JMECanvasImplementor implementor;
// #endregion Variables
// #region Getters & Setters
public Renderer getRenderer()
{
return renderer;
}
public Camera getCamera()
{
return camera;
}
public GameStateManager getStateManager()
{
return stateManager;
}
public Timer getTimer()
{
return timer;
}
public Float getTpf()
{
return tpf;
}
public Canvas getCanvas()
{
return canvas;
}
public DisplaySystem getDisplay()
{
return display;
}
public GameSettings getSettings()
{
return settings;
}
// #endregion Getters & Setters
// #region Constructors
public StandardCanvasGame()
{
this(null);
}
public StandardCanvasGame(GameSettings settings)
{
this.settings = settings;
if (this.settings == null)
{
getAttributes();
}
DisplaySystem.getDisplaySystem(this.settings.getRenderer());
}
// #endregion Constructors
// #region AbstractGame Implementation
@Override
public void start()
{
implementor = new BaseCanvasGameImplementor();
StandardJMECanvas canvasFactory = new StandardJMECanvas(settings.getWidth(), settings.getHeight(), implementor, this);
canvas = canvasFactory.getGlCanvas();
}
@Override
protected void initSystem()
{
logger.info(getVersion());
display = DisplaySystem.getDisplaySystem();
displayMins();
// initSound();
assertDisplayCreated();
if (settings.getRenderer().equalsIgnoreCase("JOGL"))
((JOGLDisplaySystem) display).initForCanvas(display.getWidth(), display.getHeight());
else
((LWJGLDisplaySystem) display).initForCanvas(display.getWidth(), display.getHeight());
logger.info("Running on: " + display.getAdapter() + "nDriver version: " + display.getDriverVersion() + "n"
+ display.getDisplayVendor() + " - " + display.getDisplayRenderer() + " - "
+ display.getDisplayAPIVersion());
renderer = display.getRenderer();
camera = display.getRenderer().createCamera(display.getWidth(), display.getHeight());
implementor.setRenderer(display.getRenderer());
/** Set a black background. */
display.getRenderer().setBackgroundColor(ColorRGBA.black.clone());
// Configure Camera
cameraPerspective();
cameraRealocate();
camera.update();
display.getRenderer().setCamera(camera);
/** Get a high resolution timer for FPS updates. */
timer = Timer.getTimer();
/** Sets the title of our display. */
String className = getClass().getName();
if (className.lastIndexOf('.') > 0)
className = className.substring(className.lastIndexOf('.') + 1);
display.setTitle(className);
if ((settings.isMusic()) || (settings.isSFX()))
{
initSound();
}
}
@Override
protected void initGame()
{
GameStateManager.create();
stateManager = GameStateManager.getInstance();
}
@Override
protected void render(float interpolation)
{
renderer.clearBuffers();
// Execute renderQueue item
GameTaskQueueManager.getManager().getQueue(GameTaskQueue.RENDER).execute();
// Render the GameStates
GameStateManager.getInstance().render(interpolation);
}
@Override
protected void update(float interpolation)
{
// Execute updateQueue item
GameTaskQueueManager.getManager().getQueue(GameTaskQueue.UPDATE).execute();
// Update the GameStates
GameStateManager.getInstance().update(interpolation);
if ((settings.isMusic()) || (settings.isSFX()))
{
AudioSystem.getSystem().update();
}
}
@Override
protected void cleanup()
{
GameStateManager.getInstance().cleanup();
try
{
if (renderer != null)
renderer.cleanup(); // causing problems! -> GLCapabilities is null!?
} catch (Exception e)
{
// e.printStackTrace();
}
TextureManager.doTextureCleanup();
TextureManager.clearCache();
JoystickInput.destroyIfInitalized();
if (AudioSystem.isCreated())
{
AudioSystem.getSystem().cleanup();
}
}
@Override
protected GameSettings getNewSettings()
{
boolean newNode = true;
Preferences userPrefsRoot = Preferences.userRoot();
try
{
newNode = !userPrefsRoot.nodeExists(this.getClass().getName());
} catch (BackingStoreException bse)
{
}
return new PreferencesGameSettings(userPrefsRoot.node(this.getClass().getName()), newNode,
"game-defaults.properties");
}
public boolean isSetup()
{
if (implementor == null)
return false;
return implementor.isSetup();
}
protected void initSound()
{
AudioSystem.getSystem().getEar().trackOrientation(camera);
AudioSystem.getSystem().getEar().trackPosition(camera);
}
private void displayMins()
{
display.setMinDepthBits(settings.getDepthBits());
display.setMinStencilBits(settings.getStencilBits());
display.setMinAlphaBits(settings.getAlphaBits());
display.setMinSamples(settings.getSamples());
}
@Override
protected void quit()
{
if (display != null)
{
display.reset();
display.close();
}
}
@Override
protected void reinit()
{
reinitAudio();
reinitVideo();
}
/**
* Not Yet Tested
*/
protected void reinitAudio()
{
if (AudioSystem.isCreated())
{
AudioSystem.getSystem().cleanup();
}
}
/**
* Not Yet Tested
*/
protected void reinitVideo()
{
displayMins();
camera = display.getRenderer().createCamera(display.getWidth(), display.getHeight());
if ((settings.isMusic()) || (settings.isSFX()))
{
initSound();
}
}
/**
* Not Yet Tested
*/
public void destroy()
{
try
{
cleanup();
} catch (Exception e)
{
e.printStackTrace();
logger.warning("Problems while destroying!");
} finally
{
quit();
}
}
// #endregion AbstractGame Implementation
// #region Other
private Object setupLock = new Object();
public void waitForSetup() throws InterruptedException
{
if (!SwingUtilities.isEventDispatchThread())
{
synchronized (setupLock)
{
if (!isSetup())
{
setupLock.wait();
}
}
}
}
protected void cameraRealocate()
{
Vector3f loc = new Vector3f(0.0f, 0.0f, 25.0f);
Vector3f left = new Vector3f(-1.0f, 0.0f, 0.0f);
Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f);
Vector3f dir = new Vector3f(0.0f, 0.0f, -1.0f);
camera.setFrame(loc, left, up, dir);
}
protected void cameraPerspective()
{
camera.setFrustumPerspective(45.0f, (float) canvas.getWidth() / (float) canvas.getHeight(), 1.0f, 1500.0f);
camera.setParallelProjection(false);
camera.update();
}
protected void cameraParallel()
{
camera.setParallelProjection(true);
float aspect = (float) canvas.getWidth() / (float) canvas.getHeight();
camera.setFrustum(-100, 1500, -50 * aspect, 50 * aspect, -50, 50);
camera.normalize();
camera.update();
}
public void resetCamera()
{
cameraRealocate();
}
// #endregion Other
protected class BaseCanvasGameImplementor extends JMECanvasImplementor
{
private Lock updateLock;
int preferredFPS = -1;
long preferredTicksPerFrame = -1;
long frameStartTick = -1;
long frames = 0;
long frameDurationTicks = -1;
@Override
public void doSetup()
{
updateLock = new ReentrantLock(true); // Make our lock be fair (first come, first serve)
lock();
initSystem();
initGame();
preferredFPS = settings.getFramerate() > 0 ? settings.getFramerate() : 60;
if (preferredFPS >= 0)
{
preferredTicksPerFrame = Math.round((float) timer.getResolution() / (float) preferredFPS);
}
super.doSetup();
synchronized (setupLock)
{
setupLock.notifyAll();
}
}
@Override
public void doUpdate()
{
// Open the lock up for just a brief second
unlock();
lock();
if (preferredTicksPerFrame >= 0)
frameStartTick = timer.getTime();
timer.update();
tpf = timer.getTimePerFrame();
update(tpf);
}
@Override
public void doRender()
{
render(tpf);
renderer.displayBackBuffer();
fixFrameRate();
}
public void lock()
{
updateLock.lock();
}
public void unlock()
{
updateLock.unlock();
}
private void fixFrameRate()
{
if (preferredTicksPerFrame >= 0)
{
frames++;
frameDurationTicks = timer.getTime() - frameStartTick;
while (frameDurationTicks < preferredTicksPerFrame)
{
long sleepTime = ((preferredTicksPerFrame - frameDurationTicks) * 1000) / timer.getResolution();
try
{
Thread.sleep(sleepTime);
} catch (InterruptedException exc)
{
logger.log(Level.SEVERE, "Interrupted while sleeping in fixed-framerate", exc);
}
frameDurationTicks = timer.getTime() - frameStartTick;
}
if (frames == Long.MAX_VALUE)
frames = 0;
}
}
}
}
and the StandarJMECanvas class:
package jmecs.app;
import java.awt.Canvas;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.concurrent.Callable;
import java.util.logging.Logger;
import com.jme.input.InputSystem;
import com.jme.input.KeyInput;
import com.jme.input.MouseInput;
import com.jme.system.DisplaySystem;
import com.jme.system.canvas.JMECanvas;
import com.jme.system.canvas.JMECanvasImplementor;
import com.jme.util.GameTaskQueue;
import com.jme.util.GameTaskQueueManager;
import com.jmex.awt.input.AWTKeyInput;
import com.jmex.awt.input.AWTMouseInput;
import com.jmex.awt.lwjgl.LWJGLAWTCanvasConstructor;
/**
* @author jecs
* @version 0.2
*
* Just works with LWJGL
*/
public class StandardJMECanvas implements JMECanvas
{
private static final Logger logger = Logger.getLogger(StandardJMECanvas.class.getName());
protected Canvas glCanvas = null;
protected JMECanvasImplementor implementor = null;
protected DisplaySystem display = null;
protected StandardCanvasGame game = null;
public StandardJMECanvas(int width, int height, JMECanvasImplementor impl, StandardCanvasGame game)
{
display = DisplaySystem.getDisplaySystem();
display.registerCanvasConstructor("AWT", LWJGLAWTCanvasConstructor.class);
glCanvas = (Canvas) display.createCanvas(width, height);
this.game = game;
// setup future resize behaviour
glCanvas.addComponentListener(new ComponentAdapter()
{
@Override
public void componentResized(ComponentEvent ce)
{
doResize();
}
});
// setup the way Key and Mouse Inputs are set on and off (focus dependant)
glCanvas.addFocusListener(new FocusListener()
{
public void focusGained(FocusEvent arg0)
{
((AWTKeyInput) KeyInput.get()).setEnabled(true);
((AWTMouseInput) MouseInput.get()).setEnabled(true);
}
public void focusLost(FocusEvent arg0)
{
((AWTKeyInput) KeyInput.get()).setEnabled(false);
((AWTMouseInput) MouseInput.get()).setEnabled(false);
}
});
// grant focus when mouse entered
glCanvas.addMouseListener(new MouseAdapter()
{
@Override
public void mouseEntered(MouseEvent e)
{
glCanvas.requestFocus();
super.mouseEntered(e);
}
});
setImplementor(impl);
if (!KeyInput.isInited())
KeyInput.setProvider(InputSystem.INPUT_SYSTEM_AWT);
if (!MouseInput.isInited())
AWTMouseInput.setup(glCanvas, false);
((AWTKeyInput) KeyInput.get()).setEnabled(true);
((AWTMouseInput) MouseInput.get()).setEnabled(true);
KeyListener kl = (KeyListener) KeyInput.get();
glCanvas.addKeyListener(kl);
setUpdateInput(true);
Callable<?> exe = new Callable<?>()
{
public Object call()
{
forceUpdateToSize();
return null;
}
};
GameTaskQueueManager.getManager().getQueue(GameTaskQueue.RENDER).enqueue(exe);
}
protected void doResize()
{
if (implementor != null && implementor.getRenderer() != null)
{
implementor.resizeCanvas(glCanvas.getWidth(), glCanvas.getHeight());
logger.info("Canvas will resize to: (" + glCanvas.getWidth() + "," + glCanvas.getHeight() + ")");
// Maintain aspect ratio
Callable<?> exe = new Callable<?>() {
public Object call() {
if(game.getCamera().isParallelProjection())
game.cameraParallel();
else
game.cameraPerspective();
return null;
}
};
GameTaskQueueManager.getManager()
.getQueue(GameTaskQueue.RENDER).enqueue(exe);
}
}
public void forceUpdateToSize()
{
// force a resize to ensure proper canvas size.
glCanvas.setSize(glCanvas.getWidth(), glCanvas.getHeight() + 1);
glCanvas.setSize(glCanvas.getWidth(), glCanvas.getHeight() - 1);
}
public JMECanvasImplementor getImplementor()
{
return implementor;
}
@Override
public void setImplementor(JMECanvasImplementor impl)
{
((JMECanvas) glCanvas).setImplementor(impl);
this.implementor = impl;
}
@Override
public void setUpdateInput(boolean doUpdate)
{
((JMECanvas) glCanvas).setUpdateInput(doUpdate);
}
public Canvas getGlCanvas()
{
return glCanvas;
}
/**
* Same as {@link getGlCanvas}
*
* @return glCanvas
*/
public Canvas getCanvas()
{
return glCanvas;
}
@Override
public int getTargetSyncRate()
{
return ((JMECanvas) glCanvas).getTargetSyncRate();
}
@Override
public boolean isDrawWhenDirty()
{
return ((JMECanvas) glCanvas).isDrawWhenDirty();
}
@Override
public boolean isUpdateInput()
{
return ((JMECanvas) glCanvas).isUpdateInput();
}
@Override
public void makeDirty()
{
((JMECanvas) glCanvas).makeDirty();
}
@Override
public void setDrawWhenDirty(boolean whenDirty)
{
((JMECanvas) glCanvas).setDrawWhenDirty(whenDirty);
}
@Override
public void setTargetRate(int fps)
{
((JMECanvas) glCanvas).setTargetRate(fps);
}
@Override
public void killGfxContext()
{
((JMECanvas) glCanvas).killGfxContext();
}
@Override
public void setAutoKillGfxContext(boolean shouldAutoKillGfxContext)
{
((JMECanvas) glCanvas).setAutoKillGfxContext(shouldAutoKillGfxContext);
}
@Override
public boolean shouldAutoKillGfxContext()
{
return ((JMECanvas) glCanvas).shouldAutoKillGfxContext();
}
}
Hope that my first contribution helps at least one folk...
Any feedback, ideas and help are very much appreciated. :) jecs
(Sorry for my english)