Unity: Всё, что вы не знали о разработке – Telegram
Unity: Всё, что вы не знали о разработке
1.74K subscribers
40 photos
101 links
Авторский канал о разработке в Unity от Alex Silaev (CTO в Zillion Whales). Mushroom Wars 2 моих рук дело.
Рассказываю об интересный кейсах, делюсь лайфхаками, решениями.
Download Telegram
В юнити можно использовать символ ~ в конце имени для исключения папки или файла. В таком случае скрипты в этой папке не будут компилироваться, а ассеты не будут импортироваться. Иногда бывает полезно.

#editor #assets
👍30🔥7🥱3🤯2😎1👾1
Немного про аллокации и кэш.

Самый простой пример аллокаций - это замыкание.
Тут остановимся подробнее. Где могут скрываться аллокации, например, в такой конструкции:
list.Where(x => x > 10).ToArray()
Очевидно, что при вызове ToArray будет создан массив. Больше никаких аллокаций в данном примере не будет. Давайте рассмотрим второй пример:

var a = 10;
list.Where(x => x > a).ToArray()

Тут к предыдущей аллокации добавляется еще замыкание с переменной a. Проблема в том, что с этим мы ничего поделать не можем.
Но давайте представим, что мы пишем свой замечательный Linq. Каким образом можно избежать аллокации? Нужно сделать просто передачу параметра:

public static void Where<TClosure, T>(this List<T> list, TClosure closure, System.Func<T, TClosure, bool> where) {
if (where.Invoke(list[i], closure) == false) ...
}

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

Аллокации при боксинге. Боксинг (boxing) - это фактически создание ValueType в куче. Это довольно затратный процесс сам по себе, но самое главное - это тот факт, что когда-нибудь GC об этом вам напомнит.
При обратном процессе (unboxing) этого не происходит.

Вообще аллокации в куче плохи тем, что они не очень cache-friedly. Но иногда нам нужно сделать аллокации. Представим ситуацию, когда мы аллоцируем массив с нодами, где у каждой ноды есть еще список нод:

var arr = new Node[1000];
for (int i = 0; i < arr.Length; ++i) {
arr[i] = new Node() {
nodes = new List<Node>(),
};
}

Обычно я встречал запись именно такую. Чем она плоха? Тем, что мы аллоцируем первую ноду, и сразу в ноде аллоцируем еще один объект (а может и несколько). Т.е. мало того, что куча в принципе не очень cache-friedly, так мы отказываемся от кэша совсем, создавая объекты таким образом. Как нужно было бы поступить?

var arr = new Node[1000];
for (int i = 0; i < arr.Length; ++i) {
arr[i] = new Node(); // Создаем ноды, чтобы GC постарался их разместить в памяти последовательно
}
for (int i = 0; i < arr.Length; ++i) {
arr[i].nodes = new List<Node>(); // Инициализируем данные каждой ноды
}

Таким образом, если мы не готовы писать cache-friedly код в целом, то хотя бы частично небольшими изменениями мы можем постараться помочь GC.

#gc #allocations #cache
👍25🔥9🕊1
После выхода Unity Jobs не сразу стало возможно создавать свои джобы, просто апи было не в публичном доступе. Сейчас же это возможно и давайте разберемся как это делать.
Для начала определимся что же мы хотим сделать. Давайте возьмем самый простой вариант и напишем аналог IJob:


public struct Job : IJobSingle {
public void Execute() {
}
}


Т.е. мы хотим сделать стандартную однопоточную джобу (многопточные делаются не сильно дольше), что нам для этого необходимо:
1. Интерфейс и скелет
2. Инициализация джобы
3. Сама джоба

С интерфейсом все просто:
https://pastebin.com/525gx464

Обратите внимание, что JobProducerType говорит нам о том, что этот интерфейс не абы кто, а это интерфейс великой джобы;)

