jMonkeyEngine 3.0 Beginner’s Guide
上QQ阅读APP看书,第一时间看更新

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.

  1. Create an AppState class CubeChaserState that extends the AbstractAppState class from the com.jme3.app.state package. This class will contain our game logic.
  2. 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) {}
    }
  3. Move the makeCubes() and myBox() methods from the CubeChaserControl class into the CubeChaserState class; we want to reuse them.
  4. Move the class fields from the CubeChaserControl class into the CubeChaserState class. Add extra class fields for the assetManager object and the application app, 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);
  5. Copy the implementation of the custom constructor of the CubeChaserControl class into the initialize() method of the CubeChaserState class. Similarly, initialize typical SimpleApplication variables such as assetManager, rootNode, and so on. You can also modify the scene graph here, as we do by calling our makeCubes() 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);
        }
  6. Move the ray casting code from the controlUpdate() method of the CubeChaserControl class into the update() method of the CubeChaserState class. Replace the if (target.equals(spatial)) line that tests whether this is one certain spatial with a test that identifies all spatials of the CubeChaserControl class in the scene. We are operating on application level now, because we access the camera and the rootNode 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());
          }
        }
      }
    }
  7. Your CubeChaserControl class is now empty, save for the rotate() method that you added to make the controlled cubes reveal themselves.
    @Override
    protected void controlUpdate(float tpf) {
        spatial.rotate(tpf, tpf, tpf);
    }
  8. Your CubeChaser class is now almost empty, and its only remaining import statement is for the SimpleApplication class! In the simpleInitApp() method, create a new CubeChaserState object. To activate this AppState, you attach it to the stateManager object of the SimpleApplication 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.