Creating a reusable AI control class

In this recipe, we will create a control that is going to steer an AI character. Using Control to do this is beneficial since it can add the AI functionality and be used together with other Controls in the game. We can use GameCharacterControl from Chapter 2, Cameras and Game Controls for both the player and AI characters by adding AIControl to its spatial. To get a quick and visual result, we'll apply it to the bullet-based BetterCharacterControl class in this recipe.

How to do it...

We need to perform the following steps to get a basic, but functional attacking (or following) AI:

  1. We begin by creating a new class called AIControl, extending AbstractControl. The core of the recipe will be based around an enum (enumeration) called state. For now it only needs two values: Idle and Follow.
  2. Add fields for BetterCharacterControl, called physicsCharacter, Booleans forward and backwards, a Vector3f field for walkDirection, and another for viewDirection. If it's going to follow something, it also needs a target field, which can be Spatial.
  3. The bulk of the logic is carried out in a switch statement in the controlUpdate method, as shown in the following code. The first case is Idle. In this case, the AI shouldn't do anything:
    switch(state){
      case Idle:
        forward = false;
        backward = false;
      break;
  4. In the Follow case, we should first check whether target is set. If there is a target, we find the direction to the target and make the AI face it by setting viewDirection, as shown in the following code:
    case Follow:
      if(target != null){
        Vector3f dirToTarget = target.getWorldTranslation().subtract(spatial.getWorldTranslation());
        dirToTarget.y = 0;
        dirToTarget.normalizeLocal();
        viewDirection.set(dirToTarget);
  5. We check the distance to the target. If it's more than 5 the AI will try to get closer. If the distance instead is less than 3, it will try to back up a bit. The AI can also lose track of the target if it is more than 20 units away. In this case, it also changes state to Idle, as shown in the following code:
    if (distance > 20f){
      state = State.Idle;
      target = null;
    } else if(distance > 5f){
      forward = true;
      backward = false;
    } else if (distance < 3f){
      forward = false;
      backward = true;
    } else {
      forward = false;
      backward = false;
    }
  6. When it comes to movement, we can get the forward facing direction with the following line of code:
    Vector3f modelForwardDir = spatial.getWorldRotation().mult(Vector3f.UNIT_Z);
  7. Depending on whether forward or backward is true, we can multiply this value with a suitable movement speed, and the call setWalkDirection on the BetterCharacterControl class with the result shown in the following code:
    if (forward) {
      walkDirection.addLocal(modelForwardDir.mult(3));
    } else if (backward) {
      walkDirection.addLocal(modelForwardDir.negate().multLocal(3));
    }
    physicsCharacter.setWalkDirection(walkDirection);
  8. Finally, we should also call setViewDirection, as shown in the following code:
    physicsCharacter.setViewDirection(viewDirection);

How it works…

Using BetterCharacterControl, we get a lot of functionality for free. We only need a couple of Booleans to keep track of movement, and two Vector3f instances for directions. Target is what the AI will focus on (or follow, for now).

If we're familiar with TestBetterCharacter from jMonkeyEngine's test examples, we can recognize the movement handling from that class. For now, we only use the forward/backward functionality. It is a good idea to keep the rotation code as well, just in case we would like it to turn more smoothly in the future. The walkDirection vector is 0 by default. It can either be sent as it is sent to physicsCharacter, in which case the character will stop, or be modified to move in either direction. The viewDirection vector is simply set to look at the target for now, and passed on to physicsCharacter.

The logic in the Follow case mentioned previously is mostly there to have something to test with. Even so, it's AI behavior that seems to be sufficient for many MMOs. Once a target has been acquired, it will try to keep itself at a certain distance. It can also lose track of the target if it gets too far away. In this case, it falls back to the Idle state.

There's more…

By linking this recipe together with Chapter 4, Mastering Character Animations, we can easily make Jaime play some animations while he's moving.

Start by adding the AnimationManagerControl class to the AI character using the following code:

aiCharacter.addControl(new AnimationManagerControl());

We need to tell it to play animations. In AIControl, find the forward and backwards brackets inside the controlUpdate method and add the following lines:

if (forward) {
            ... spatial.getControl(AnimationManagerControl.class).setAnimation(AnimationManagerControl.Animation.Walk);
  } else if (backward) {
            ... spatial.getControl(AnimationManagerControl.class).setAnimation(AnimationManagerControl.Animation.Walk);
  } else {
spatial.getControl(AnimationManagerControl.class).setAnimation(AnimationManagerControl.Animation.Idle);
}

There's more…

Let's create a test case world we can use for both this and many of the following recipes. First we need a world with physics:

BulletAppState bulletAppState = new BulletAppState();
stateManager.attach(bulletAppState);

We will need some kind of object to stand on. The PhysicsTestHelper class has a few example worlds we can use.

We load up good old Jaime. Again, we use the BetterCharacterControl class since it offloads a lot of code for us. Since the Bullet physics world is different from the ordinary scenegraph, Jaime is added to physicsSpace as well as to the rootNode, as shown in the following code:

bulletAppState.getPhysicsSpace().add(jaime);
rootNode.attachChild(jaime);

We also need to add our newly created AI control using the following code:

jaime.addControl(new AIControl());

There's one more thing we need to do for this to work. The AI needs to track something. The easiest way we can get a moving target is to add a CameraNode class and supply cam from the application, as shown in the following code:

CameraNode camNode = new CameraNode("CamNode", cam);
camNode.setControlDir(CameraControl.ControlDirection.CameraToSpatial);
rootNode.attachChild(camNode);

We set camNode to be the target, as shown in the following code:

jaime.getControl(AIControl.class).setState(AIControl.State.Follow);
jaime.getControl(AIControl.class).setTarget(camNode);

If we're familiar with cameras in OpenGL, we know they don't really have a physical existence. A CameraNode class in jMonkeyEngine gives us that. It tracks the camera's position and rotation, giving us something easy to measure. This will make it easier for us when we want the AI to follow it, since we can use the convenience of it being spatial.

For this reason, we can set CameraNode to be its target.

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

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