Posts
Search
Contact
Cookies
About
RSS

Unity 3d - Player control and physics

Added 19 Jan 2021, 10:36 p.m. edited 18 Jun 2023, 1:12 a.m.

There are very many ways to control your player in a Unity game, in this case I had a number of specific requirement.

Initially I looked at a few peoples implementations of a kinematic controller, but this has a few drawbacks. For example if you want to collide with a static object you basically need to replicate how the physics engine deals with inter-penetration (often sweeping shapes to check for potential penetrations). This has the side effect of making other dynamic objects with a rigid bodies immovable by your player, you can work around this, but to do it properly soon gets rather complex, and frankly is likely to introduce even more corner cases...

I decided to experiment with a simple rigid body controller (so pushing other object around, not to mention collisions, just works). Initially to stop the inertia effect of thrust, I added a decent value to the bodies drag property. This works okay right up to the point that you leave the ground. The drag property also effects the Y axis (altitude) so gravity gets effected as well, while you could add an extra force to "put back" the gravity, actually calculating the effect of the drag property and calculating what force you need to add back in for gravity can be problematic, so I opted to set the drag property to zero and implement drag manually.

Its worth noting at this point that while Unity itself doesn't have any units per se, the physics engine itself does. Each 1 unit of movement along an axis is 1 metre of distance, a rigid body's mass is in kilograms and force is measured in Newtons. In order to apply forces in a smooth manner (where a physics step might not happen at a reliable frequency under extreme load) its important to use the current delta time step. In effect this fraction of a second is scaling your forces over the course of 1 second, you are applying a slice of this force, proportionally to the fraction of a second the physics update is covering.

