Is it possible to optimize this character controller any further?

Hi guys,

I’ve been working on creating a controller for handling simple collisions in Unity for an online game (just a hobby, not intending it to be miraculous) and I have carried this controller nearly as far as I have experience to take it.

[Here is the controller script]

I have avoided using Unity’s built in solutions for a few specific reasons:

1. I have implemented client-side prediction prior to creating this using the CharacterController, but I want to use the 2D tools instead. Using a rigidbody2D to check for collisions isn’t an option for this, as these collisions are checked and handled on the next FixedUpdate().
While this is perfectly fine for games not using client side prediction, I need to be able to check for collisions on all stored inputs after receiving a result from the server in a single frame. Performance here, running multiple checks in a frame, isnt an issue because this will be done on the client, and only for the local player. In fact, the checks will only be performed on the client at ALL by the player, so really this controller only exists on the non-local players to be a part of all spawned player prefabs and to inform all dependent scripts of a server-controlled move.
So I needed to be able to check for collisions immediately when I get back a resulting position from my inputs sent to the server, and set my player to that position, clear out inputs stored from a tick prior to the one received, and rerun all those inputs.

2. I wanted control over my movement, absolutely. I didn’t want realistic physics, or any of that. All I needed was movement and simple collisions. My character is designed for a top-down game and can move freely from tiles in 8 directions. I also wanted to be able to slide across colliders when moving in angles that are not perpendicular to the surface. This also works well using a circular collider as players slide around one another.
Using this controller, if I wanted to I could also check the normal I’m getting and know the angle of the collider I’m up against. This could be used for a number of things. I also could modify my players speed when sliding across a face, to simulate friction. I’m quite happy with the functionality of the controller, and the possibilities of extending it, even in my rather novice experience level.

3. I wanted to be able to run my checks at a much lower rate without sacrificing accuracy, such as missing collisions due to low FixedUpdate() framerate. This would, I feel, make things much easier performance-wise, as well as lowering the amount of data that needs to be sent to the server.
Initially I was testing this controller to run at a FixedUpdate interval of 20 times a second (0.05). It worked well and felt responsive, and I was able to have 400 controllers moving around and colliding accurately without severe framedrops on my laptop. However, much higher than that and it slowed to a crawl. I’m not aiming to do very detailed and extensive research on optimization and work on this endlessly, as I’m still semi early in my project (I know early optimization is bad, but I’m looking to improve not only the controller but my knowledge and scripting as well). However, I would love to be able to increase that performance greatly, as ultimately I feel this will be the primary load on the server.
I ended up testing the controller at a FixedUpdate interval of 10 times a second (0.01) and forgot I had set my time to that interval. Upon finishing the controller I realized it still was working very smoothly at that rate. On the server, a seperate gameobject holding all visuals of the player smoothly lerps to the new position on each move, called from the controller.

So.

Having said all of that, if you don’t feel like reading through my (hopefully not absolutely garbage) code, the controller works using CircleCastAll(). This way I don’t miss any collisions even at 10 frames per second. I loop through all returned colliders and ignore any triggers and any colliders belonging to the player the controller belongs to.
I then calculate the closest points on collision, and the closest point to that collision on my collider, and set my position to where my colliders centroid would be on that collision. I realize that this is returned through the circlecast, but the way it works out of the box is prone to errors when right up against a collider when the colliders overlap inside one another, and breaks the collision handling. Therefore, its calculated manually.
Doing this makes the player “stick” to the object it collided with unless the player moves away. I wanted the player to slide along the collider smoothly, but I didn’t want to miss collisions along the way, or move into/through another collider if sliding too far in a single frame.
So I in this first pass I also calculate a vector of the slide. If the slide vector isn’t zero, I rerun the system again using that slide vector as the input. I circlecast again in that direction, but this time just backing out on the first collision and setting my player to that position if I get a hit, and if not, move the entirety of the slide.

Hopefully that explanation wasn’t too lengthy and made some sense. Right away, you can see that if I were to handle the slide and the initial move based off the same circlecast, I could immediately increase performance 100%. The slowdowns on my system only occur when all the controllers are colliding and attempting to slide at once. The main performance hog in the Profiler is CircleCast, being called twice per FixedUpdate, per controller.
For the life of me I cannot figure out how, however, to avoid the second CircleCast. I’m casting from my current position to my new position based on the received input, as if I were unimpeded. the collisions returned determine where I would be on the first hit. I cannot then determine other colliders moved in a different direction along that collider without checking a second time, or is there another way?

