Creating a reusable character control

To start off the chapter, we will create a class that we can use for various character-controlled purposes. The example describes an FPS character, but the method is the same for any player-controlled character.

The Control class we'll build will be based on BetterCharacterControl. It might be a good idea to have a look at the class or the TestBetterCharacter example from the jMonkeyEngine test package if you want to find out how this works. Another good starting point would be the input examples from the same package.

Getting ready

The BetterCharacterControl class is based on physics and requires a BulletAppState class to be set up in the application. The steps required to do this are described in the The ImageGenerator class section in Appendix, Information Fragments. To find out more about bullet and physics, refer to Chapter 8, Physics with Bullet.

How to do it...

Perform the following set of steps to create a reusable character control:

  1. Start by creating a new class called GameCharacterControl, which extends BetterCharacterControl. This class also needs to implement ActionListener and AnalogListener. The idea here is to feed this class with actions that it can handle. To control the movement of a character, use a series of Booleans as follows:
    boolean forward, backward, leftRotate, rightRotate, leftStrafe, rightStrafe;
  2. Also, define a float field called moveSpeed, which will help you control how much the character will move in each update.

    The control Booleans you added are set in the implemented onAction method. Note that a key will always trigger !isPressed when released (note that a key always triggers isPressed == false when released):

    public void onAction(String action, boolean isPressed, float tpf) {
      if (action.equals("StrafeLeft")) {
        leftStrafe = isPressed;
      } else if (action.equals("StrafeRight")) {
          rightStrafe = isPressed;
    
      } else if (action.equals("MoveForward")) {
          forward = isPressed;
    
      } else if (action.equals("MoveBackward")) {
          backward = isPressed;
    
      } else if (action.equals("Jump")) {
          jump();
      } else if (action.equals("Duck")) {
          setDucked(isPressed);
    
      }
    }
  3. Now that you have handled the key input, put the control Booleans to be used in the update method. You might recognize the code if you've looked at TestBetterCharacter. The first thing it does is get the current direction the spatial object is facing in order to move forward and backwards. It also checks which direction is left for strafing, as follows:
    public void update(float tpf) {
      super.update(tpf);
      Vector3f modelForwardDir = spatial.getWorldRotation().mult(Vector3f.UNIT_Z);
      Vector3f modelLeftDir = spatial.getWorldRotation().mult(Vector3f.UNIT_X);
      walkDirection.set(0, 0, 0);
  4. Depending on your Booleans, the following code modifies walkDirection. Normally, you would multiply the result by tpf as well, but this is already handled in the BetterCharacterControl class as follows:
    if (forward) {
      walkDirection.addLocal(modelForwardDir.mult(moveSpeed));
    } else if (backward) {
      walkDirection.addLocal(modelForwardDir.negate().multLocal(moveSpeed));
    }
    if (leftStrafe) {
      walkDirection.addLocal(modelLeftDir.mult(moveSpeed));
    } else if (rightStrafe) {
      walkDirection.addLocal(modelLeftDir.negate().multLocal(moveSpeed));
    }
  5. Finally, in the setWalkDirection method, apply walkDirection as follows:
    BetterCharacterControl.setWalkDirection(walkDirection);
  6. The preceding code handles moving forward, backward, and to the side. The turning and looking up and down actions of a character is normally handled by moving the mouse (or game controller), which is instead an analog input. This is handled by the onAnalog method. From here, we take the name of the input and apply its value to two new methods, rotate and lookUpDown, as follows:
    public void onAnalog(String name, float value, float tpf) {
      if (name.equals("RotateLeft")) {
        rotate(tpf * value * sensitivity);
      } else if (name.equals("RotateRight")) {
      rotate(-tpf * value * sensitivity);
      } else if(name.equals("LookUp")){
      lookUpDown(value * tpf * sensitivity);
      } else if (name.equals("LookDown")){
      lookUpDown(-value * tpf * sensitivity);
      }
    }
  7. Now, start by handling the process of turning the character left and right. The BetterCharacterControl class already has nice support for turning the character (which, in this case, is the same thing as looking left or right), and you can access its viewDirection field directly. You should only modify the y axis, which is the axis that goes from head to toe, by a small amount as follows:
    private void rotate(float value){
      Quaternion rotate = new Quaternion().fromAngleAxis(FastMath.PI * value, Vector3f.UNIT_Y);
      rotate.multLocal(viewDirection);
      setViewDirection(viewDirection);
    }
  8. In order to handle looking up and down, you have to do some more work. The idea is to let the spatial object handle this. For this, you need to step back to the top of the class and add two more fields: a Node field called head and a float field called yaw. The yaw field will be the value with which you will control the rotation of the head up and down.
  9. In the constructor, set the location of the head node. The location is relative to the spatial object to an appropriate amount. In a normally scaled world, 1.8f would correspond to 1.8 m (or about 6 feet):
    head.setLocalTranslation(0, 1.8f, 0);
  10. Next, you need to attach the head node to spatial. You can do this in the setSpatial method. When a spatial is supplied, first check whether it is a Node (or you wouldn't be able to add the head). If it is, attach the head as follows:
    public void setSpatial(Spatial spatial) {
      super.setSpatial(spatial);
      if(spatial instanceof Node){
        ((Node)spatial).attachChild(head);
      }
    }
  11. Now that you have a head that can rotate freely, you can implement the method that handles looking up and down. Modify the yaw field with the supplied value. Then, clamp it so that it can't be rotated more than 90 degrees up or down. Not doing this might lead to weird results. Then, set the rotation for the head around the x axis (think ear-to-ear) as follows:
    private void lookUpDown(float value){
      yaw += value;
      yaw = FastMath.clamp(yaw, -FastMath.HALF_PI, FastMath.HALF_PI);
      head.setLocalRotation(new Quaternion().fromAngles(yaw, 0, 0));
    }
  12. Now, we have a character that can move and rotate like a standard FPS character. It still doesn't have a camera tied to it. To solve this, we're going to use the CameraNode class and hijack the application's camera. CameraNode gives you the ability to control the camera as if it were a node. With setControlDir, we instruct it to use the location and rotation of spatial as follows:
    public void setCamera(Camera cam){
      CameraNode camNode = new CameraNode("CamNode", cam);
      camNode.setControlDir(CameraControl.ControlDirection.SpatialToCamera);
      head.attachChild(camNode);
    }

    Note

    Cameras are logical objects and are not part of the scene graph. The CameraNode keeps an instance of Camera. It is a Node and propagates its own location to the Camera. It can also do the opposite and apply the Camera's location to CameraNode (and thus, any other spatial object attached to it).

  13. To use GameCharacterControl in an application, add the following lines of code in the simpleInit method of an application. Instantiate a new (invisible) Node instance that you can add to the GameCharacterControl class. Set the application's camera to be used as a character, and add it to physicsSpace as follows:
    Node playerNode = new Node("Player");
    GameCharacterControl charControl = new GameCharacterControl(0.5f, 2.5f, 8f);
    charControl.setCamera(cam);
    playerNode.addControl(charControl);
    charControl.setGravity(normalGravity);
    
    bulletAppState.getPhysicsSpace().add(charControl);

How it works...

The BetterCharacterControl class of jMonkeyEngine already has a lot of the functionalities to handle the movement of a character. By extending it, we get access to it and we can implement the additional functionality on top of it.

The reason we use Booleans to control movement is that the events in onAction and onAnalog are not fired continuously; they are fired only when they're changed. So, pressing a key wouldn't generate more than two actions, one on pressing it and one on releasing it. With the Boolean, we ensure that the action will keep getting performed until the player releases the key.

This method waits for an action to happen, and depending on the binding parameter, it will set or unset one of our Booleans. By listening for actions rather than inputs (the actual key strokes), we can reuse this class for non-player characters (NPCs).

We can't handle looking up and down in the same way as we perform sideways rotations. The reason is that the latter changes the actual direction of the movement. When looking up or down, we just want the camera to look that way. The character is usually locked to the ground (it would be different in a flight simulator, though!).

As we can see, the BetterCharacterControl class already has ways to handle jumping and ducking. Nice!

There's more...

Let's say we would rather have a third-person game. How difficult would it be to modify this class to support that? In a later recipe, we will look at jMonkeyEngine's ChaseCamera class, but by inserting the following two lines of code at the end of our setCamera method, we will get a basic camera that follows the character:

camNode.setLocalTranslation(new Vector3f(0, 5, -5));
camNode.lookAt(head.getLocalTranslation(), Vector3f.UNIT_Y);

It's all handled by CamNode, which offsets the camera's location in relation to its own (which follows the head node). After moving CamNode, we make sure that the camera also looks at the head (rather than the default forward).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.172.38