Далее, иницилизация джобы. Мы должны вызвать сообщить шедуллеру, что в принципе у нас есть чего крутить. Для того, чтобы это заработало, нам нужно объявить инициализацию и вызвать ее в методе Schedule:
https://pastebin.com/rZ4GCbYx

Ну и финально - мы добавляем в наш Execute само выполнение:
https://pastebin.com/vKhUmDKX

#jobs
👍14
🥕Из собеса
Я на собеседованиях спрашиваю простой вопрос:
Почему нельзя написать transform.position.y = 123f;?
Ответы бывают разные от банального "не знаю" и "ну юнити не дает" до единственно верного 🙂
Вообще довольно забавно, что мало кто из разрабов в принципе вникает в суть происходящего, а она банальна:
transform.position - это getter, а get - это метод. А Vector3 - это структура. Вот и получается, что при вызове get мы делаем копию структуры и пытаемся ее менять. А это очевидно, что не приведет ни к чему.

#interview #code
👍25😁8👎2😘2😢1
О том как мы траву сначала нарисовали, а затем сожгли 🙂

У нас была задача нарисовать траву, много травы, которая шевелится, когда юниты по ней ходят, а еще можно в нее бабахнуть бомбочкой и она сгорит, а если рядом будет еще трава, то она загорится и т.д. 🙂

У нас 2д-игра, карту мы рисуем тайлами, траву мы тоже рисуем тайлами, но на специальной тайлмапе.
Во время загрузки игры, эта тайлмапа выключается, а каждая нарисованная травинка превращается в партикл и рисуется уже ParticleSystem 🙂 Т.е. для контенщика ничего не меняется в его пайплайне, а для рендера становится все гораздо лучше.

Как шевелить. Тут мой любимый способ: добавляем камеру, которая снимает исключительно юнитов (можно сетить пиксель в текстуру - не принципиально). Текстура - это вся карта. Дальше в шейдере травы читаем из текстуры пиксель и применяем с этой силой шевеление вертексов (чем выше вертекс, тем больше он шевелится и т.д.).

Как поджигать. Для того, чтобы решить задачу с поджиганием, нам нужно построить граф, мы это делаем на этапе загрузки карты, где каждая травинка знает о соседях (по дистанции). Для того, чтобы найти быстро нужную ноду - у нас есть выключенная тайлмапа, которая даст нам индекс в массиве из мировой точки.
Когда травинка загорается - через X секунд горения она поджигает всех своих соседей, если они еще не горят. Когда травинка сгорает - она оставляет след, который тоже пропадает через какое-то время.

Ну и да, горящая трава убивает юнитов 🙂

#shaders #graphs #code #2d
👍22🔥14🤯6
Как устроен MemoryAllocator в ME.ECS/ME.BECS

MemoryAllocator проще всего представить как один большой неразрывный массив байт.
Чтобы положить туда данные - нужно всего лишь знать по какому индексу это делать.
Для этого аллокатор разбивается на блоки. В пустом аллокаторе блок всего один, он занимает всю область памяти от начала и до конца.
Блок - это структура, у которой есть часть заголовка (с указателями на следующий/предыдущий блоки, состоянием "свободен"/"занят" и размером блока) и следом сами данные.

[block_size][state][prev][next][user_data]

Когда мы просим аллокатор дать нам память определенного размера, нам нужно найти свободный блок памяти. Тут мы просто переходим от первого блока до последнего и ищем блок подходящего размера. Если блок не нашли - добавляем новый. А возвращаем мы не unsafe-указатель, а собственный указатель, в котором записан тот самый индекс в нашем массиве байт.
Когда мы просим освободить память, мы выставляем блоку состояние "свободен" и мерджим его с соседними свободными блоками, если такие есть.

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

Таким образом мы получаем следующие бенефиты:
1. Большой кусок памяти, который мы можем скопировать/передать по сети/уничтожить очень быстро;
2. Выдаваемые указатели можно так же передавать по сети, т.к. они будут валидны на любом клиенте, если тот имеет такой же аллокатор;
3. Нам не нужно заботиться об уничтожении данных, достаточно просто убить весь аллокатор.

