Pull to refresh

Comments 41

За материал спасибо, но размещать 10 листингов кода без каких-то пояснений — не самый хороший тон. Если нужно просто поделиться кодом, если github, bitbucket или что-нибудь еще. Если нужно пояснить какой-то участок, в этом случае его нужно вставить в статью. В конечном итоге, если кто-то что-то захочет проверить, придется копипастить все со статьи, собирать самому проект и т.д.
Ваша правда, не подумал об этом. Залил на ведёрко.
Это работает быстрее, т.к. не используется Sqrt.

Mathf.Pow(attackMinimumDistance, 2)

Матерь божья… Т.е. извлекать квадратный корень — это плохо, а возводить в степень — это нормально? :) Надо просто писать как
attackMinimumDistance * attackMinimumDistance
Если честно, не заметил разницы под профайлером. И то, и то считается примерно за 0.01 мсек.
Десктопный профайлер и дохлая мобильная железка, например, это разные вещи, об этом нужно думать наперед. :) Возможно это оптимизация компилятором и там генерируется код для умножения, но не факт, что под все платформы оно будет срабатывать именно так, а не использовать медленный FPU.
Я не разрабатываю ПО для мобильных устройств, потому и не задумываюсь об этих платформах. Предполагается, что разработчик мобильных игр весьма неплохо шарит в оптимизации :)
Да б-г с ним с этим перемножением, темболее, как написали ниже, Math.Pow (а в него идёт вызов из Mathf.pow, как и из многих других методов Mathf) шустрее, а там, вроде-как, методы extern (т.е. реализованы нативно). У него там всё кишит GetComponent'ами. Даже выборка из словаря по какому-нибудь ключу работает быстрее, чем GetComponent. Плюс, обращение к transform и т.д. это вроде-как тоже GetComponent. Автору — вместо этого всего, используйте ссылки на нужные обьекты класса ИИ (а не геймобьекта, вы все-равно ведь назначаете это поле!), а вместо FindGameObjectsWithTag используйте статическую коллекцию. Зачем, чёрт возьми, что-то искать, когда вы можете сохранить ссылки на них в коллекции? Подход «декстопы мощные, всё будет хорошо» работает ровно до тех пор, пока сцена является вот таким вот демо с парой обьектов.
там, вроде-как, методы extern (т.е. реализованы нативно)

Так это накладные расходы на маршалинг в обе стороны, что не есть хорошо.
вместо FindGameObjectsWithTag используйте статическую коллекцию.

Ну однократный поиск объектов по тегу в Awake не так страшен, если инстансов таких компонентов не сотни. Но таки да, об этом нужно думать.
Math.Pow() в C#, емнип, работает быстрее, чем простое перемножение. Сам удивился, когда услышал.
И в моно тоже, на всех версиях?
Это далеко не так. Давным-давно делал скрин участка кода на память, производительность вырасла просто в разы. (из какого то класса к графическому движку, который часто вызывался)
Еще такая штука. Постоянно используется запрос к трансформу GameObject-а (ГО), типа такого:
Target.transform.position

Так делать не стоит, transform — это просто хелпер для поиска компонента Transform среди всех компонентов на ГО и его возврата каждый раз. Т.е. надо закешировать трансформ в локальную переменную и использовать ее во всем методе. То же самое можно сказать про transform.position, transform.forward / up / right — все эти хелперы вычисляются каждый раз при обращении к конкретному get-еру по всей иерархии ГО. Лечение аналогичное — кеширование хотя бы на уровне метода.
Именно так я и делал в одном из своих проектов.

private Transform myTransform;

private void Awake()
{
	myTransform = transform;
}
Вы уверены насчет position, forward и прочих? Насколько я понял, transform — это одно из полей, которые были созданы для упрощение получения доступа к наиболее часто используемым компонентам. Однако position и подобные уже не являются компонентами, следовательно поиск по иерархии тут неуместен, на мой, не очень осведомленный, взгляд.
Во-первых, transform (с маленькой буквы) это свойство.
Во-вторых, называть Transform (с большой буквы, класс) компонентом для доступа к другим — дичайшее кощунство. Transform олицетворяет собой матрицу трансформации и удобный интерфейс к ней.
В третьих, эти «часто используемые свойства» на самом деле обьявлены в UnityEngine.Component, а UnityEngine.Transform только наследует их. В Transform обьявлены только следующие свойства:

public int childCount { get; }
public Vector3 eulerAngles { get; set; }
public Vector3 forward {get; set; }
public Vector3 localEulerAngles {get;set;}
public Vector3 localPosition { get;set; }
public Quaternion localRotation { get;set; }
public Vector3 localScale{get;set;}
public Matrix4x4 localToWorldMatrix{get; }
public Vector3 lossyScale{get;}
public Transform parent{ get;set;}
public Vector3 position{ get;set;}
public Vector3 right{get;set;}
public Transform root{get;}
public Quaternion rotation{get;set;}
public Vector3 up{get;set; }
Боюсь, вы меня неправильно поняли, и, к сожалению, ответили на все кроме моего вопроса.
Я не говорил что Transform это компонент для доступа к другим — я лишь сказал что это компонент. Но в то же время, это, само собой разумеется, и класс тоже.
Так же, по-прежнему, свойство transform (именно свойство, не сам класс) было создано для того, чтобы не использовать GetComponent(Transform), насколько я понимаю. Правда, я не знаю, быстрее ли доступ к компоненту через это свойство (кешируется ли этот компонент?), или же это было сделано просто для более короткой и удобной записи.

Мой вопрос: сопоставима ли разница во времени при получении доступа к компоненту через свойство по отношению к получению доступа через поиск (GetComponent) и получение доступа к свойству компонента напрямую по отношению к получению через кешированную переменную — например:

myObject.GetComponent(Transform);
// по отношению к 
myObject.transform;

// и

var t : Transform = myObject.transform;
...
t.position.Set(4, 8, 15);

// по отношению к

var p : Vector3 = myObject.transform.position;
...
p.Set(4, 8, 15);
Первый вариант — абсолютно идентично. Второй вариант — нерабочий, Vector3 — это MarshalByValue, т.е. вернется копия (структура) положения, а не указатель на инстанс.
Тогда что имел в виду 6opoDuJIo, написав выше
То же самое можно сказать про transform.position, transform.forward / up / right (...) Лечение аналогичное — кеширование хотя бы на уровне метода.
?
Или это касается только unity-c#?
Вроде как это написал я :) transform.forward и прочие глобальные вектора — это не просто переменные, это пары Getter / Setter, выполняющие кучу вычислений при каждом запросе. Пример:
public Vector3 forward {
    get {
        return localToWorldMatrix.MultiplyPoint(Vector3.forward);
    }
    set {
    // Расчет обратной операции с коррекцией всего трансформа
    }
}

Т.е. если в своем методе постоянно обращаться напрямую к transform.forward, то это будет двойной фейл:
1. Обращение к transform. Надо сделать локальную переменную, в которую сделается один запрос:
var cachedTransform = transform;
// bla-bla-bla with cachedTransform

2. Обращение к forward. Надо сделать локальную переменную, в которую сделается один запрос:
var forward = cachedTransform.forward;

// bla-bla-bla with forward

Так понятнее?

Или это касается только unity-c#?

UnityScript транслируется в итоге все-равно в MSIL, причем местами ужасного качества (часть реализации скрывается удобными обертками, раздувающими код и снижающими скорость работы). Т.е. это не C#-фишка, это фишка .net-платформы.
Пример обертки на UnityScript, скрывающей саму суть свойства:
transform.position.x = 10;

Так можно писать, ибо внутри оно после трансляции соберется во что-то типа такого:
var pos = transform.position;
pos.x = 10;
transform.position = pos;

Т.е. для программера на UnityScript невдомек, что есть какое-то различие между свойством и полем — он может менять что захочет, компилятор UnityScript нафигачит кучу оберток и все будут довольны.
Понятно, спасибо за прояснение. И извините что спутал автора ^_^"
Умножение идёт на кватернион а не на матрицу — смотрите декомпил.
Абсолютно все-равно, что на что умножается. Суть в том, что это не бесплатно и внутреннего кеширования нет, т.к. ручной кеш дает хороший прирост производительности в любом случае.
>1. Обращение к transform. Надо сделать локальную переменную, в которую сделается один запрос:
Всё верно. Тут всё достаточно понятно.
В своём коде я добавил поле public Transform tr; в базовый класс для всех объектов, и инициализирую его в Awake(). Сначала сделал его приватным, но потом оказалось, что лучше его сделать публичным. И теперь у всех объектов есть уже инициализированное поле tr с кэшированной ссылкой на компонент. Удобно.

>2. Обращение к forward. Надо сделать локальную переменную, в которую сделается один запрос:
А это верно до тех пор, пока не поменяем rotation. Тогда transform.forward будет показывать на новое направление, а старое закэшированное значение
>var forward = cachedTransform.forward;
будет всё ещё показывать на старое направление. Но само обращение к cachedTransform.forward будет верным.
>>Я не говорил что Transform это компонент для доступа к другим
Постом выше:
>>Насколько я понял, transform — это одно из полей, которые были созданы для упрощение получения доступа к наиболее часто используемым компонентам.
И это:
>>Правда, я не знаю, быстрее ли доступ к компоненту через это свойство (кешируется ли этот компонент?)
Насколько мне известно, нет, компонент не кешируется. Мобильным разработчикам обычно советуют избегать обращения к свойствам и GetComponent именно поэтому.
>>Мой вопрос: сопоставима ли разница во времени при получении доступа к компоненту через свойство по отношению к получению доступа через поиск (GetComponent) и получение доступа к свойству компонента напрямую по отношению к получению через кешированную переменную
Обращение по ссылке — меньше одного тика, т.е. меньше 100 наносекунд. Не думаю что GetComponent быстрее.
Вот смотрите, если ГО, для которого надо узнать позицию в глобальных координатах, имеет 10 родителей, каждый из которых имеет свой трансформ, то для получения конечного трансформа нужно перемножить все матрицы по иерархии для получения конечной, по которой надо будет трансформировать локльную нулевую точку:
// Это псевдокод, реально не проверялся, но идея та же.

Matrix4x4 GetLocalToWorldMatrix() {
    var lastParent = transform;
    var mat = Matrix4x4.SetTRS(lastParent.localPosition, lastParent.localRotation, lastParent.localScale);
    while (lastParent != transform.parent) {
        var parent = transform.parent;
        mat = Matrix4x4.SetTRS(parent.localPosition, parent.localRotation, parent.localScale) * mat;
        lastParent = parent;
    }
}
Vector3 GetPosition() {
    return GetLocalToWorldMatrix().MultiplyPoint(transform.localPosition);
}

Vector3 GetForward() {
    return GetLocalToWorldMatrix().MultiplyPoint(Vector3.forward) - GetLocalToWorldMatrix().MultiplyPoint(transform.localPosition);
}

Теоретически, матрица должна кешироваться и считаться один раз, но ведь иерархию можно менять по 10 раз за фрейм, как и любой трансформ в иерархии и если каждый раз пересчитывать для всех дочерних объектов на автомате, то никаких мощностей не хватит, так что вряд ли оно кешируется. Т.е. есть уклон в сторону гибкости общей системы ценой потенциального падения скорости при незнании математики расчетов — все отдано на откуп пользователю.
Ступил :)
Vector3 GetPosition() {
    return GetLocalToWorldMatrix().MultiplyPoint(Vector3.zero);
}

Vector3 GetForward() {
    return GetLocalToWorldMatrix().MultiplyPoint(Vector3.forward) - GetPosition();
}
К чему это?
Внутренние значения rotation и position все-равно назначаются (и получаются) где-то в нативном коде. Но по идее, считаться должна только локальная матрица. По идее так: При рендере — локальная матрица умножается на матрицы всех обьектов выше по иерархии. Но можно и без этого, конечно, можно перемножить ещё при изменении иерархии. А изменение иерархии 10 раз за фрейм — это пшик. Плюс, не забывайте что эти значения (позиция и прочее) могут быть перерассчитаны только при обращении к ним.
p.s. Transform — это компонент, и вы его не конструируете из матрицы, это самостоятельный класс.
Transform — это компонент, и вы его не конструируете из матрицы, это самостоятельный класс.

