Time for action – get these cubes under control

One type of dedicated jMonkeyEngine class that encapsulates a spatial's behavior is the Control class from the com.jme3.scene.control package. You can create a Control object based on the code mentioned earlier, and add it to an individual cube. This prompts the cube to automatically test its own distance to the camera, and move away when the player looks at it! Seeing is believing:

  1. Create a CubeChaserControl class. Make it extend the AbstractControl class from the com.jme3.scene.control package.
  2. Implement abstract methods of the CubeChaserControl class with the following template:
        @Override
        protected void controlUpdate(float tpf) { }
        protected void controlRender(RenderManager rm, ViewPort vp) { }
        public Control cloneForSpatial(Spatial spatial) {
            throw new UnsupportedOperationException(
                      "Not supported yet.");
        }
  3. Move the ray field from CubeChaser class to the CubeChaserControl class. Create additional class fields, a com.jme3.renderer.Camera field called cam, and a com.jme3.scene.Node field called rootNode.
    private Ray ray = new Ray();
    private final Camera cam;
    private final Node rootNode;
  4. Add a custom constructor that initializes cam and rootNode.
    public CubeChaserControl(Camera cam, Node rootNode) {
        this.cam = cam;
        this.rootNode = rootNode;
      }
  5. Move the ray casting block from the simpleUpdate() method of the CubeChaser class to the controlUpdate() method of the CubeChaserControl class.
    protected void controlUpdate(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();
          // interact with target
        }
      }
  6. Test whether I (the controlled spatial, represented by the predefined spatial variable) am a target (one of the cubes that the player is looking at). If yes, test whether the player (represented by the camera) is close enough to chase me. Replace the interact with target comment with the following conditional statement:
    if (target.equals(spatial)) {
      if (cam.getLocation().distance(spatial.getLocalTranslation()) < 10) {
        spatial.move(cam.getDirection());
      }
    }
  7. Back in the CubeChaser class, you want to add this control's behavior to some of the spatials. We create lots of spatials in the makeCubes() method; let's amend it so that every fourth cube receives this control while the rest remain unfazed. You can create instances of the control with the main camera and rootNode object as arguments. Replace the rootNode.attachChild() code line in the makeCubes() method with the following code:
    Geometry geom = myBox("Cube" + i, loc, ColorRGBA.randomColor());
    if (FastMath.nextRandomInt(1, 4) == 4) {
      geom.addControl(new CubeChaserControl(cam, rootNode));
    }
    rootNode.attachChild(geom);

Before you run this sample, make sure that you have indeed removed all code from the simpleUpdate() method of the CubeChaser class; it is no longer needed. By using controls, you can now chase specific group of cubes—defined by the subset that carries the CubeChaserControl class. Other cubes are unaffected and ignore you.

Tip

There are two equivalent ways to implement controls. In most cases, you will just extend the AbstractControl class from the com.jme3.scene.control package. Alternatively, you could also implement the control interface directly; you only implement the interface yourself in cases where your control class already extends another class, and therefore cannot extend AbstractControl.

If you get tired of finding out which cubes are chasable, go back to the CubeChaserControl class and add spatial.rotate(tpf, tpf, tpf); as the last line of the controlUpdate() method. Now the affected cubes spin and reveal themselves!

Tip

Note that various built-in methods provide you with a float argument, tpf. This float is always set to the current time per frame (tpf), equal to the number of seconds it took to update and render the last video frame. You can use this value in the method body to time actions, such as this rotation, depending on the speed of the user's hardware—the tpf is high on slow computers, and low on fast computers. This means, the cube rotates in few, wide steps on slow computers, and in many, tiny steps on fast computers.

What just happened?

Every control has a controlUpdate() method that hooks code into the game's main loop, just as if it were in the simpleUpdate() method. You always add a control instance to a spatial; the control doesn't do anything by itself. Inside the control instance, you reach up to that spatial by referring to the predefined spatial variable. For example, if you add control instance c17 to geometry instance cube17, then the spatial variable inside c17 equals cube17. Every transformation that c17 applies to spatial, is applied directly to cube17.

One control class (such as the CubeChaserControl class) can control the behavior of several spatials that have something in common—such as being chasable in this example. However, each spatial needs its own control instance.

One spatial can be affected by zero or more controls at the same time. You want to teach an old spatial new tricks? If you ever decide to change a game mechanic, you only have to modify one control; rebuild the application, and all controlled spatials immediately adhere to it. With only a few lines of code, you can add behavior to, or remove behavior from, an arbitrary number of spatials. Encapsulating behavior into controls is very powerful!

If you need various groups of nodes that share behavior, don't create subclasses of the Spatial class with added custom methods! You will get into hot water soon, because using inheritance, a spatial would be locked into the one role it inherited—for example, a shopkeeper NPC could never fight back to defend its shop, because its parent is not a fighter class. Controls on the other hand are modular and can add or remove roll-based behavior easily—every NPC with an ArcheryControl method can use crossbows and bows; every NPC with the MerchantControl method can buy and sell items, and so on.

The spatial object has a few interesting accessors, including a getControl() method that gives you access to other controls added to the same spatial. There is no need to pass objects as references if they are already accessible via a custom control's accessors. When you look at the custom constructor of the CubeChaserControl class, you see that we actually pass the camera and the rootNode object into it. It's untypical for a control to need that much information about the scene graph or the camera. The ideal control class is self-contained and gets all the information it needs directly from its spatial, using custom accessors that you define. Let's make it better!