Атомарные операции.
#threading
int i = 123; // всегда атомарноlong j = 234L; // атомарно на x64, но кому сейчас надо x32?i++; // никогда не атомарно, т.к. мы читаем данные, увеличиваем, а потом записываем#threading
👍9🥱7🤔1
Реализация
Мы заводим int поле и используем его в качестве идентификатора для операции блокирования. Другими словами, пока не будет вызван
#threading
lock, которую я использую в ME.BECSpublic static void Lock(ref int lockIndex) {
for (;;) {
if (System.Threading.Interlocked.Exchange(ref lockIndex, 1) == 0) {
break;
}
}
}
public static void Unlock(ref int lockIndex) {
System.Threading.Interlocked.Exchange(ref lockIndex, 0);
}Мы заводим int поле и используем его в качестве идентификатора для операции блокирования. Другими словами, пока не будет вызван
Unlock, второй поток не пройдет через Lock. Из минусов такого подхода - если ваш код между этими вызовами упадет по исключению, то все ожидающие потоки повиснут. Для этого я написал дополнение, которое выходит из цикла с ошибкой, если мы ждем слишком долго. Как я писал выше в одном из постов, лучше вообще избегать блокировки и Interlocked, но в редких случаях без них не обойтись.#threading
👍3
Рефлексия.
Можно определить отсутствие кода в методе, используя вызов
т.к. по сути там может быть 2 инструкции. Я использую этот метод только в редакторе, т.к. результат работы метода в билде будет зависеть от сборки.
#reflection
Можно определить отсутствие кода в методе, используя вызов
methodInfo.GetMethodBody().GetILAsByteArray()?.Length <= 2т.к. по сути там может быть 2 инструкции. Я использую этот метод только в редакторе, т.к. результат работы метода в билде будет зависеть от сборки.
#reflection
👍2🥴1👻1
Существует такой аттрибут
Метод будет компилироваться только если существует дефайн
#defines
Conditional, который дает возможность отключать/включать куски кода по дефайну.[Conditional("DEBUG")] void Method() {...}Метод будет компилироваться только если существует дефайн
DEBUG. Метод должен быть void.#defines
👍23🤔3🥱3🤯1
Многие встречали и даже использовали
#unsafe
memcpy. В юнити это UnsafeUtility.MemCpy. Но там есть еще UnsafeUtility.MemMove. Главное отличие в том, что memcpy работает быстрее, т.к. не делает дополнительных проверок на общую область памяти, а вот memmove делает. Проще говоря, не используйте memcpy, если два блока пересекаются, т.к. это приведет к неопределенному результату. Т.е. когда вы копируете данные из одного массива в другой - память никак не пересекается и можно использовать memcpy. Но если же вы хотите "подвинуть" данные в одном массиве, то используйте memmove.#unsafe
👍8
Дефайны можно объявлять не только в
Внутри можно делать ссылки на другие
#defines
ProjectSettings, но и в файлах csc.rsp. При этом эти файлы могут располагаться как в корне (В папке Assets), так и у каждой ассембли отдельно.Внутри можно делать ссылки на другие
csc.rsp, подробнее можно почитать тут: https://docs.unity3d.com/530/Documentation/Manual/PlatformDependentCompilation.html.#defines
Unity3D
Unity - Manual: Platform Dependent Compilation
The Unity Manual helps you learn and use the Unity engine. With the Unity engine you can create 2D and 3D games, apps and experiences.
👍24
Джобы.
Вы, наверное, слышали про
1. Если вы читаете трасформы и не меняете их - помечайте их как
2. Раскидайте элементы по нескольким рутам иерархии, группировав их по 64/128/256 штук. То есть если у вас на сцене в руте 10к
#jobs
Вы, наверное, слышали про
IJobParallelForTransform. Это такая джоба, которая с удовольствием даст доступ к массиву трасформов ваших GameObject. Но существует несколько интересных моментов, которые повлияют на производительность:1. Если вы читаете трасформы и не меняете их - помечайте их как
ReadOnly (Запускайте джобу через ScheduleReadOnly), это повлияет на выполнение джобы.2. Раскидайте элементы по нескольким рутам иерархии, группировав их по 64/128/256 штук. То есть если у вас на сцене в руте 10к
GameObject и вы хотите их подвигать, то создайте N пустых GameObject и вложите в них по 64/128/256 ваших объектов. Если этого не сделать, то ваша джоба будет всегда ожидаться главным потоком независимо от отсутствия Complete.#jobs
👍17🔥4
Используйте блоки кода вида
для того, чтобы избежать конфликтов локальных переменных и перестать выдумывать нечитаемые названия типа i, k, j, z etc 🙂
#code
{
// block 1
}
{
// block 2
}для того, чтобы избежать конфликтов локальных переменных и перестать выдумывать нечитаемые названия типа i, k, j, z etc 🙂
#code
👍19🥱4👎3🔥2😁1
Джобы.
Все знают, что есть
Т.е. указатель на массив элементов и количество этих самых элементов. Прочитаны эти данные будут только после того как все зависимости джобы будут завершены.
#jobs
Все знают, что есть
IJobParallelFor, мы туда передаем 2 параметра - количество элементов и сколько элементов будет попадать в одну итерацию. Но давайте представим ситуацию, что мы не знаем сколько элементов у нас будет (например, если у нас предыдущая джоба заполняет коллекцию). Таким образом мы не можем создать джобу. На самом деле для такого кейса существуют "отложенные джобы" ScheduleParallelForDeferArraySize. Чтобы завести такую джобу нужно передать указатель на Sequential-структуру, в которой первые 2 поля будут такие:T* list;int count;Т.е. указатель на массив элементов и количество этих самых элементов. Прочитаны эти данные будут только после того как все зависимости джобы будут завершены.
#jobs
👍16🔥1
Записывайте большие числа читаемо.
#code
const long SOME_CONST = 1000000000L; // пример плохой записи
const long SOME_CONST = 1_000_000_000L; // пример хорошей записи
#code
👍28👾3❤2🤬1💯1
Существует метод
#jobs
JobHandle.ScheduleBatchedJobs. Он позволяет "отправить" новую джобу на обработку. Если его не вызвать - джоба будет отправлена "когда-нибудь позже". Но не стоит его писать после шедулинга каждой джобы, т.к. это по сути форсированная отправка из локальной очереди в поток, а это занимает время.#jobs
👍5
Регаемся на мит, который мы проведем как и в прошлый раз в гугл мите:
https://unsafecsharp.timepad.ru/event/2439793/
Событие пройдет в эту субботу с 17:00 до 18:00 (в прошлый раз мы просидели полтора часа, так что имейте ввиду).
#event
https://unsafecsharp.timepad.ru/event/2439793/
Событие пройдет в эту субботу с 17:00 до 18:00 (в прошлый раз мы просидели полтора часа, так что имейте ввиду).
#event
👍18👎1
Существует возможность подсказать компилятору что метод должен быть заинлайнен, для этого нужно добавить аттрибут
При этом объявляю
Но нужно понимать, что аттрибут агрессинвного инлайна не гарантирует факт инлайна, он лишь подсказывает, что этот метод хорошо бы заинлайнить. Если дело касается хот частей, то лучше использовать "ручной инлайн", т.е. нужно переносить код самостоятельно.
#code
System.Runtime.CompilerServices.MethodImplAttribute(MethodImplOptions.AggressiveInlining). Я обычно пишу гораздо короче:[INLINE(256)]При этом объявляю
using INLINE = System.Runtime.CompilerServices.MethodImplAttribute.
Но нужно понимать, что аттрибут агрессинвного инлайна не гарантирует факт инлайна, он лишь подсказывает, что этот метод хорошо бы заинлайнить. Если дело касается хот частей, то лучше использовать "ручной инлайн", т.е. нужно переносить код самостоятельно.
#code
👍7🤔1
Как работает
Вы наверняка использовали эту структуру данных для хранения уникальных данных, но скорее всего вы слышали, что поиск в этой структуре занимает
Давайте разебермся откуда берется этот загадочный
Реализаций
У вас есть значение, которое вы хотите положить в коллекцию, допустим оно равно 12. Чтобы получить поиск элемента за
Но что если значение будет 1_000_000 или вообще отрицательным? Получается, что использовать массив таким образом мы уже не можем. Для этого давайте вызовем метод
Для того, чтобы решить эту задачу, давайте заведем массив "вёдер" или проще говоря "массив списков" (это для простоты понимания). Мы будем хранить элемент в одном из этих ведер (или buckets). Количество ведер изначально зададим, например, 10. Таким образом, чтобы засунуть число 12 в одно из этих бакетов, нам нужно найти остаток от деления 12 % 10 = 2. Вот и наше ведёрко найдено. А дальше мы выполняем операцию
Получается, что добавление элемента будет действительно
Но что же с поиском элемента?
Нетрудно догадаться, что если мы таким образом добавили элемент в ведро, то и найти его нужно таким же образом. Ищем остаток от деления, находим ведро, а потом ...ой. А потом нам нужно перебрать все элементы в ведре, чтобы найти нужный 🙂 И получается, что никаким
#datastructures #algorithms
HashSet?Вы наверняка использовали эту структуру данных для хранения уникальных данных, но скорее всего вы слышали, что поиск в этой структуре занимает
O(1), поэтому вы ее и используете.Давайте разебермся откуда берется этот загадочный
O(1) и каким образом он вообще получается.Реализаций
HashSet может быть много, но смысл остается примерно таким:У вас есть значение, которое вы хотите положить в коллекцию, допустим оно равно 12. Чтобы получить поиск элемента за
O(1), самое простое - это завести массив bool, где индекс будет равен этому числу, а значение true или false. Таким образом мы получаем супер-быстрое добавление элементов и супер-быструю проверку на существование.Но что если значение будет 1_000_000 или вообще отрицательным? Получается, что использовать массив таким образом мы уже не можем. Для этого давайте вызовем метод
GetHashCode(), который нам вернет hash от элемента (т.к. далеко не факт, что мы будет добавлять в коллекцию только числа, там же могут быть и структуры и классы). Таким образом получим некое число в любом случае. Но снова число может быть любым.Для того, чтобы решить эту задачу, давайте заведем массив "вёдер" или проще говоря "массив списков" (это для простоты понимания). Мы будем хранить элемент в одном из этих ведер (или buckets). Количество ведер изначально зададим, например, 10. Таким образом, чтобы засунуть число 12 в одно из этих бакетов, нам нужно найти остаток от деления 12 % 10 = 2. Вот и наше ведёрко найдено. А дальше мы выполняем операцию
buckets[index].Add(value), т.е. добавляем наш элемент в список.Получается, что добавление элемента будет действительно
O(1).Но что же с поиском элемента?
Нетрудно догадаться, что если мы таким образом добавили элемент в ведро, то и найти его нужно таким же образом. Ищем остаток от деления, находим ведро, а потом ...ой. А потом нам нужно перебрать все элементы в ведре, чтобы найти нужный 🙂 И получается, что никаким
O(1) тут и не пахнет. Но мы же знаем, мы же слышали. Мозг скорее всего уловил O(1), но не уловил слова типа "среднее" или "условное", что подразумевает, что в лучшем случае O(1), а в худшем O(n), что понятно, если мы будем добавлять разные истансы одного объекта в HashSet, но при этом метод GetHashCode будет возвращать нам всегда одно и то же число, например.#datastructures #algorithms
👍26❤2👎1🥱1
🥕Лайфхак
Если вам нужен сильный full-screen blur эффект, можно использовать буффер из нужной камеры и шейдер с
На практике мы такое используем для фона в UI, чтобы сильно "размывать" задний фон.
#shaders #blur
Если вам нужен сильный full-screen blur эффект, можно использовать буффер из нужной камеры и шейдер с
tex2Dlod для чтения текстуры маленького размера, тем самым будет казаться, что размытие достигается шейдером, хотя на самом деле этот эффект будет от текстуры низкого разрешения + фильтрации.На практике мы такое используем для фона в UI, чтобы сильно "размывать" задний фон.
#shaders #blur
👍30🔥9🤔4
Немного про аллокации и кэш.
Самый простой пример аллокаций - это замыкание.
Тут остановимся подробнее. Где могут скрываться аллокации, например, в такой конструкции:
Очевидно, что при вызове ToArray будет создан массив. Больше никаких аллокаций в данном примере не будет. Давайте рассмотрим второй пример:
Тут к предыдущей аллокации добавляется еще замыкание с переменной
Но давайте представим, что мы пишем свой замечательный Linq. Каким образом можно избежать аллокации? Нужно сделать просто передачу параметра:
Т.е. чтобы избежать аллокаций в замыканиях, нужно передавать все используемые параметры.
Аллокации при боксинге. Боксинг (boxing) - это фактически создание
При обратном процессе (unboxing) этого не происходит.
Вообще аллокации в куче плохи тем, что они не очень cache-friedly. Но иногда нам нужно сделать аллокации. Представим ситуацию, когда мы аллоцируем массив с нодами, где у каждой ноды есть еще список нод:
Обычно я встречал запись именно такую. Чем она плоха? Тем, что мы аллоцируем первую ноду, и сразу в ноде аллоцируем еще один объект (а может и несколько). Т.е. мало того, что куча в принципе не очень cache-friedly, так мы отказываемся от кэша совсем, создавая объекты таким образом. Как нужно было бы поступить?
Таким образом, если мы не готовы писать cache-friedly код в целом, то хотя бы частично небольшими изменениями мы можем постараться помочь
#gc #allocations #cache
Самый простой пример аллокаций - это замыкание.
Тут остановимся подробнее. Где могут скрываться аллокации, например, в такой конструкции:
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 не сразу стало возможно создавать свои джобы, просто апи было не в публичном доступе. Сейчас же это возможно и давайте разберемся как это делать.
Для начала определимся что же мы хотим сделать. Давайте возьмем самый простой вариант и напишем аналог
Т.е. мы хотим сделать стандартную однопоточную джобу (многопточные делаются не сильно дольше), что нам для этого необходимо:
1. Интерфейс и скелет
2. Инициализация джобы
3. Сама джоба
С интерфейсом все просто:
https://pastebin.com/525gx464
Обратите внимание, что
Далее, иницилизация джобы. Мы должны вызвать сообщить шедуллеру, что в принципе у нас есть чего крутить. Для того, чтобы это заработало, нам нужно объявить инициализацию и вызвать ее в методе Schedule:
https://pastebin.com/rZ4GCbYx
Ну и финально - мы добавляем в наш Execute само выполнение:
https://pastebin.com/vKhUmDKX
#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
🥕Из собеса
Я на собеседованиях спрашиваю простой вопрос:
Почему нельзя написать
Ответы бывают разные от банального "не знаю" и "ну юнити не дает" до единственно верного 🙂
Вообще довольно забавно, что мало кто из разрабов в принципе вникает в суть происходящего, а она банальна:
#interview #code
Я на собеседованиях спрашиваю простой вопрос:
Почему нельзя написать
transform.position.y = 123f;?Ответы бывают разные от банального "не знаю" и "ну юнити не дает" до единственно верного 🙂
Вообще довольно забавно, что мало кто из разрабов в принципе вникает в суть происходящего, а она банальна:
transform.position - это getter, а get - это метод. А Vector3 - это структура. Вот и получается, что при вызове get мы делаем копию структуры и пытаемся ее менять. А это очевидно, что не приведет ни к чему.#interview #code
👍25😁8👎2😘2😢1
О том как мы траву сначала нарисовали, а затем сожгли 🙂
У нас была задача нарисовать траву, много травы, которая шевелится, когда юниты по ней ходят, а еще можно в нее бабахнуть бомбочкой и она сгорит, а если рядом будет еще трава, то она загорится и т.д. 🙂
У нас 2д-игра, карту мы рисуем тайлами, траву мы тоже рисуем тайлами, но на специальной тайлмапе.
Во время загрузки игры, эта тайлмапа выключается, а каждая нарисованная травинка превращается в партикл и рисуется уже ParticleSystem 🙂 Т.е. для контенщика ничего не меняется в его пайплайне, а для рендера становится все гораздо лучше.
Как шевелить. Тут мой любимый способ: добавляем камеру, которая снимает исключительно юнитов (можно сетить пиксель в текстуру - не принципиально). Текстура - это вся карта. Дальше в шейдере травы читаем из текстуры пиксель и применяем с этой силой шевеление вертексов (чем выше вертекс, тем больше он шевелится и т.д.).
Как поджигать. Для того, чтобы решить задачу с поджиганием, нам нужно построить граф, мы это делаем на этапе загрузки карты, где каждая травинка знает о соседях (по дистанции). Для того, чтобы найти быстро нужную ноду - у нас есть выключенная тайлмапа, которая даст нам индекс в массиве из мировой точки.
Когда травинка загорается - через X секунд горения она поджигает всех своих соседей, если они еще не горят. Когда травинка сгорает - она оставляет след, который тоже пропадает через какое-то время.
Ну и да, горящая трава убивает юнитов 🙂
#shaders #graphs #code #2d
У нас была задача нарисовать траву, много травы, которая шевелится, когда юниты по ней ходят, а еще можно в нее бабахнуть бомбочкой и она сгорит, а если рядом будет еще трава, то она загорится и т.д. 🙂
У нас 2д-игра, карту мы рисуем тайлами, траву мы тоже рисуем тайлами, но на специальной тайлмапе.
Во время загрузки игры, эта тайлмапа выключается, а каждая нарисованная травинка превращается в партикл и рисуется уже ParticleSystem 🙂 Т.е. для контенщика ничего не меняется в его пайплайне, а для рендера становится все гораздо лучше.
Как шевелить. Тут мой любимый способ: добавляем камеру, которая снимает исключительно юнитов (можно сетить пиксель в текстуру - не принципиально). Текстура - это вся карта. Дальше в шейдере травы читаем из текстуры пиксель и применяем с этой силой шевеление вертексов (чем выше вертекс, тем больше он шевелится и т.д.).
Как поджигать. Для того, чтобы решить задачу с поджиганием, нам нужно построить граф, мы это делаем на этапе загрузки карты, где каждая травинка знает о соседях (по дистанции). Для того, чтобы найти быстро нужную ноду - у нас есть выключенная тайлмапа, которая даст нам индекс в массиве из мировой точки.
Когда травинка загорается - через X секунд горения она поджигает всех своих соседей, если они еще не горят. Когда травинка сгорает - она оставляет след, который тоже пропадает через какое-то время.
Ну и да, горящая трава убивает юнитов 🙂
#shaders #graphs #code #2d
👍22🔥14🤯6