Another thing I haven’t been able to figure out to test, is if there would be a performance gain if I spaced out these checks to more frames. Yes, I’m only running the moves 10 times a second, but each frame of those ten are where ALL the calls are going off. Would there be a performance gain if I utilized frames between fixed update calls? Would this work by queuing up how many moves I need to execute, and only executing X number of those move calls in FixedUpdate, then carrying over the rest to Update? These would then be carried out still before the next FixedUpdate so there isnt overlap.

Apologies for the lengthy post, if any of you find this interesting or have any input, or just want to take this and expand it for yourself, feel free. I would love some ideas or input from anyone more experienced than myself to improve, or some general improvements to the code itself. I’ve simply been trying to teach myself C# for a while as a hobby and I’m always open to critique.

If you do decide to try it out, just keep this in mind. It will work at 10 FixedUpdates per second, but to mitigate the input lag from applying those inputs on the FixedUpdate without missing any inputs, I had to hande them slightly differently than just storing the inputs in Update and applying them in FixedUpdate.

I have an extra Vector2 to temporarily store the collected input in Update seperate from the main input Vector2, and I only set that when the input isn’t zero. When the next FixedUpdate rolls around, I compare the two input Vectors, and if they aren’t both zero, I use the non-zero value. Otherwise I use zero and don’t call move.

This way, even if I press and release an axis key before FixedUpdate is called, the controller will still process that input anyway. This would likely feel bad at any lower of a FixedUpdate interval but its hard to notice any input lag this way. Also, since its using CircleCast instead of a fully custom collision detection, it supports all of the 2D colliders, hopefully even into the future when the new Tile Collider is added.

Anyway, thanks to all that suffered through that long ass post. I would love any and all help here, I can’t seem to take it much further.

EDIT:

So until this point I had been running all my tests in-editor, not really checking much besides the profiler to see what was costing the most, and just eyeing when the frame drops happened.

I decided to test out something I wrote in the above post, and use frames between the very low FixedUpdate frames to handle calling Move() on the controller, and also test running the application and checking the actual frames per second there to test.

By running around 100 move calls (maximum 200 circlecasts per frame) in the first fixed update call, and then 100 Move calls each frame afterwards, I was able to have over 1000 controllers accurately checking collisions in editor without dropping below 60 fps. Outside of the editor in a build, I got over 2500 controllers moving without dropping below 60 fps.

It seems this is the answer and no optimization of the controller itself is needed! If I need to, I still havent implemented the NonAlloc version of circlecast which I’ll do next.

On the server, only players will use this controller, or need to, so I could use the regular rigidbody2d for any NPC’s or enemies and just send positions to the clients if that is more optimized than this. Also, this load would only occur if ALL players on the server were moving and colliding with something within 0.2 seconds. Even if all players are moving at the same time, the load is greatly decreased if they arent all colliding with something that FixedUpdate as well.

I hope this helps someone if anyone comes across needing this! Thanks to all for the suggestions.

This video has lots of tips (for general unity script optimizations)

Thank you! I loved Inside and that was an interesting presentation.

Sorry, I’m at work and not really able to fully read and digest all of this, but off the top take a look at Physics2D.CircleCastNonAlloc. If you find that the number of colliders returned (or needed, if for instance you only care about the first 2 or 3) is fairly consistent, you can use this function. Anything that doesn’t fit in the supplied array will be discarded. The benefit of this is that the initial array never needs to be resized (which truly means recreated every frame) and can be reused. You’ll have better memory and garbage collection performance.

Thank you! This is exactly the kind of advice i was looking for. Honestly, i feel i could get away with a regular CircleCast returning only the first hit collider, but i have no way of culling out triggers and my own collider without a mess of layers. Thank you for pointing this out.

You’re welcome.

In your project settings, physics2D, uncheck “Queries Hit Triggers” and “Queries Start in Colliders” and that should filter both of those things for you.

I could be wrong, but I believe I tried this, and it worked as you’d expect for raycasts but not for CircleCasts, it always returned my own collider. The other issue is I couldnt support trigger colliders at all.

Ah, sorry of course. I forgot we were talking about circle casts and not rays.

No problem! I haven’t switched to the Non-alloc version of CircleCast yet, but tonight I tested spacing out the CircleCast calls over the Update frames called between the FixedUpdate calls (at 60 fps, this is 6 extra frames). I edited the post above if you’d like to read it more in-depth but I was able to get 2500 controllers moving around and checking collisions on my laptop at 60 fps.

Thanks for your suggestions and help!