0
\$\begingroup\$

I have a flocking script for some ducks based on a youtube video I watched. The script works really well and I added my own basic obstacle avoidance system. However, performance is shocking! On average, every bird seems to take away 1 fps. That doesn't sound too bad but it scales really poorly. I have a 4km x 4km map, and I want a lot of birds. With no birds, I have about 130-140fps. With 100 birds, that drops to 40-50fps. I would really like to be able to increase the number of birds to at least 200 and still have around 100fps. I know my code isn't very well optimized but I just don't how to do that. Below is are my two flocking scripts. I know for a fact that it is these scripts because the profiler has the flock.update() at about 20-30%. Each bird also has a ragdoll system so the player can shoot them and they will fall out of the sky. This means that each bird has a rigidbody and collider on each joint. I've also added an LOD system so the birds are culled outside a certain distance. Could someone please take a quick look at them and see if I'm doing something particularly expensive that could be done better?

GlobalFlock.cs

public class GlobalFlock : MonoBehaviour
{
    [SerializeField]
    private GameObject birdPrefab;
    [SerializeField]
    private Transform parent;

    [SerializeField]
    private int tankSize = 200;
    private float yOffset = 10;

    private static int numBirds = 100;
    public static GameObject[] allBirds = new GameObject[numBirds];

    void Start()
    {
        for (int i = 0; i < numBirds; i++)
        {
            //creating a random position within a 'tank' to spawn each bird
            Vector3 pos = new Vector3(Random.Range(-tankSize, tankSize), Random.Range(yOffset, yOffset + 1), Random.Range(-tankSize, tankSize));

            //makes the tanks position reletive to this transform
            pos += transform.position;

            //instantiating birds and parenting them to an empty to reduce clutter in the heirachy
            allBirds[i] = (GameObject)Instantiate(birdPrefab, pos, Quaternion.Euler(0, Random.Range(-360, 360), 0));
            allBirds[i].transform.parent = parent;
        }
    }
}

Flock.cs

public class Flock : MonoBehaviour
{
    private float speed;
    private float startSpeed;
    private float rotationSpeed = 4.0f;
    private Vector3 averageHeading;
    private Vector3 averagePosition;
    private float neighbourDistance = 4.0f;
    private bool flee;
    private Vector3 fleePos;
    private float fleeDistance = 100.0f;
    private float fleeRotationSpeed = 6.0f;
    private Transform[] rayTransforms;
    private int layer = 16;
    private int layerMask;
    private Vector3 goalPos;
    private Vector3 childPos;

    [SerializeField]
    private float rayLength;

    [HideInInspector]
    public bool ragdoll;

    private States states;

    void Start()
    {
        speed = Random.Range(4, 6);
        startSpeed = speed;
        states = FindObjectOfType<States>();

        rayTransforms = transform.GetChild(1).GetComponentsInChildren<Transform>();

        childPos = transform.GetChild(1).position;

        layerMask = ~(1 << layer);
    }

    void Update()
    {
        if (!ragdoll)
        {
            //randomly changing the goal position 5 times out of 10000
            if (Random.Range(0, 10000) < 3)
            {
                goalPos = new Vector3(Random.Range(-2000, 2000),
                                      Random.Range(0, 200),
                                      Random.Range(-2000, 2000));
            }

            //scare the birds - flee
            if (states.shoot)
            {
                fleePos = states.transform.GetChild(0).position;

                if (Vector3.Distance(fleePos, this.transform.position) < fleeDistance)
                {
                    flee = true;
                }
            }
            else
            {
                if (Vector3.Distance(fleePos, this.transform.position) >= fleeDistance)
                {
                    flee = false;
                }
            }

            if (Random.Range(0, 5) < 1)
                ApplyRules();

            transform.Translate(0, 0, Time.deltaTime * speed);
        }
    }

