Performance issues with RTS game - constantly searching for enemy units

I am making an RTS game where I want roughly 1000 unit maximum.

I am encountering a performance issue, I want to be able to detect enemies within the detectionRadius. Currently I am doing it by looping the list of enemy objects and simply getting the distance between the unit and each enemy, if an enemy is within range → Attack!

This worked fine with < 100 units, but once I start getting to 300-400, it’s taking up 16ms per frame, taking up 53% of CPU (editor taking rest).

Here is my code:

for (int i = 0; i < enemies.Count; i++)
            {
                Transform target = enemies[i].transform;

                float distance = Vector3.Distance(unit.transform.position, target.position);

                if (distance < unit.detectionRadius)
                {
                    unit.stateMachine.ChangeState(new MoveIntoAttackRangeState(unit, target));
                }
            }

I have tried using Unity - Scripting API: Vector3.sqrMagnitude instead of Vector3.distance but results were the same.

Is there some smarter way to manage this scenario? Is there any way around calling this each frame (except running it in a coroutine/skipping frames) on each unit? If there is no other way to do it, can I make my code more performant?

You do not need to search all units. Either split your map into areas and search only those who are in the same and neighbor map area or use unity collision triggers for view radius. Do not repeat search for all units. If you found what Unity A is in range for Unit B, then unit B is in range for unit A too.

Ok, I am very new to this, but how about setting up a circular collider on the unit, then setting the collider to be a trigger. When an enemy enters the collider, attack!

Are colliders more performant in this scenario? I read that they are not. Ill give it a try! This would introduce a second issue for me tho, since I have one detectionRadius, and one attackRadius, and I can only have one spherical collider.
It would also mean that I couldnt use the cylinder collider representing the body to detect hits anymore.

Great idea about only checking interaction for one unit and setting it on both :slight_smile:

Put your trigger areas onto child objects of your actor & that will solve your collision or trigger issue.

If you want to still check distances, do use sqrMagnitude it saves sqrt calls which can be slow when you have alot of checks.

Look into quadtree’s for partitioning your units to reduce how many you need to check.

Only update distance checks when an object moves only update them for what is overlapping in your quadtree.

(Please excuse my poor reading skills, I did not see that you mentioned sqrmag test results in your post.)
Have you tried using the less expensive Vector3.sqrMagnitude calculation instead of the Vector3.distance function?

I usually abuse the physics engine for this type of thing using sphere overlap which I outlined in an earlier post. There’s a lot of optimization already built into the physics engine that helps with these kinds of performance issues.

Your two detection / attack radius values can be accommodated by swapping out the range value and don’t actually need two colliders. Swap the greater / lesser of the two ranges/radius to implement “If you’re not detected then you’re not in range either so I don’t need to do two checks” kind of logic.

Sphere Colliders essentially have to do the same thing - take every pair of sphere colliders, check their distance. The thing is, Nvidia has spent thousands and thousands of hours on optimizing the physics engine, so it’s going to be a lot faster than whatever code you’re going to be writing to do the same thing.

So all of the advice for how to optimize the code that’s given here (split the map into chunks, use square distances, etc) will already have been covered.

BUT, it’s not free. Other things that are not related to the distance check can end up interacting with these colliders. For example, if other objects are checking for collision against them, then adding those colliders adds to the general physics cost of your game. It’s going to be cheaper than 16ms per frame, but it’s still bad.

The way to fix that is to put your distance check spheres on their own layer, and make sure that layer only collides with the things you’re checking for the distance to, by changing the layer collisions settings in the physics settings of your project.

2 Likes

Thanks, sounds like a good way to approach this is to have an OnTriggerEnter on only one type of unit, lets say we put it on the Allies, looking for enemies.

I can then attach a child to my Ally called “Radius”, and attach a Sphere Collider to this child.

I then make sure the Enemy is on Layer “Humanoid” and adjust Physics accordingly, i.e making sure my Radius collider can ONLY collide with humanoids.

I can then do something like this:

private void OnTriggerEnter(Collider enemy)
    {
        if(searching)
            // State = GetIntoAttackRange(ally)
           //  State = GetIntoAttackRange(enemy)
           //  collider.radius = attackRadius

        if(gettingIntoAttackRange)
           // State = Attack(ally)
           // Since attackRadius can be different between units, I cannot set the enemy to attack, but instead need to make this same check on the enemy.
    }

The above will set both Units to get into attack range, and when they are, they will start attacking. I think this might be a decent solution, feedback is appriciated!

Edit: It seems Unity cannot use 2 different layers on child/parent like this, if I set parent to “Humanoid” and Radius child to “HumanoidRadius”, and try to only detect the “Humanoid” layer, I still detect the Radius child as well. I.e it seems the parent layer is “master”.

Should I use one layer for enemy, and one for ally. Or is it OK to just use one “Humanoid” Layer and Tag them “Enemy” or “Ally”, then do a .CompareTag("TAG")? Or is that more expensive?

I changed my code to this and generally my gameplay performance went up quite a bit!

