« Previous: Starter Tutorial 10 - Hello LevelOfDetail
Next: Wiki frontpage »
(Tip: Up-to-date source files for the tutorials are always in the repository)
This tutorial will take away some of the mystery of SimpleGame and what it does in the background. We introduce InputHandlers, KeyBindings, DisplaySystem, Camera, an optional WireframeState, the update cycle and rendering. You will learn how to create your own custom game class (here HelloSimpleGame) derived from BaseGame.
import java.util.logging.Level; import java.util.logging.Logger; import com.jme.app.BaseGame; import com.jme.app.SimpleGame; import com.jme.image.Texture; import com.jme.input.FirstPersonHandler; import com.jme.input.InputHandler; import com.jme.input.KeyBindingManager; import com.jme.input.KeyInput; import com.jme.input.MouseInput; import com.jme.input.joystick.JoystickInput; import com.jme.light.PointLight; import com.jme.math.Vector3f; import com.jme.renderer.Camera; import com.jme.renderer.ColorRGBA; import com.jme.scene.Node; import com.jme.scene.Spatial; import com.jme.scene.Text; import com.jme.scene.Spatial.TextureCombineMode; import com.jme.scene.shape.Box; import com.jme.scene.state.BlendState; import com.jme.scene.state.LightState; import com.jme.scene.state.TextureState; import com.jme.scene.state.WireframeState; import com.jme.scene.state.ZBufferState; import com.jme.system.DisplaySystem; import com.jme.system.JmeException; import com.jme.util.TextureManager; import com.jme.util.Timer; import com.jme.util.geom.Debugger; /** * Started Date: Jul 29, 2004<br> * <br> * * Is used to demonstrate the inner workings of SimpleGame. * * @author Jack Lindamood */ public class HelloSimpleGame extends BaseGame { private static final Logger logger = Logger.getLogger(HelloSimpleGame.class .getName()); public static void main(String[] args) { HelloSimpleGame app = new HelloSimpleGame(); app.setConfigShowMode(ConfigShowMode.AlwaysShow); app.start(); } /** The camera that we see through. */ protected Camera cam; /** The root of our normal scene graph. */ protected Node rootNode; /** Handles our mouse/keyboard input. */ protected InputHandler input; /** High resolution timer for jME. */ protected Timer timer; /** The root node of our text. */ protected Node fpsNode; /** Displays all the lovely information at the bottom. */ protected Text fps; /** Simply an easy way to get at timer.getTimePerFrame(). */ protected float tpf; /** True if the renderer should display bounds. */ protected boolean showBounds = false; /** A wirestate to turn on and off for the rootNode */ protected WireframeState wireState; /** A lightstate to turn on and off for the rootNode */ protected LightState lightState; /** Location of the font for jME's text at the bottom */ public static String fontLocation = "com/jme/app/defaultfont.tga"; /** * This is called every frame in BaseGame.start() * * @param interpolation * unused in this implementation * @see com.jme.app.AbstractGame#update(float interpolation) */ protected final void update(float interpolation) { /** Recalculate the framerate. */ timer.update(); /** Update tpf to time per frame according to the Timer. */ tpf = timer.getTimePerFrame(); /** Check for key/mouse updates. */ input.update(tpf); /** Send the fps to our fps bar at the bottom. */ fps.print("FPS: " + (int) timer.getFrameRate()); /** Call simpleUpdate in any derived classes of SimpleGame. */ simpleUpdate(); /** Update controllers/render states/transforms/bounds for rootNode. */ rootNode.updateGeometricState(tpf, true); /** If toggle_wire is a valid command (via key T), change wirestates. */ if (KeyBindingManager.getKeyBindingManager().isValidCommand( "toggle_wire", false)) { wireState.setEnabled(!wireState.isEnabled()); rootNode.updateRenderState(); } /** If toggle_lights is a valid command (via key L), change lightstate. */ if (KeyBindingManager.getKeyBindingManager().isValidCommand( "toggle_lights", false)) { lightState.setEnabled(!lightState.isEnabled()); rootNode.updateRenderState(); } /** If toggle_bounds is a valid command (via key B), change bounds. */ if (KeyBindingManager.getKeyBindingManager().isValidCommand( "toggle_bounds", false)) { showBounds = !showBounds; } /** If camera_out is a valid command (via key C), show camera location. */ if (KeyBindingManager.getKeyBindingManager().isValidCommand( "camera_out", false)) { logger.info("Camera at: " + display.getRenderer().getCamera().getLocation()); } if (KeyBindingManager.getKeyBindingManager().isValidCommand("exit", false)) { finish(); } } /** * This is called every frame in BaseGame.start(), after update() * * @param interpolation * unused in this implementation * @see com.jme.app.AbstractGame#render(float interpolation) */ protected final void render(float interpolation) { /** Clears the previously rendered information. */ display.getRenderer().clearBuffers(); /** Draw the rootNode and all its children. */ display.getRenderer().draw(rootNode); /** * If showing bounds, draw rootNode's bounds, and the bounds of all its * children. */ if (showBounds) Debugger.drawBounds(rootNode, display.getRenderer()); /** Draw the fps node to show the fancy information at the bottom. */ display.getRenderer().draw(fpsNode); /** Call simpleRender() in any derived classes. */ simpleRender(); } /** * Creates display, sets up camera, and binds keys. Called in * BaseGame.start() directly after the dialog box. * * @see com.jme.app.AbstractGame#initSystem() */ protected final void initSystem() { try { /** * Get a DisplaySystem acording to the renderer selected in the * startup box. */ display = DisplaySystem.getDisplaySystem(settings.getRenderer()); /** Create a window with the startup box's information. */ display.createWindow(settings.getWidth(), settings.getHeight(), settings.getDepth(), settings.getFrequency(), settings .isFullscreen()); /** * Create a camera specific to the DisplaySystem that works with the * display's width and height */ cam = display.getRenderer().createCamera(display.getWidth(), display.getHeight()); } catch (JmeException e) { /** * If the displaysystem can't be initialized correctly, exit * instantly. */ logger.log(Level.SEVERE, "Could not create displaySystem", e); System.exit(1); } /** Set a black background. */ display.getRenderer().setBackgroundColor(ColorRGBA.black.clone()); /** Set up how our camera sees. */ cam.setFrustumPerspective(45.0f, (float) display.getWidth() / (float) display.getHeight(), 1, 1000); 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, 0f, -1.0f); /** Move our camera to a correct place and orientation. */ cam.setFrame(loc, left, up, dir); /** Signal that we've changed our camera's location/frustum. */ cam.update(); /** Assign the camera to this renderer. */ display.getRenderer().setCamera(cam); /** Create a basic input controller. */ FirstPersonHandler firstPersonHandler = new FirstPersonHandler(cam); /** Signal to all key inputs they should work 10x faster. */ firstPersonHandler.getKeyboardLookHandler().setActionSpeed(10f); firstPersonHandler.getMouseLookHandler().setActionSpeed(1f); input = firstPersonHandler; /** Get a high resolution timer for FPS updates. */ timer = Timer.getTimer(); /** Sets the title of our display. */ display.setTitle("SimpleGame"); /** Assign key T to action "toggle_wire". */ KeyBindingManager.getKeyBindingManager().set("toggle_wire", KeyInput.KEY_T); /** Assign key L to action "toggle_lights". */ KeyBindingManager.getKeyBindingManager().set("toggle_lights", KeyInput.KEY_L); /** Assign key B to action "toggle_bounds". */ KeyBindingManager.getKeyBindingManager().set("toggle_bounds", KeyInput.KEY_B); /** Assign key C to action "camera_out". */ KeyBindingManager.getKeyBindingManager().set("camera_out", KeyInput.KEY_C); KeyBindingManager.getKeyBindingManager().set("exit", KeyInput.KEY_ESCAPE); } /** * Creates rootNode, lighting, statistic text, and other basic render * states. Called in BaseGame.start() after initSystem(). * * @see com.jme.app.AbstractGame#initGame() */ protected final void initGame() { /** Create rootNode */ rootNode = new Node("rootNode"); /** * Create a wirestate to toggle on and off. Starts disabled with default * width of 1 pixel. */ wireState = display.getRenderer().createWireframeState(); wireState.setEnabled(false); rootNode.setRenderState(wireState); /** * Create a ZBuffer to display pixels closest to the camera above * farther ones. */ ZBufferState buf = display.getRenderer().createZBufferState(); buf.setEnabled(true); buf.setFunction(ZBufferState.TestFunction.LessThanOrEqualTo); rootNode.setRenderState(buf); // -- FPS DISPLAY // First setup blend state /** * This allows correct blending of text and what is already rendered * below it */ BlendState as1 = display.getRenderer().createBlendState(); as1.setBlendEnabled(true); as1.setSourceFunction(BlendState.SourceFunction.SourceAlpha); as1.setDestinationFunction(BlendState.DestinationFunction.One); as1.setTestEnabled(true); as1.setTestFunction(BlendState.TestFunction.GreaterThan); as1.setEnabled(true); // Now setup font texture TextureState font = display.getRenderer().createTextureState(); /** The texture is loaded from fontLocation */ font.setTexture(TextureManager.loadTexture(SimpleGame.class .getClassLoader().getResource(fontLocation), Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear)); font.setEnabled(true); // Then our font Text object. /** This is what will actually have the text at the bottom. */ fps = Text.createDefaultTextLabel("FPS label", ""); fps.setCullHint(Spatial.CullHint.Never); fps.setTextureCombineMode(TextureCombineMode.Replace); // Finally, a stand alone node (not attached to root on purpose) fpsNode = new Node("FPS node"); fpsNode.attachChild(fps); fpsNode.setRenderState(font); fpsNode.setRenderState(as1); fpsNode.setCullHint(Spatial.CullHint.Never); // ---- LIGHTS /** Set up a basic, default light. */ PointLight light = new PointLight(); light.setDiffuse(new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f)); light.setAmbient(new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f)); light.setLocation(new Vector3f(100, 100, 100)); light.setEnabled(true); /** Attach the light to a lightState and the lightState to rootNode. */ lightState = display.getRenderer().createLightState(); lightState.setEnabled(true); lightState.attach(light); rootNode.setRenderState(lightState); /** Let derived classes initialize. */ simpleInitGame(); /** * Update geometric and rendering information for both the rootNode and * fpsNode. */ rootNode.updateGeometricState(0.0f, true); rootNode.updateRenderState(); fpsNode.updateGeometricState(0.0f, true); fpsNode.updateRenderState(); } protected void simpleInitGame() { rootNode.attachChild(new Box("my box", new Vector3f(0, 0, 0), new Vector3f(1, 1, 1))); } /** * Can be defined in derived classes for custom updating. Called every frame * in update. */ protected void simpleUpdate() { } /** * Can be defined in derived classes for custom rendering. Called every * frame in render. */ protected void simpleRender() { } /** * unused * * @see com.jme.app.AbstractGame#reinit() */ protected void reinit() { } /** * Cleans up the keyboard. * * @see com.jme.app.AbstractGame#cleanup() */ protected void cleanup() { logger.info("Cleaning up resources."); KeyInput.destroyIfInitalized(); MouseInput.destroyIfInitalized(); JoystickInput.destroyIfInitalized(); } }
Wow. So much you’ve taken for granted happens in SimpleGame. No place to start but the beginning:
public static void main(String[] args) { HelloSimpleGame app = new HelloSimpleGame(); app.setConfigShowMode(ConfigShowMode.AlwaysShow); app.start(); }
Let’s look at app.start():
public final void start() { logger.info("Application started."); try { getAttributes(); if (!finished) { initSystem(); assertDisplayCreated(); initGame(); // main loop while (!finished && !display.isClosing()) { // handle input events prior to updating the scene // - some applications may want to put this into update of // the game state InputSystem.update(); // update game state, do not use interpolation parameter update(-1.0f); // render, do not use interpolation parameter render(-1.0f); // swap buffers display.getRenderer().displayBackBuffer(); Thread.yield(); } } } catch (Throwable t) { logger.logp(Level.SEVERE, this.getClass().toString(), "start()", "Exception in game loop", t); if (throwableHandler != null) { throwableHandler.handle(t); } } cleanup(); logger.info( "Application ending."); if (display != null) display.reset(); quit(); }
Don’t let it scare you. Here’s the order:
This basic order exist in most rendering systems.
Lets look at initSystem():
protected final void initSystem() { try { /** * Get a DisplaySystem acording to the renderer selected in the * startup box. */ display = DisplaySystem.getDisplaySystem(settings.getRenderer() ); ... /** * Create a camera specific to the DisplaySystem that works with the * display's width and height */ cam = display.getRenderer().createCamera( display.getWidth(), display.getHeight() ); } catch ( JmeException e ) { /** * If the displaysystem can't be initialized correctly, exit * instantly. */ logger.log(Level.SEVERE, "Could not create displaySystem", e); System.exit( 1 ); } /** Set a black background. */ display.getRenderer().setBackgroundColor( ColorRGBA.black.clone() );
Here we use the values you input in the jME screen to create our game’s settings. Notice DisplaySystem is a singleton class that we initialize with the static function getDisplaySystem.
This line simply sets the background (sky) color; nothing too complex here:
/** Set a black background.*/ display.getRenderer().setBackgroundColor(ColorRGBA.black);
Next we create our camera:
cam = display.getRenderer().createCamera( display.getWidth(), display.getHeight());
This creates a camera that uses the renderer’s given width and height.
Now for the math craziness! Next I setup the view frustum:
cam.setFrustumPerspective(45.0f,
(float) display.getWidth() /
(float) display.getHeight(), 1, 1000);
This frustum is how jME culls away objects that are not being viewed from the camera's angle:
I got this picture from Frustum Culling. This link also contains a very good article about Frustum Culling, which I highly recommend if you’re interested in the inner workings of jME.
As you can see, a frustum in our case is just a pyramid with the tip cut off. We define our frustum with the view angle, ratio of width/height on the screen, and the near/far planes. It looks something like the picture, except it’s 3D. Everything we will see is in the orange. After defining what my camera will see, I move the camera back a bit and signal to the camera that I’ve made changes:
/** Set up how our camera sees. */ cam.setFrustumPerspective(45.0f, (float) display.getWidth() / (float) display.getHeight(), 1, 1000); 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, 0f, -1.0f); /** Move our camera to a correct place and orientation. */ cam.setFrame(loc, left, up, dir); /** Signal that we've changed our camera's location/frustum. */ cam.update(); /** Assign the camera to this renderer. */ display.getRenderer().setCamera(cam);
The function setFrame defines the camera’s location with a position of the camera, the direction it’s looking, and the directions of left and up according to the camera. After all of the camera information is set, I must update the camera (similar to updating renderstates, geometry, and sounds) and set the camera to the renderer.
Next, we create basic movement keys using a predefined class called FirstPersonHandler:
/** Create a basic input controller. */ FirstPersonHandler firstPersonHandler = new FirstPersonHandler(cam); /** Signal to all key inputs they should work 10x faster. */ firstPersonHandler.getKeyboardLookHandler().setActionSpeed(10f); firstPersonHandler.getMouseLookHandler().setActionSpeed(1f); input = firstPersonHandler;
FirstPersonHandler extends InputHandler. InputHandler classes are used to control the inputs for your game from keyboards and mice. In your own game you would make a class extend InputHandler and define your own ways to handle inputs. For here, we just use FirstPersonHandler. It defines the mouse look and ASDW movement keys.
Notice we set the key speed 10x faster. This tells the key readers in FirstPersonHandler to work 10x faster. You could, for example, use this in your game to toggle running and walking.
Next, I get a timer to use for my FPS updates:
/** Get a high resolution timer for FPS updates. */ timer = Timer.getTimer();
This timer uses ‘ticks’ to get a very accurate measure of time between frames. We will use the timer in our updating. Near the end, I enable statistic counting and set a title for my display:
/** Sets the title of our display. */ display.setTitle("SimpleGame");
Finally, I bind the keys T,L,B,C (and more…) to various commands and am done with my system initialization.
/** Assign key T to action "toggle_wire". */ KeyBindingManager.getKeyBindingManager().set("toggle_wire", KeyInput.KEY_T); /** Assign key L to action "toggle_lights". */ KeyBindingManager.getKeyBindingManager().set("toggle_lights", KeyInput.KEY_L); /** Assign key B to action "toggle_bounds". */ KeyBindingManager.getKeyBindingManager().set("toggle_bounds", KeyInput.KEY_B); /** Assign key C to action "camera_out". */ KeyBindingManager.getKeyBindingManager().set("camera_out", KeyInput.KEY_C); KeyBindingManager.getKeyBindingManager().set("exit", KeyInput.KEY_ESCAPE); .....
Now that the system is initialized, I setup the game parts. I start with creating my rootNode, then I create renderstates for my rootNode:
/** * Create a wirestate to toggle on and off. Starts disabled with default width of 1 pixel. */ wireState = display.getRenderer().createWireframeState(); wireState.setEnabled(false); rootNode.setRenderState(wireState); /** Create a ZBuffer to display pixels closest to the camera above farther ones. */ ZBufferState buf = display.getRenderer().createZBufferState(); buf.setEnabled( true ); buf.setFunction( ZBufferState.TestFunction.LessThanOrEqualTo ); rootNode.setRenderState( buf );
The first is a wirestate. That’s used whenever you press T. It draws the meshes as wires, not completely filled triangles. Afterwards I create a ZBufferState that is used to tell rootNode how to render pixels that are on top of each other on the screen. The intuitive way is to draw the pixels closest to your eye, which is what I do by setting buf to draw LEQUAL, i.e. pixels less than or equal to the pixel already there.
The following lines are setting up a graphnode in order to show statistics about the current scene.
if (Debug.stats) { graphNode = new Node( "Graph node" ); graphNode.setCullHint( Spatial.CullHint.Never ); statNode.attachChild(graphNode); setupStatGraphs(); setupStats(); }
The last thing I create are lights, which allow me to see what is going on. Lights are attached to LightStates, which are attached to Spatials. First, I create the light:
// ---- LIGHTS /** Set up a basic, default light. */ PointLight light = new PointLight(); light.setDiffuse(new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f)); light.setAmbient(new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f)); light.setLocation(new Vector3f(100, 100, 100)); light.setEnabled(true);
It is a PointLight (like a lightbulb) with some basic ambient and diffuse colors, located at position 100,100,100. The next step after creating a light is to attach it to a LightState and put the LightState into my rootNode so the light can shine on all the objects below rootNode:
/** Attach the light to a lightState and the lightState to rootNode. */ lightState = display.getRenderer().createLightState(); lightState.setEnabled(true); lightState.attach(light); rootNode.setRenderState(lightState);
None of this should be new. After letting users modify their game like they want with simpleInitGame, geometry and renderstates are updated:
/** Let derived classes initialize. */ simpleInitGame(); /** Update geometric and rendering information for both the rootNode and statNode. */ rootNode.updateGeometricState(0.0f, true); rootNode.updateRenderState(); statNode.updateGeometricState( 0.0f, true ); statNode.updateRenderState();
The 0 is a time value passed to all controllers and the true signals that the function should update things at and below the object. The functions updateGeometricState and updateRenderState must be called whenever geometry or renderstates are changed.
After setting up the game correctly, we enter an infinite while loop of update/render. First, let’s look at update. The first thing in update is to get the correct time between frames:
protected final void update(float interpolation) { /** Recalculate the framerate. */ timer.update(); /** Update tpf to time per frame according to the Timer. */ tpf = timer.getTimePerFrame();
This is done by updating my timer object and asking for the time in seconds between this update call and the last. Once I have the correct time per frame, I check for key updates inside input, update the statistics-node if enabled and process the GameTaskQueueManager's UPDATE-Queue (that is used for executing thread-safe code.
BaseSimpleGame:
/** Check for key/mouse updates. */ updateInput(); /** update stats, if enabled. */ if (Debug.stats) { StatCollector.update(); }
Now that the basic updates are done the user's simpleUpdate method is called which gives the user the possibilty to add his own nodes and spatails to the scene. Afterwards the geometry information of everything in the scene graph is updated, from rootNode down:
SimpleGame.java:
simpleUpdate(); /** Update controllers/render states/transforms/bounds for rootNode. */ rootNode.updateGeometricState(tpf, true); statNode.updateGeometricState(tpf, true);
The final part of update() is some simple checking for key presses. Note that if a key press changes a renderstate, the function updateRenderState() is called.
BaseSimpleGame.class:
/** If toggle_wire is a valid command (via key T), change wirestates. */ if ( KeyBindingManager.getKeyBindingManager().isValidCommand( "toggle_wire", false ) ) { wireState.setEnabled( !wireState.isEnabled() ); rootNode.updateRenderState(); } /** If toggle_lights is a valid command (via key L), change lightstate. */ if ( KeyBindingManager.getKeyBindingManager().isValidCommand( "toggle_lights", false ) ) { lightState.setEnabled( !lightState.isEnabled() ); rootNode.updateRenderState(); } /** If toggle_bounds is a valid command (via key B), change bounds. */ if ( KeyBindingManager.getKeyBindingManager().isValidCommand( "toggle_bounds", false ) ) { showBounds = !showBounds; } /** If camera_out is a valid command (via key C), show camera location. */ if ( KeyBindingManager.getKeyBindingManager().isValidCommand( "camera_out", false ) ) { logger.info( "Camera at: " + display.getRenderer().getCamera().getLocation() ); } if ( KeyBindingManager.getKeyBindingManager().isValidCommand( "exit", false ) ) { finish(); }
After each update cycle, rendering occurs. Statistical information for jME include the vertex and triangle counters you see at the bottom. Whenever a vertex or triangle is rendered, a counter increases. This is just a simple vertex=0;triangle=0; so that stat information is reset every frame. Next we clear the buffers:
Renderer r = display.getRenderer(); /** Clears the previously rendered information. */ r.clearBuffers();
This erases what we’ve previously drawn so we can draw something new. Without it, the frames would appear to stack on top of one another. Next, I draw the rootNode and statsNode:
super.render(interpolation); Renderer r = display.getRenderer(); /** Draw the rootNode and all its children. */ r.draw(rootNode); /** Call simpleRender() in any derived classes. */ simpleRender(); /** Draw the stats node to show our stat charts. */ r.draw(statNode); doDebug(r);
You use display.getRenderer() to get whatever rendering environment you’re currently using (most likely LWJGL), and then use its draw() method to draw the rootnode, which will automatically draw its children. Once done, I allow users to define their own rendering method with simpleRender, so they can customize it when they instantiate HelloSimpleGame.
That’s it! All of that goes on behind our back when we use SimpleGame.
SimpleGame is mainly intended for beginners to get started quickly and easily. But know that you know how it works, you can create your own base class like HelloSimpleGame that extends BaseGame. You can swap the FirstPersonHandler for a ThirdPersonHandler, customize the Key Bindings, etc!