Я просто показал математику, как оно может считаться.

По идее так: При рендере — локальная матрица умножается на матрицы всех обьектов выше по иерархии.

Нельзя — при апдейте оно уже должно быть актуальным для корректных расчетов юзвера, в этом и вся проблема с отсутствием встроенного кеша. Я могу перекраивать иерархию сотни ГО хоть каждый фрейм — это тяжело пересчитывать при изменении каждого Transform.parent.

Причем во время рендера оно либо еще раз пересчитывается, либо использует последние использованные данные после LastUpdate — это нужно для быстрого отсечения по Renderer.Bounds до посылки на визуализацию. Если поменять трансформы во время рендера, то они применятся на следующий фрейм, что говорит о кешировании данных на время рендера.
Ну и вдогонку — при батчинге юнити собирает свой внутренний меш (видно по измененным координатам, приезжающим в шейдер), собираемый как раз при анализе потенциально видимых Renderer-ов.
>> при апдейте оно уже должно быть актуальным для корректных расчетов юзвера
Значение позиции, вращения, значения localScale и lossyScale, Renderer.bounds инкапсулируются соответствующими им свойствами. Что происходит внутри — пёс его знает, ибо extern и [WrapperlessIcall].
К комменту выше — Transform.forward является произведением вращения rotation на Vector3.forward.
Transform.forward является произведением вращения rotation на Vector3.forward.

Ну так ведь rotation тоже надо посчитать и кешировать внутри нельзя — юзверь может двигать / поворачивать любой ГО из иерархии. Вполне возможно, что считается только LocalToWorld матрица, из которой уже декомпозицией извлекаются глобальные position, rotation (с переводом в кватернион), lossyScale.
Когда будет готово целиком, опубликуйте ссылку на скачивание.
К слову, старенькую TD Гений Обороны от Alawar считаю одной из лучших среди всех Tower Defence.
//Задаём позицию по умолчанию для камеры, здесь выставлена моя — меняйте под себя
public float DefaultCameraPosX = 888.0f;
public float DefaultCameraPosY = 50.0f;
public float DefaultCameraPosZ = 1414.0f;

Лучше будет сделать одно поле
public Transform initialPosition;

куда в редакторе выставить какой-нибудь объект-пустышку, служащий отправной точкой для камеры. Никаких магических чисел, всё наглядно и меняется мышкой в случае чего.
private void Update()
{
float smoothCamSpeed = CameraSpeed * Time.smoothDeltaTime; //множим скорость перемещения камеры на сглаженную версию Time.deltaTime

Насколько я понял, камера движется с постоянной скоростью. Обычно эффектнее выглядит плавное перемещение, для которого удобно использовать Vector3.SmoothDamp. Только с плавностью не надо перебарщивать, чтобы не выбесить игрока :)
Вот так бывает, выложил человек урок с целью научить других, а в итоге его самого учат best practices :)
Просто возможностей и путей по оптимизации в Unity3D очень много, я всех не знаю :)
Да я не в пику вам, просто отметил. Когда я пару лет назад тут пытался написать серию туториалов, такого фидбэка не было, хоть там и было к чему придраться. Растёт аудитория девелоперов на Юнити, вырабатываются эти самые best practices, приятно это видеть.
Для тех кто заинтересовался оптимизацией очень советую прочитать и проверить используете ли вы следующие 50 советов 50 Tips for Working with Unity (Best Practices)/. Очень помогло мне когда пришло время оптимизировать проект на Unity3d для мобилок особенно для андроида.
Магический костыль для кеширования transform. Меняем MonoBehaviour на CachingMonoBehaviour, и обращение к GetComponent при доступе к transform будет происходить всего один раз лениво:
public class CachingMonoBehaviour : MonoBehaviour {

    // Transform
    private Transform _cachedTransform;
    private bool _cachedTransformIsSet;

    public new Transform transform {
        get {
            if (!_cachedTransformIsSet) {
                _cachedTransform = GetComponent<Transform>();
                _cachedTransformIsSet = true;
            }
            return _cachedTransform;
        }
    }

}
Sign up to leave a comment.

Articles