Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Оптимизируем Boid’ов на Unity

Anna | 18.06.2014 | нет комментариев

Знаете ли вы, что кузнечики, будучи кинутыми в ведёрко, начинают маршировать по кругу как на картинке выше? Правда сверху не кузнечики, а Boids — модель коллективного поведения птичек, пчёлок, рыбок и иной живности. Невзирая на простоту модели, она показывает эмерджентные свойства: боиды собираются в кучу, летают стаями по кругу, нападают на людей.

Это вторая часть статьи, посвящённая разным хитростям оптимизации Unity и C#, которые увеличивают эффективность алгорифма из первой части в пару десятков раз.

Парочка модификаций

Напомню на чём мы остановились. Boid.cs из предыдущей части без оптимизаций

using UnityEngine;

public class Boid : MonoBehaviour
{
    public Vector3 velocity;

    private float cohesionRadius = 10;
    private float separationDistance = 5;
    private Collider[] boids;
    private Vector3 cohesion;
    private Vector3 separation;
    private int separationCount;
    private Vector3 alignment;
    private float maxSpeed = 15;

    private void Start()
    {
        InvokeRepeating("CalculateVelocity", 0, 0.1f);
    }

    void CalculateVelocity()
    {
        velocity = Vector3.zero;
        cohesion = Vector3.zero;
        separation = Vector3.zero;
        separationCount = 0;
        alignment = Vector3.zero;

        boids = Physics.OverlapSphere(transform.position, cohesionRadius);
        foreach (var boid in boids)
        {
            cohesion  = boid.transform.position;
            alignment  = boid.GetComponent<Boid>().velocity;

            if (boid != collider && (transform.position - boid.transform.position).magnitude < separationDistance)
            {
                separation  = (transform.position - boid.transform.position) / (transform.position - boid.transform.position).magnitude;
                separationCount  ;
            }
        }

        cohesion = cohesion / boids.Length;
        cohesion = cohesion - transform.position;
        cohesion = Vector3.ClampMagnitude(cohesion, maxSpeed);
        if (separationCount > 0)
        {
            separation = separation / separationCount;
            separation = Vector3.ClampMagnitude(separation, maxSpeed);
        }
        alignment = alignment / boids.Length;
        alignment = Vector3.ClampMagnitude(alignment, maxSpeed);

        velocity  = cohesion   separation * 10   alignment * 1.5f;
        velocity = Vector3.ClampMagnitude(velocity, maxSpeed);
    }

    void Update()
    {
        if (transform.position.magnitude > 25)
        {
            velocity  = -transform.position.normalized;
        }

        transform.position  = velocity * Time.deltaTime;

        Debug.DrawRay(transform.position, separation, Color.green);
        Debug.DrawRay(transform.position, cohesion, Color.magenta);
        Debug.DrawRay(transform.position, alignment, Color.blue);
    }
}

Начнём с нескольких косметических изменений, которые упростят дальнейшую работу и приблизят код к тому, что может встретиться в реальной жизни. Поменяем модельку боида, Дабы она была огромнее схожа на птицу и в то же время содержала поменьше треугольников. Простенькой пирамидки из Blender’а будет довольно. Кидаем файл .blend в папочку плана, выделаем в инспекторе и в настройках импорта отключаем лишнее. Копируем ветхий префаб и делаем новейший, на котором будем ставить эксперименты.

От того что у префаба сейчас возникло направление, в Update скрипта стоит добавить вращение. Для поворота объектов есть большое число вариантов, но мы возьмём Vector3.RotateTowards, потому как он примитивный и нам всё равно без разницы. Вначале проверяем необходимо ли вообще что-то делать, потом плавно поворачиваем.

if (velocity != Vector3.zero && transform.forward != velocity.normalized)
{
    transform.forward = Vector3.RotateTowards(transform.forward, velocity, 10, 1);
}

Заодно переделаем код, тот, что расставляет боиды на сцене. Мусорить в иерархии — плохая практика, следственно спрячем все боиды с поддержкой Transform.parent.

var boid = Instantiate(boidPrefab, Random.insideUnitSphere * 25, Quaternion.identity) as Transform;
boid.parent = transform;

Приступаем к делу

Начнём с тривиального. В нашем цикле три раза выполняется вычитание transform.position — boid.transform.position. Это нехорошо, отменнее запихнем итог в переменную. На сотне боидов это может не иметь значения, но на паре тысяч в цикле да ещё и несколько раз в секунду разница теснее будет.

var vector = transform.position - boid.transform.position;
if (boid != collider && vector.magnitude < separationDistance)
{
    separation  = vector / vector.magnitude;
    separationCount  ;
}

Там же неподалёку есть Vector3.magnitude, тот, что требует вычисления квадратного корня. Для сопоставления расстояний его дозволено заменить Vector3.sqrMagnitude. Заодно поменяем magnitude в формуле вычисления взвешенного вектора, это не крепко повлияет на итог.

if (boid != collider && vector.sqrMagnitude < separationDistance * separationDistance)
{
    separation  = vector / vector.sqrMagnitude;
    separationCount  ;
}
…
if (transform.position.sqrMagnitude > 25 * 25)
{
    velocity  = -transform.position.normalized;
}

Transform и GetComponent

В нашем коде вызов transform встречается огромнее дюжины раз и нередко происходит в цикле. Умножаем это на число боидов и получается печальная картина. За доступом к transform’у на самом деле скрывается драгоценный поиск компонента. Дабы этого избежать, закешируем его в отдельной переменной во времяAwake. Это событие вызывается до начала игры во время загрузки. Заодно дозволено поменять вызов трансформа из коллайдера на вызов публичной переменной скрипта, а сопоставление с собственным коллайдером на условие с квадратом расстояния.

public Transform tr;
void Awake()
{
    tr = transform;
}

Заменим все обращения к transform на tr.

foreach (var boid in boids)
{
    var b = boid.GetComponent<Boid>();
    cohesion  = b.tr.position;
    alignment  = b.velocity;

    if (vector.sqrMagnitude > 0 && (tr.position - b.tr.position).magnitude < separationDistance)
    {
        separation  = (tr.position - b.tr.position) / (tr.position - b.tr.position).magnitude;
        separationCount  ;
    }
}

Оптимизируем дальше

Ну что, теснее гораздо отменнее, но FPS всё равно проседает, когда боиды крепко сближаются. А всё потому, что Physics.OverlapSphere начинает захватывать всё большее число коллайдеров и мы получаем фактически ту же самую квадратичную трудность простого перебора по каждому боидам.

Согласно интернету, ласточки в стаях ориентируются каждого по полудюжине соседей. Чем боиды дрянней? Берём и тривиально ограничиваем цикл ещё одним условием. Для 2-х условий отменнее подойдёт цикл for. Помимо того, имеет толк ограничить не только наивысшее число соседей, но и минимальное. Добавим условие выхода, если неподалеку нет соседей. Помимо того, нам придётся изменить знаменатель в вычислении векторов, напротив при огромный скученности соседей у боидов не будет шанса выбраться.

private int maxBoids = 5;
…
boids = Physics.OverlapSphere(tr.position, cohesionRadius);
if (boids.Length < 2) return;
…
for (var i = 0; i < boids.Length && i < maxBoids; i  )
{
    var b = boids[i].GetComponent<Boid>();
    cohesion  = b.tr.position;
    alignment  = b.velocity;
    var vector = tr.position - b.tr.position;
    if (vector.sqrMagnitude > 0 && vector.sqrMagnitude < separationDistance * separationDistance)
    {
        separation  = vector / vector.magnitude;
        separationCount  ;
    }
}
cohesion = cohesion / (boids.Length > maxBoids ? maxBoids : boids.Length);

Сейчас самая основная задача — мы слишком Зачастую обновляем вектор velocity. Немножко шагнём в сторону и наведём порядок в инспекторе, Дабы было проще настраивать алгорифм. Сделаем все значимые переменные публичными, но некоторые спрячем с поддержкой признака HideInInspector и добавим парочку новых. Добавляем параметр tick, подставляем его в вызывалку по таймеру.

public int turnSpeed = 10;
public int maxSpeed = 15;
public float cohesionRadius = 7;
public int maxBoids = 10;
public float separationDistance = 5;
public float cohesionCoefficient = 1;
public float alignmentCoefficient = 4;
public float separationCoefficient = 10;
public float tick = 2;

[HideInInspector] public Vector3 velocity;
[HideInInspector] public Transform tr;
…
InvokeRepeating("CalculateVelocity", 0, tick);

Дальше делаем финт ушами и выставляем частоту обновления 2 секунды. Да-да, вы не ослышались, в двадцать раз реже, чем у нас было. Заодно подкорректируем множители. Сейчас взамен сотни боидов мы можем создавать тыщу.

Оптимизировали-оптимизировали, да не выоптимизировали

Новая задача. Раз в две секунды у всех боидов запускается вычисление новых векторов и возникает невидимая пульсация. Результат, безусловно, увлекательный, но птицы так не умеют. Делаем ещё одну примитивную оптимизацию — разносим вычисления по времени с поддержкой Random.value.

InvokeRepeating("CalculateVelocity", Random.value * tick, tick);

Ну и Дабы старт симуляции не выглядел слишком необычно, в Awake тоже добавим элемент случайности изRandom.onUnitSphere.

velocity = Random.onUnitSphere * maxSpeed;

Приглядываемся к нашему коду ещё внимательнее.

var b = boids[i].GetComponent<Boid>();
…
var vector = tr.position - b.tr.position;

За создание временных переменных в цикле гневный мусоросборщик рано либо поздно отъест у нас процессор. Если мы знаем, что регулярно делаем одни и те же действия, то дозволено сделать непрерывные переменные.

private Boid b;
private Vector3 vector;
private int i;

Невозможно также забывать, что изредка взамен оптимизации кода отменнее оптимизировать логику. В Update у нас есть проверка на выход за границы сферы, где применяется нормализация.

velocity  = -tr.position.normalized;

Это слишком точная функция для такой цели. Если нам необходим не суровый единичный вектор, а только направление, то вектор дозволено легко поделить.

velocity  = -tr.position/25;

