The beauty of AppStates and controls

The ideal jMonkeyEngine application has an empty simpleUpdate() method in its main class—all entity behavior would be neatly modularized and encapsulated in controls and AppState objects.

The ideal jMonkeyEngine application's simpleInitApp() method would have only two lines of code: one that creates a custom StartScreenAppState instance, and a second line that attaches it to the stateManager object of the SimpleApplication class. Let's look at one simple example of how you can structure your application using several modular AppState objects:

  1. The StartScreenAppState tab would display a main menu with buttons, such as play, options, and quit. It has a visible mouse pointer and its inputManager object responds to clicks on buttons.
  2. When the user clicks on the options button, the StartScreenAppState method attaches an OptionsScreenAppState object and detaches itself.
  3. The OptionsScreenAppState object displays user preferences and offers a user interface to customize options, such as keyboard layout, graphic quality, or difficulty.
  4. When the user clicks on the save button, the OptionsScreenAppState object attaches the StartScreenAppState method again, and detaches itself.
  5. When the user clicks on the play button, the StartScreenAppState method attaches the GameRunningAppState object, and detaches itself.
  6. The GameRunningAppState object generates the new level content and runs the game logic. The GameRunningAppState object can attach several subordinate AppState objects—for example, a WorldManagerState object that attaches nodes to the rootNode object, a PhysicsState object that handles falling and colliding objects, and a ScreenshotAppState object that saves rendered scenes as image files to the user's desktop. The inputManager object of the AppState class hides the visible cursor and switches to in-game inputs, such as click-to-shoot and WASD-keys navigation.
  7. When the user presses the Esc key, you save the game state, the GameRunningAppState object attaches the StartScreenAppState method, and detaches itself and its subordinate AppState objects.
  8. When the user clicks on the quit button on the StartScreenAppState method, the game quits.

Many games also offer a key that pauses and resumes the game. The game loop freezes, but the game state remains in memory. In this chapter, you learned three ways of adding interaction to a game, namely the InputListener objects, the simpleUpdate() method, Controls, and AppState objects.

To implement pausing for your game, you need to write code that saves the current state and switches off all the in-game interactions again, one by one. This paused state is similar to the non-game StartScreenAppState method or the OptionsScreensAppState state. This is why many games simply switch to the OptionsScreensAppState state while paused. You can also use a conditional to toggle an isRunning boolean variable that temporarily disables all update loops.

A paused game typically also temporarily switches to a different set of the input manager settings to stop responding to user input—but make sure to keep responding to your resume key!

Pop quiz – how to control game mechanics

Q1. Combine the following sentence fragments to form six true statements:

The simpleUpdate() method... b) An AppState object... c) A Control...

  1. … lets you add accessors and class fields to a spatial.
  2. … lets you hook actions into the main update loop.
  3. … can be empty.
  4. … can initialize a subset of the scene graph.
  5. … defines a subset of application-wide game logic.
  6. … defines a subset of game logic for one type of spatial.

Have a go hero – shoot down the creeps!

