top of page
Search
  • Writer's pictureNic Van Zuylen

Cosmosis - The Challenges of the Making a Unity Player Controller

Updated: Oct 2, 2019

Unity is a well-known and common choice for developing a wide variety of games. This all-round functionality often leaves something to be desired when developing specific types of games, such as first person games.


During the development of the first person boss fighting game, Cosmosis. I have found several difficult issues to overcome, usually stemming from the limited functionality of Unity's CharacterController component. The component is very useful in developing a player controller due to how it automatically resolves environmental collisions and returns a modified velocity after collisions have occurred, and so I chose to use it for that purpose. However, it does not properly account for some of the requirements of modern first person games. Below I will highlight and explain some of these issues, and the elegant solutions I used to fix them.


"Floaty" Jumping


When coding a jump into your controller, you may find people complain the jumping feels "floaty". This is a situation where the period between rising and falling during a jump feels unnaturally long, causing a jump that will feel like you're on the moon even under Earth gravity!


Thankfully during the development of the player controller in Cosmosis, my designer came to the rescue, and linked me a video called Math for Game Programmers: Building a Better Jump, where the presenter Kyle Pittman described a more physically accurate method for jumping and falling, using the common physical formula for projectile motion.


Thanks to Kyle Pittman's presentation, one can calculate an accurate impulse based off of jump height and duration using the formula: v = 2h / t. The equivalent code is shown below, where jump duration should be halved:

With that impulse calculated (and it only needs to be calculated once then stored) it can be added to the vertical velocity when the jump button is pressed, just like you would in a typical jumping function.


Still this won't cause an accurate result until the acceleration due to gravity is adjusted according to the formula, where: g = -2h / t². The equivalent code is shown below:

This can be subtracted from the vertical velocity (Don't forget to multiply by deltatime!)

to result in a jump where you will reach the specified height from the ground and land again within the specified jump duration. (Provided you land at the same height as you jumped from.)


Kyle also described how you can specify a maximum horizontal distance a jump can cover and find an appropriate jump duration from it. All you need to do for this to work is simply divide the desired jump distance by the player's maximum movement speed, and use the result as the jump duration.


Ground Detection


If you have defined separate behaviour for grounded and non-grounded movement, you may find sometimes you will experience non-grounded movement whilst actually still grounded. If this is the case you may have used the CharacterController.isGrounded property to detect if the player is grounded.


CharacterController.isGrounded, sadly, is a very unreliable method to detect if the player is grounded, as on any even remotely uneven surface this property will return false even if the player is clearly grounded.


Since Unity's built-in CharacterController ground detection is faulty, you will have to write your own ground detection algorithm, and it's harder than you may think. One of the most reliable and useful methods is to use a spherecast (a raycast where the end is a sphere rather than a point) to find objects below the player. If you send this spherecast downwards from the centre of the player, and the hit distance is less than or equal to half of the player's height, you can safely assume the player's feet are touching that object's surface.


Much of my ground detection code has been written from trial and error. For me, I found the code below works well for ground detection for a player controller using Unity's CharacterController component:

This code will send an infinite distance spherecast downwards from the player's centre, and check if the hit distance (if it hit anything) is less than half the player's height minus the player's capsule radius multiplied by 0.7.


Sloped Movement


When moving up and down sloped surfaces you may find the ride to be a little bumpier than expected. This can occur as when moving down a slope the forces are applied parallel to the horizontal axes, causing the player to move into the air and fall back onto the surface repeatedly. When moving up a surface, you may find the player's movements to be very high friction and inconsistent. This is because the physics engine thinks you're attempting to move into the ramp, rather than up it!


Thankfully, if you have implemented a raycasting solution to grounded movement detection, or you have found another way to find the normal vector for the sloped surface you stand upon. You can cross product that normal with the vector pointing right from the player's look direction (could be the camera transform's right vector for first person) to find surface-parallel movement vector to your right, finally you can then cross product that right vector and the normal to find your surface-parallel forward movement vector. From there, you can move the player along those axes when standing on a surface to fix both the issues I described above. Below is some code for the cross products:

This code is best ran whenever the ground detection returns true. Below you can see a visual representation of the normal and surface axes:


Here you can see the surface normal and movement axes on this slope. Green = Normal, Blue = Surface Forward, Red = Surface Right

Slope Limit and Sliding Down Slopes


While Unity's CharacterController component does have a built-in slope limit, all it does is simply prevent the player from moving up slopes of such an angle, and does nothing to move them down the slopes.


If you've ever played Skyrim, you probably at some point have attempted to climb over a mountain, and found that once the slope gets too steep you can't move up it any further. This is the same thing Unity's CharacterController's slope limit does, but in Skyrim you can also still jump while on these slopes, potentially overcoming the slope limit by other means. The game does nothing to prevent you from still climbing these slopes via jumping.


Annoyingly, there is no way to tell if the player has reached the slope limit using the built-in functionality, so you will have to write your own to detect if the player has reached the slope limit on the surface they're standing on. My method to detect this ties in closely with the surface movement axes solution for sloped movement I described above. To find the angle of the current slope, you can find the slope-local axes by using the code below:

What this does is find the axis which is considered to be moving up the slope, and dot products it with the world up vector. The resulting dot product value can then be multiplied by 90 to get the angle in degrees, then the angle can be compared against the slope limit to find if the slope limit has been reached!


Assuming we are standing on a slope beyond the slope limit, now that we know we are not supposed to be standing here, we can find the component of our velocity pushing against the surface normal by dot producting the velocity and the negative normal, and multiplying the normal by the result. Once the component is obtained, we can multiply it by 1.1 (or some other slight increase) and add it back to the velocity. This will now push us away from the surface ever so slightly, causing us to slide down the surface. Below is the code to push the player away from the surface:

Once implemented, you will find you can no longer stand on surfaces beyond the slope limit, and will slide down them! Also, don't forget to set your grounded value to false, to treat this sliding as if you're falling.


Once you're finished, the result should look something like this!

Limiting Movement Speed


When you first write the code to limit the player's movement speed, you might simply choose to cap the player's maximum velocity to the maximum movement speed. While this is a simple and effective solution, if you want it to be possible for additional velocity to be added by external forces, you will need to increase the velocity cap to allow for greater speeds from these external forces. While this works if you try it you will also find your maximum movement speed now matches that new cap, they are both effectively the same value!


So to fix this we need a way to limit movement speed to a separate maximum to the velocity cap. The method I devised to solve this is to simply not add any further velocity in the direction of movement to the player when they attempt to move in that direction, this way the velocity will never exceed that speed from player movement.


This works by taking the component of the velocity in the desired movement direction, and clamping it between 0 and the maximum movement speed, and then subtracting it from the maximum movement speed. The acceleration due to movement can then be multiplied with this value, making it approach zero as the velocity reaches the maximum movement speed in that direction, preventing any further acceleration in that direction! Below you can see the code to make this work:

Now you should be able to raise the velocity cap without raising the maximum movement speed. When you attempt to move with the new cap, you will find you never exceed the maximum movement speed!


Now you should be able to add external forces and exceed that max movement speed, limited only by velocity cap. This is great for momentum-based movement in games!

If you pay attention to the speed value in the bottom-left corner. My maximum movement speed is 15m/s. After pulling myself towards the beam you may notice I've exceeded 15m/s after landing, this is because of the external pulling force!

That is all of the major issues I had while making the player controller for Cosmosis solved! I hope you have learned something new from reading this article and thanks for stopping by!


21 views0 comments
bottom of page