Need help optimizing distance calculations

Hi, I am currently trying to improve the performance of my 3d tower defense game. The main bottleneck for now is how I move all the enemies along the path (composed of waypoints). After profiling the game, I found that the movement script, more precisely the distance calculations are a huge hit in performances (30% sometimes).

Here is the entire code for the enemy movements :

using UnityEngine;
using System.Collections.Generic;

[RequireComponent(typeof(Enemy))]

public class EnemyMovement : MonoBehaviour
{
    private Transform target;
    private int waypointIndex = 0;
    public Vector3 offset;
    public float rightLeftOffset;

    public float turnSpeed;

    private Enemy enemy;

    public float percentDistanceMade;

    private Waypoints waypoints;


    void Start()
    {
        enemy = GetComponent<Enemy>();

        if(enemy.isFlying)
        {
            waypoints = MapData.airPaths[enemy.path-1];
        }
        else
        {
            waypoints = MapData.groundPaths[enemy.path-1];
        }
       
        //second waypoint because 0 is spawn
        target = waypoints.points[1];

        rightLeftOffset = Random.Range(-1.7f, 1.7f);
    }

    void FixedUpdate()
    {
        Movements();
    }

    void Movements()
    {
        // TestIfCloseToOtherZombie(); 
        //chopper la direction dans laquelle aller
        Vector3 dir = target.position - ((transform.position-offset) - (rightLeftOffset*transform.right));
        //déplacer l'enemy
        transform.Translate(dir.normalized * enemy.speed * Time.fixedDeltaTime, Space.World);
        Vector3 rotDir = new Vector3(dir.x, 0, dir.z);
        if(rotDir != Vector3.zero)
        {
            Quaternion lookRotation =  Quaternion.LookRotation(rotDir);
            transform.rotation = Quaternion.Lerp(transform.rotation, lookRotation, turnSpeed * Time.fixedDeltaTime);
        }

        //marge d'erreur pour éviter qu'il bug dans le waypoint (+ de speed = + de  marge sinon bug)
        if(Vector3.Distance(((transform.position-offset) - (rightLeftOffset*transform.right)), target.position) <= enemy.wayPointPrecision)
        {  
            GetNextWaypoint();
        }

        GetCurrentPosition();
    }

    private void GetNextWaypoint()
    {
        //vérifie si il y a un waypoint prochain, sinon c'est la fin du parcours
        if(waypointIndex >= waypoints.points.Length -1){
            EndPath();
            return;
        }

        waypointIndex++;
        target = waypoints.points[waypointIndex];
    }

    private void EndPath()
    {
        if(enemy.isBoss)
        {
            PlayerStats.lives = 0;
        }
        else
        {
            PlayerStats.lives--;
        }

        enemy.RemoveFromList();

        PlayerStats.enemyThatGotIntoTown++;

        Destroy(gameObject);
    }

    public void GetCurrentPosition()
    {
        float dist =0f;
        for    (int i = 0; i < waypointIndex-1; i++) // les waypoints précédents
        {
            dist += Vector3.Distance(waypoints.points[i].transform.position, waypoints.points[i+1].transform.position);
        }

        if(waypointIndex >= 1) //le waypoint acutel
        {
            dist += Vector3.Distance(waypoints.points[waypointIndex-1].transform.position, transform.position);
        }

        dist = (dist/waypoints.totalGroundDistance) * 100;
        percentDistanceMade = dist;
    }

    private void OnDrawGizmos()
    {
        Gizmos.DrawLine(transform.position,((transform.position-offset) - (rightLeftOffset*transform.right)));
    }

}

The part that consumes the most performance according to the profiler is GetCurrentPosition. The result of this function is also used by turrets to determine which enemies are the first ones to arrive (if the turret is set to attack the first for example). I don’t really know how I can improve this part of the code without loosing track of the individual position of each enemies, which is essential to the turrets.

Here is a pic of the map and the path enemies take :

Also I put all movements inside FixedUpdate because drops in fps caused the enemies to miss checkpoints and teleport but I know it is not the right thing to do…

Thanks for any help !

For starters, calling transform.position is a bit slow, as it’s better to cache the component of transform, since transform itself is equivalent to saying GetComponent<Transform>().

So caching that, like I do Transform trans; then in Start() trans = transform may seem out of the way, but is a micro optimization. And you will notice the difference when using hundreds of transform calls.

Another thing I did was round positions(you have to make your own Vector3 method), and give a similar variable like lastKnownPosition to check against knownPosition. This way, when the original setter of the position may be reading decimal values, the core checks you have determining aren’t reading every frame. They will for many frames still see that if (knownPosition == lastKnownPosition) equals true, that they get a break from thinking(that frame). Which results in many frames not needing to constantly think(or calculate), resulting in better performance.

Also I notice you are not using Object Pooling, if you plan to re-use enemies(or projectiles!), for multiple waves, it’s best to re-use them. Just disable their GameObject when dead, and re-enable it in place of Instantiation. I personally make my own form of Object Pooling, so I can’t speak to unity’s built-in method of it. I just like mine better because I made it, lol…

But the less code needs to think, even just 1 frame worth of a break, and especially not having to constantly iterate through Lists, and not needing GetComponent() as much as humanly possible? Can give awesome performance boosts! :slight_smile:

Instead of calculating the distance over and over again, you could just pre-compute the distances at the beginning and store them in the waypoints. The basic version would be to just store the length of individual edges but, since you have a clear start and end, you could just store the full distance along the path on each node.

You should only use FixedUpdate for calculations that interact with physics. Your overshoot problem is less likely to occur with FixedUpdate but the fixed time step can also skip if it’s too far behind, in which case you’ll see the same problem.

Instead of Translate I’d use Vector3.MoveTowards, this method makes sure you don’t overshoot the target.

I’m using this slightly modified version that also tells you when the target is reached, which is a bit more efficient thang doing an additional distance check:
code

/// <summary>
/// Version of Vector3.MoveTowards that reports if target was reached.
/// Based on https://github.com/Unity-Technologies/UnityCsReference/blob/50e3fc2c283dccfeee8a09de5f06c5c118c0835d/Runtime/Export/Math/Vector3.cs#L60C9-L60C61
/// </summary>
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public static Vector3 MoveTowards(Vector3 current, Vector3 target, float maxDistanceDelta, out bool reached)
{
    // avoid vector ops because current scripting backends are terrible at inlining
    float toVector_x = target.x - current.x;
    float toVector_y = target.y - current.y;
    float toVector_z = target.z - current.z;

    float sqdist = toVector_x * toVector_x + toVector_y * toVector_y + toVector_z * toVector_z;

    if (sqdist == 0 || (maxDistanceDelta >= 0 && sqdist <= maxDistanceDelta * maxDistanceDelta)) {
        reached = true;
        return target;
    }

    reached = false;
    var dist = (float)Math.Sqrt(sqdist);
    return new Vector3(current.x + toVector_x / dist * maxDistanceDelta,
        current.y + toVector_y / dist * maxDistanceDelta,
        current.z + toVector_z / dist * maxDistanceDelta);
}

You should aim for something like this:

    void Update()
    {
        if (counter>0)
        {
            transform.position+=velocity;
            counter-=1;
        }
        else
            GetVelocity();
    }

Thanks a lot for your reply, I didn’t know about the transform.position being so ineffective. I will implement it right away.

I have already heard about pooling but I am not really sure how I should try to implement it. Should I instantiate all enemies before a wave and then enable them when they spawn, should I spawn 100 of them and enable / disable them when I need or should I instantiate them when there is not enough of them but keep them for the next waves ? I don’t really know how unity handles loads of disabled gameobjects.

Thanks a lot for your reply. I didn’t know about this method and will take a look.

Have you considered using ECS+Burst+Jobs?

Check at 19:31 for the speed improvements.

Or just the job system (with burst). There are jobs that can modify the transform of each transform in an array