    void ApplyRules()
    {
        GameObject[] gos;
        gos = GlobalFlock.allBirds;

        Vector3 vCenter = Vector3.zero;
        Vector3 vAvoid = Vector3.zero;
        float gSpeed = 0.1f;

        float dist = 0;

        int groupSize = 0;

        foreach (GameObject go in gos)
        {
            if (go != this.gameObject && go)
            {
                dist = Vector3.Distance(go.transform.position, this.transform.position);
                if (dist <= neighbourDistance)
                {
                    vCenter += go.transform.position;
                    groupSize++;

                    if (dist < 1.0f)
                    {
                        vAvoid += this.transform.position - go.transform.position;
                    }

                    Flock anotherFlock = go.GetComponent<Flock>();
                    gSpeed += anotherFlock.speed;
                }
            }
        }

        Ray lRay = new Ray(childPos, rayTransforms[1].forward * rayLength);
        Ray rRay = new Ray(childPos, rayTransforms[2].forward * rayLength);
        Ray uRay = new Ray(childPos, rayTransforms[3].forward * rayLength);
        Ray dRay = new Ray(childPos, rayTransforms[4].forward * rayLength);

        if (Physics.Raycast(dRay, rayLength, layerMask))
        {
            transform.localEulerAngles += new Vector3(-1, 0, 0);
        }
        else if (Physics.Raycast(uRay, rayLength, layerMask))
        {
            transform.localEulerAngles += new Vector3(1, 0, 0);
        }
        else if (Physics.Raycast(lRay, rayLength, layerMask))
        {
            transform.localEulerAngles += new Vector3(0, 1, 0);
        }
        else if (Physics.Raycast(rRay, rayLength, layerMask))
        {
            transform.localEulerAngles += new Vector3(0, -1, 0);
        }


        if (flee)
        {
            Vector3 newFleePos = new Vector3(fleePos.x, transform.position.y, fleePos.z);
            Vector3 direction = transform.position - newFleePos;

            speed = Mathf.Lerp(speed, 9.0f, Time.deltaTime * 2);

            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * fleeRotationSpeed);
        }
        else
        {
            if (groupSize > 0)
            {
                vCenter = vCenter / groupSize + (goalPos - this.transform.position);
                speed = (speed == startSpeed) ? (gSpeed / groupSize) : Mathf.Lerp(speed, startSpeed, Time.deltaTime);

                Vector3 direction = (vCenter + vAvoid) - transform.position;

                if (direction != Vector3.zero)
                {
                    transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * rotationSpeed);
                }
            }
            else
            {
                speed = Mathf.Lerp(speed, startSpeed, Time.deltaTime);

                Vector3 direction = goalPos - this.transform.position;

                if (direction != Vector3.zero)
                {
                    transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * rotationSpeed);
                }
            }
        }
    }
}
\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

I can't speak directly for your solution, but here are some general flocking performance tips that apply.

Probably the biggest thing:

For every duck, you're checking every other duck. This is a complexity problem as well as a duplication problem. If you moved these checks to a manager class, you could do half as many distance checks.

Think of it like this. The distance between ducks A->B is the same as the distance of ducks B->A. So save the distance somewhere and use it twice.

If you really want this to scale, look into spatial partitioning like Oct/quadtrees or a grid system. That way you only compare against objects that actually matter.

Also, do you need proper distance? Or can squared distance work? Right now you're doing n^2 square roots, which is a big time sink.

More general stuff:

Rather than constantly using transform.position and transform.rotation, use a Vector for pos and a Quat for rotation, then assign them once at the end of the update. Every assign to transform has additional overhead as it does other unnecessary calculations under the hood.

Are they raycasts for obstacle avoidance? If so, do you really need casts in any direction other than forward?

You should try to avoid Quaternion.Slerp. Try using something like Lerp instead. Not the huuuugest deal, but slerp requires a trig while lerp does not.

Not sure how you're handling the ragdoll, but it doesn't need to be on all the time. Have it disabled, then get enabled on death.

Other options

Try parallelism. If you want thousands of ducks you'll probably need to go to the gpu with compute shaders, but for something that scales decently well, if you create some worker threads to do all your math in parallel you can cut down on time.

Use a coroutine to separate your calculations over several frames. You don't need to update your flocking behavior every frame. 3-4 times a second should be fine. This means you can spread the performance cost over 20ish frames rather than one, this is a BIG improvement.

\$\endgroup\$
0
\$\begingroup\$

Zebraman already gave a great overview of the biggest wins you're likely to get.

In particular, the suggestion of using a manager class to update the whole flock will also save the engine overhead of calling each bird's Update() separately, which can become significant over hundreds of instances.

If you keep your live birds in a set order, then the manager won't need to check for null or self-interactions in its inner loop, the way your code does now with its if (go != this.gameObject && go)

A few other considerations to go along with what's already been said:

  • You're storing a list of all bird GameObjects, then using GetComponent<Flock> O(n^2) times every frame to get at the components you need. Instead, store this as an array of Flock components so you can access them directly. Unity has optimized reaching from arbitrary components to the parent gameObject/transform, going the other way requires a search, which is pretty redundant.

  • If your birds have a collider and rigidbody for processing hits, you may want to move them using velocity / impulses / forces / torques, rather than setting their transform position & rotation directly. When you do that, from the the physics engine's point of view the object has just teleported, and this can sometimes incur extra processing to make sure it hasn't jumped into a position intersecting other colliders. (In older versions of Unity, this caused noticeable performance hits for static colliders and large trigger colliders in particular)

  • Rather than running your ApplyRules logic randomly (which could lead to performance spikes where occasionally many birds decide to raycast on the same frame because of bad die rolls), try time-slicing it. As your manager runs its tick, choose one block of the birds array to get their rules updated in this pass - then next frame, move onto the next block of birds. This saves random branching that can frustrate modern processor optimizations.

    Now, you might find groups of birds all changing direction in synch doing it this way. If you keep your update budget similar to what you have now, where each bird gets an update every 5 frames, then this synchronization shouldn't be very noticeable. You can also blend the resulting rotation and speed changes over the following frames. Another trick is to make your update block size relatively prime with the length of your birds array - that way birds that update together once won't update together every time, making any synchronicity fleeting & more coincidental-looking.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .