Making a character cover feature for a stealth game in Unity
January 02, 2023
This post shows my attempt at creating a wall cover feature for my character. Inspired by the stealth action game, Metal Gear Solid, the idea is that the character is pressed against a wall to provide cover, allowing the character to sneak by and evade an enemy character.
Before getting in to the details, I’ll run through the animations which needed making to get this to look legit.
For the first animation, in order to get the player to animate 180 degrees against the wall, I made an “Into cover” animation, where the player turns the body around to rotate and press against the wall:
Once pressed against the wall, I also made a wall idle animation where the character is pressed against the wall but stationary.
Then there’s the animation to shift the character left or right while their back is against the wall. Remember, at this point their body is still rotated 180 degress from their initial position.
Finally when the player releases the game buttons, the character should move away from the wall, which I made another animation for, called out of cover animation:
Using script to run code when the character is moving head on to a wall
With the animations outlined, the next step was to work out how to play the animations when the character is moving perpendicularly towards the wall. There’s a load of ways to do this, and the route I landed on might not be the best performant way:
void OnControllerColliderHit(ControllerColliderHit hit)
{
if (hit.gameObject.tag == "HuggableWall")
{
Vector3 movementDirectionVector = new Vector3(Input.GetAxis("Horizontal"), 0.0f, Input.GetAxis("Vertical"));
Vector3 pushDirectionVector = new Vector3(hit.moveDirection.x, 0.0f, hit.moveDirection.z);
// if is pushing towards wall
if ((Vector3.Distance(movementDirectionVector, pushDirectionVector) == 0.0f || movementDirectionVector == pushDirectionVector))
{
animator.SetBool("Is Hugging Wall", true);
}
}
}
This uses the OnControllerColliderHit method which is available because out character has the CharacterController component attached. The parameter passed in (hit) contains data on the direction that the character is pushing, and also that the wall is facing. This data lets us create an if
statement to run when the player is pushing perpendicular towards the wall.
From the snippet above, you can see I then initiate the animation to hug the wall. This is where my first roadblock came.
First attempt at having the character cover against the wall
Although the animation looked great, after also adding in the wall idle animation I discovered that the player would glitch in the middle of the animation:
After a lot of debugging I found the cause was because, although the character had rotated 180 degrees when performing the animation, it was only the character body that had rotated, and whole model (root) was still facing the wall. The best way to describe it is to see how the animation works:
This is a screen shot from the Unity animation preview window. The red line shows the way the character is facing, but the blue line shows the root of the model which is still facing the opposite direction when the animation finishes. This caused a glitch because after the IntoHug animation finishes, the character instantly turns the root of the model to face AWAY from the wall which is where the root of the model faces in the following animation - the hug idle animation.
One way I tried to combat this is to listen for when the IntoHug animation finished in the script, and rotate the character root 180 degress to match the root rotation of the following hug idle animation - something like this (in semi-psuedo code as I don’t have my original attempt to hand):
void OnControllerColliderHit(ControllerColliderHit hit)
{
if (hit.gameObject.tag == "HuggableWall")
{
Vector3 movementDirectionVector = new Vector3(Input.GetAxis("Horizontal"), 0.0f, Input.GetAxis("Vertical"));
Vector3 pushDirectionVector = new Vector3(hit.moveDirection.x, 0.0f, hit.moveDirection.z);
// if is pushing towards wall
if ((Vector3.Distance(movementDirectionVector, pushDirectionVector) == 0.0f || movementDirectionVector == pushDirectionVector))
{
animator.SetBool("Is Hugging Wall", true);
}
if(/* "Is Hugging Wall" animation has finished */) {
transform.Rotate(0, 180, 0); // rotate to match root rotation
}
}
}
Although this kind of worked, it was sometimes a little slow so you could see a few milliseconds of the character facing the wrong direction. This method of listening for animation finish events also has a bit of a code smell in my opinion, as it can easily lead to spaghetti code of event listeners.
The final working attempt at cover feature
After many hours of Googling and learning different parts of Unity and Blender, I landed on a fundamental part of animation that I’d not known until this point, which is root motion.
I based another post on this recently, but at the time of making this cover system I didn’t know anything about it. The other blog post goes in to a detail about cover vs in place motion, so I’ll skip all that here.
It’s also worth noting that it’s at this point I decided to switch from Rigify to Maximo for rigging my character, because Rigify rig was not importing to Unity correctly for me.
Because my problem was the character’s root rotation being different from the model rotation, it made sense to keep the model and root of the model facing the same direction at all times. In terms of creating that in Blender, it means ensuring that the root of the object rotates along with the rest of the body:
Notice the rotation values moving from 0 to 180 degrees in the above screen shot as the animation progresses. Also the values of those transforms are at the bottom of the action editor under OBJECT TRANSFORMS.
When in Unity, as long as the character has humanoid animation type and apply root motion is on (more on all that in this post), the whole character rotates when the animation plays:
From then on, the hug idle and exit hug animations should all also use root motion to move the character, and keep the root rotation set in such a way that works for the animation context. It’s helpful to play with the settings such as root transform rotation/position as shown in the screenshot above, for all animations.
Heres’s the code which links all the above together, allowing the toggling of different animations:
// Hugging
[SerializeField] private bool isHuggingWall = false;
[SerializeField] private Vector3 hugPushDirection;
void Update()
{
if (isHuggingWall)
{
animator.applyRootMotion = true;
MoveHuggingWall();
}
else
{
animator.applyRootMotion = false;
Move();
}
}
private void MoveHuggingWall()
{
Vector3 movementDirectionVector = new Vector3(Input.GetAxis("Horizontal"), 0.0f, Input.GetAxis("Vertical"));
Vector3 inputDirection = movementDirectionVector.normalized;
Vector3 forwardDirection = transform.forward; // the same as hit.normal / hugPushNormal
animator.SetBool("Hug Left", false);
animator.SetBool("Hug Right", false);
if ((forwardDirection == Vector3.right || forwardDirection == Vector3.left))
{
if (Input.GetAxisRaw("Vertical") == 1)
{
animator.applyRootMotion = true;
animator.SetBool("Hug Left", true);
}
if (Input.GetAxisRaw("Vertical") == -1)
{
animator.applyRootMotion = true;
animator.SetBool("Hug Right", true);
}
}
if ((forwardDirection == Vector3.back || forwardDirection == Vector3.forward))
{
if (Input.GetAxisRaw("Horizontal") == 1)
{
animator.applyRootMotion = true;
animator.SetBool("Hug Left", true);
}
if (Input.GetAxisRaw("Horizontal") == -1)
{
animator.applyRootMotion = true;
animator.SetBool("Hug Right", true);
}
}
// if not pushing towards wall
if (Vector3.Distance(inputDirection, hugPushDirection) > 0.9)
{
animator.SetBool("Is Hugging Wall", false);
animator.SetBool("Hug Left", false);
animator.SetBool("Hug Right", false);
}
}
void OnControllerColliderHit(ControllerColliderHit hit)
{
if (hit.moveDirection.y < -0.3f) return;
if (hit.gameObject.tag == "HuggableWall")
{
Vector3 movementDirectionVector = new Vector3(Input.GetAxis("Horizontal"), 0.0f, Input.GetAxis("Vertical"));
Vector3 pushDirectionVector = new Vector3(hit.moveDirection.x, 0.0f, hit.moveDirection.z);
// if is pushing towards wall
if ((Vector3.Distance(movementDirectionVector, pushDirectionVector) == 0.0f || movementDirectionVector == pushDirectionVector) && !isHuggingWall)
{
animator.SetBool("Is Hugging Wall", true);
isHuggingWall = true;
hugPushDirection = hit.moveDirection;
}
}
}
Starting off from the OnControllerColliderHit
, if the character is pushing towards the wall perpendicular, then begin the IntoHug animation (which uses root motion to turn the root of the character around 180 degrees, as explained earlier), and set isHuggingWall class property to true.
Further up in the Update method, once isHuggingWall is changed to true, the Move method is no longer used, and the MoveHuggingWall method is ran instead. This stops the usual third person movement of the character and limits the input to only the keys which are suitable for cover movement.
There’s a few if statements in that method which all control what happens when the character is in cover. For example, if ((forwardDirection == Vector3.right || forwardDirection == Vector3.left)) is a condition for if the character is facing left or right when in cover, as opposed to covering from the top or bottom directions. And within each of these is separate calls to the animator to initiate a left or right animation: animator.SetBool(“Hug Right”, true);
These animations happen because the SetBool method updates the conditions defined in the animator controller:
The above screen shot shows the Hug Right animation which should only happen when Hug Right is set to true, for example.
As I created these animations based on root motion in blender, there’s no need to set any speed values in the code, as the while point of root motion is to have the character move at a speed and distance defined in the animation itself.
Senior Engineer at Haven