All of life is about compromise. I started off making my mechs use the CharacterController, but shied away from it as that meant I had to reimplement lots of physics. I replaced it with a Rigidbody-based system, but that started randomly bouncing off the floor and jumping was dangerously unpredictable. In movement system rewrite number three, I seem to have ended up with… a mix of both.
It took me far too long to understand why this third approach works, but I think I’ve got it now. Since movement controllers seem to be a perennial topic in the commUnity, it’s time for me to add some words to the mix!
Predictable Manoeuvres in the Game
Since I’m making a top-down action-RPG with platforming elements, and not a racing game, movement needs to be instantly responsive and predictable. Walk and run speeds need to be consistent, as does the amount of space and height covered by a jump.
If you need predictable movement, Unity says, use the CharacterController component.
CharacterController is annoying because it has only tenuous connections to the physics engine. Although it appears like a capsule, from all the work I did using it back in the day it felt like it didn’t really exist unless it was moving — so while it acted fine in isolation, it choked up a lot when against other units and rigidbody crates. It could not push nor be pushed naturally.
So CharacterController requires you to implement gravity and pushing for yourself. Then you have to add loads of extra junk to make it cope with slopes properly (or at all). In summary: it gets complicated fast.
Using a rigidbody instead of a CharacterController offers many simplifications to the control logic but comes with a loss in predictability. As a physics object it is completely at the whims of the physics simulation, which answers to no man.
The biggest problem is the matter of friction — turn it on and your unit can barely move, but turn it off and your unit slides around like it’s on ice. The answer, according to the resources I looked at, was to turn off all friction and fiddle with drag instead — zero drag while airborne but quite high on the ground, so your unit can start moving at full speed immediately when propelled but will halt in short order when the player releases the button and the force is stopped. I don’t really understand physics but even to me that felt like one hell of a fudge. (Okay, fine, it’s video games, everything is some level of fudge.)
The problem I’d been having more recently was that, because gravity is always acting on a rigidbody, moving it around means it regularly slips into and is repelled out of the ground — it doesn’t actually move cleanly across a surface. This is by tiny increments but it’s often enough to make a complete mess of determining whether the unit is grounded or not. Although it’s a capsule with a rounded base, stepping up over small ledges always caused a stutter too. Going over the lip of a hill resulted in a very small jump into the air. None of this was strictly game-breaking but it felt bad.
Which left me with nowhere to go. The narrative has always been CharacterController or Rigidbody: pick one. There is no third option.
Except then I saw a tutorial, by Minions Art on her patreon. She seemed to illustrate, with some incredibly simple bits of code, a stunningly smooth character movement system, built off a rigidbody. How could this possibly be?
I shrugged and started trying to implement it. It still took a few hours before I started to comprehend the magic.
I believe the critical part is this: the rigidbody never touches the ground, and is instead held aloft by raycasts. Because it’s off the ground, there are no more issues with the collider intersecting the ground and bouncing out of as you move along: a raycast is always accurate and does not drift into the ground as the physics simulation advances. Which means you always know where the ground is, and whether or not your unit is definitely standing on it, with perfect precision.
The peculiar thing here is that although the collider is balanced on a raycast or two, the rounded base of the collider can still hit things on the way down. One of my biggest issues with the CharacterCollider was it getting caught on awkward corners and slopes while having gravity applied, which using a rigidbody solved. Here, if none of the rays hits the ground, then the character is still considered to be airborne — it will fall and its rounded base will cause it to be dunted away from steep slopes until one or more of the rays hits a flat surface.
The other important part is that it functions on setting the velocity of the rigidbody directly, rather than using ApplyForce (which has always had dangerously unpredictable results for me). Setting velocity directly still gives you natural pushing of other rigidbodies, and because it’s still non-kinematic it can be pushed, but it still obeys the rest of the universe and so can’t go into walls.
The hybridisation comes in because you do have to implement gravity again. The rigidbody is now perched on raycasts and we don’t need the simulation intruding upon those. At this stage I’m actually okay with this — I don’t think I want strictly realistic gravity for my mechs anyway, considering my world is not 1:1 scale with the physics engine. Knockback forces must also be done manually, as you cannot ApplyForce and forget anymore — but again, less realistic and more predictable knockback behaviour suits my needs (I’m sure you could reimplement totally accurate forces if you really wanted to).
Yes, I think all that makes sense. What about the downsides?
I haven’t profiled any of this but I assume it comes at some cost: previously I was doing a single spherecast to assist groundedness checks, whereas I’m now doing 3 raycasts to hold the rigidbody aloft (I couldn’t find any intel on the cost of a sphere versus a ray — a part of me is hoping that a sphere is more than 3x heavier than a ray so I won’t feel anything).
I also have to implement some peculiar shenanigans for my moving platforms, since a unit’s collider may now be touching the side of one directly or hovering above it via the raycasts. For even more fun, it may swap between touching via either mechanism within the same frame.
But I reckon I’ve got all that working well enough. If performance is an issue, then I have to suck up smaller levels with fewer units; it’s not the end of the universe (and I think I’m still harsher on the render pipeline at this point anyway). The important thing is that, fingers crossed, three rewrites later, I finally have a fully reliable unit movement system.