
Time for action – get into the right AppState of mind
When you notice that a control class has ambitions to make decisions on the application level, this is a sign that this control class has evolved to something else—an application state (AppState). We want the application to do the same thing as before—let us chase the random subset of cubes that carry the CubeChaserControl
class.
- Create an
AppState
classCubeChaserState
that extends theAbstractAppState
class from thecom.jme3.app.state
package. This class will contain our game logic. - Copy the following minimum template into the
CubeChaserState
class:public class CubeChaserState extends AbstractAppState { @Override public void update(float tpf) {} @Override public void cleanup() {} @Override public void initialize(AppStateManager stateManager, Application app) {} }
- Move the
makeCubes()
andmyBox()
methods from theCubeChaserControl
class into theCubeChaserState
class; we want to reuse them. - Move the class fields from the
CubeChaserControl
class into theCubeChaserState
class. Add extra class fields for theassetManager
object and the applicationapp
, because we are now operating on the application level.private SimpleApplication app; private final Camera cam; private final Node rootNode; private AssetManager assetManager; private Ray ray = new Ray(); private static Box mesh = new Box(Vector3f.ZERO, 1, 1, 1);
- Copy the implementation of the custom constructor of the
CubeChaserControl
class into theinitialize()
method of theCubeChaserState
class. Similarly, initialize typicalSimpleApplication
variables such asassetManager
,rootNode
, and so on. You can also modify the scene graph here, as we do by calling ourmakeCubes()
method.@Override public void initialize(AppStateManager stateManager, Application app) { super.initialize(stateManager, app); this.app = (SimpleApplication) app; this.cam = this.app.getCamera(); this.rootNode = this.app.getRootNode(); this.assetManager = this.app.getAssetManager(); makeCubes(40); }
- Move the ray casting code from the
controlUpdate()
method of theCubeChaserControl
class into theupdate()
method of theCubeChaserState
class. Replace theif (target.equals(spatial))
line that tests whether this is one certain spatial with a test that identifies all spatials of theCubeChaserControl
class in the scene. We are operating on application level now, because we access the camera and therootNode
object.public void update(float tpf) { CollisionResults results = new CollisionResults(); ray.setOrigin(cam.getLocation()); ray.setDirection(cam.getDirection()); rootNode.collideWith(ray, results); if (results.size() > 0) { Geometry target = results.getClosestCollision().getGeometry(); if (target.getControl(CubeChaserControl.class) != null) { if (cam.getLocation(). distance(target.getLocalTranslation()) < 10) { target.move(cam.getDirection()); } } } }
- Your
CubeChaserControl
class is now empty, save for therotate()
method that you added to make the controlled cubes reveal themselves.@Override protected void controlUpdate(float tpf) { spatial.rotate(tpf, tpf, tpf); }
- Your
CubeChaser
class is now almost empty, and its only remaining import statement is for theSimpleApplication
class! In thesimpleInitApp()
method, create a newCubeChaserState
object. To activate thisAppState
, you attach it to thestateManager
object of theSimpleApplication
class.@Override public void simpleInitApp() { flyCam.setMoveSpeed(100f); CubeChaserState state = new CubeChaserState(); stateManager.attach(state); }
When you run the CubeChaser
class now, you get the same chasing behavior, but your code is modular. Now you understand why it is best practice to move tests and action implementations from the simpleUpdate()
method into control and the AppState
classes.
What just happened?
A control encapsulates a subset of accessors and update behaviors on the level of spatials, while an AppState
class encapsulates a subset of the scene graph and of scene-wide behaviors. A control only has access to its spatial
, while an AppState
class has access to the whole application via this.app
.
Certain kinds of behavior, such as animations, playing audio, or visual effects, or the rotate()
call in this example, are independent of objects on the application level. This behavior only needs access to its spatial. Therefore, the controlUpdate()
method of the CubeChaserControl
class is the perfect place to encapsulate this spatial-level behavior. A spatial may need access to global game state data other than SimpleApplication
fields, such as points or level—in this case, pass your central game state object in the control's constructor (and not the whole SimpleApplication
instance).
The cube-chasing code snippet, however, needs access to SimpleApplication
fields (here camera and rootNode
) to perform its collision detection. Therefore, the update()
method of the CubeChaserState
class is the ideal class to encapsulate this type of application-level behavior. You see the different use cases?
Every AppState
class calls the initialize()
method when it is activated and the cleanup()
method when it is deactivated. So if you want this AppState
class to bring certain spatials, lights, sounds, inputs, and so on with it, you override the initialize()
method. When you deactivate an AppState
class, its update()
method loop stops. If you want the detached AppState
to take away the things it brought, you must similarly override the cleanup()
method and detach those nodes, and so on.
You activate AppState
classes by attaching them to the application state manager, and deactivate them by detaching them from it. Each SimpleApplication
class inherits one application state manager—you access it via the this.app.getStateManager()
method or the stateManager
object. You can attach several AppState
classes to your stateManager
object, but the same state can only be attached once.