Monday, July 2, 2018

Simple Arcade Game 3: Scenegraph

Hello again and welcome to another jMonkey Engine tutorial! Today we are going to take a detailed look at probably the most important aspect of jMonkey Engine, it's Scenegraph.

A great overview of how jme uses it's scenegraph is available here, but I'll run a brief overview for those who want to dive right in.

The Scene in jme is made up of Spatials, mostly Nodes and Geometries. Nodes do not have any visual elements, but can have children elements. Geometries have meshes and materials and will be rendered, these are the elements you actually see! All spatials must be connected to the RootNode, which is the top most node in the scene. You can easily make complex or large scale models with many moving parts by mixing Nodes and Geometries.

The most important thing to remember: DO NOT EXTEND NODE, GEOMETRY OR SPATIAL. Instead you should store your game data seperately, in the Spatial's UserData container, or as a Control which is attached to a spatial.

UserData can be any Object and is easily written by using Spatial.setUserData(String key, Object data), and later retrieved using Spatial.getUserData(String key).

Controls are a lot like AppStates, except they only work on the spatial they are attached to. We will go over Controls in a later tutorial, but if you are interested in making your own controls now they are easily attached to a spatial using Spatial.addControl(Control control), and retrieved by using Spatial.getControl(Class<T> controlType) where T extends Control.

We are going to make a new appstate for our game, let's call it "GameState". We need to override the "initialize" method as this is where we will be modifying our scene.


public class GameState extends AbstractAppState{
    private Spatial player;
    private Node scene;

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        SimpleApplication application = (SimpleApplication)app;
        AssetManager am = application.getAssetManager();
        Node rootNode = application.getRootNode();
        scene = new Node("Game_Scene");
        rootNode.attachChild(scene);
    }
    
}

Lets go ahead and cast the Application app to a SimpleApplication so we can access our RootNode and the AssetManager. We should also define a Spatial object that will reference the Players spatial so we can easily find it later. We also define a Node for the Scene. Create a new Node with a string argument for the node's name and attach it to the root node via the Node.attachChild() method. We will be attaching all the game objects to the game scene instead of the root node to keep them separated and easy to work with!

Now let's define a new method to create our player object. We will eventually be using a Dragon model, but for now let's simple spawn a box to represent our player. First we define a new method, createPlayer(AssetManager am). We are going to pass the assetManager from our Application object to this method to allow the use of pre-defined material definitions which really speed this process.

public class GameState extends AbstractAppState{

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        SimpleApplication application = (SimpleApplication)app;
        AssetManager am = application.getAssetManager();
        Node rootNode = application.getRootNode();
        scene = new Node("Game_Scene");
        rootNode.attachChild(scene);

        player = createPlayer(am);
        scene.attachChild(player);
    }

    private Spatial createPlayer(AssetManager am){
        Mesh mesh = new Box(0.5f,0.5f,1f);
        Geometry geo = new Geometry("Player", mesh);
        Material mat = new Material(am, "Common/MatDefs/Misc/Unshaded.j3md");
        geo.setMaterial(mat);
        return geo;
    }
}

Spawning a model is as simple as creating a geometry, giving it a mesh and a material. Here we use the Box shape to create our mesh. With 3 floating point arguments we construct a box that extends into the x, y and z axis by the given amount. This will give us a box that is actually 1x1x2. Simply pass the mesh to the Geometry as an argument along with a String name to attach the mesh to the geometry.

Next we create the material. Here we are using a Material Definition which is how jmonkey defines what pixel and vertex shaders control the material. We are using the built-in material definition for Unshaded objects by simply defining the path to the j3md file. You can view other material definitions included in the jme3 library in the jme3-core.jar/Common/MatDefs. Call Geometry.setMaterial() and we are finished.

In the initialize method add the 2 lines player = createPlayer(am) and scene.attachChild(player).

This is enough to have our GameState spawn a player object and attach it to the GameScene node, which in turn is attached to the RootNode and will be rendered. Now we need to attach the GameState to the AppStateManager, let's do this in our MainMenu state so in the future we can have a button start the game.

Open our MainMenu appstate and add the following lines.

public class MainMenu extends AbstractAppState {
    private AppStateManager stateManager;
    private float countDown = 3f;
    private BitmapText text;

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        this.stateManager = stateManager;
        
        ...
    }
    
    @Override
    public void update(float tpf) {
        if((countDown -= tpf) <= 0){
            stateManager.detach(this);
            stateManager.attach(new GameState());
        }
    }
    
    ...
}

First we need to define 2 new fields, stateManager and countDown. The state manager is passed via the initialize() method when the AppState is attached to the AppStateManager. Go ahead and assign this via "this.stateManager = stateManager" so we can access it later.

Then in our update loop we are going to count down from 3. Once our countdown is equal to or less than 0, we will remove MainMenu from our AppStateManager and attach a new GameState. Do this by simple subtracting tpf from countDown once a frame and checking with a basic if statement. Remember, in a video game you NEVER want to use wait() as this will completely lock your thread from doing other things. Making simple timers like the one above is easy and guarantees that your other appstates have a chance to  run their updates while we wait.

Now if you press Play you will see our lovely Main Menu for approximately 3 seconds before being taken to our game state, and spawning in a simple, white cube!

No comments:

Post a Comment