With either the basic MoveTowards or your version of it, enemies overshoot waypoints and try to go back and so on when fps are low (I changed back to update method just to try). Enemies will rotate indefinitely on waypoints :9196106--1282046--Sans titre.jpg

  void Update()
    {
        Movements();
    }

    void Movements()
    {
        //chopper la direction dans laquelle aller
        Vector3 dir = target.position - ((trans.position-offset) - (rightLeftOffset*trans.right));

        //déplacer l'enemy
        // trans.Translate(dir.normalized * enemy.speed * Time.fixedDeltaTime, Space.World);
        trans.position = Vector3.MoveTowards(trans.position, (target.position + offset) + (rightLeftOffset*trans.right), enemy.speed * Time.deltaTime);

        Vector3 rotDir = new Vector3(dir.x, 0, dir.z);
        if(rotDir != Vector3.zero)
        {
            Quaternion lookRotation =  Quaternion.LookRotation(rotDir);
            trans.rotation = Quaternion.Lerp(trans.rotation, lookRotation, turnSpeed * Time.deltaTime);
        }

        // if(reachedTarget)
        // {
        //     GetNextWaypoint();
        // }

        //marge d'erreur pour éviter qu'il bug dans le waypoint (+ de speed = + de  marge sinon bug)
        if(Vector3.Distance(((trans.position-offset) - (rightLeftOffset*trans.right)), target.position) <= enemy.wayPointPrecision)
        {
            GetNextWaypoint();
        }

        GetCurrentPosition();
    }

EDIT ::

It is doing that because my calculation of the left-right offset (which is used to make zombies spread on the path and not all going the exact same route) is slower to rotate the enemy and the zombie cannot pass the checkpoint because he is turning and the point that validates the ckeckpoint is not precisely at the right spot. I currently don’t know how to fix that without loosing the feature.

It is recommended to have a number(max) before hand, and Instantiate while the map is loading, turn them all “off”, and only cycle through their list, find one in the list that is if (!gameObject.activeSelf) and set it’s active to true, and in the same frame give new position, and/or rotation/velocity. It’s particular void OnEnable() should be where you put things like health = maxHealth;, so when it’s “re-made” it starts fresh with variables needed for it’s lifeTime.

It could be up for debate just using Unity’s built-in Object Pooling methods, but personally I had many issues with it, and it felt too far out of the way, for something I found was so simple. An example of my structure:

public class Master : MonoBehaviour // mono here since I use Inheritance
{
    public static List<Projectile> allBullets = new List<Projectile>();
    public static List<Projectile> allPlasma = new List<Projectile>();
    public static List<Projectile> allLasers = new List<Projectile>();
  
    public Projectile GetProjectileFromList(List<Projectile> list, Vector3 position, GameObject prefab)
    {
        if (list.Count > 0)
        {
            for (int i = 0; i < list.Count; i++)
            {
                if (!list[i].gameObject.activeSelf)
                {
                    list[i].trans.position = position;
                    list[i].gameObject.SetActive(true);
                    return list[i];
                }
            }
        }
        Instantiate(prefab, position, Quaternion.identity);
        return list[list.Count - 1];
    }
}
public class Projectile : Master // or other grouped parent
{
    // variables used by all projectile types
    // public functions used by each type(groups)
    // NO Awake(), Start(), Update(), etc...
}
public class Bullet : Projectile
{
    void Awake() // needed for list proper calculations
    {
        allBullets.Add(this);
        // set base damage, lifeTime, knockBack, etc...
    }

    // if hits enemy = gameObject.SetActive(false);
  
    void OnEnable() // or OnDisable()
    {
        lifeTime = 0;
        damage = baseDamage;
        // other resets, etc...
    }      
}
public class Gun : Master // or other grouped parent
{
    void Shoot()
    {
        Bullet bullet = (Bullet)GetProjectileFromList(
            allBullets, shootPos, bulletPrefab);
        bullet.rotation = shootDirection;
        bullet.shootSpeed = gunShootSpeed;
        // any other changes for bullet, etc...
    }
}