Реализацию можно посмотреть тут:
https://github.com/chromealex/csharp-memory-allocator

#memoryallocator #code #allocator #unsafe
👍191
Все мы знаем, что в Unity Profiler можно включить Deep и посмотреть что там вообще происходит в ваших методах.
Проблема только в том, что в больших проектах это тормозит, занимает кучу времени, а в профайлере нужно долго потом искать проблемные места.
Для этого можно использовать вот такую конструкцию:

var marker = new ProfilerMarker("My Marker");
marker.Begin();
// тут код, который мы хотим проверить в профайлере
marker.End();

Более того, этот маркер можно использовать в Burst.

#profiling #code
👍474🔥4
В Unity добавили аттрибут HideInCallstack, который пока не работает 🙂 Но использовать его уже можно, когда заработает - тогда и заработает.

https://docs.unity3d.com/ScriptReference/HideInCallstackAttribute.html

#unity #code #logs #attributes
😁23👍5🤣3🐳1🤨1👀1
false sharing - это ситуация, когда несколько потоков одновременно обращаются к разным переменным, которые находятся в одной кеш-линии. Кеш-линия - это минимальная единица данных, которая копируется из оперативной памяти в кеш процессора.

Когда несколько потоков работают с переменными, расположенными в одной кеш-линии, происходит постоянный обмен этой кеш-линией между кешами потоков, что приводит к накладным расходам на копирование кеш-линии из оперативной памяти в кеш и обратно. Это может существенно ухудшить производительность программы.

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

#threading #cache
👍12🤯21
Как устроен QuadTree?
QuadTree - это структура данных, которая используется для разбиения двумерного пространства на более мелкие области. Каждый узел дерева представляет собой квадратную область (ячейку) внутри основной области. Если ячейка слишком большая, то она разбивается на четыре одинаковых подъячейки, каждая из которых может быть либо пустой, либо содержать объекты.

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

Для добавления объектов в QuadTree необходимо сперва знать, какой ячейке они принадлежат. Каждый объект добавляется в самую мелкую ячейку, которая полностью охватывает его. Если ячейка становится слишком заполненной, то она разбивается на более мелкие. Проще говоря, нужно делать rect.Contains() несколько раз, чтобы понять куда отнести элемент.

Поиск объектов в QuadTree осуществляется путем перебора узлов дерева. Начиная с корня, мы проверяем, находится ли ячейка, в которой мы ищем объект, в пределах текущей ячейки узла. Если да, то мы переходим к следующему уровню дерева и продолжаем поиск в ячейках потомков. Если нет, то мы переходим к следующему соседнему узлу.

Преимущества QuadTree заключаются в том, что он позволяет быстро находить объекты в двумерном пространстве, а также быстро выполнять операции вставки и удаления объектов. Недостатком может быть то, что при неправильном выборе размера ячеек и глубины дерева, может возникнуть слишком большая структура, что негативно скажется на производительности.

На практике мы такое используем для поиска целей для атаки.

Существует еще и Octree для 3D, алгоритм работы по сути ничем не отличается.

#datastructures #algorithms
👍22
Ребят, хотел бы порекомендовать личный блог моего товарища, которого наверняка многие знают;)

Задачи для Unity разработчиков, актуальные новости игровой разработки, авторские мысли. Всё это есть в блоге Григория Дядиченко.

Если вам интересна Unity разработка, виртуальная и дополненная реальность, фриланс или аутсорс бизнес — подписывайтесь на канал!
🔥112👍1
Я уже писал о том, что можно контролировать партиклы из кода. Но я не написал о том, что можно одним вызовом Emit запустить партикл систему, и на каждый такой вызов будет воспроизводиться система.

