- jMonkeyEngine 3.0 Beginner’s Guide
- Ruth Kusterer
- 1588字
- 2025-04-04 22:38:53
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:
- The
StartScreenAppState
tab would display a main menu with buttons, such as play, options, and quit. It has a visible mouse pointer and itsinputManager
object responds to clicks on buttons. - When the user clicks on the options button, the
StartScreenAppState
method attaches anOptionsScreenAppState
object and detaches itself. - The
OptionsScreenAppState
object displays user preferences and offers a user interface to customize options, such as keyboard layout, graphic quality, or difficulty. - When the user clicks on the save button, the
OptionsScreenAppState
object attaches theStartScreenAppState
method again, and detaches itself. - When the user clicks on the play button, the
StartScreenAppState
method attaches theGameRunningAppState
object, and detaches itself. - The
GameRunningAppState
object generates the new level content and runs the game logic. TheGameRunningAppState
object can attach several subordinateAppState
objects—for example, aWorldManagerState
object that attaches nodes to therootNode
object, aPhysicsState
object that handles falling and colliding objects, and aScreenshotAppState
object that saves rendered scenes as image files to the user's desktop. TheinputManager
object of theAppState
class hides the visible cursor and switches to in-game inputs, such as click-to-shoot and WASD-keys navigation. - When the user presses the Esc key, you save the game state, the
GameRunningAppState
object attaches theStartScreenAppState
method, and detaches itself and its subordinateAppState
objects. - 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...
- … lets you add accessors and class fields to a spatial.
- … lets you hook actions into the main update loop.
- … can be empty.
- … can initialize a subset of the scene graph.
- … defines a subset of application-wide game logic.
- … 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!
- Create a
GamePlayAppState
class that extends theAbstractAppState
class. Move the code that initializes the scene nodes from thesimpleInitApp
()
method into theGamePlayAppState
object'sinitialize()
method. In thecleanup()
method, write the code that detaches these nodes along the same lines. Add an instance of this state to thestateManager
object of the Main class. - To have a game state to interact with, you first need to define user data. In the
GamePlayAppState
class, defineinteger
class fields for the player'slevel
,score
,health
, andbudget
, and a Boolean variablelastGameWon
, including accessors. In your code that creates towers, use thesetUserData()
method to give each tower index andchargesNum
fields. Where you create creeps, use thesetUserData()
method to give each creep index and health fields. - 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.) - Create a
CreepControl
class that extends theAbstractControl
class and lets creep spatials communicate with the game. The constructor takes theGamePlayAppState
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 theaddControl(new CreepControl(this))
method on each creep. - Implement the
controlUpdate()
method of theCreepControl
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. - Create a
TowerControl
class that extends theAbstractControl
class and lets tower spatials communicate with the game. The constructor takes theGamePlayAppState
object instance as an argument because the towers need access to the list of creeps and thebeam_node
object. Write accessors for the tower's user data (index, height, and ChargesNum). In the code block where you create towers, use theaddControl(new TowerControl(this))
method on each tower. - Implement the
controlUpdate()
method of theTowerControl
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 theGamePlayAppState
object, and uses thecreep_geo.getControl(CreepControl.class)
method to get access to theCreepControl
object. The tower determines whether the distance between itself and each creep is lower than a certain value. If yes, it collects theCreepControl
object into areachable
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 thecom.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 thebeam 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'sCharge
array and stop shooting. - 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 anActionListener
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 fieldselected
(otherwiseselected
should be -1). If the player presses the charge key, and the budget is not zero, use, for example, therootNode.getChild("tower-" + selected).getControl(TowerControl.class)
method to get theTowerControl
instance. Add a newCharge
object to the tower control, and subtract the charge's price from the budget. - In the
GamePlayAppState
object'supdate()
method, you maintain timed events using two float class fields,timer_budget
, andtimer_beam
. ThetimerBudget
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. TheTimer_beam
field does the same, but it only waits for one second before it clears all accumulated beam geometries from thebeam Node
node. - In the
GamePlayAppState
object'supdate()
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 thelastGameWon
variable to false and detach thisGamePlayAppState
object (which ends the game). If the player is still healthy and no creeps are alive, then the player has won. Set thelastGameWon
object to true and detach thisGamePlayAppState
object (which ends the game). Else, the player is healthy but the creeps are still attacking. TheTowerControl
class and theCreepControl
class handle this case.