Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Feedback Need help optimizing distance calculations

Discussion in 'Scripting' started by joxthebest314, Aug 4, 2023.

  1. joxthebest314

    joxthebest314

    Joined:
    Mar 31, 2020
    Posts:
    95
    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 :

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3.  
    4. [RequireComponent(typeof(Enemy))]
    5.  
    6. public class EnemyMovement : MonoBehaviour
    7. {
    8.     private Transform target;
    9.     private int waypointIndex = 0;
    10.     public Vector3 offset;
    11.     public float rightLeftOffset;
    12.  
    13.     public float turnSpeed;
    14.  
    15.     private Enemy enemy;
    16.  
    17.     public float percentDistanceMade;
    18.  
    19.     private Waypoints waypoints;
    20.  
    21.  
    22.     void Start()
    23.     {
    24.         enemy = GetComponent<Enemy>();
    25.  
    26.         if(enemy.isFlying)
    27.         {
    28.             waypoints = MapData.airPaths[enemy.path-1];
    29.         }
    30.         else
    31.         {
    32.             waypoints = MapData.groundPaths[enemy.path-1];
    33.         }
    34.        
    35.         //second waypoint because 0 is spawn
    36.         target = waypoints.points[1];
    37.  
    38.         rightLeftOffset = Random.Range(-1.7f, 1.7f);
    39.     }
    40.  
    41.     void FixedUpdate()
    42.     {
    43.         Movements();
    44.     }
    45.  
    46.     void Movements()
    47.     {
    48.         // TestIfCloseToOtherZombie();
    49.         //chopper la direction dans laquelle aller
    50.         Vector3 dir = target.position - ((transform.position-offset) - (rightLeftOffset*transform.right));
    51.         //déplacer l'enemy
    52.         transform.Translate(dir.normalized * enemy.speed * Time.fixedDeltaTime, Space.World);
    53.         Vector3 rotDir = new Vector3(dir.x, 0, dir.z);
    54.         if(rotDir != Vector3.zero)
    55.         {
    56.             Quaternion lookRotation =  Quaternion.LookRotation(rotDir);
    57.             transform.rotation = Quaternion.Lerp(transform.rotation, lookRotation, turnSpeed * Time.fixedDeltaTime);
    58.         }
    59.  
    60.         //marge d'erreur pour éviter qu'il bug dans le waypoint (+ de speed = + de  marge sinon bug)
    61.         if(Vector3.Distance(((transform.position-offset) - (rightLeftOffset*transform.right)), target.position) <= enemy.wayPointPrecision)
    62.         {  
    63.             GetNextWaypoint();
    64.         }
    65.  
    66.         GetCurrentPosition();
    67.     }
    68.  
    69.     private void GetNextWaypoint()
    70.     {
    71.         //vérifie si il y a un waypoint prochain, sinon c'est la fin du parcours
    72.         if(waypointIndex >= waypoints.points.Length -1){
    73.             EndPath();
    74.             return;
    75.         }
    76.  
    77.         waypointIndex++;
    78.         target = waypoints.points[waypointIndex];
    79.     }
    80.  
    81.     private void EndPath()
    82.     {
    83.         if(enemy.isBoss)
    84.         {
    85.             PlayerStats.lives = 0;
    86.         }
    87.         else
    88.         {
    89.             PlayerStats.lives--;
    90.         }
    91.  
    92.         enemy.RemoveFromList();
    93.  
    94.         PlayerStats.enemyThatGotIntoTown++;
    95.  
    96.         Destroy(gameObject);
    97.     }
    98.  
    99.     public void GetCurrentPosition()
    100.     {
    101.         float dist =0f;
    102.         for    (int i = 0; i < waypointIndex-1; i++) // les waypoints précédents
    103.         {
    104.             dist += Vector3.Distance(waypoints.points[i].transform.position, waypoints.points[i+1].transform.position);
    105.         }
    106.  
    107.         if(waypointIndex >= 1) //le waypoint acutel
    108.         {
    109.             dist += Vector3.Distance(waypoints.points[waypointIndex-1].transform.position, transform.position);
    110.         }
    111.  
    112.         dist = (dist/waypoints.totalGroundDistance) * 100;
    113.         percentDistanceMade = dist;
    114.     }
    115.  
    116.     private void OnDrawGizmos()
    117.     {
    118.         Gizmos.DrawLine(transform.position,((transform.position-offset) - (rightLeftOffset*transform.right)));
    119.     }
    120.  
    121. }
    122.  
    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 :
    Screenshot_1-4-2023_12-30-58.png

    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 !
     
  2. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    For starters, calling
    Code (CSharp):
    1. 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! :)
     
    Last edited: Aug 4, 2023
    ijmmai and joxthebest314 like this.
  3. Adrian

    Adrian

    Joined:
    Apr 5, 2008
    Posts:
    1,051
    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 (CSharp):
    1.  
    2. /// <summary>
    3. /// Version of Vector3.MoveTowards that reports if target was reached.
    4. /// Based on https://github.com/Unity-Technologies/UnityCsReference/blob/50e3fc2c283dccfeee8a09de5f06c5c118c0835d/Runtime/Export/Math/Vector3.cs#L60C9-L60C61
    5. /// </summary>
    6. [MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
    7. public static Vector3 MoveTowards(Vector3 current, Vector3 target, float maxDistanceDelta, out bool reached)
    8. {
    9.     // avoid vector ops because current scripting backends are terrible at inlining
    10.     float toVector_x = target.x - current.x;
    11.     float toVector_y = target.y - current.y;
    12.     float toVector_z = target.z - current.z;
    13.  
    14.     float sqdist = toVector_x * toVector_x + toVector_y * toVector_y + toVector_z * toVector_z;
    15.  
    16.     if (sqdist == 0 || (maxDistanceDelta >= 0 && sqdist <= maxDistanceDelta * maxDistanceDelta)) {
    17.         reached = true;
    18.         return target;
    19.     }
    20.  
    21.     reached = false;
    22.     var dist = (float)Math.Sqrt(sqdist);
    23.     return new Vector3(current.x + toVector_x / dist * maxDistanceDelta,
    24.         current.y + toVector_y / dist * maxDistanceDelta,
    25.         current.z + toVector_z / dist * maxDistanceDelta);
    26. }
    27.  
     
  4. zulo3d

    zulo3d

    Joined:
    Feb 18, 2023
    Posts:
    546
    You should aim for something like this:

    Code (CSharp):
    1.     void Update()
    2.     {
    3.         if (counter>0)
    4.         {
    5.             transform.position+=velocity;
    6.             counter-=1;
    7.         }
    8.         else
    9.             GetVelocity();
    10.     }
     
    Last edited: Aug 7, 2023
  5. joxthebest314

    joxthebest314

    Joined:
    Mar 31, 2020
    Posts:
    95
    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.
     
  6. joxthebest314

    joxthebest314

    Joined:
    Mar 31, 2020
    Posts:
    95

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

    dlorre

    Joined:
    Apr 12, 2020
    Posts:
    700
    Have you considered using ECS+Burst+Jobs?



    Check at 19:31 for the speed improvements.
     
    Last edited: Aug 5, 2023
  8. DevDunk

    DevDunk

    Joined:
    Feb 13, 2020
    Posts:
    4,396
    Or just the job system (with burst). There are jobs that can modify the transform of each transform in an array
     
  9. joxthebest314

    joxthebest314

    Joined:
    Mar 31, 2020
    Posts:
    95

    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 : Sans titre.png

    Code (CSharp):
    1.   void Update()
    2.     {
    3.         Movements();
    4.     }
    5.  
    6.     void Movements()
    7.     {
    8.         //chopper la direction dans laquelle aller
    9.         Vector3 dir = target.position - ((trans.position-offset) - (rightLeftOffset*trans.right));
    10.  
    11.         //déplacer l'enemy
    12.         // trans.Translate(dir.normalized * enemy.speed * Time.fixedDeltaTime, Space.World);
    13.         trans.position = Vector3.MoveTowards(trans.position, (target.position + offset) + (rightLeftOffset*trans.right), enemy.speed * Time.deltaTime);
    14.  
    15.         Vector3 rotDir = new Vector3(dir.x, 0, dir.z);
    16.         if(rotDir != Vector3.zero)
    17.         {
    18.             Quaternion lookRotation =  Quaternion.LookRotation(rotDir);
    19.             trans.rotation = Quaternion.Lerp(trans.rotation, lookRotation, turnSpeed * Time.deltaTime);
    20.         }
    21.  
    22.         // if(reachedTarget)
    23.         // {
    24.         //     GetNextWaypoint();
    25.         // }
    26.  
    27.         //marge d'erreur pour éviter qu'il bug dans le waypoint (+ de speed = + de  marge sinon bug)
    28.         if(Vector3.Distance(((trans.position-offset) - (rightLeftOffset*trans.right)), target.position) <= enemy.wayPointPrecision)
    29.         {
    30.             GetNextWaypoint();
    31.         }
    32.  
    33.         GetCurrentPosition();
    34.     }
    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.
     
    Last edited: Aug 5, 2023
  10. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    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:
    Code (CSharp):
    1. public class Master : MonoBehaviour // mono here since I use Inheritance
    2. {
    3.     public static List<Projectile> allBullets = new List<Projectile>();
    4.     public static List<Projectile> allPlasma = new List<Projectile>();
    5.     public static List<Projectile> allLasers = new List<Projectile>();
    6.  
    7.     public Projectile GetProjectileFromList(List<Projectile> list, Vector3 position, GameObject prefab)
    8.     {
    9.         if (list.Count > 0)
    10.         {
    11.             for (int i = 0; i < list.Count; i++)
    12.             {
    13.                 if (!list[i].gameObject.activeSelf)
    14.                 {
    15.                     list[i].trans.position = position;
    16.                     list[i].gameObject.SetActive(true);
    17.                     return list[i];
    18.                 }
    19.             }
    20.         }
    21.         Instantiate(prefab, position, Quaternion.identity);
    22.         return list[list.Count - 1];
    23.     }
    24. }
    Code (CSharp):
    1. public class Projectile : Master // or other grouped parent
    2. {
    3.     // variables used by all projectile types
    4.     // public functions used by each type(groups)
    5.     // NO Awake(), Start(), Update(), etc...
    6. }
    Code (CSharp):
    1. public class Bullet : Projectile
    2. {
    3.     void Awake() // needed for list proper calculations
    4.     {
    5.         allBullets.Add(this);
    6.         // set base damage, lifeTime, knockBack, etc...
    7.     }
    8.  
    9.     // if hits enemy = gameObject.SetActive(false);
    10.  
    11.     void OnEnable() // or OnDisable()
    12.     {
    13.         lifeTime = 0;
    14.         damage = baseDamage;
    15.         // other resets, etc...
    16.     }      
    17. }
    Code (CSharp):
    1. public class Gun : Master // or other grouped parent
    2. {
    3.     void Shoot()
    4.     {
    5.         Bullet bullet = (Bullet)GetProjectileFromList(
    6.             allBullets, shootPos, bulletPrefab);
    7.         bullet.rotation = shootDirection;
    8.         bullet.shootSpeed = gunShootSpeed;
    9.         // any other changes for bullet, etc...
    10.     }
    11. }
    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. :)
     
    joxthebest314 likes this.
  11. zevonbiebelbrott

    zevonbiebelbrott

    Joined:
    Feb 14, 2021
    Posts:
    102
    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.
     
    Last edited: Aug 5, 2023
    joxthebest314 likes this.
  12. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,509
    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.
     
    Ryiah and joxthebest314 like this.
  13. joxthebest314

    joxthebest314

    Joined:
    Mar 31, 2020
    Posts:
    95

    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.
     
  14. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    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:


    Just to give an idea of how complex the game I mentioned was. So it should be very possible to get to +100 enemies. :cool:
     
    joxthebest314 likes this.
  15. zevonbiebelbrott

    zevonbiebelbrott

    Joined:
    Feb 14, 2021
    Posts:
    102
    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 ;)

    You should look into
    Struct
    s 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:

    Code (CSharp):
    1.  
    2. public struct Fish
    3. {
    4.     public enum FishState { Idle, Moving }
    5.  
    6.     public Transform trans;
    7.  
    8.     public FishState fishState;
    9.     public float idleTimer;
    10.     public float maxIdleTime;
    11.  
    12.     public Vector3 departure;
    13.     public Vector3 destination;
    14.     public float lerpTime;
    15.     public float speed;
    16.  
    17.  
    18.     public float waterHeight;
    19.  
    20.     public Fish(Transform tran)
    21.     {
    22.         trans = tran;
    23.  
    24.         fishState = FishState.Idle;
    25.         idleTimer = 1f;
    26.         maxIdleTime = 1f;
    27.  
    28.         departure = destination = trans.position;
    29.         lerpTime = 1f;
    30.         speed = 1f;
    31.  
    32.         waterHeight = trans.position.y;
    33.  
    34.         trans.GetComponent<Collider>().isTrigger = true;
    35.  
    36.     }
    37.  
    38.  
    39.  
    FishManager.cs:

    Code (CSharp):
    1.  
    2.     void Awake()
    3.     {
    4.         SpawnFishes();
    5.     }
    6.  
    7.     void SpawnFishes()
    8.     {
    9.         fishList = new();  // same as new List<Fish>();
    10.         for (int i = 0; i < spawnAmount; i++)
    11.         {
    12.             Vector3 randomPosition = territoryCollider.GetRandomPoint();
    13.             GameObject fishObj = Instantiate(fishPrefab,
    14.                                              randomPosition,
    15.                                              Quaternion.identity);
    16.  
    17.             Fish fish = new Fish(fishObj.transform);
    18.             fishList.Add(fish);
    19.         }
    20.  
    21.         fishCount = spawnAmount;
    22.     }
    23.  
    24.     void FixedUpdate()
    25.     {
    26.         for (int i = 0; i < fishList.Count; i++)
    27.         {
    28.             Fish fish = fishList[i];
    29.  
    30.             if (fish.fishState == Fish.FishState.Idle)
    31.             {
    32.                 if (fish.idleTimer < fish.maxIdleTime)
    33.                 {
    34.                     fish.idleTimer += Time.deltaTime;
    35.                     Debug.Log("fish " + i + " idleTimer: " + fish.idleTimer);
    36.                 }
    37.                 else
    38.                 {
    39.                     fish.fishState = Fish.FishState.Moving;
    40.  
    41.                     fish.destination = territoryCollider.GetRandomPoint();
    42.                     fish.destination.y = fish.waterHeight;
    43.  
    44.                     fish.lerpTime = 0f;
    45.                     fish.speed = UnityEngine.Random.Range(0.014f, MaxSpeed);
    46.                     Debug.Log("fish " + i + " started moving at speed: " + fish.speed);
    47.                 }
    48.             }
    49.             else if (fish.fishState == Fish.FishState.Moving)
    50.             {
    51.                 Debug.Log("fish " + i + " is moving to his destination");
    52.  
    53.                 if (fish.trans.position == fish.destination)
    54.                 {
    55.                     fish.fishState = Fish.FishState.Idle;
    56.                     fish.idleTimer = 0f;
    57.                     fish.maxIdleTime = UnityEngine.Random.Range(4f, 8f);
    58.                 }
    59.                 fish.trans.position = Vector3.MoveTowards(fish.trans.position, fish.destination, fish.speed);
    60.             }
    61.             fishList[i] = fish;
    62.         }
    63.     }
    64.  
    Make sure to remove all Debug statements from your code when youre done debugging aswell, because it can cause performance issues.
     
    Last edited: Aug 6, 2023
  16. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    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.
     
  17. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    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 :(
     
    zevonbiebelbrott likes this.
  18. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    Which reminds me, your collision method might be causing a huge performance loss. What does your "bullet hit enemy" snippet look like? @joxthebest314
     
  19. Adrian

    Adrian

    Joined:
    Apr 5, 2008
    Posts:
    1,051
    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.
     
    wideeyenow_unity likes this.
  20. zevonbiebelbrott

    zevonbiebelbrott

    Joined:
    Feb 14, 2021
    Posts:
    102
    I just tested this with 4 struct and 500 struct fishes, 0 FPS drop on my end.
    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...
     
    wideeyenow_unity likes this.
  21. zevonbiebelbrott

    zevonbiebelbrott

    Joined:
    Feb 14, 2021
    Posts:
    102
    400 is not a lot ahaha, with Dots you could maybe get 4K, depending on what calculations the peons are doing...
    I think just changing to structs would atleast get you to 1500 peons, but as Ive just noticed with my fishes, I can easily do 1500 fishes, and still no framerate drop, that is untill you look at the fishes and the camera renders them.. it means the camera rendering 1500 objects is much slower than fish transform updates... btw, my fishes are quads, so again optimized af lol

    Also im using a shared instanced material, so i had 1495 saved by batching out of 1500. So no, even rendering and draw calls are optimized, but still they are slow it seems...
     
  22. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    I have the same problem all the time, the "Camera.Renderer" is always using massive ms for "Transparent.geometry" or something weird like that. I've tried googling what that was all about, and for the most part, every post said "just deal with it".. lol

    But yeah, it's not even the script calls that hurt me, it's always the camera.
     
  23. icauroboros

    icauroboros

    Joined:
    Apr 30, 2021
    Posts:
    99
    Hey I just upload my benchmark to GitHub, it can be useful
    https://github.com/burak-efe/ECS_Rotating_Cubes_Benchmark
    There is some tips about Unity performance:
    • There is mainly two type of performance issue on CPU side.
    • 1) Latency: CPU waits accessing elements from ram or cache,
    • 2 ) ALU (math): CPU doing some real work.
    • If you use Unity engine as standard (No DOTS) you are probably Latency bound and also your ALU operations unnecessarily slow (due to mono).
    • To get faster ALU (math):
    • use il2cpp (fast). (Editor always compiles code to mono if not burst compiled, do not trust on editor mode profiling)
    • move your math heavy operations to burst compiled code (faster)
    • move your math heavy operations to burst compiled Parallel Jobs (fastest)
    • To Lowering Latency:
    • instead of having many monobehavior and Update(), create a manager and Update() all object in it.
    • Use struct instead of classes (i think its not worth it)
    • Lower nesting of hierarchy.
    • Use ECS(fastest, hardest)
     
    zevonbiebelbrott likes this.
  24. zevonbiebelbrott

    zevonbiebelbrott

    Joined:
    Feb 14, 2021
    Posts:
    102
    I agree with everything you said, except this. Simply switching from classes to structs and a manager class increases performance by an order of magnitude for me.

    However after having said all that, for this specific question, I think the only needed change to the code is changing the GetPosition() method, to what I described a few posts above. Going from 1000s of Sqrt calls per frame to 0. And since he said that code is where the bottleneck is...
     
    icauroboros likes this.
  25. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,128
    Speaking of which there is a method available that has higher performance if you need to write both the position and rotation.

    https://docs.unity3d.com/ScriptReference/Transform.SetPositionAndRotation.html
     
    angrypenguin likes this.
  26. icauroboros

    icauroboros

    Joined:
    Apr 30, 2021
    Posts:
    99
    I did not recommend it because it makes the game harder to maintain and extend in my experience.
     
  27. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,128
    DOTS requires a very different mentality than MonoBehaviours. I spent a few days picking it up and I didn't see much of a difference between the struct and class approaches aside from a little more ease of use and a large performance difference.
     
  28. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,509
    For first-pass, broad investigation I do this too. But then I move on to the Profiler pretty much straight away, because it's a much better tool for the job. If you don't use it then you're making life unnecessarily hard for yourself. When you're only looking at an aggregate data source you're introducing a huge amount of noise which can easily obfuscate important information.

    Also, it's important to realise that any data that isn't collected from a build on the target device is pretty much invalid. Is is entirely possible for a change to be faster on one machine and slower on another, so testing anywhere other than your target devices is error prone at best.

    There are other tools worth knowing about, too:
    - If you're trying to measure the performance of different parts of a method, then System.Diagnostics.Stopwatch is great. This is really useful if you've identified a script or even a method which you know is taking a while, but you don't know which part of the algorithm is taking up the time.

    - More specific profiling tools. Many vendors provide tools to measure how software performs with their specific hardware and/or software. So when the info you get from generic tools isn't detailed enough, or when you need to make it faster across multiple platforms that require different optimisations, these tools will help you to make informed decisions about each.
     
  29. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    Absolutely! Highly recommend it, to anyone unaware of it's existence. :cool:
     
  30. icauroboros

    icauroboros

    Joined:
    Apr 30, 2021
    Posts:
    99
    Or better just use unity profiler's own marker, also it can be nested

    Code (CSharp):
    1.        public void HeavyMethod()
    2.         {
    3.             Profiler.BeginSample("Profiling all Method");
    4.  
    5.             Profiler.BeginSample("profiling just a line");
    6.              blabla.GetDistance();
    7.             Profiler.EndSample();
    8.  
    9.             Profiler.BeginSample("profiling for loop");
    10.             for (int meshIndex = 0; meshIndex < Count; meshIndex++)
    11.             {
    12.                  //another code
    13.             }
    14.             Profiler.EndSample();
    15.  
    16.             Profiler.EndSample();
    17.         }
     
  31. joxthebest314

    joxthebest314

    Joined:
    Mar 31, 2020
    Posts:
    95

    Yes like you guessed it's bad. However, I am currently not using any towers or bullets, just trying to improve enemies alone because they seem to be the biggest hit in performance. I will redo most of the logic after I fixed the performances of enemies.
     
  32. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    I just found my old code, for the tower defense game I was playing around with. Now I don't remember how exactly performant it was, and I'm sure it could be improved on:
    Code (CSharp):
    1. public class Enemies : // parent class
    2. {
    3.     public List<Route> myPath = new List<Route>();
    4.    
    5.     public void FollowPath()
    6.     {
    7.         if (myPath.Count > 0)
    8.         {
    9.             Vector3 nextPosition = myPath[0].trans.position;
    10.  
    11.             if (DistanceTo(nextPosition) > moveSpeed)
    12.             {
    13.                 TurnTowards(nextPosition);
    14.                 trans.Translate(0, 0, moveSpeed);
    15.             }
    16.             else
    17.             {
    18.                 //print("reached path, removing it");
    19.                 myPath.RemoveAt(0);
    20.             }
    21.         }
    22.     }
    23. }
    Code (CSharp):
    1. public class Grunt : Enemies
    2. {
    3.     void Update()
    4.     {
    5.         FollowPath();
    6.  
    7.         float speed = (transform.position - mLastPosition).magnitude * fps;
    8.         mLastPosition = transform.position;
    9.        
    10.         centerPos = new Vector3(trans.position.x, myCollider.bounds.size.y / 2f, trans.position.z);
    11.         leadPos = centerPos + trans.forward * speed;
    12.         Debug.DrawLine(leadPos, centerPos);
    13.     }
    14. }
    Code (CSharp):
    1. public class Turret : // parent class
    2. {
    3.     if (nearestEnemy != null)
    4.     {
    5.         Vector3 aimAt = nearestEnemy.leadPos;
    6.         float distance = Vector3.Distance(nearestEnemy.trans.position, trans.position);
    7.         float fraction = distance / shootSpeed;
    8.         Vector3 leadShot = FindAlongLine(nearestEnemy.centerPos, nearestEnemy.leadPos, fraction);
    9.  
    10.         TurretTurnTowards(leadShot);
    11.  
    12.         if (IfTurretIsAimedAt(leadShot))
    13.         {
    14.             shootTimer++;
    15.             if (shootTimer > fireRate)
    16.             {
    17.                 ShootBullet(shootSpeed);
    18.                 shootTimer = 0;
    19.                 print("rounds fired");
    20.             }
    21.         }
    22.     }
    23. }
    But hopefully something helps, or sparks a new idea for ya! Code can be be your worst enemy, or your best friend, you just gotta keep at it. :)
     
    joxthebest314 likes this.
  33. joxthebest314

    joxthebest314

    Joined:
    Mar 31, 2020
    Posts:
    95

    Thanks a lot, that gave me some more fps.
     
  34. joxthebest314

    joxthebest314

    Joined:
    Mar 31, 2020
    Posts:
    95
    DevDunk likes this.
  35. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,509
    First of all, how do you know that they're a significant performance hit?

    There's a process which you should always apply to optimisation:
    1) Measure
    2) Change
    3) Measure again

    So, how much of a performance hit are your enemies?

    Here's another tip: don't measure their performance impact in FPS. Measure it in milliseconds (ms). This is because the change in FPS will vary depending on the original FPS, where the ms remain stable.

    For instance, if I tell you that the enemies in my game are taking 1.5ms then that's a known figure. It doesn't matter if my game is running at 10fps or 1000fps, 1.5ms is 1.5ms, it always means the same thing. However, if I tell you that they're taking 5 frames you actually don't know anything - if my game started at 10fps then 5 frames is huge, where if my game started at 1000fps then it's tiny.

    So, the big question: how many ms are your enemies taking at the moment?


    Secondly, don't let optimisation keep you from finishing the game!
    It is far more important that people can play your game and give you feedback than that it is running faster. Obviously you don't want to ship it with performance issues, but at this stage I wouldn't let "my enemies aren't optimised" stop me from putting the rest of the game in there.

    Plus, the way that you optimise your enemies, and whether or not they even need optimisation, may be different depending on the performance characteristics of everything else. It can be a decent learning exercise, so if that's your goal then go for it. But if your goal is to finish the game then I'd get the guts of it in place and then start looking for where to optimise things.

    As you get more experience you'll be able to write stuff that is more optimal the first time around, and make informed decisions about where to optimise systems early vs. leave them until later.
     
    Last edited: Aug 8, 2023
    joxthebest314 and Ryiah like this.
  36. joxthebest314

    joxthebest314

    Joined:
    Mar 31, 2020
    Posts:
    95

    Thanks a lot for your response. I know they are responsible for the huge hit in perf because I made a test level only to test specific things one by one, eliminating doubt.

    Here is a before and after they spawn :
    upload_2023-8-10_18-45-4.png

    After :

    upload_2023-8-10_18-45-41.png

    Here is a pic of the profiler when the enemies are here :

    upload_2023-8-10_18-55-58.png

    This is also after I did some of the upgrades / changes talked before on this thread, which helped a lot but I am not done yet. I also need to optimise this because it is game breaking, 100 enemies is not a lot so the game cannot go further without me figuring it out (also this is without turrets).

    The new issues seem to be related to graphics rather than scripts like before. I will need to investigate this more because I don't know much about it.
     
  37. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    All I see is your camera.rendering is going nuts with chewing up milliseconds... oof!

    I'd click further down their hierarchy to see what is mainly doing that, but I can already say BATCHING!

    Any time you plan on having many of the same materials or skins, GPU instancing is one way to quell the camera render, but there are many others as well.

    So yeah, I don't think it's your code at all, it's your scene rendering. I'd google as much as you can on handling camera performance. :D
     
    joxthebest314 likes this.
  38. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    * also UI performance
     
    joxthebest314 likes this.
  39. joxthebest314

    joxthebest314

    Joined:
    Mar 31, 2020
    Posts:
    95

    Okay, I will take a look at what should be done. Thanks a lot. Here is more of the profiler :

    upload_2023-8-10_19-10-12.png
     
  40. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    batchingReference.jpg
    As you will see in this scene, which I admit isn't a lot, it's roughly trying to render 1050 objects and their materials/textures. But when you batch them, the GPU is only "getting" 61 of them for reference, and just duplicating after the "render bottleneck".

    Which I'm sure I only quickly messed with a few of those materials, but total running batches should be around 50 max(depending on how many different textures) give or take.
     
    joxthebest314 likes this.
  41. joxthebest314

    joxthebest314

    Joined:
    Mar 31, 2020
    Posts:
    95

    Yes, I just learned about static batching, which I applied to all the non moving parts of the scene. But it is not made for moving animated things (i think). The issues seem to come from the animations of the enemies. I didn't take any precautions while making them so maybe I made something very performance expensive.
     
  42. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    I'm pretty sure it doesn't matter on moving or not. Or maybe I'm confusing two different things and calling it one?

    But it's basically like caching a component for the CPU, having something batched for the GPU works the same way. It get's the texture in question one time, and after that the camera easily duplicates and moves them to wherever they need to be placed(stretched/rotated/etc...) each frame.

    I'm very foggy on the details, will have to re-visit that again and refresh my memory. But you're at least on the right track, just keep researching. :)
     
    joxthebest314 likes this.
  43. zevonbiebelbrott

    zevonbiebelbrott

    Joined:
    Feb 14, 2021
    Posts:
    102
    You might be confusing static mesh batching with instanced material batching, both will save you tons of ms!
     
  44. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,509
    You are. Static batching is for static (non-moving) things. It works by combining meshes at load time.

    Dynamic batching is for small objects, and it has the CPU combine them into batches each frame. It's a compromise which doesn't always make things faster. Not sure if it applies to skinned stuff at all.

    Zevon already mentioned material batching.

    Another to look into is "instanced rendering", though it requires custem effort.

    As you have 100 enemies on screen, I'd consider not using a skinned renderer. For instance, if they all use the same animation all the time, you could bake out the frames as meahes and swap between them to animate. Or you could split the enemies into parts which move independently.

    If skinned meshes are the way to go, keep the bone count down and the bone per vertas low as it can be.
     
    joxthebest314 likes this.
  45. joxthebest314

    joxthebest314

    Joined:
    Mar 31, 2020
    Posts:
    95
    I found a huge issue about the way I did my enemies. As it was the first game in 3d that i ever did, I made the enemies myself in blender without knowing much. I ended up with a skinned mesh renderer on each part of the body (legs, arms, etc) because I didn't merge them into one mesh in blender. I did that in the first place because my enemies have some variations between each other (one has a broken arm, the other not) so I would just enable / disable the correct mesh inside the model to do so.

    upload_2023-8-11_11-34-33.png

    Now that I know I wasn't supposed to do that, I have some questions about the variations I mentioned before. How am I supposed to make them random ? Should I have a prefab for each combinations ?

    Anyway thanks a lot for the really useful help everybody gave me, I am now able to spawn enemies without loosing ridiculous amount of performance (not everything is perfect but the game is now playable) !
     
  46. zevonbiebelbrott

    zevonbiebelbrott

    Joined:
    Feb 14, 2021
    Posts:
    102
    As you said yourself already, combine all separate meshes into one big mesh in Blender, because each separate mesh is a separate draw call from the Renderer, so yes it will be very slow, especially with multiple materials aswell.
    Always go with 1 mesh and 1 material, thats the most performant, if you need different textures on a single material you can use an Atlas for that.

    Finally if you want custom models, its best to make separate enemies in Blender, Broken arm zombie, normal zombie, fat zombie, etc. If possible, have them all share the same material though and make it instanced, for batching.
     
    joxthebest314 likes this.
  47. Adrian

    Adrian

    Joined:
    Apr 5, 2008
    Posts:
    1,051
    As long as the bones stay the same (hierarchy, names, initial position), you can use the same set of animations with different meshes. This way, you could make one set of animations but then have many different meshes that you can choose randomly.

    You can also expose all or specific bones of your animations to the hierarchy, so they show up as game objects in the imported model prefab ("Optimize Game Object" and "Extra Transforms to Expose" in the model importer). Then you can add additional visuals to the game objects that then move with the animations. You don't want to go overboard with that but it's a good way to add random variations to some basic models, e.g. adding different heads/hats, weapons, armor etc.
     
    joxthebest314 likes this.
  48. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    In my particular case, each thing is a tile, which gets made by custom code. So I wasn't able to do static batching, which I wish I could make an Instantiated object "scene static", but it is impossible to do.

    So I guess my scenario is instance/material batching, as all of my materials have this selected:
    GPUinstancing.jpg

    But that setting can only be applied to a material, as the texture used by blender(that you have to import into Unity) I don't think can have any special calls like this. Maybe if you make the texture into a material, it can?

    I remember hearing that as well, just have one big texture that has a bunch of smaller textures in it, and there's a way to crop out specific sections. But I never played around with that yet, so I can't give any tips on it.

    But for sure learn Blender, I dabble with it here and there, but it is a life saver when you just need a few modifications to a model(especially root position). But learning how to use (the same)textures for different models, and just modifying the UV map for each model, will definitely help with render performance.

    Also look into LOD(level of detail), if something is pretty far from camera you don't need to have full detail, so that can also give great performance results!

    I'm sure there's a ton of other things I forgot, but mainly because I haven't needed them yet, learning is by doing. But learn all you can, so when I have questions later you can answer them for me, lol :p
     
    joxthebest314 likes this.
  49. zevonbiebelbrott

    zevonbiebelbrott

    Joined:
    Feb 14, 2021
    Posts:
    102
    Are you sure its impossible? I thought Albion online did something similar and they used prefabs for their static tiles.
     
  50. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    As far as I researched, the only objects you can declare as static are ones that are pre-made(placed) in the editor, and do not move, change, or even rotate.

    And to the best of my recollection, it's because of the way Unity works. If you were to make your own game engine, map out the modular(or procedural) generation, then save the scene all as static objects, I'm sure that could work.

    But I only remember wasting a day looking into it, eventually gave up and just let that dream go.