private void OnTriggerEnter(Collider other)
    {
        if (!other.CompareTag(TAGS.ALLY))
            return;

        if (unit.searching)
        {
            unit.stateMachine.ChangeState(new MoveIntoAttackRangeState(unit, other.transform));
        }
    }

Basically I use a Layer that only detects Humanoids and Default (need Default layer to detect things like projectiles). I then Tag them as “Enemy” or “Ally” and use that.

The problem now is the crazy spike I get when OnTriggerEnter gets called 100+ times (i.e 2 groups of opponents meeting and starting to fight):

5276244--528615--cpu1.jpg

Is there something more I could do to reduce this?
(The other % is due to drawing gizmos and not batching meshes yet).

If you’re willing to rewrite significant amount of code for performance improvement, you can learn to use DOTS. DOTS is a different way to write code that is drastically more efficient, particularly for large numbers of objects. It is harder to do, though, and as mentioned would involve rewriting quite a lot of code. But, you’d have incredible performance with tons and tons of units. (One of their DOTS demos got up to 100k units running at something like 30fps, as a point of reference)

1 Like

Yeah I’ve looked at it a lot but I simply don’t have the time to learn it at the moment =/. Since Im not using THAT many units, I thot I could get around it.

Is perhaps overlapsphere better for performance?

One thing you can do is to simply stop doing range checks as long as you have a valid enemy. That’d prevent all of that spike you’re seeing.

You can do this with overlap spheres, or by turning off the collider. If you go for something like that, I’d use an overlap sphere since it’s easier to do overlap spheres when you need to than it is to manage a collider being on or off.

Overlap spheres should (in theory) be more expensive if you do them every frame (since they can’t be batched by the physics engine), but if you do them every now and again it’s a lot less work to deal with. You’ll need to draw some gizmos to visualize your agent’s range, but that’s pretty eary.

If you need to check more often (eg. to find a better target), a possible optimization is time-slicing. Simply only allow a certain number of agents to check their range every frame. The game will look pretty much identical to the player the enemies checks a couple of times per second instead of every frame, but it’ll be a lot cheaper to do.

That’s a useful general purpose optimization that can be done for all kinds of things.

1 Like

What do you mean by “stop doing range checks” in this scenario? Since I’m not measuring distance anymore.
OnTriggerEnter is skipped if the unit has an enemy (I.e searching == false in code above). Or do you mean to turn off collider completely while I have an enemy? Would that stop OnTriggerEnter from taking up resources?

The biggest spike I see is when they actually collide, when they are not colliding it doesn’t use that many resources.

I use a coroutine that is executed every .2 seconds. However during debugging I run it in update.
I guess OnTriggerEnter is called every frame either way.

Yes and yes.

1 Like

Thanks man.

I’ll do some more testing but surely overalapsphere every .2 seconds must be better than calling OnTriggerEnter every frame.

Back to your original code, it sounds like you’re running that every frame. I’d just test switching it to once per second. At 60 FPS that alone would cut your CPU usage down from this code by roughly 98%. You probably wouldn’t notice much difference in gameplay, and you can play with the wait time to get a good balance.

Also a lot of other good suggestions in this thread.

private float timeNextCheck = 0f;
private float timeBetweenChecks = 1f;

void Update()
{
    if (Time.time >= timeNextCheck)
    {
        //do distance check here

        timeNextCheck = Time.time + timeBetweenChecks;
    }
}

Hi,

As I said, I already run in in a coroutine :slight_smile: Not in Update. I only run it in update during performance testing. I run my state machine every .2 seconds usually. It might be worth splitting it up a bit so that I dont do range detection from my state-machine, and thereby can run it at another interval that is even further apart :slight_smile:

For anybody in the future that might need advice on this:

I ended up isolating my enemies, allies, enemyprojectiles, allyprojectiles on seperate layers. Basically these are the only things in my games that exist in mass.

To search for opponents I use overlapSphere that only checks for 1 layer, run every .5 seconds. For my projectiles I use simple OnTriggerEnter, they also only collide with one layer.

I also use an object pool for the projectiles.

Basically 99% of physics costs are now gone, amazing!
Right now the Animator is taking up 40% of my cpu, and the rest is from the editor. Will def. have to look into making my animations more performant.

For those of you that like numbers, I went from 32 FPS to 112 :slight_smile:

3 Likes

This is a nice thread and I hope the lessons get encapsulated:

Lesson 1 - You can lean more on your GPU to use a sphere collider rather than using a distance calculation and comparing it to a float/int.

Lesson 2 - Double-Dip comparing: If you find the Meeple A is 450 game-units from Meeple B, you can write that value to both of their tables after only 1 comparison and not bother checking again for Meeple A when your loop gets around to Meeple B, cutting cost in half off the top

Lesson 3 - for RTS usage - target-finding for most of your Meeps, once they have a target, stop looking for new ones.

Lesson 4 - You don’t necessarily need to poll every frame!

2 Likes

The Animation component was twice as fast as the Animator. Last time I checked, the Animation component is still the fastest way to play animations in Unity.

Doing your own, custom stack in Playables is faster than the Animator, but not as fast as the Animation component.

1 Like