Detecting cover automatically in a third-person game

Cover shooters is an ever-popular genre in today's console games. How does one code a system that recognizes and allows players to take cover? There are several ways to do this, but basically, there are two main branches, each with their benefits and drawbacks. The first branch is one where a level designer places logical cover items around the environments or where they are baked into models by an artist. This could be as simple as a bounding volume, or it could be complex with directional data as well. This has a benefit for the programmer in that it's easy to recognize when a player is inside them by comparing bounding volumes. Another benefit is that the designer has full control over where there is cover and where there isn't. A drawback is that it is labor-intensive for the designer or artist and might be inconsistent to the player.

The method we'll implement is one where there is no pregenerated cover, and it's checked in runtime. No additional work is required for a designer or artist, except that the models that are used need to be of a certain height to be recognized as cover (and work with animations).

Normally, there are two different kinds of cover: a low cover that characters can crouch behind and shoot over. The other one is full height cover, where characters stand next to the edge of it and shoot around the corner. In some games, it's only possible to use full height covers where it's also possible to shoot from them, such as corners.

Once the character is in cover, certain movement restrictions usually apply. In most games, the player can move sideways along the cover. In some games, moving backwards will release the character from the cover, while in others, you have to toggle the cover button. We'll implement the latter.

Getting ready

Let's define in more detail what we'll implement and how. We'll use Rays to detect whether the player is covered or not and KeyTrigger to toggle the entering or exiting cover. If you're not familiar with the concept of Rays, you can, for example, have a look at the Firing in FPS or Selecting units in RTS recipes in this chapter. Cover can be anything in the scene above a certain height. All of the action in this recipe will be handled by GameCharacterControl from the Following a character with ChaseCamera recipe. There are two separate areas we need to look at. One is the cover detection itself, and the other is related to how the character should behave when in cover.

How to do it...

