Friday, April 13, 2018

Simple Arcade Game 2: Intro to Appstates

Welcome back to another lesson in making a simple arcade game in jMonkey Engine. last time we got the jme sdk installed and created our project. Today we will look at jMonkeys powerful appstates, input handling and a first glance at the scenegraph.

Let's open up our project and take a look at the example class that was provided.

public class Main extends SimpleApplication {

    public static void main(String[] args) {
        Main app = new Main();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        Box b = new Box(1, 1, 1);
        Geometry geom = new Geometry("Box", b);

        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Blue);
        geom.setMaterial(mat);

        rootNode.attachChild(geom);
    }

    @Override
    public void simpleUpdate(float tpf) {
        //TODO: add update code
    }

    @Override
    public void simpleRender(RenderManager rm) {
        //TODO: add render code
    }
}

The first part is our java main method. This is called by the jvm, and here we create our jmonkey application.

Next is the SimpleApplication method, simpleInitApp(). This is called after the application is created and has finished starting up all of jmonkey's dependencies, but before simpleUpdate and simpleRender. Here you can initialize the game state, create game objects and modify the scene graph. By default it creates a box mesh, wraps it in a geometry, assigns a blue material to the geometry and attaches the geometry to the scene graph. We'll discuss geometries and the scene graph in more detail later.

Next up is the simpleUpdate method from SimpleApplication. SimpleUpdate is called once every frame and is where you should store game logic that happens over time. Frames are called as fast as possible, and while your game is small it's very possible to see 4000+ frames called every second. The argument "tpf" (Time Per Frame) is used to provide you with the amount of time, in seconds, the previous frame took to execute. This value is what you will use to move objects smoothly and consistently.

The last method created by default is the simpleRender method. We will not be using the simpleRender method for our simple arcade game and it is safe to simply remove it for now. SimpleRender is called after the simpleUpdate method and can be used to modify how the scene is rendered through the RenderManager. It is not safe to modify the scenegraph from this method.

You may be tempted to start writing your game logic and putting it all in the nice simpleUpdate method, and you wouldn't be wrong to do so. Super small games can easily exist entirely within one class and you could get away with using your SimpleApplication for most of your game logic, but did you know there's a better way?

JMonkey provides us with a concept called "App States". An appstate is a confined version of simpleInit and simpleUpdate which can be attached to a simple application and every frame all of your appstates will be executed before calling simpleUpdate.

Why would you want to use an appstate? The answer is simple, you can seperate un related code, easily pause/unpause or remove game logic simply by attaching or removing your appstates. You can have a main menu appstate, which you disable when you start your game appstate. Your game appstate could create several additional app states to process the game state. When you press the pause button you can easily pause only the game appstate while activating a new pause menu appstate and so on. Appstates make game design easy and robust and are one of jmonkey's most powerful features. Let's go ahead and create a main menu state right now!


In the sdk right click the package that contains your Main class and select "New/Other." From there select JME3 Classes category and under File Type choose "New AppState."


Let's name our new appstate "MainMenu" and click finish. You should have a class named "MainMenu" with the following code:

public class MainMenu extends AbstractAppState {
    
    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        //TODO: initialize your AppState, e.g. attach spatials to rootNode
        //this is called on the OpenGL thread after the AppState has been attached
    }
    
    @Override
    public void update(float tpf) {
        //TODO: implement behavior during runtime
    }
    
    @Override
    public void cleanup() {
        super.cleanup();
        //TODO: clean up what you initialized in the initialize method,
        //e.g. remove all spatials from rootNode
        //this is called on the OpenGL thread after the AppState has been detached
    }
}

Looks familiar doesn't it? You have an initialize method which is called before any update method the frame the appstate gets added to your application. Next is the update method, called once a frame before the SimpleApplication's update.

The last method is a new one, cleanup(). Cleanup is called before any update on the frame after an appstate has been removed from the application. This is how you can remove any objects associated with your appstate from the scenegraph.

Now lets have this appstate display something when we activate it!

Out of the box JME doesn't have the most robust built in gui library, but it does have plenty to get us started. We'll make use of the BitmapFont and BitmapText classes to display a simple label on the screen.

First let's add a private field to MainMenu to hold a reference to our BitmapText object. Then we need to load the default font file provided with jme. To load assets we'll use the Applications asset manager. BitmapFont has a method, "createLabel" that will create a jme spatial we can attach to the scene graph. To display the text we need to attach it to SimpleApplications default gui node. We'll need to cast app to accomplish this.

public class MainMenu extends AbstractAppState {
    private BitmapText text;

    @Override
    public void initialize(AppStateManager stateManager, Application app){
        //jME has a built in font which can be loaded from "Interface/Fonts/Default.fnt"
        BitmapFont font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
        text = font.createLabel("Hello jMonkey Engine!");
        ((SimpleApplication)app).getGuiNode().attachChild(text);
    }

This will attach our text to the origin point of the Gui Node. We'll go more in depth with the scene graph next time, for now you only need to know there are 2 scenes included with SimpleApplication, the Gui scene and the Root scene.

Let's hop back to our Main class and create an instance of our appstate. Go ahead and delete everything in simpleInitApp(), we don't want the blue cube anymore.


public class Main extends SimpleApplication {

    public static void main(String[] args) {
        Main app = new Main();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        MainMenu menu = new MainMenu();
        stateManager.attach(menu);
        Box b = new Box(1, 1, 1);
        Geometry geom = new Geometry("Box", b);

        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Blue);
        geom.setMaterial(mat);

        rootNode.attachChild(geom);
    }

    @Override
    public void simpleUpdate(float tpf) {
        //TODO: add update code
    }
}

Now if you hit play, you don't see anything. This is because of how jmonkey handles the gui scene. Our text is spawning at 0,0 and going DOWN, which pushes it off of our screen! not good, let's fix it! Go back to the MainMenu appstate and let's move the text.

public class MainMenu extends AbstractAppState {
    private BitmapText text;
    
    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        
        BitmapFont font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
        text = font.createLabel("Hello jMonkey Engine!");
        
        //set the text to the center of the screen
        Camera cam = app.getCamera();
        int width = cam.getWidth();
        int height = cam.getHeight();
        float x = (width/2)-(text.getLineWidth()/2);
        float y = (height/2)+(text.getLineHeight()/2);
        text.setLocalTranslation(x, y, 0);

        ((SimpleApplication)app).getGuiNode().attachChild(text);
    }
    
    @Override
    public void update(float tpf) {
        //TODO: implement behavior during runtime
    }
    
    @Override
    public void cleanup() {
        super.cleanup();
        text.removeFromParent();
    }
}

Wait, that's a lot of new code! It's ok, first we need our applications resolution, the easiest way to get this is from our camera's width and height. Next we find the center. The gui origin is the bottom left, and text grows from the top down. Lastly we need to set the translation with setLocalTranslation.

Now is also a good time to remove the text from the scenegraph in our cleanup method, this way when the appstate is closed the text will go with it!

Now try pressing play. Your message should be nicely visible in the center of the screen!


That's the basics to Appstates, you can create them, attach and detach them to add and remove game behaviors. There is also a convenient setEnabled() method you can use to pause an appstate you don't want to remove completely. We'll be making extensive use of appstates to get our simple arcade game running well!