Для этого нужно указать sub emitter у основной партикл системы, а все модули основной системы отключить. Мы это использовали для поджигания травы (Кто не видел пост - https://news.1rj.ru/str/unsafecsharp/48), чтобы нарисовать огонь для каждой травинки.

Да, весь прикол в том, что вся эта радость будет рисоваться в 1 DrawCall, т.к. партикл система знает все, что нужно о своих подсистемах, за что ей отдельное спасибо.

В итоге мы контролируем огонь через Emit + GetParticles/SetParticles, а что там за огонь - это уже vfxер нарисует, настраивая обычную систему. Имейте ввиду, что ограничение в подсистемах на maxParticles должен быть расчитан на все системы, а не на одну.

#particles #code #performance
👍20👏1
Немного про код-стайл.

Лет 5-6 назад я встретил в одном из плагинов facebook следующую запись:

public some::Item Method() {
...
}



Я особо не задумывался зачем оно надо, но потом я сформулировал идею: использовать :: между using-сокращением и типом, т.е. когда мы пишем using someVar = Some.Type;
Тогда в коде будет

someVar::Example


Позже я стал замечать примерно такую же концепцию в разных фреймах:

global::SomeClass.someVar


#code #codestyle
👍4
Немного занимательного Burst и векторизации

Мы хотим выполнить несколько итераций (jobCount) и внутри каждой итерации еще по 128 итерации. Внутри всего этого мы хотим посчитать сумму из NativeArray input и положить в массив NativeArray arrSum:

for (int j = 0; j < jobCount; ++j) {
for (int i = 0; i < 128; ++i) {
var sum = arrSum[i];
sum += input[(j * 128) + i];
arrSum[i] = sum;
}
}

Как видно из примера, массив input намного больше, чем arrSum. Мы думаем-думаем и решаем оптимизировать наш код. Получается примерно следующее:


for (int i = 0; i < 128; ++i) {
var sum = arrSum[i];
for (int j = 0; j < jobCount; ++j) {
sum += input[(j * 128) + i];
}
arrSum[i] = sum;
}

Т.е. мы поменяли местами, т.к. и ежу понятно, что чем меньше мы обращаемся к массиву, тем быстрее должно работать.

Пример на самом деле плохой, но показывает нам, что нужно проверять код в Burst Inspector.
И да, первый вариант векторизуется и будет работать быстрее.

#burst #vectorization #performance
👍6🤯42
Используйте битмаски. Я редко встречаю код с битмасками, уж не знаю почему, но в основном люди предпочитают обходить их стороной. Разбираемся, ведь в них нет ничего сложного.

Битовые маски могут использоваться для манипулирования отдельными битами в числе. Это может быть полезно, например, чтобы проверить, является ли определенный бит установленным или снятым.


int value = 0b1011; // ставим дефолтное значение

// проверка, является ли второй бит установленным
if ((value & (1 << 1)) != 0) {
// бит установлен
}

// установка третьего бита
value |= (1 << 2); // теперь value == 0b1111

// сброс третьего бита
value &= ~(1 << 2); // теперь value == 0b1011


Можно использовать биты в enum, записывать их можно по-разному:

enum MyEnum {
None = 0,
Value1 = 1 << 0,
Value2 = 1 << 1,
Value3 = 1 << 2,
Value4 = 1 << 3,
Value5 = 1 << 4,
Value1OrValue3 = Value1 | Value3,
}

enum MyEnum {
None = 0,
Value1 = 0x1,
Value2 = 0x2,
Value3 = 0x4,
Value4 = 0x8,
Value5 = 0x10,
Value1OrValue3 = Value1 | Value3,
}

Записи эквиваленты друг другу, я встречал оба варианта.

Для вывода можно использовать аттрибут System.Flags, но он не является обязательным, хотя влияет на отображение в Unity Inspector и на вывод в лог.

#bitmask #code
👍22🔥1
Давайте теперь про Layout структур.

Давайте представим, что у нас есть 2 структуры:

public struct V3 {
public float3 x;
}

public struct V4 {
public float4 x;
}


А теперь напишем джобу, которая перекладывает данные из одного массива в другой:

[BurstCompile]
public struct MyJob : IJob {
[ReadOnly] public NativeArray<V3/V4> source;
public NativeArray<V3/V4> dest;
public void Execute() {
for (int i = 0; i < source.Length; ++i) {
dest[i] = source[i];
}
}
}


Какой вариант джобы будет работать быстрее?
Логика подсказывает нам, что V3, т.к. данных копировать нужно меньше, да и вообще размер будет намного меньше.
Давайте разберемся же, что там получается на выходе:
Для V3 варианта мы должны скопировать структуру значение за значением, т.е. 3 раза.
Для V4 варианта мы вроде должны скопировать 4 значения. Но тут вламывается векторизация и выходит, что вариант V4 будет работать примерно на треть быстрее, чем вариант V3.
Но не расстраивайтесь, можно все исправить: (да, можно исправить разными способами)

public struct V3 {
public float3 x;
public float _;
}


#burst #struct #structlayout #alignment #simd
👍11🤯91
Мы знаем, что структуры нужно инициализировать в конструкторе полностью:


struct MyStruct {
public int field1;
public int field2;
...
public int fieldN;
public MyStruct(int field1) {
this.field1 = field1;
// тут нужно инициализировать все поля
this.field2 = default;
...
this.fieldN = default;
}
}


Иногда полей много и можно написать гораздо короче:

struct MyStruct {
public int field1;
public int field2;
...
public int fieldN;
public MyStruct(int field1) {
this = default;
this.field1 = field1;
}
}


#structs #lifehack
👍44🤯20🔥7
Для дебага полезная штука - написать свой Proxy:

[System.Diagnostics.DebuggerTypeProxyAttribute(typeof(DebugClass))]
public class YourClass { ... }


Где DebugClass - это отдельный класс, который может содержать геттеры и поля. Еще у него должен быть конструктор, который будет принимать инстанс YourClass.
Это сильно помогает при дебаге сложных штук.

#debug #code
👍12🤔2🔥1
Про CC коллекции.

CC (`Concurrent Collections`) коллекции - это набор коллекций данных, разработанных для работы в многопоточной среде. Одной из особенностей CC коллекций является их lock-free (без блокировок) реализация, которая позволяет не блокировать весь многопоточный поток при обращении к коллекции.

Все CC коллекции стараются обходиться без lock, т.е. в нормальном режиме работы - либо вообще без lock, либо в редких исключениях его использование.
Давайте разберем простой пример, чтобы было понятно как именно работают такие коллекции.
Допустим, что нам нужно написать коллекцию Stack<> (возьмем самую простую). В однопоточной реализации мы используем массив элементов + индекс, который говорит нам где мы находимся в данный момент. При Push мы просто кладем элемент по индексу и увеличиваем индекс, а при Pop просто уменьшаем индекс. Ну еще при Push нам нужно проверить размер массива и сделать новый, если это нужно.

А теперь в многопоточность.
Как реализовать такую коллекцию? Давайте не будем вообще создавать никаких массивов, а будем использовать односвязный список из нод. Node - это объект, который имеет указатель на предыдущий элемент и данные внутри себя.
Коллекция же имеет только ссылку на head-ноду. При добавлении элемента нам нужно создать ноду и каким-то образом ее запихнуть к последней, используем Interlocked.CompareExchange и заменяем head на наш элемент. При Pop делаем обратную операцию.

#multithreading #threading #collections #lockfree
👍12🤯2
🥕Задачка
У вас есть строка вида «aabbbccde»
Вам нужно написать алгоритм, который уберет все дубликаты символов, которые стоят вместе, то есть останется только «abcde». Для строки «aabbccaabb» ответ будет «abcab».
Нужен алгоритм, использовать всякие regex нельзя;)
🥱10👍3🤔2