Simplistic Cover in Unreal Engine (3): 3D Space and the Cover Search Volume

Previous Post:
http://d.hatena.ne.jp/caelk/20160223/1456219054

So right now with what we've gone over, we have the general idea of how a cover system can be set up. Now, I could've gone over basic edge detection for a general (though incomplete) overview, but we've about hit the point where reality comes and bites us in the neck.

See, if we were in a flat world with flat floors to walk on and nothing else, things would be really simple. We'd only have to design a cover system in 2D space, since the player would always stay on the same xy-plane, and the math would be really easy. But uh... that'd be pretty plain world you'd be in, and not even one that's easily believable, right? So we're going to have to start thinking about how to design our cover system so that it can handle the one thing that will wreck our mathematical world.

A triangular wedge.

Oh dear god! Can I walk up that? Nooooo, why must this third dimension spoil my plans to do this the lazy way?!?

... All joking aside, ramps like these really will make things more complex. First, I talked about the cover search volume before - that's the invisible box that we look inside to see if there's any cover to be found around the player. If we don't align that to the floor that we're on, and if the floor we're on is a ramp or something, that box is going to go through the ramp.

So if this ramp ends in a low wall that you could take cover behind, you won't be able to take cover behind it unless you're edge up against it. The search volume doesn't rotate - it clips through the ramp. There's going to be an entire 180 degree arc where the only thing that can be picked up from a cover search is the ramp itself. Now, there are other tests we can do to call the walkable side of the ramp invalid cover, but the bigger problem is with our search volume. We'll deal with this for now.

If we're on a ramp, our cover search needs to follow the slope of the ramp when looking for cover. Of course, when we're on flat ground, it needs to follow the flat slope of the ground.

We're going to have to apply some rotation to the cover search volume based on the floor we're on every frame, which means we'll have to make a function and call it from Tick. I called this function UpdateSearchVolumeOrientation.

We'll need to know four things.

  • The character forward vector, parallel to the floor plane.
  • The floor up vector (or floor normal), which is the normal vector of the floor we're on.
  • The floor forward vector, which is a vector that always points up the floor surface.
  • A right vector, which is orthogonal the floor up vector and the floor forward vector.

The floor normal is pretty easy, since Unreal Engine stores that itself.

FHitResult FloorHit = this->CharacterMovement->CurrentFloor.HitResult;


if (FloorHit.bBlockingHit) {
    FloorUpVector = FloorHit.Normal;
} else { // No floor - player could be airborne.
    FloorUpVector = FVector::UpVector;
}

That first line there is where Unreal Engine stores information about the floor the character is standing on - in a hit result. You can think of it as Unreal shooting a line straight down from the character for a short distance. If it hits something, it's considered floor, and it stores the result in that FHitResult.

That FHitResult contains information about that line trace, including the normal of the surface it hit. That's our floor up vector. There's an additional few lines there that say if there is no floor (i.e. the character is falling), then we just calculate things as though the character were on flat ground, when the floor up vector is the same as the world up vector.

We're actually gonna go for the right vector next, since the calculations are much easier and a lot more lax. We can just take the cross product of the floor normal and the world up vector, and we'll get a vector that points orthogonally to the right of each. Of course, this is assuming that up in the game world is always (0, 0, 1) - if your game mucks around with gravity, you'll probably have to cross it with the negative gravity vector instead. Also, you'll have to prepare for times when the floor normal is the same as the up vector - I replace it with the negative world forward vector when that happens.

Lemme switch to mathematical notation for this.

Vr = cross(Vn, Vu)


For the variables:

  • Vr, the right vector,
  • Vn, the floor normal, and
  • Vu, normally the world up vector.



Where:

  • cross(v1, v2) is the cross product of vectors v1 and v2, and
  • Vu is (0, 0, 1) unless equal to Vn, in which case it is (-1, 0, 0).

And from there, you can get the floor forward vector. I actually store it as a class variable. Just remember that Unreal uses a left-handed coordinate system.

