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.
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.
To implement automatic cover detection, perform the following steps:
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;
checkCover
needs to be created. It takes Vector3f
as the input and is the position from where the rays originate need to be originated.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);
lowCollisions
and highCollisions
to keep a track of how many collisions we've had.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);
y
to lowHeight
as follows:leftDir.setY(lowHeight);
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(); }
if(lowCollisions == 3){ ray.setOrigin(spatial.getWorldTranslation().add(0, 0.5f, 0)); structures.collideWith(ray, collRes); Triangle t = new Triangle(); collRes.getClosestCollision().getTriangle(t);
viewDirection
:viewDirection.set(t.getNormal().negate());
hasLowCover
and hasHighCover
fields accordingly.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; } }
!inCover
criterion to the original if
statement, since outside cover, this should work like it worked previously.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);
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); } }
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
.ToggleCover
, which is added to InputAppState
:inputManager.addMapping(InputMapping.ToggleCover.name(), new KeyTrigger(KeyInput.KEY_V));
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.
18.117.138.104