That’s basically how my tower defense game runs. But In my particular “GetFromList” you’ll see that I actually Instantiate if the original List shows no objects being free to pull from List. I really haven’t noticed any real frame loss from doing it that way, as I super restrict a lot of things from thinking all the time. But the main gain from object pooling is not having to constantly [Instantiate, Destroy, Garabage Collection, etc…].

But if you know you’ll only want a max of 100 of a type of enemy, or say no more than 1000 bullets, you could just use an array and Instantiate all of them at “level load” phase of the game, and just cycle through that.

I would argue my method might be hard to understand, or to implement, so just take it with a grain of salt. :slight_smile:

Another suggestion is to remove the script from all your enemies and make an EnemyManager.cs class, which calls

Update(){
foreach(Transform transform in myEnemiesArray){
// Update each transforms position.
}
}```

From there it should be easy to change it to a NativeArray and Jobify it if necessary, but this should speed it up a lot aswell.
However, looking at your screenshots, there arent that many enemies? Are you sure its not just your potato and it will run fine on normal/higher end systems? (Just asking)

Also GetCurrentPosition() is a method you use to see how far along the enemy is on the path?
An easier way would be to calculate ```pathTotalDistance```, (only once at startup)
Then everytime you move an enemy, you simply add the distance it has just moved to its ```totalMovedDistance```..
Then if you want to know its percentage along the path you simply do
```percent = totalMovedDistance / pathTotalDistance; ```.
Your percentage will be between 0 and 1, so multiply it by 100 to get a real percentage, but its not necessary, in your code you should use the 0 to 1 value anyway.

Find and read the Unity blog post about optimising the hierarchy.

Calls to .transform have been better than a GetComponent for a while now, but the cost of reading and writing .position and other properties depends on how your hierarchy is set up, and what other conponents parent/child objects have. (Note that those things look like variables, but many are function calls with potentially expensive side effects.)

The cost of calculating a distance and moving along a path likely has nothing to do with the actual calculations you are doing.

So you say it is better to have one script to move all my enemies rather than one script per enemy ? Also yes there is a potentially a lot of enemies sometimes because I made an endless mode which increases enemies count each wave and that is why I wanted to optimise things because after 50-100 enemies the game is at 20fps.

Is there a reliable way I could measure impact on performance to compare between methods of movement other than looking at fps ?

Thanks a lot for your response.

Looking at fps, whether with “stats” tab in the editor, or your own homemade declaration of fps(which I do) is a good way to tell. My game setups I usually limit to 60fps, so by looking at CPU milliseconds, I know that “performant” running methods should be keeping me around 16-17ms(while in Editor).

Now the Editor itself does use up performance, as you’ll notice when building a standalone the frame loss is way less than you experienced while Editor testing. Which is recommended to use Unity profiler on standalone builds, but I occasionally do use the profiler while still in Editor(But ignoring editor stats vs player stats).

But in my one game(3D), I can have up to 400 peons, who use complex seeing/thinking/doing, from being hungry to bored to interested in finding a mate(4000 lines of code), and can run 55-60fps while in the editor. So you should be able to shoot past 100 semi-AI-like entities no problem.

Now however, that was my older code when I went on a witch-hunt to never use GetComponent() except for in Awake/Start(), so I used a lot of around-the-bush methods, to which I found with recent benchmarks, GetComponent() is actually faster(now) than it was before. So I have yet to go back and fix my “performant” methods, and see how much more I can push.

Thought I had a better saved vid, but only found one of my older tests:

https://www.youtube.com/watch?v=iTjOp-ChGIk

Just to give an idea of how complex the game I mentioned was. So it should be very possible to get to +100 enemies. :sunglasses:

Yes, if each enemy has its own script it will slow down your performance a ton!
If you have 1 manager script with an array, that loops through it will be much better :wink:

You should look into Structs aswell, and implement a Struct instead of a Class for your enemies.
This way the enemy will not be a monobehaviour/component so you cant drag drop it in the inspector.
Instead an enemy will be a Struct, just like a Vector3 for example, pure code:

public struct Fish
{
    public enum FishState { Idle, Moving }

    public Transform trans;

    public FishState fishState;
    public float idleTimer;
    public float maxIdleTime;

    public Vector3 departure;
    public Vector3 destination;
    public float lerpTime;
    public float speed;


    public float waterHeight;

    public Fish(Transform tran)
    {
        trans = tran;

        fishState = FishState.Idle;
        idleTimer = 1f;
        maxIdleTime = 1f;

        departure = destination = trans.position;
        lerpTime = 1f;
        speed = 1f;

        waterHeight = trans.position.y;

        trans.GetComponent<Collider>().isTrigger = true;

    }

FishManager.cs:

    void Awake()
    {
        SpawnFishes();
    }

    void SpawnFishes()
    {
        fishList = new();  // same as new List<Fish>();
        for (int i = 0; i < spawnAmount; i++)
        {
            Vector3 randomPosition = territoryCollider.GetRandomPoint();
            GameObject fishObj = Instantiate(fishPrefab,
                                             randomPosition,
                                             Quaternion.identity);

            Fish fish = new Fish(fishObj.transform);
            fishList.Add(fish);
        }

        fishCount = spawnAmount;
    }

    void FixedUpdate()
    {
        for (int i = 0; i < fishList.Count; i++)
        {
            Fish fish = fishList[i];

            if (fish.fishState == Fish.FishState.Idle)
            {
                if (fish.idleTimer < fish.maxIdleTime)
                {
                    fish.idleTimer += Time.deltaTime;
                    Debug.Log("fish " + i + " idleTimer: " + fish.idleTimer);
                }
                else
                {
                    fish.fishState = Fish.FishState.Moving;

                    fish.destination = territoryCollider.GetRandomPoint();
                    fish.destination.y = fish.waterHeight;

                    fish.lerpTime = 0f;
                    fish.speed = UnityEngine.Random.Range(0.014f, MaxSpeed);
                    Debug.Log("fish " + i + " started moving at speed: " + fish.speed);
                }
            }
            else if (fish.fishState == Fish.FishState.Moving)
            {
                Debug.Log("fish " + i + " is moving to his destination");

                if (fish.trans.position == fish.destination)
                {
                    fish.fishState = Fish.FishState.Idle;
                    fish.idleTimer = 0f;
                    fish.maxIdleTime = UnityEngine.Random.Range(4f, 8f);
                }
                fish.trans.position = Vector3.MoveTowards(fish.trans.position, fish.destination, fish.speed);
            }
            fishList[i] = fish;
        }
    }

Make sure to remove all Debug statements from your code when youre done debugging aswell, because it can cause performance issues.

I would second argue that, lol, as I mentioned I have 400 peons each with their own(4000 line) script, each moving themselves according to their whims or responsibilities as a worker.

Now I’ll argue with myself.. If I ever learned how to use structs, or even Unity DOTS methods, I’m sure I could push closer to 1000 peons.. But I’m just stuck in my own ways :frowning:

Which reminds me, your collision method might be causing a huge performance loss. What does your “bullet hit enemy” snippet look like? @joxthebest314

While I think a lot of the general optimization advice in this thread is sound, a lot of it is also way out of proportion for the problem at hand. 100 enemies are not very many, nothing you should need to bust out the job system for. Basic optimization should easily make that work on the main thread. You should first focus on avoiding doing work multiple times, doing the work more efficiently and avoiding doing work in the first place.

If you say you want to have 1000s of enemies in the end or want to have very complex calculations for each enemy, only then jobifying/DOTS/DoD becomes essential.

I’d try to separate the calculations. First calculate the position along the path, straight between waypoints, making sure this works properly. Then add the offset only as a second step, adding it on top of the “pure” position along the path. This allows you debug the path following on its own by disabling the offset and try out different offset behaviours more easily, without disturbing the underlying path movement.

I just tested this with 4 struct and 500 struct fishes, 0 FPS drop on my end.
9197789--1282529--500FishTest.png

Now changing the list to a NativeArray and using Jobs woul be trivial, but should increase performance even more if you need like 5k enemies…