Wall Running in Unity: Making It Work, Making It Feel Good
I’m currently using Unity to develop a first person platformer in which the player can run on walls.
Here is my progress so far:
Basic wall running
We don’t need much to create the wall running effect:
- A 3d environment filled with colliders which act as walls
- A player whose movement we will abstractly define as a 3d vector every frame. I use the built in Character Controller1 as a base
- Raycasts2: lasers we can fire at the environment that will report information about things they hit3
The algorithm for basic wall running looks like this:
- Fire a short raycast to the left and right of the player
- RaycastHit.collider doesn’t equal null? That might be a wall…
- RaycastHit.normal dot Vector3.Up equals 0? If yes, the two vectors are orthagonal which means the wall candidate is perpendicular to the ground. That’s a runnable wall!
- If both raycasts hit wall runnable walls, discard the one further away and keep the closest
- If we have a runnable wall, enter wall running state (otherwise, exit here and wait until next frame)
- RaycastHit.normal X Vector3.Up (cross product) gives us the orthagonal vector - which is the vector along the wall
- Tell our character to move along this vector. Do something with vertical velocity (like ignore or limit it)
That’s it! Super simple. To make wall running work at consistent distances I raycast in multiple directions around the player like so.
In addition, I lock the player direction to forward along the wall while wall running (and then allow camera movement independently) to ensure wall checking is done consistently along the wall. In other words, even if the player looks away from their wall their ‘body’ is still perpendicular to it, and thus raycasts will be correctly sent towards it.
Another key ingredient is to make the player wall run at a consistent distance from the wall. RaycastHit.normal and RaycastHit.point can be used to create a unity plane4. Plane.GetDistanceToPoint will tell us the current distance from wall. If the player is not at the desired distance, I add a small vector in the appropriate direction to the vector along the wall we calculated earlier. An even simpler approach would be to set the transform of the player directly to the appropriate distance.
The final steps are to add a check to make sure the player is not on the ground, and to add a “wall run” button.
And that’s it! Functional wall running!
Making it feel good
To make wall running feel good we need to give the player feedback as they do it. We can do this by adjusting the physics of the wall run (e.g. what happens to vertical velocity during the wall run?) and by adding sound effects. We can also add screen effects that give the wall running a more distinct visual without touching the physics.
The most common camera FX, seen in Mirror’s Edge and Titanfall among other games, is a camera tilt during the wall run. Creating this effect is very simple. I use a tweening library called Leantween 5. There are many other tweening libraries.
The leantween version of this looks like:
var rotateOut = LeanTween.rotateLocal(movementController.cameraRoll, new Vector3(0, 0, 12.5f * direction), timeToSnap).setEase(LeanTweenType.easeInOutCubic);
I’ll let you investigate the syntax but the punchline is that once this method has been called cameraRoll will be adjusted by 12.5 degrees over “timeToSnap” seconds.
This camera tilt makes it clear to the player when wall running is happening.
To make it clear that a wall run is about to end, we can slowly reduce the camera tilt such that the camera returns to 0 degrees of tilt at the same time as the player is ejected from the wall (appropriate when there is a time limit on wall running). You can see this effect in Titanfall 2.
Titanfall 2 does another very useful thing. The player never wants to look into the wall; it stops them from seeing where they are going. We can add a smooth rotation away from the wall so that the player doesn’t need to adjust the view themselves and can concentrate on planning their route through the level.
Because we want to allow the player to adjust their rotational view even as the game adjusts it automatically Leantween is not an appropriate solution. I check the angle between the current camera horizontal look direction and the desired minimum horizontal look direction (5 degrees away from the wall) - and perform a small rotation towards the minimum look direction every frame as necessary. The further into the wall the player is looking, the quicker this rotation happens.
Here is a comparison of wall running with and without camera FX. The camera look rotation effect especially can be quite subtle.
Jumping off walls
Wall jumping is relatively straight forward. The small gotcha is stopping the player from instantly reconnecting to the wall they just jumped from.
One way to do this is to add a cooldown to wall running. This also helps from a game design perspective because it stops players being able to infinitely climb a single wall.
I keep a record of the last RaycastHit.normal that caused the player to enter or kept the player in a wall running state. If the player is currently not in the wall running state and a new wall detected has the same RaycastHit.normal as the previous wall, it is not considered a viable wall. After some time, I reset the stored normal to null.
As soon as a player hits a new wall, the last RaycastHit.normal is overriden, effectively removing the cooldown on that wall. This means a player can bounce between two walls as fast as they can travel between them. I’m happy with this behaviour.
Catching on corners
After much playtesting I discovered an edge case that was incredibly frustrating. If a player went near a wall intending to pass it, wall running would still sometimes be triggered interrupting the players movement.
To fix this I added an additional check. After viable walls have been found, I do a final forward raycast with long range to check that the player is looking at one of the viable walls. Only if the player has their view centered on a wall can they run on it.
This is illustrated best by the below gif (it may be difficult to see but there is a crosshair that indicates exactly where the player is looking).
This forward check needs to be disabled if the player is already wall running because we want to allow the player to look away from walls while doing so.
The Big One
A whole host of obscure edge cases were solved with one change. Here is the example I struggled to understand for days.
Player is moving roughly towards a corner. He intends to wall run on Wall A. He is closer to Wall B.
Wall B is discarded as a viable candidate because the player is not looking at it.
Player reaches Wall A and begins wall running.
The requirement to be facing the wall is dropped. Even though player has begun wall running on Wall A, player is still closer to Wall B (position seen in the diagram) and immediately starts wall running down Wall B.
The solution is to record the point at which the player connected with the wall. Note This point is at which the raycast hit the wall first, not where the player was located at the time. When checking for wall candidates, we can then discard any raycast hits that land ‘behind’ this point.
When the player is located behind this first hit point (happens at the start of the wall run), I probe a special calculated raycast directly to the hitpoint, to ensure the current wall is correctly checked.
The best way to figure this stuff out is to obsessively play and study other games. Titanfall 2 is a huge inspiration, especially for camera effects. Nevertheless, it’s also important to have a strong vision about what you’re doing differently.. There are a few techniques Titanfall 2 uses that increase the flow substantially, but that wouldn’t be appropriate for my game. One thing they do is increase player speed during wall runs above the maximum otherwise allowed. It incentivises chained wall runs and feels awesome - but in a game with no friction like mine, that’s a recipe for distaster.
Here’s the final product again:
If you have any questions feel free to mail me or tweet me (I don’t use twitter proactively but I do receive notifications for tweets).