Мы можем скостить ещё пару миллисекунд расчётов, если перенесем поворот боидов в отдельную функцию и будем запускать по таймеру.

InvokeRepeating("UpdateRotation", Random.value, 0.1f);
…
void UpdateRotation()
{
    if (velocity != Vector3.zero && model.forward != velocity.normalized)
    {
        model.forward = Vector3.RotateTowards(model.forward, velocity, turnSpeed, 1);
    }
}

Физика

Вы вероятно подметили, что мы вовсе не используем физику, если не принимать в расчёт поиск коллайдеров. Мы можем сэкономить ещё немножко источников, если перенесем боидов в обособленный слой, будем искать коллайдеры с поддержкой LayerMask и отключим проверку соударений между боидами в настройках физики.

public LayerMask boidsLayer;
…
boids = Physics.OverlapSphere(tr.position, cohesionRadius, boidsLayer.value);

Кучу FPS дозволено получить если выкрутить на минимум Solver Iteration Count в Physics Manager. Помимо того, дозволено испробовать поиграться с Fixed Timestep и Maximum Allowed Timestep в Time Manager, но если крепко увлечься, то симуляция станет хаотичной и непривлекательной.

Ещё один ньюанс связан с вращением. Когда мы поворачиваем модель, мы поворачиваем привязанный к ней сферический коллайдер. Дорого и непотребно. Задача решается отделением модели от коллайдера в иерархии. Так дозволено выиграть ещё пяток FPS.

public Transform model;
…
if (velocity != Vector3.zero && model.forward != velocity.normalized)
{
    model.forward = Vector3.RotateTowards(model.forward, velocity, turnSpeed, 1);
}

Завершение

На этом всё. Основную часть источников отъедает перемещение кучи объектов в кадре и поиск соседей. С первым трудно что-то сделать, а вот для второго необходимо вообще перестать применять физику и поменять её на хитроумные конструкции данных, но это теснее тема для отдельной статьи. Верю, что умные програжители в комментариях предложат свои варианты убыстрения боидов.

Оптимизированная версия Boid.cs

using UnityEngine;

public class Boid : MonoBehaviour
{
    public int turnSpeed = 10;
    public int maxSpeed = 15;
    public float cohesionRadius = 7;
    public int maxBoids = 10;
    public float separationDistance = 5;
    public float cohesionCoefficient = 1;
    public float alignmentCoefficient = 4;
    public float separationCoefficient = 10;
    public float tick = 2;
    public Transform model;
    public LayerMask boidsLayer;

    [HideInInspector] public Vector3 velocity;
    [HideInInspector] public Transform tr;

    private Collider[] boids;
    private Vector3 cohesion;
    private Vector3 separation;
    private int separationCount;
    private Vector3 alignment;

    private Boid b;
    private Vector3 vector;
    private int i;

    void Awake()
    {
        tr = transform;
        velocity = Random.onUnitSphere*maxSpeed;
    }

    private void Start()
    {
        InvokeRepeating("CalculateVelocity", Random.value * tick, tick);
        InvokeRepeating("UpdateRotation", Random.value, 0.1f);
    }

    void CalculateVelocity()
    {
        boids = Physics.OverlapSphere(tr.position, cohesionRadius, boidsLayer.value);
        if (boids.Length < 2) return;

        velocity = Vector3.zero;
        cohesion = Vector3.zero;
        separation = Vector3.zero;
        separationCount = 0;
        alignment = Vector3.zero;

        for (i = 0; i < boids.Length && i < maxBoids; i  )
        {
            b = boids[i].GetComponent<Boid>();
            cohesion  = b.tr.position;
            alignment  = b.velocity;
            vector = tr.position - b.tr.position;
            if (vector.sqrMagnitude > 0 && vector.sqrMagnitude < separationDistance * separationDistance)
            {
                separation  = vector / vector.sqrMagnitude;
                separationCount  ;
            }
        }

        cohesion = cohesion / (boids.Length > maxBoids ? maxBoids : boids.Length);
        cohesion = Vector3.ClampMagnitude(cohesion - tr.position, maxSpeed);
        cohesion *= cohesionCoefficient;
        if (separationCount > 0)
        {
            separation = separation / separationCount;
            separation = Vector3.ClampMagnitude(separation, maxSpeed);
            separation *= separationCoefficient;
        }
        alignment = alignment / (boids.Length > maxBoids ? maxBoids : boids.Length);
        alignment = Vector3.ClampMagnitude(alignment, maxSpeed);
        alignment *= alignmentCoefficient;

        velocity = Vector3.ClampMagnitude(cohesion   separation   alignment, maxSpeed);
    }

    void UpdateRotation()
    {
        if (velocity != Vector3.zero && model.forward != velocity.normalized)
        {
            model.forward = Vector3.RotateTowards(model.forward, velocity, turnSpeed, 1);
        }
    }

    void Update()
    {
        if (tr.position.sqrMagnitude > 25 * 25)
        {
            velocity  = -tr.position / 25;
        }
        tr.position  = velocity * Time.deltaTime;
    }
}

Исходники на GitHub | Онлайн версия для владельцев Unity Web Player

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB