Я уже писал о том, что можно контролировать партиклы из кода. Но я не написал о том, что можно одним вызовом
Для этого нужно указать sub emitter у основной партикл системы, а все модули основной системы отключить. Мы это использовали для поджигания травы (Кто не видел пост - https://t.iss.one/unsafecsharp/48), чтобы нарисовать огонь для каждой травинки.
Да, весь прикол в том, что вся эта радость будет рисоваться в 1 DrawCall, т.к. партикл система знает все, что нужно о своих подсистемах, за что ей отдельное спасибо.
В итоге мы контролируем огонь через
#particles #code #performance
Emit запустить партикл систему, и на каждый такой вызов будет воспроизводиться система.Для этого нужно указать sub emitter у основной партикл системы, а все модули основной системы отключить. Мы это использовали для поджигания травы (Кто не видел пост - https://t.iss.one/unsafecsharp/48), чтобы нарисовать огонь для каждой травинки.
Да, весь прикол в том, что вся эта радость будет рисоваться в 1 DrawCall, т.к. партикл система знает все, что нужно о своих подсистемах, за что ей отдельное спасибо.
В итоге мы контролируем огонь через
Emit + GetParticles/SetParticles, а что там за огонь - это уже vfxер нарисует, настраивая обычную систему. Имейте ввиду, что ограничение в подсистемах на maxParticles должен быть расчитан на все системы, а не на одну.#particles #code #performance
Telegram
Unity: Всё, что вы не знали о разработке
О том как мы траву сначала нарисовали, а затем сожгли 🙂
У нас была задача нарисовать траву, много травы, которая шевелится, когда юниты по ней ходят, а еще можно в нее бабахнуть бомбочкой и она сгорит, а если рядом будет еще трава, то она загорится и т.д.…
У нас была задача нарисовать траву, много травы, которая шевелится, когда юниты по ней ходят, а еще можно в нее бабахнуть бомбочкой и она сгорит, а если рядом будет еще трава, то она загорится и т.д.…
👍20👏1
Немного про код-стайл.
Лет 5-6 назад я встретил в одном из плагинов facebook следующую запись:
Я особо не задумывался зачем оно надо, но потом я сформулировал идею: использовать
Тогда в коде будет
Позже я стал замечать примерно такую же концепцию в разных фреймах:
#code #codestyle
Лет 5-6 назад я встретил в одном из плагинов facebook следующую запись:
public some::Item Method() {
...
}
Я особо не задумывался зачем оно надо, но потом я сформулировал идею: использовать
:: между using-сокращением и типом, т.е. когда мы пишем using someVar = Some.Type;Тогда в коде будет
someVar::Example
Позже я стал замечать примерно такую же концепцию в разных фреймах:
global::SomeClass.someVar
#code #codestyle
👍4
Используйте битмаски. Я редко встречаю код с битмасками, уж не знаю почему, но в основном люди предпочитают обходить их стороной. Разбираемся, ведь в них нет ничего сложного.
Битовые маски могут использоваться для манипулирования отдельными битами в числе. Это может быть полезно, например, чтобы проверить, является ли определенный бит установленным или снятым.
Можно использовать биты в
Записи эквиваленты друг другу, я встречал оба варианта.
Для вывода можно использовать аттрибут
#bitmask #code
Битовые маски могут использоваться для манипулирования отдельными битами в числе. Это может быть полезно, например, чтобы проверить, является ли определенный бит установленным или снятым.
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
Для дебага полезная штука - написать свой Proxy:
Где
Это сильно помогает при дебаге сложных штук.
#debug #code
[System.Diagnostics.DebuggerTypeProxyAttribute(typeof(DebugClass))]
public class YourClass { ... }
Где
DebugClass - это отдельный класс, который может содержать геттеры и поля. Еще у него должен быть конструктор, который будет принимать инстанс YourClass.Это сильно помогает при дебаге сложных штук.
#debug #code
👍12🤔2🔥1
Используйте указание на конкретный тип в enum, если вы используете меньше, чем int:
Таким образом:
Будет запаковано как 4 байта. Но не забывайте, что любая математика с enum приводит к int 🙂
#enum #code #structlayout
enum MyEnum : byte {
Value1,
Value2,
Value3,
Value4,
}
Таким образом:
struct Test {
public MyEnum e1;
public MyEnum e2;
public MyEnum e3;
public MyEnum e4;
}
Будет запаковано как 4 байта. Но не забывайте, что любая математика с enum приводит к int 🙂
#enum #code #structlayout
👍19🔥4
Рандом
Мы знаем, что существует такой метод, который нам вернет случаное число (псевдослучайное, если точнее).
Итак, обычно это выглядит как-нибудь так:
То есть есть некий класс рандома, который что-то делает и на выходе выдает "случайное" число. Как это работает?
На самом деле все довольно прозаично. Для того, чтобы выдать "случайное число", нужно знать некое другое число (или
Т.е. мы фактически в каждый момент времени уже имеем случайное число - это время.
А теперь каким образом работает этот самый
Random внутри себя хранит
Т.е. сейчас мы при каждом обращении к NextValue к seed прибавляем некое число, изменяя этот самый seed таким образом, чтобы при следующем обращении нам выдали уже другое число.
На этом месте можно рассмотреть вариант
По сути мы сдвигаем значение seed, получая "рандомное" число. Отсюда, кстати, и ограничение, что seed не может быть 0, т.к. чего бы мы там не двигали 0 всегда останется нулем.
Это я все к тому, что такой рандом можно передавать по сети и "откатывать", т.к. оно всегда будет выдавать некую последовательность чисел, основанную на этом самом
то на другом клиенте мы точно так же получим эти же числа в этой же последовательности. Т.е. чтобы "откатиться" на какое-то состояние рандома назад, нужно всего лишь знать его seed в тот момент времени.
Ремарка: Алгоритмов рандома множество, некоторые легко предугадать, другие - сложнее, но качество функций рандома сводится к тому, чтобы выдавать равномерное распределение чисел, и чем больше оно равномерно, тем лучше считается функция рандома.
#random #code #algorithms
Мы знаем, что существует такой метод, который нам вернет случаное число (псевдослучайное, если точнее).
Итак, обычно это выглядит как-нибудь так:
var rnd = new Random();
var value = rnd.NextValue();
То есть есть некий класс рандома, который что-то делает и на выходе выдает "случайное" число. Как это работает?
На самом деле все довольно прозаично. Для того, чтобы выдать "случайное число", нужно знать некое другое число (или
seed). Обычно дефолтное значение этого самого seed берется из тиков, времени, да чего угодно положительного.Т.е. мы фактически в каждый момент времени уже имеем случайное число - это время.
А теперь каким образом работает этот самый
rnd.NextValue()?Random внутри себя хранит
seed + в зависимости от реализации рандома может хранить другие параметры. Мне нравится больше всего самая простая реализация только с seed:
struct Random {
uint seed;
uint NextValue() {
var next = this.seed;
this.seed += 123;
return next;
}
}
Т.е. сейчас мы при каждом обращении к NextValue к seed прибавляем некое число, изменяя этот самый seed таким образом, чтобы при следующем обращении нам выдали уже другое число.
На этом месте можно рассмотреть вариант
Unity.Mathematics:
uint next = seed;
seed ^= seed << 13;
seed ^= seed >> 17;
seed ^= seed << 5;
return next;По сути мы сдвигаем значение seed, получая "рандомное" число. Отсюда, кстати, и ограничение, что seed не может быть 0, т.к. чего бы мы там не двигали 0 всегда останется нулем.
Это я все к тому, что такой рандом можно передавать по сети и "откатывать", т.к. оно всегда будет выдавать некую последовательность чисел, основанную на этом самом
seed. Т.е. взяв на одном клиенте 5 чисел, начиная с seed = 5: 5, 15, 60, 70, 1то на другом клиенте мы точно так же получим эти же числа в этой же последовательности. Т.е. чтобы "откатиться" на какое-то состояние рандома назад, нужно всего лишь знать его seed в тот момент времени.
Ремарка: Алгоритмов рандома множество, некоторые легко предугадать, другие - сложнее, но качество функций рандома сводится к тому, чтобы выдавать равномерное распределение чисел, и чем больше оно равномерно, тем лучше считается функция рандома.
#random #code #algorithms
🔥15👍11🥱3
Бинарный поиск
Допустим, что у нас есть отсортированный массив чисел:
Как нам определить, что в этом массиве есть какое-то число?
Самый простой вариант - пройти линейно от первого элемента до последнего. Таким образом мы получаем
Но как это сделать быстрее?
На этом месте те, кто не знает, может остановиться и подумать
На самом деле существует небольшой лайфхак: если вы видите что-нибудь отсортированное (массив это или матрица - не важно) и задача - поиск, то сложность всегда будет
Итак, для начала нам нужно взять центральный индекс, то есть
Т.е. если центральный элемент не тот, который мы ищем, то мы сразу отсеиваем половину элементов массива и берем индекс снова центральный от подмассива.
Ищем 2:
Т.е. мы за 3 итерации нашли число. Вот это отбрасывание половины и есть
#search #binary #code #algorithms
Допустим, что у нас есть отсортированный массив чисел:
1 2 6 8 56 234 745 998 1010Как нам определить, что в этом массиве есть какое-то число?
Самый простой вариант - пройти линейно от первого элемента до последнего. Таким образом мы получаем
O(n) (Если кто не видел пост https://t.iss.one/unsafecsharp/97).Но как это сделать быстрее?
На этом месте те, кто не знает, может остановиться и подумать
На самом деле существует небольшой лайфхак: если вы видите что-нибудь отсортированное (массив это или матрица - не важно) и задача - поиск, то сложность всегда будет
log(n). В теории можно придумать алгоритм, который будет еще умнее, но сложность от этого не изменится.Итак, для начала нам нужно взять центральный индекс, то есть
length / 2. После чего у нас массив разделится на два подмассива, при этом слева элементы будут всегда меньше искомого, а справа - больше.Т.е. если центральный элемент не тот, который мы ищем, то мы сразу отсеиваем половину элементов массива и берем индекс снова центральный от подмассива.
Ищем 2:
[1 2 6 8 56 234 745 998 1010]
[1 2 6 8 56]
[1 2 6]Т.е. мы за 3 итерации нашли число. Вот это отбрасывание половины и есть
log(n).#search #binary #code #algorithms
Telegram
Unity: Всё, что вы не знали о разработке
Немного про оценку сложности алгоритмов.
Вы, наверное, не раз сталкивались с таким понятием как сложность алгоритмов. Существует несколько нотаций, которыми можно описать сложность алгоритма, в основном используется О-нотация (или О-большое), т.к. она описывает…
Вы, наверное, не раз сталкивались с таким понятием как сложность алгоритмов. Существует несколько нотаций, которыми можно описать сложность алгоритма, в основном используется О-нотация (или О-большое), т.к. она описывает…
🥱16👍12🔥5
Рекурсивный метод можно представить в виде цикла.
Допустим, у нас есть простой метод для перебора чего-либо:
Мы видим, что если на вход передать ноду графа и значение, то мы в итоге найдем нужную ноду, либо вернем null.
Чем плох такой вариант? Дело в том, что каждый вход в метод
Что же делать?
Давайте избавимся от рекурсивного вызова метода. Самый простой вариант это сделать - использовать Stack или Queue:
Из минусов - придется объявить коллекцию, куда мы будем складывать элементы. из плюсов - можно обойти граф любой глубины.
#recursion #code #algorithms
Допустим, у нас есть простой метод для перебора чего-либо:
class Node {
public int value;
public Node[] children;
}
Node FindRecursively(Node root, int value) {
if (root.value == value) return root;
for (int i = 0; i < root.children.Length; ++i) {
var node = FindRecursively(root.children[i], value);
if (node != null) return node;
}
return null;
}Мы видим, что если на вход передать ноду графа и значение, то мы в итоге найдем нужную ноду, либо вернем null.
Чем плох такой вариант? Дело в том, что каждый вход в метод
FindRecursively будет создавать стек под этот метод, т.е. чем больше мы используем в методе всяких переменных, тем быстрее закончится место в нашем стеке, если представить, что граф у нас довольно большой.Что же делать?
Давайте избавимся от рекурсивного вызова метода. Самый простой вариант это сделать - использовать Stack или Queue:
Node Find(Node root, int value) {
var queue = new Queue<Node>();
queue.Enqueue(root);
while (queue.Count > 0) {
var node = queue.Dequeue();
if (node.value == value) return node;
for (int i = 0; i < node.children.Length; ++i) {
queue.Enqueue(node.children[i]);
}
}
return null;
}
Из минусов - придется объявить коллекцию, куда мы будем складывать элементы. из плюсов - можно обойти граф любой глубины.
#recursion #code #algorithms
👍21🔥6🥱2
Пишите non-alloc методы.
Мы часто пишем подобные методы:
В этом методе мы просто собираем элементы и возвращаем.
При этом создаем список, создание которого мы не можем запретить извне. Для этого лучше писать таким образом:
Таким образом контроль над списком может быть таким:
#gc #code #allocations
Мы часто пишем подобные методы:
List<int> GetItems() {
var items = new List<int>();
...
return items;
}
В этом методе мы просто собираем элементы и возвращаем.
При этом создаем список, создание которого мы не можем запретить извне. Для этого лучше писать таким образом:
void GetItems(List<int> items) {
...
}
Таким образом контроль над списком может быть таким:
var list = GetFromPool();
GetItems(list);
...
ReturnToPool(list);
#gc #code #allocations
👍27🔥6🗿3👎2
Ленивый if
Условие всегда ленивое и хочет побыстрее выйти.
Если v1 будет true, то что там дальше его не будет интересовать:
Таким образом, если у нас есть такой код:
Выглядит хоть и симпатично, но совершенно непроизводительно.
Лучше писать так:
Естественнно нужно понимать, что CalcV2 вызываться не будет, если CalcV1 вернет true, поэтому не нужно на это расчитывать. Но я надеюсь, что вы это знаете :)
#code #performance #basics
Условие всегда ленивое и хочет побыстрее выйти.
Если v1 будет true, то что там дальше его не будет интересовать:
if (v1 == true || v2 == true) {...}
Таким образом, если у нас есть такой код:
var v1 = CalcV1();
var v2 = CalcV2();
if (v1 == true || v2 == true) {...}
Выглядит хоть и симпатично, но совершенно непроизводительно.
Лучше писать так:
if (CalcV1() == true || CalcV2() == true) {...}
Естественнно нужно понимать, что CalcV2 вызываться не будет, если CalcV1 вернет true, поэтому не нужно на это расчитывать. Но я надеюсь, что вы это знаете :)
#code #performance #basics
👍27🥱13🔥8
Вот такую задачку мы задаем на собесе, чтобы понять понимает ли человек основы и вообще работал ли он со структурами (или только с классами):
#interview #code
void Method(IInterface obj) {}
public struct S : IInterface {}
public struct A : IInterface {}
void Something() {
Method(new S());
Method(new A());
}
interface IInterface {…}
Но вопрос мы ставим так: «какие проблемы вы видите с этим кодом?». А дальше внимательно слушаем, если ответ типа «ну тут нужно все вообще переписать», то так и будет в вашем прод проекте, все перепишут, а в итоге результата будет ноль;)#interview #code
👍16
Умножение vs Деление
Надеюсь, что вы все в курсе, что операция деления выполняется намного медленнее операции умножения. Если нет, то имейте ввиду.
Я часто встречаю вот такой код:
И мне все время хочется такой код написать так:
Еще можно заменять по тому же принципу и любые другие константы. Но что делать, если у нас деление не на константу, а на
#basics #performance #code
Надеюсь, что вы все в курсе, что операция деления выполняется намного медленнее операции умножения. Если нет, то имейте ввиду.
Я часто встречаю вот такой код:
a += b / 2f;
И мне все время хочется такой код написать так:
a += b * 0.5f;
Еще можно заменять по тому же принципу и любые другие константы. Но что делать, если у нас деление не на константу, а на
x? Да все просто, делаем y = 1f / x и используем уже y.#basics #performance #code
🔥26🥴15👍10🤔7👌7❤2💯1😴1
Texture2D.PackTexturesМы часто используем этот метод для динамического создания атласа, когда, например, мы загружаем аватарки игроков, а какие они будут мы заранее не знаем. Или мы формируем атлас для боя, когда игроки могут выбрать какие-нибудь скины юнитов, а нам все еще нужен 1 draw call ;)
#atlas #runtime #code #api
🔥41👍9
StaticBatchingUtility.CombineПозволяет комбайнить меши, которые находятся на GameObject. Довольно удобно, чтобы не ползать по иерархии и не собирать их там.
Есть способ комбинировать меши и без этого:
mesh.CombineMeshesНо придется передавать туда уже структуры, в которых описываются матрицы и мешки.
Мы такое часто использовали для 3D, когда мы создавали одну мешку и вместо 1к объектов получали всего один. Есстественно, нужно понимать, что батчить нужно по-материально, т.е. какой-то код с этой логикой нужно будет все же написать.
#batching #static #code #api
🔥13❤2👾2
CullingGroup APIВ юнити есть замечательная штука, которой мало кто пользуется. На самом деле дает возможность считать отсечение примитивами. На практике я такое часто использую для того, чтобы знать какие объекты нужно просчитывать, а какие - нет. Наверное, это апи можно считать уже устаревшим, т.к. приходят всякие brg, которые умеют в culling, плюс это апи не умеет в burst. Но на самом деле я все равно его использую, т.к. даже на уровне представления оно дает заметный прирост, если самому отключать аниматоры/рендеры и прочие штуки.
https://docs.unity3d.com/Manual/CullingGroupAPI.html
#culling #code #api
👍19🔥10🌭1👻1
Texture2D.GenerateAtlas
Я уже писал про PackTextures, но эта штука ломается, если невозможно запаковать текстуры, т.к. их размер превышает максимальный размер атласа. Для этого можно использовать GenerateAtlas, т.к. этот метод ничего не делает с текстурами, а только работает с ректами и возвращает true, если все объекты поместятся в атлас. То есть можно сначала вызвать его, а потом использовать PackTextures, либо запаковать самостоятельно, используя SetPixels.#api #code #unity
🔥15👍3
Instantiate<T>()Я часто встречаю в коде вот такой вариант:
var instance = Instantiate(prefabGo);
instance.GetComponent<MyComponent>();
Дело в том, что можно использовать такой вариант:
var instance = Instantiate(prefabMyComponent);
Который сразу вернет нам ссылку на инстанс нашего объекта.
#lifehack #basics #instantiate #code
🥱67👍18🔥6🥴5🌚1💯1
CallerFilePathAttribute
Этот аттрибут позволяет получить путь к файлу, из которого происходит вызов метода:
Еще обратите внимание, что есть CallerMemberName (из какого метода вызов) и CallerLineNumber (на какой строке).
#debug #csharp #code
Этот аттрибут позволяет получить путь к файлу, из которого происходит вызов метода:
void Sample([CallerFilePath] string file = null) {
UnityEngine.Debug.Log(file);
}
Еще обратите внимание, что есть CallerMemberName (из какого метода вызов) и CallerLineNumber (на какой строке).
#debug #csharp #code
4🔥34❤5👍3
NoAlias
Этот аттрибут используется для Burst и позволяет экономить на инструкциях.
Например:
В данном случае Burst не знает, пересекаются ли данные a и b (хранятся ли в памяти в одном месте или нет).
То есть тут будет 3 инструкции mov, 2 для установки значения, а третья - для чтения этого значения. Если же добавить аттрибут NoAlias, возврат данных будет без дополнительной загрузки:
Аттрибут можно использовать для NativeArray (контейнеров), которые не пересекаются, и в качестве возврата из методов [return: NoAlias]. При этом это имеет смысл, если метод не заинлайнен, т.к. при инлайне аттрибут не будет иметь смысла.
#noalias #burst #code
Этот аттрибут используется для Burst и позволяет экономить на инструкциях.
Например:
int Method(ref int a, ref int b) {
b = 13;
a = 42;
return b;
}
В данном случае Burst не знает, пересекаются ли данные a и b (хранятся ли в памяти в одном месте или нет).
То есть тут будет 3 инструкции mov, 2 для установки значения, а третья - для чтения этого значения. Если же добавить аттрибут NoAlias, возврат данных будет без дополнительной загрузки:
int Method([NoAlias] ref int a, ref int b) {
b = 13;
a = 42;
return b;
}
Аттрибут можно использовать для NativeArray (контейнеров), которые не пересекаются, и в качестве возврата из методов [return: NoAlias]. При этом это имеет смысл, если метод не заинлайнен, т.к. при инлайне аттрибут не будет иметь смысла.
#noalias #burst #code
🔥20❤1
Простые лайфхаки
Часто встречаю в разных проектах подобные конструкции (на самом деле обычно они страшнее, но я убрал все лишнее):
На самом деле этот код можно записать проще:
Аналогично можно делать и обратную историю:
В целом подобных штук можно найти немало, которые сэкономят время и приведут код с красивому и читаемому виду.
#lifehack #basics #code
Часто встречаю в разных проектах подобные конструкции (на самом деле обычно они страшнее, но я убрал все лишнее):
var result = false;
if (Method1() == true) result = true;
if (Method2() == true) result = true;
if (Method3() == true) result = true;
...
return result;
На самом деле этот код можно записать проще:
var result = false;
result |= Method1();
result |= Method2();
result |= Method3();
...
return result;
Аналогично можно делать и обратную историю:
var result = true;
result &= ...
В целом подобных штук можно найти немало, которые сэкономят время и приведут код с красивому и читаемому виду.
#lifehack #basics #code
🔥30👍7🤡6❤5😐4