WolframAlpha can be a useful tool here just to double check that your forces are in the right ballpark, for example you can calculate the effect of 200N on 80KG but sometimes posing the question correctly to WolframAlpha can be harder than working it out manually (sadly one of the "four pillars" isn't all it could be!) The resultant 2.5m/s is a rather fast walk or a jog, but that's not allowing for the drag.

// strong "drag" that doesn't effect gravity
// avoids hovercraft like skid steering
Vector3 vtmp = body.velocity;
vtmp.y = 0;
body.AddForce(-vtmp * body.mass * Time.deltaTime * 40f, ForceMode.Impulse);

The Y axis of the players velocity is completely ignored, in addition the force doesn't provide 100% drag, the player shouldn't come to an instant stop but rather, quickly slow down, this helps keep things looking smooth. As the players speed gets higher so the "drag" force get higher, eventually the drag balances the player force, meaning you get a cap on the velocity, in effect a "terminal velocity" but along the ground.

Almost immediately after doing this I noticed that if the player had what should be a sliding contact at a shallow angle with a static wall, the player immediately oriented towards the collision surface. This is obviously not desired behaviour, and is caused in part by the physics friction material properties. A solution to this is to give the player a custom physics material with 0 friction parameters and also set the friction combine properties to multiply. Making the players surface frictionless allows the player to slide along the length of walls, and as we are already implementing drag (without attention to surface friction) it doesn't mess up movement along the ground. You will also need to zero the bounce parameter as this won't play nicely with platforms, this isn't an issue as you really don't want your player bouncing around all over the place.

Making the player frictionless does have a small unintended consequence, in that the player will slowly slide down inclines (most noticeable when the player isn't giving inputs), this could be worked around quite easily, equally it could actually be a useful game mechanic. For example some very steep inclines could be used as jump off points that you need to leave quickly. This would help the player jump further distances than they would normally be able to, but be just a little bit trickier than doing the two jumps with a flat surface halfway between. For now I'm leaving this "feature" in, but I do like it when an unexpected "feature" sparks inspiration !

Another thing to look out for is what happens when the player is going down steps or ramps. As the main forward force of the player is always horizontal the player will tend to bunny hop down stairs and ramps. The simple solution to this is to add an extra down force if the player is in contact with the ground. This gives the player enough downwards velocity to keep the player in contact with the surface as you'd expect. This force also contributes to platforms working as needed, but if we want more than simple lifts (elevators) then we also need to add some lateral velocity when we are on a platform.

Initially I was using a Ray cast downwards to get the players "altitude" this can be problematic for a few reasons, the player might not be at exactly the same height depending on the surface, there may be a very slight extra interpenetration and as a Ray has no width you can miss features that are not exactly under the centre of the player. In the end I scrapped using a Ray and instead added an extra sphere collider to the player set as a trigger. This collider is positioned so that its bottom is below the players collision capsule, just enough so that it can help handle going down stairs and ramps.

The player behaviour needs to take note of what's going on with the contacts, this is for two reasons, checking to see if the player is on the ground, and also if the contact has a rigid body (to help with platforms)

private void OnTriggerStay(Collider other)
{
    isGrounded = true;
    groundedOn = other.attachedRigidbody;   // can be null...
}

private void OnTriggerEnter(Collider other)
{
    isGrounded = true;
    groundedOn = other.attachedRigidbody;
}

private void OnTriggerExit(Collider other) 
{
    isGrounded = false;
    groundedOn = null;
}

Using a trigger in this manner ends up being less problematic and makes the code more straightforward additionally if the players centre is over an edge, but still on a surface with a ray it looks like the player isn't grounded, using a trigger means the player is still grounded right up until they leave the edge.

Its worth noting a quote from the Unity manual (always a much better source than IntelliGuess) "For reading the delta time it is recommended to use Time.deltaTime instead because it automatically returns the right delta time if you are inside a FixedUpdate function or Update function."

// extra down force to stop hopping down ramps and stairs...
if (isGrounded) {
    body.AddForce(-body.transform.up * body.mass * 90f * Time.deltaTime, ForceMode.Impulse);
}

Dealing with platform movement is easy once we know when the player is on a platform.

// player gets moved when on platforms
if (groundedOn) {
    body.AddForce(groundedOn.velocity * body.mass * Time.deltaTime * 40f, ForceMode.Impulse);
}

This force is effected by the drag force that's added, so if the drag force is changed, this force will likely need modifying too. As the platforms are moving between their waypoints using MovePosition the platforms aren't teleporting but actually moving properly, so they have velocity as they should, this is used to move the player, making the player behave as expected regardless of the platforms movement.

Mouse look is somewhat trivial and actually aided by the fact that the main camera is attached to the player. The players heading rotates the whole player while looking up and down just effects the camera rotation, both the player and the camera only have 1 degree of rotational freedom. Because the players collision mesh is a capsule it is rotationally symmetrical around the Y axis, this means we can get away with directly controlling the rotation rather than using forces. However due to both input processing and the fact that both of the update methods are running at different frequencies you can just about see that the camera rotation isn't quite as smooth as it could be.

// rotate player heading
body.transform.localRotation = Quaternion.Slerp(body.transform.localRotation, 
                                                Quaternion.AngleAxis(rotX, Vector3.up),
                                                0.5f);
// rotate camera up/down
cam.transform.localRotation =  Quaternion.Slerp(cam.transform.localRotation, 
                                                Quaternion.AngleAxis(rotY, Vector3.right),
                                                0.5f);

While you could average rotations to smooth things out, this could potentially add latency, fortunately continually using a "slerp" (spherical linear interpolation) halfway between the intended rotation and the current rotation gives a smooth rotation, and its much less problematic to implement.

I won't go into the details of how I'm handling the jumping in this controller, as its really already getting somewhat specific to my end use. Basically if the player is on the ground then a decent upwards force is applied when the player jumps, there is a cool down counter to ensure that the force isn't applied more than once during the initial stages of the jump.

Its worth mentioning the platform script again, there is a list of waypoints for each platform, and this is where the Unity editor really scores. By repositioning the platform you can copy its position and then paste that position into the appropriate point in the waypoints list, this makes creating a path for a platform very quick and convenient. Alas you can't past a Vector3 value into a Vector4 property, so I had to make a separate list of the pause times for each waypoint, its important to make sure this array is the same size as the waypoint list. If a waypoint results in a large change in direction you should probably put in a tiny pause of 0.2 seconds just so you don't loose the player, similarly you need to be careful that the platform speed isn't set too fast.

When setting up the player you will need a capsule with collider and a camera as its child, its also important to freeze the X & Z axis rotations otherwise the player will fall over. While interpolation and continuous collision detection can be expensive, it's worth setting it for any body that has a camera attached.

A realistic mass is important not only for the player but also for any objects you want to interact with. A 1 metre cube with the default mass of 1 kg has a lower density than air, and will behave in a very unrealistic manner if your player lands on it. Those 1 metre cubes you have kicking around for the player to move and get up onto might need to be quite a bit heavier than you'd expect. Its worth finding a density calculator and plugging in some volumes and masses just to see if the resulting density is at least in the same ball park as real materials.

Finally I've attached the player and platform scripts here

Enjoy!