I don’t like a lot of collision response systems in modern games. They feel far too floaty. You can almost feel the player's collision capsule rolling over obstacles. It feels as if the player is just a floating camera as opposed to a physical object moving through the world. It’s predominantly the reason as to why games from the 90s and early 2000s feel faster and more responsive; the player was treated as a moving physical object. One explanation for this is because of the ever increasing amount of polygons to calculate. While engines are becoming more capable of rendering complex graphics and 3D models, there are more surfaces to calculate for collision detection and response. As such, various methods have been used to circumvent complex and slow collision systems. I suspect these methods have persevered because of how simple and effective they are, but they have sacrificed the concise feeling of momentum as a result. The concept of treating the player as a solid object is important as it connects the player with the game environment. They need to be aware of their surroundings and take caution bumping into objects. This can greatly assist in creating a much more intense and engaging game.
Something that irks me is why there are little to no guides on how to recreate the feel of the proverbial 90s character controller. You would think something considered so basic would be commonplace by now, however extensive searches online will yield very little in terms of tutorials. Instead one is essentially forced to read Harvard papers and somehow wrap their head around complex spatial partitioning algorithms. There’s no harm in education yourself in these areas, however if you want to develop a game with a particular feel it can become an immense obstacle that can cause burn out. So this article here is an attempt to solve this issue for any fellow amateur developer that generally has no idea what he's doing. I have managed to program a collision response script that has a more 90s approach and avoids that floaty feeling which is plaguing many games at the moment.
The Player is an Object
For the purpose of this article, the games that define the “being a physical object” concept are Quake and Half-Life. In a nutshell, these games treat the player as if they were a solid object and thus react to collisions in the world as an object obeying the laws of physics should. A brief example of what I am talking about is how these games handle the player colliding with surfaces that are slightly beneath them while jumping. This can be observed if you try to jump up a staircase. If the player does not clear the step then their velocity would be clipped against it, as if their feet were nipping the edge of the step. Half-Life introduced the crouch jumping concept; allowing the player to tuck their legs mid-air (i.e. reduce the height of the player's collision shape) in order to clear the object beneath them. This is treating the player as an object. They have legs, legs collide with other objects, so you take that into consideration while jumping. Most modern games don’t do this anymore. Instead if you try to jump up a staircase your velocity will not be clipped. Their position may remain as if they are flush against a wall, but once they clear the step their velocity will kick in again and cause the player to clear the obstacle. There is almost no feeling of physical momentum here and it feels like you are a ball rolling over a sharp edge. So the problem identified here is the way in which collision response against steps and vertical surfaces are handled.
Most modern engines come with pre-made character controllers that handle basic movement and for non-complex games they work great. However, problems begin to arise when you start to introduce geometry with degrees of verticality and the biggest issue you’ll find is handling movement on steps and staircases. This issue is wide spread and leaves many amateur developers scratching their heads. The good news is that several popular solutions can be found on the web and implemented without much difficulty. The bad news is that they are severely limited and they neglect treating the player as a solid object. So I’ll go through two popular methods first before describing the solution I came up with.
Raycasts are very easy to implement and have a huge range of applications. They one of the primevals of collision detection and response. Using the player’s current velocity, you can cast a ray ahead of the player to check for any raised ground they may potentially run into. If a step is detected you can simply shift the player’s position up so they are standing on the step. This is cheap, simple and easy to do. The problem is that a single raycast is very thin. This means that if the player is standing on a grate then the raycast may slip between the gaps and incorrectly assume the player is falling or walking off a ledge. Additionally, if the player is standing on a triangular/pyramid shape the tip will be ignored and the player’s collision shape may catch on the sharp point, causing odd movement. You could introduce more raycasts to simulate the player’s collision shape, however it still does not offer a great degree of accuracy since there will always be gaps and using hundreds or thousands of raycasts in an attempt to fill the gaps will begin to severely impact performance. You could temporarily circumvent these problems by vastly reducing the complexity of your geometry, and if your game is simple enough then it is by all means a good solution. But this solution does not follow our prime rule: the player is a moving, physical object. A single raycast line does not represent a solid object very well at all.
Raycast lines can slip through gaps in the geometry
Another popular solution is to add invisible collision ramps to your geometry so the player can smoothly move up steps. All that is required from the collision response is clipping the player’s velocity along the surface of the slope. For most pre-made character controllers it works rather well and is a great solution for simple geometry. Using something similar to the raycast method described above could also work well with invisible ramps. The problem with this method is that if you start making bigger and more complex levels you will potentially double the number of collision objects the engine is required to calculate. Not only that, but you will have to spend time placing ramps on every surface you want the player to be able to move along, increasing the length of the level design process which could be time better spent developing other areas of your game. Now you could automate this process, but complex geometry could make that extremely difficult due to esoteric edge cases and, again, you’ll be spending more precious development time coding it. There is also the issue of collision ramps conflicting with some of the geometry of your level (e.g. tight ledges or crevasses) which may begin to restrict your level design. This solution also does not treat the player as an object in the game world, since their velocity and momentum is influenced by “magical” and “invisible” ramps and the player can jump up staircases with little to no resistance. We start entering that floaty feeling again...
Using collision shapes instead of raycast lines or invisible ramps is a better choice for a number of reasons. A collision shape roughly represents the player model and can be treated like a solid object that can be projected in a given direction. The benefit here over traditional raycasting is that they won’t slip through gaps in the geometry. Popular shapes are boxes, capsules and cylinders, although any other shape may be used depending on the dimensions of the player model. For instance, if the player is a rounded spherical creature then you would most likely want to use a sphere.
A common collision shape used for character controllers is a capsule. Capsules can be incredibly efficient to calculate as it can be represented by a line with an infinite number of spheres between each end point. The trick here is that you can you can shift a sphere up and down on the line segment and do a simple sphere collision test, which is very fast since you only need to check a colliding surface against the radius of the sphere. However the rounded bottom of capsules can cause a variety of problems. When the player approaches ledges the rounded bottom of the capsule can mistakenly think the player is not touching the ground and cause them to unexpectedly slip off. The rounded bottom can also cause difficulties with sharp edges, specifically when it comes to determining which normal surface to slide against. A solution to this would be to use point based collision detection, where the surface normal is calculated from the collision point in relation to the player’s position, which can then be used to roll the player capsule over the edge. This may be beneficial to some as step climbing is already somewhat implemented, however this is specifically what I am trying to avoid. I want the player’s velocity to clip against sharp edges, I don’t want it to roll over sharp edges.
Capsules will sometimes not detected edges due to its rounded bottom.
The ideal collision shape to use would be a cylinder. The rounded sides will slide along walls smoothly and the flat bottom reduces the risk of the player slipping when they approach edges, creating the illusion that their back foot is still touching ground. The only downside is that cylinder shapes require slightly more calculations to define its smooth sides and flat top and bottom, plus some engines do not even allow for cylinder shapes to be used for collision detection. The last time I used Unity it was not an option, plus collision detection was primarily point based. I do believe Unreal engine has the ability to use cylinders but I have almost zero experience with that engine, so I couldn't tell you the pro's and con's of using them. Godot allows for cylinders to be used as collision shapes, which is one of the many reasons as to why I use it. You can simply take a copy of the player's collision shape and project it like a raycast line. We call this process "tracing".
Collision Shape Tracing
Tracing involves projecting the player's collision shape along their current velocity to detect collisions. If a collision is detected it will return the fraction of the distance covered, the normal of the surface it collided with and the position of the collision shape the instant it collided with something (which we refer to as the 'end position'). While the concept is relatively simple there is a caveat that must be dealt with. We cannot directly trace along the player's velocity vector since it may stop short against small objects that are directly in front of the player. We must first trace directly above the player, using a step-height constant as the distance. Not only will it avoid catching small objects but it will also detect any collisions above the player. Then we do a second trace, starting from the end position of the first trace and going along the player's forward and right (XZ axis) velocity vector multiplied by the frametime. Then we cast down from the second trace until we reach the player's original vertical (Y axis) position. If a step is detected then we simply set the player's position to the collision shape's position. Finally, we clip the player's velocity along the step surface to redirect momentum correctly.
It's important to trace directly above before tracing along the player's XZ velocity.
Moving the player up steps is as simple as setting their position to the collision shape's position when it collides with the step.
We are, in a sense, instantly teleporting the player up and along steps. The horizontal transformation is smooth because the collision shape's position we are "teleporting" to is the player's horizontal velocity multiplied by the frametime. Conversely, the vertical position is changed instantaneously. This is undesirable since walking up steps in real life is generally much smoother. This issue can be solved by setting the camera's height as its previous Y position, subtracting it from the current Y position and using the difference to smoothly lerp upwards. The only slight problem with this solution is that the camera will drag behind slightly when riding elevators, so you may wish to setup a few flags for when the player is standing on an elevator. This effect is best viewed when running up a slope in Quake or Half-Life; you will notice the camera will move upwards slightly the instant you stop moving up the slope. It is also very obvious when riding elevators in Quake.
If the player is airborne we do not want to call the step function because we want the player's legs to clip against vertical surfaces of a step. To achieve this we can simply trace the player's collision shape directly along their velocity vector and don't bother about tracing up a step height and back down again. We clip the player's velocity against any colliding surfaces, which will cause the player's XZ velocity to halt whilst (generally) preserving their Y velocity.
We do not use step detection while airborne. The player's XZ velocity will stop while their Y velocity retains momentum.
This delivers the effect of the player's legs nipping the edge of the obstacle below.
Final Thoughts and Considerations
The method of using collision shapes can be potentially expensive depending on how collision shapes are calculated in the engine of your choosing. I couldn’t get it to work in Unity since the collision shapes are very odd in that engine; one collision shape casting method would give me the distance and surface normal but go through objects, another method would collide against objects correctly but not return appropriate collision details, another would collide with every object in a given vicinity and require additional sorting to find the nearest object, and so on and so forth. I gave up but I’m positive it can be done somehow.
I have this system implemented in Godot 3.3 at the moment and it works well so far. There were some oddities with velocity clipping in previous versions of Godot and some of it may appear every now and then, so it’s still a work in progress. I am waiting on Godot 4 to be released specifically for their cylinder collision shape improvements which they have mentioned in previous blog updates, so hopefully that’ll solve some of the clipping issues. I haven’t tested the controller with concave objects or hugely complex environments yet, so that is also something to take into consideration. I have only used cylinder collision shapes with the trace function. I couldn't get capsules to work was I was programming it, but I haven't tried them recently as cylinders work perfectly fine for now. You may have to edit "trace.gd" in order to get other collision shapes to work.
You can view how I have implemented everything in the github repository (link below). You may notice that I am not exclusively using trace functions for collision detection. This is for several reasons:
- The trace function isn't terribly efficient as I need to use two to three different collision shape casting methods in order to retrieve collision information such as surface normals, distance fractions and so on.
- Detecting collision while airborne uses Godot's built in "move_and_collide" function, since it does exactly what I need it to do, so no additional tracing is necessary.
I am using "move_and_slide" for the initial move while grounded. If a detected surface has surface normal with a Y axis greater than 0.7 (non-steep ground) then I don't need to bother using a trace to check for steps.
The trace function is instantiated as singleton, so it can be used in various scripts across your project for many other needs e.g. in a previous project I was using it to check for collisions above the player if they are crouching and release the crouch button. I am also using it to detect if the player is grounded or not by tracing down from the player's current position by 0.1 units, just like how it is done in the original Quake source code.
The controller now uses move_and_collide for ground movement and the step function is nested inside a continuous collision detection loop. Clipping velocity during and after a move_and_slide call causes inconsistent movement. Sometimes the velocity would halt before resuming, the effect of which was obvious if the player was running against a long straight wall. Velocity clipping was not smooth. I ended up dividing the player's velocity up into 'x' amounts and checking for steps incrementally. This has solved the problem for now, but more testing still remains.
The project is licensed under GNU v3 since it uses a significant amount of Quake source code. If this article and/or the project on github has helped in anyway I would appreciate a credit.