Return to the Tower Defense game that you created in the previous chapter—let's add some interaction to this static scene of creeps, towers, and the player base. You want the player to click to select a tower, and press a key to charge the tower as long as the budget allows. You want the creeps to spawn and walk continuously along the z axis and approach the base. Each creep that reaches the base decreases the player's health until the player loses. You want the towers to shoot at all creeps in range as long as they are charged with ammunition. Killing creeps increases the player's budget, and when the last creep is destroyed, the player wins. Use what you learned in this chapter!

  1. Create a GamePlayAppState class that extends the AbstractAppState class. Move the code that initializes the scene nodes from the simpleInitApp () method into the GamePlayAppState object's initialize() method. In the cleanup() method, write the code that detaches these nodes along the same lines. Add an instance of this state to the stateManager object of the Main class.
  2. To have a game state to interact with, you first need to define user data. In the GamePlayAppState class, define integer class fields for the player's level, score, health, and budget, and a Boolean variable lastGameWon, including accessors. In your code that creates towers, use the setUserData() method to give each tower index and chargesNum fields. Where you create creeps, use the setUserData() method to give each creep index and health fields.
  3. Create a Charges class. The charges that the towers shoot are instances of a plain old Java object that stores a damage value and the number of remaining bullets. (You can extend this class and add accessors that support different types of attacks, such as freeze or blast damage, or different forces.)
  4. Create a CreepControl class that extends the AbstractControl class and lets creep spatials communicate with the game. The constructor takes the GamePlayAppState object instance as an argument—because the creep needs access to player budget and player health. Write accessors for the creep's user data (index and health). In the code block where you create creeps, use the addControl(new CreepControl(this)) method on each creep.
  5. Implement the controlUpdate() method of the CreepControl class to define the behavior of your creep spatials: creeps storm the base. As long as the creep's health is larger than zero, it moves one step closer to the player base (at the origin). It then tests whether its z coordinate is zero or lower. If yes, it has stormed the player base: the creep subtracts one life from the player's health and it detaches itself. If the creep's health is below zero, the towers have killed the creep: the creep adds a bonus to the player budget, and detaches itself.
  6. Create a TowerControl class that extends the AbstractControl class and lets tower spatials communicate with the game. The constructor takes the GamePlayAppState object instance as an argument because the towers need access to the list of creeps and the beam_node object. Write accessors for the tower's user data (index, height, and ChargesNum). In the code block where you create towers, use the addControl(new TowerControl(this)) method on each tower.
  7. Implement the controlUpdate() method of the TowerControl class to define the behavior of your tower spatials: towers shoot at creeps. Each tower maintains an array of charges. If the tower has one or more charges, it loops over the ArrayList of creep objects that it gets from the GamePlayAppState object, and uses the creep_geo.getControl(CreepControl.class) method to get access to the CreepControl object. The tower determines whether the distance between itself and each creep is lower than a certain value. If yes, it collects the CreepControl object into a reachable ArrayList. If this list has one or more elements, creeps are in range, and the tower shoots one bullet of the top charge at each reachable creep until the charge is empty. For now, we use the com.jme3.scene.shape.Line class to visualize the shots as plain lines that are drawn from the top of the tower towards the location of the creep. Attach all lines to the beam Node object in the scene graph. Each hit applies the charge's damage value to the creep, and decreases the number of bullets in the charge. If a charge's bullet counter is zero, you remove it from the tower's Charge array and stop shooting.
  8. In the Main.java simpleInitApp() method, create an input mapping that lets the player select a tower with a left-click, and one that charges the selected tower by pressing a key. Implement an ActionListener object that responds to these two mappings. When the player clicks to select, use ray casting with a visible mouse pointer to identify the clicked tower, and store the tower's index in a class field selected (otherwise selected should be -1). If the player presses the charge key, and the budget is not zero, use, for example, the rootNode.getChild("tower-" + selected).getControl(TowerControl.class) method to get the TowerControl instance. Add a new Charge object to the tower control, and subtract the charge's price from the budget.
  9. In the GamePlayAppState object's update() method, you maintain timed events using two float class fields, timer_budget, and timer_beam. The timerBudget variable adds the tpf (time per frame in seconds) to itself until it is larger than 10 (such seconds), then it resets itself to zero and increases the player budget. The Timer_beam field does the same, but it only waits for one second before it clears all accumulated beam geometries from the beam Node node.
  10. In the GamePlayAppState object's update() method, you also check whether the player has won or lost. Test whether health is lower than or equal to zero. If yes, you set the lastGameWon variable to false and detach this GamePlayAppState object (which ends the game). If the player is still healthy and no creeps are alive, then the player has won. Set the lastGameWon object to true and detach this GamePlayAppState object (which ends the game). Else, the player is healthy but the creeps are still attacking. The TowerControl class and the CreepControl class handle this case.