To implement automatic cover detection, perform the following steps:

  1. There are a few new fields we need to introduce to keep track of things. It's not enough to simply send one ray from the center to detect the cover, so we'll need to cast from the edges or near edges of the player model as well. We call this offset playerWidth. The inCover variable is used to keep track of whether the player is in cover mode or not (toggled). The hasLowCover and hasHighCover variables are set in the cover-detection method and are a way for us to know whether the player is currently within limits of a cover (but not necessarily in the cover mode). The lowHeight and highHeight variables are the heights where we'll cast Ray from in order to check for cover. The structures variable is everything we should check for cover against. Don't supply rootNode here or we'll end up colliding with ourselves:
    private float playerWidth = 0.1f;
    private boolean inCover, hasLowCover, hasHighCover;
    private float lowHeight = 0.5f, highHeight = 1.5f;
    private Node structures;
  2. Now let's move to the fun part, which is detecting cover. A new method called checkCover needs to be created. It takes Vector3f as the input and is the position from where the rays originate need to be originated.
  3. Next, we define a new Ray instance. We don't set the origin yet; we just set the direction to be the same as the character's viewDirection and a maximum length for it (and this may vary depending on the context and game) as follows:
    Ray ray = new Ray();
    ray.setDirection(viewDirection);
    ray.setLimit(0.8f);
  4. We define two integer fields called lowCollisions and highCollisions to keep a track of how many collisions we've had.
  5. Next, we populate a new field called leftDir. This is the direction that is to the left of the character. We multiply this by playerWidth to get the left extreme to look for cover in, as follows:
    Vector3f leftDir = spatial.getWorldRotation().getRotationColumn(0).mult(playerWidth);
  6. We'll start by checking for low covers and set y to lowHeight as follows:
    leftDir.setY(lowHeight);
  7. Then, we create a for loop that sends three Rays: one at the left extreme of the player, one in the center, and one to the right. This is done by multiplying leftDir with i. The loop must then be duplicated for the upper Rays as well:
    for(int i = -1; i < 2; i++){
      leftDir.multLocal(i, 1, i);
      ray.setOrigin(position.add(leftDir));
      structures.collideWith(ray, collRes);
      if(collRes.size() > 0){
      lowCollisions++;
      }
      collRes.clear();
    }
  8. In order to be considered to be inside range of a cover, all three (left, middle, and right) Rays must hit something. A high cover always has a low cover as well, so we can check to see whether we've hit the low cover first. If we did, we do one more Ray check to find out the normal of the actual triangle hit. This will help us align the model with the cover:
    if(lowCollisions == 3){
      ray.setOrigin(spatial.getWorldTranslation().add(0, 0.5f, 0));
      structures.collideWith(ray, collRes);
    
      Triangle t = new Triangle();
      collRes.getClosestCollision().getTriangle(t);
  9. The opposite of the triangle's normal should be the character's new viewDirection:
    viewDirection.set(t.getNormal().negate());
  10. Finally, we check whether we also have high cover and set the hasLowCover and hasHighCover fields accordingly.
  11. To restrict movement, the onAction method needs some modifications. The first criterion we check is whether the toggle cover button is pressed. If we're already in cover, we'll release the character from the cover. If we're not in cover, we check whether it's possible to go into cover:
    if(binding.equals("ToggleCover") && value){
      if(inCover){
        inCover = false;
      } else {
        checkCover(spatial.getWorldTranslation());
        if(hasLowCover || hasHighCover){
          inCover = true;
        }
      }
  12. In the following bracket, we limit movement to left and right if we're inside cover. If neither of the preceding statements applies, movement should be handled as usual. If we didn't want the player to be able to move inside cover, we'd be done by now.
  13. Since we want to mimic popular cover-based games though, we have some more work ahead of us.
  14. At the top of the update method, we have code to set the direction of the character based on the camera's rotation. We need to change this a bit, since once the character is inside cover, it should move based on the direction of the cover rather than the camera. To achieve this, we add a !inCover criterion to the original if statement, since outside cover, this should work like it worked previously.
  15. Then, if we are in cover, we base modelForwardDir and modelLeftDir on the rotation of the spatial, as follows:
    modelForwardDir = spatial.getWorldRotation().mult(Vector3f.UNIT_Z);
    modelLeftDir = spatial.getWorldRotation().mult(Vector3f.UNIT_X);
  16. Once the movement has been applied to the walkDirection vector but before it is applied it to the character, we check whether the character will still be inside cover after moving:
    if(walkDirection.length() > 0){
     if(inCover){
     checkCover(spatial.getWorldTranslation().add(walkDirection.multLocal(0.2f).mult(0.1f)));
        if(!hasLowCover && !hasHighCover){
          walkDirection.set(Vector3f.ZERO);
        }
      }
  17. We add the current walkDirection vector to the position of the player and check for cover at that position. If there is none, the movement is not allowed and we set walkDirection to 0.
  18. Now all that's needed is a new mapping for ToggleCover, which is added to InputAppState:
    inputManager.addMapping(InputMapping.ToggleCover.name(), new KeyTrigger(KeyInput.KEY_V));

How it works...

Each time the player presses the ToggleCover key or button, a check will be run to see whether there is cover within range. Three rays are cast forward from a low height, one at the left edge of the model, one from the center, and one from the right. Since leftDir is multiplied by -1, 0, and 1 on the x and z axes, we get the offset to the left- and right-hand side of the center position. To be considered inside cover, all three must have collided with something. This ensures that the player model is wholly covered.

The Ray won't stop just because it collides with something, and if the cover is thin, it might continue through the back side of it, generating additional collisions. We only want to count one collision per ray, though (the closest), which is why we only increase lowCollisions by one.

The high cover is checked after the low cover, because in general, there is never any cover that only covers the upper body.

Once it's decided that the character is inside cover and the player wants to move, we need to check whether the player will still be inside cover at the new position. This is so that the player doesn't accidentally exit cover and end up getting killed. To avoid unnecessary performance hits, we don't want to do this every frame. We do this only if there has actually been some movement happening.

See also

  • To get the most out of this, we will need suitable animations. Refer to Chapter 4, Mastering Character Animations, to get a few ideas on how to do this.
..................Content has been hidden....................

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