Vf = cross(Vr, Vn)


For the variables:

  • Vf, the floor forward vector,
  • Vr, the right vector, and
  • Vn, the floor normal.



Where:

  • cross(v1, v2) is the cross product of vectors v1 and v2.

And with this, the floor forward vector will always point up whatever surface we're on, never down.

The last piece is the character forward vector - which direction the character is facing - which must be parallel to the floor. If the character is on a ramp, facing straight forwards in what would normally be (1, 0, 0), this vector must instead point up the ramp. Something like (0.71, 0.0, 0.71) for a ramp with a 45 degree incline.

Let's think about the floor for a bit. The floor is essentially a plane in 3D space, and you may have noticed me refer to it as the floor plane before. We already know the normal of this plane. We've called it the floor normal or the floor up vector, but with just that one vector, the floor plane is mathematically defined. Our character may be facing some direction in absolute coordinates, but if we project that vector onto the floor plane, we'll get the direction the character faces parallel to the floor.

Yup, planar projection. You only need the plane normal (our floor normal) and the vector to project onto it (the absolute character facing vector) to do this. I must've used this graphic a billion times by now, but...


Va = this->GetActorRotation().Vector();


Pn = dot(Va, Vn) * Vn
Vc = normalize(Va - Pn)


For the variables:

  • Va, absolute character facing vector,
  • Vn, the floor normal,
  • Pn, the projection onto the floor normal, and
  • Vc, the character forward vector parallel to the floor plane.



Where:

  • dot(v1, v2) is the dot product of vectors v1 and v2, and
  • normalize(v) is the normalized vector v.

Alright, we got all four parts! Now we can start rotating the cover search volume. The first thing to do is really simple.

SearchVolumeRotation = CharacterForwardVector.Rotation();


this->CoverSearchVolume->SetWorldRotation(SearchVolumeRotation);

This takes care of the yaw and pitch of the rotation, and will orient the search volume in the direction the character is facing. If he's directly facing the slope of the ramp, this make the search volume run up and down it.


The only problem remaining is what happens when he starts turning left or right.

Directly forward and backward look right, but directly left and right don't follow the slope of the floor at all. We're going to have to introduce some amount of roll, which is the only rotation axis not set by that call to FVector::Rotation(). By observation, we can determine:

The maximum amount of roll is pretty easy: it's the same as the floor's angle of incline.

Rmax = 90 - acos(dot(Vf, Vu))


For the variables:

  • Rmax, maximum amount of roll for the floor, and
  • Vf, the floor forward vector, and
  • Vu, the world up vector (0, 0, 1).



Where:

  • acos(x) is the inverse cosine of x in degrees, and
  • dot(v1, v2) is the dot product of vectors v1 and v2.



Vu is not substituted for anything in this calculation.

How much of this roll we use is determined by how much we've deviated from the floor forward vector. When facing directly left or right, we need the full amount of roll, when facing directly forward or backwards, we need none of it. Notice what happens with the dot product as we make our way around the right vector.

Falls right in line, doesn't it?

Rc = dot(Vc, Vr)
R = Rmax * Rc


For the variables:

  • Rc, the roll coefficient,
  • Vc, the character forward vector parallel to the floor plane,
  • Vr, the right vector,
  • R, the calculated amount of roll, and
  • Rmax, maximum amount of roll for the floor.


Excellent! With that, orienting the cover search volume is done, but I should point out that while mostly perfect (for me), there are still things you'll have to design around.

First, the cover search volume is aligned to the floor you're currently on. If you have a short ramp with cover at the top of it, the search volume won't see that cover from the bottom of the ramp, since the search volume will be aligned to be parallel to the floor at the base of the ramp. It's not until you get on the ramp itself that the search volume gets enough pitch rotation to catch the wall at the end of the ramp. With large enough ramps, you can get around this, but it's something to keep in mind.

Also remember that since the cover search is based on objects that overlap the search volume, you have to mark anything you want to be used as cover as something that responds to overlaps.