Unity: Всё, что вы не знали о разработке

Description
Авторский канал о разработке в Unity от Alex Silaev (CTO в Zillion Whales). Mushroom Wars 2 моих рук дело.
Рассказываю об интересный кейсах, делюсь лайфхаками, решениями.
Advertising
We recommend to visit
HAYZON
HAYZON
5,791,257 @hayzonn

👤 𝐅𝐨𝐮𝐧𝐝𝐞𝐫: @Tg_Syprion
🗓 ᴀᴅᴠᴇʀᴛɪsɪɴɢ: @SEO_Fam
Мои каналы: @mazzafam

Last updated 1 month, 1 week ago

Architec.Ton is a ecosystem on the TON chain with non-custodial wallet, swap, apps catalog and launchpad.

Main app: @architec_ton_bot
Our Chat: @architec_ton
EU Channel: @architecton_eu
Twitter: x.com/architec_ton
Support: @architecton_support

Last updated 1 month ago

Канал для поиска исполнителей для разных задач и организации мини конкурсов

Last updated 1 month, 3 weeks ago

1 month ago

if lock if

У меня в BECS очень много всяких вариантов локов, например, для ресайзов вида:

```

if (obj.isCreated == false) {
obj = new Obj();
}

```

Но поскольку мне нужно многопоточность, самый простой вариант обернуть это в лок:

```

lock (lockObj) {
if (obj.isCreated == false) {
obj = new Obj();
}
}

```

Но поскольку операция создания объекта происходит крайне редко (тут даже скорее лучше бы подошел пример с ресайзом массива), то какой смысл тратить перф на блокировку? Никакого.

```

if (obj.isCreated == false) {
lock (lockObj) {
if (obj.isCreated == false) {
obj = new Obj();
}
}
}

```

Получается такая матрешка, но давайте резберемся что будет для нескольких потоков:

  1. Поток №1 входит в условие if (obj.isCreated == false);
  2. Поток №2 тоже может успеть войти в это условие;
  3. Поток №1 блокирует объект;
  4. Поток №2 ожидает снятия блокировки;
  5. Поток №1 создает объект и записывает его;
  6. В этом месте любое количество других потоков может войти в метод и уже использовать наш объект;
  7. Поток №1 выходит из блокировки, освобождая поток №2
  8. Поток №2 проверяет еще раз if (obj.isCreated == false) и оказывается, что ничего делать уже не нужно.

Таким образом если какая-то операция происходит довольно редко, то такая реализация ленивой инициализации в многопоточной среде увеличит производительность.
Естественно, я не использую конструкцию lock, заменяя ее CompareExchange, но для примера привожу именно lock.

Важное уточнение: isCreated = true вашего объекта в конструкторе должен идти последним, т.е. когда мы уже можем использовать объект.

#multithreading #csharp #lock

1 month ago

Новости BECS

Как мне тут сказали в чатике по ецс: "У тебя фреймворк не многопоточный, т.к. при использовании компонентов в двух джобах у тебя не будет исключения о race condition". Ну сказано - сделано. Теперь есть.

А теперь подробнее с какими трудностями пришлось столкнуться при реализации.

У меня есть 2 интерфейса:

```

IJobParallelForAspect
IJobParallelForComponents

```

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

В BECS реализация джоб выглядит примерно так:

```

public struct Job : IJobParallelForComponents {
public void Execute(in JobInfo jobInfo, in Ent ent, ref C1 c1, ref C2 c2) {...}
}

```

Т.е. не нужно никаких создавать дополнительных полей. И тут внимательные читатели спросят "у тебя же ref для компонента, а как же права RO/WO/RW?". На самом деле ref тут исключительно для удобства, магия происходит на уровне кодогена.
А именно: когда вы написали код метода Execute, я его разбираю и нахожу все обращения ко всем компонентам и соотвественно могу выяснить что вы с ним делаете: например, только читаете или только пишите или и то и другое.
В этом разборе я составляю список используемых компонентов для конкретной джобы.
Если с компонетами можно было так не заморачиваться, то с аспектами так не выйдет, т.к. внутри аспекта по сути может быть 10 компонентов, а джобе вы используете только 1 или 2, например. Таким образом 2 параллельно запущенные джобы не дали бы обращаться к одному аспекту в параллель, т.к. внутри был бы лок на все компоненты. Поэтому и пришла идея парсить код метода Execute на предмет фактического использования компонентов.

В итоге кодоген создает вот такие данные для каждой джобы

```

public struct JobDebugDataXXX {
[NativeDisableUnsafePtrRestriction] public MyJob jobData;
[NativeDisableUnsafePtrRestriction] public CommandBuffer* buffer;
public RefRW c0;
public SafetyComponentContainerRO C1;
public SafetyComponentContainerWO C2;
public SafetyComponentContainerRO ParentComponent;
}

```

Если код Execute будет таким:

```

void Execute(in JobInfo jobInfo, in Ent ent, ref C1 c1) {
ent.GetParent().Get().data = c1.data;
}

```

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

#becs #ecs #IL #codegenerator

1 month, 3 weeks ago

where T

Давайте представим ситуацию, нам нужно реализовать 2 метода с одинаковой сигнатурой, но фильтровать разные интерфейсы:

```

public interface I1 {}
public interface I2 {}

public class MyClass {

public void Method<T>() where T : struct, I1 {} public void Method<T>() where T : struct, I2 {}

}

```

Такой код не скомпилится, т.к. сигнатуры методов одинаковые, разные только условия выбора.
Как можно это исправить? Самый простой вариант - объявить методы с разными именами:

```

Method1
Method2

```

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

Что делать?

Объявляем наши интерфейсы и класс:

```

public interface I1 {}
public interface I2 {}
public class MyClass {}

```

Пишем два экстеншена:

```

public static class MyClassExt1 {
public static void Method(this MyClass obj) where T : struct, I1 {}
}

public static class MyClassExt2 {
public static void Method(this MyClass obj) where T : struct, I2 {}
}

```

И вуаля 🙂 На самом деле в большинстве случаев такой хак будет работать, т.к. компилятор по сути будет считать, что это 2 разных класса (MyClassExt1, MyClassExt2) и в них объявлены свои методы. А при компиляции они все равно развернутся в Method_I1, Method_I2 и не будут никаким образом мешать друг другу.

#lifehack #csharp

2 months, 4 weeks ago

Quaternion vs Euler

На самом деле довольно интересная тема. Многие (особенно начинающие) не особо понимают в чем разница, т.к. кватернион хрен знает что это, там если умножить на вектор вроде как вектор повернет (но это не точно), а вот euler ваще классная штука (ведь не даром поворот в трансформе 3мя флотами записан (нет)).

Но давайте вспомним (или узнаем) про gimbal lock. Это такое положение осей, когда две оси сложились, а третья не в состоянии повлиять на вращение в нужную нам сторону (на рисунке зеленая и синия ось сложились в одну плоскость, а красная в состоянии вращать только в одном направлении).

Вообще euler angles - это понятное представление осей для человека, где есть понятный поворот x, y, z. Но в играх предпочтительно использовать quaternion, т.к. он не подвержен проблеме gimbal lock.

Другое дело quaternion. В реальности это гиперкомплексные числа.
Если не вдаваться в подробности математики (может стоит?), а остановиться на том, что мы используем в юнити, то вот несколько моментов:
1. Quaternion не может содержать 4 значения 0;
2. Существуют проблемы с точностью float, которые могут приводить к невалидному состоянию;
3. Умножение кватерниона на вектор - повернут вектор;
4. Умножение кватерниона на кватернион - сложение углов;
5. Кватернион можно представить в виде матрицы;
6. Порядок умножения кватернионов важен;
7. Умножение quaternion на inverse(quaternion) вычитает повороты;

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

#quaternion #euler #math

3 months ago

UI Toolkit

Это некое подобие html/css. Почему подобие? Потому что многих стандартных вещей нет, а текущий стандарт css ушел довольно далеко от uss. То есть это примерно как верстать сейчас сайтики под Internet Explorer 6.0, оно вроде называется похоже, но большую часть просто не поддерживает.

Где это использовать?
Я бы рекомендовал это использовать только для editor-tools, еще может быть в каких-нибудь рантайм штук в билде типа дев консоли. Для продакшена еще далеко, да и есть много подводных камней.

Для редактора:
PropertyDrawer. Если в IMGUI нужно использовать два метода, если высота элемента больше стандартной линии, то при использовании UI Toolkit можно использовать только один метод:

```

VisualElement CreatePropertyGUI(SerializedProperty property) {
...
}

```

EditorWindow. Для отрисовке в окне существует стандартный метод:

```

private void CreateGUI() {
...
}

```

CustomEditor. Для отрисовки редактора для всего скрипта, нужно использовать метод:

```

VisualElement CreateInspectorGUI() {
...
}

```

Как это работает?

По сути UIToolkit - это контейнеры, которые можно заполнять объектами, которые сами по себе тоже являются контейнерами. Если знакомы с HTML, то контейнер в UITK - это тег.
В любом варианте из перечисленных выше, у нас есть VisualElement - это рутовый контейнер, в который мы можем вкладывать свои:

```

Label myLabel;
VisualElement CreateInspectorGUI() {
// Создаем свой контейнер
var myRoot = new VisualElement();
// Добавляем файл со стилями
myRoot.styleSheets.Add(Resources.Load("MyStyle"));
// Создаем label
var label = new Label("Hello World");
label.AddToClassList("my-label");
this.myLabel = label;
// Добавляем в иерархию
myRoot.Add(label);
// Возвращаем корневой объект
return myRoot;
}

void UpdateLabel() {
// Просто обновляем текст у объекта
this.myLabel.text = "New text";
}

```

А в MyStyle.uss:

```

// Наводим всякие красивости
.my-label {
font-size: 20px;
border-radius: 5px;
background-color: red;
}

```

Что хорошего?

  1. Многопоточность. Я не уверен, что прям все апи многопоточное, но большая часть точно, во всяком случае с тем, что я сталкивался. Это значит, что обновлять данные у элемента можно из потоков.
  2. Быстро работает. На самом деле IMGUI работает тоже быстро, если не использовать GUILayout, просто там схема работы "Перемешали логику и визуалку", поэтому чтобы отрисовать элементы - нужно пройтись по всей логике.
  3. Визуалка зависит от стилей, а не от настроек элементов в коде.
  4. Не нужно тратить CPU на подготовку отрисовки каждый кадр, т.к. структурно меняется все очень редко.

Что плохого?

  1. Если структурных изменений очень много, то нужно будет написать пулинг объектов, чтобы переиспользовать элементы, а не создавать их заново.
  2. Нет поддержки партиклов и похоже что это будет еще не скоро. На самом деле одна из основных проблем почему UITK не нужно использовать в рантайме.
  3. Нет поддержки анимаций. Есть стандартные анимации типа transition, но боюсь что этого недостаточно, чтобы сделать красивые штуки.
  4. Нет возможности указать слой, т.е. рисуется все в том порядке, в котором было объявлено.

Резюмирую

Могу сказать, что я использую UITK во всех своих тулзах, т.к. это работает намного быстрее IMGUI и можно добиться красивого результата с меньшими затратами. Но именно для продакшена он не готов.

#uitoolkit #ui

3 months ago
Статические лямбды

Статические лямбды

Я уже писал пост о том, что лямбды - это плохо, т.к. в основном лямбды используют ради замыканий, которые в свою очередь приводят к аллокациям, а аллокации - это ~~плохо~~ медленно 🙂

Видимо, разработчики шарпа тоже решили исправить ситуацию и добавили слово static к определению лямбды:

```

Method(static () => …);
```

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

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

#basics #staticlambda

8 months, 2 weeks ago

Самый простой вопрос на собеседовании

И снова про собесы) Как определить попадает ли точка в радиус?
Казалось бы, простой вопрос, но большинство отваливаются на одном из этапов. Диалог получается примерно такой (есть вариации, но направление вопросов у меня всегда одно):
- Вычесть из точки центр
- Это будет вектор, что дальше?
- Взять длину и сравнить с радиусом
- Ок, а как посчитать длину?
- Взять magnitude/Vector2.Distance (тут вопрос зачем тогда первое действие делали)
- Ок, а что внутри у magnitude ну или «как в принципе считается длина вектора»? (По ощущениям тут отваливается процентов 50)
- Теорема Пифагора
- Ок, а почему тогда мы изначально не посчитали квадрат?
- …

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

Кстати, когда я задаю этот вопрос, мне очень и очень стыдно, т.к. он действительно очень простой.
Но после него (если успешно прошли все ответы) я задаю вопрос «ну а если это будет эллипс?»

#interview #unity

8 months, 2 weeks ago

bool не является blittable типом

Давайте для начала разберемся что такое blittable типы и чем они отличаются от unmanaged типов.
В документации шарпа будет написано примерно следующее: blittable типы - это такие типы, которые могут содержать blittable типы :)
Это я, конечно, пошутил, но для неподготовленного человека объясню:
Любой примитив (кроме bool) или любая структура, которая содержит примитивы (кроме bool) или blittable структуры.

Unmanaged типы - это неуправляемые GC типы, т.е. структуры, которые содержат примитивы (любые).

То есть по факту получается, что unmanaged и blittable очень близки, но на самом деле сильно разные.
Blittable типы - это такие типы, которые в памяти на любом компутере будут выглядеть одинаково. Отсюда и проблема с bool, который на разных окружениях может занимать 1 (байт), 2 (шорт) или даже 4 байта (т.к. хранится в виде int).

Unmanaged же не гарантируют ровным счетом ничего подобного, да и не нужно ему это совсем.

Эта инфа будет полезна при бинарной сериализации структур.

#unmanaged #blittable #serialization

8 months, 2 weeks ago

Как перемещать персонажа по точкам правильно

Многие из вас делали перемещение персонажа по нодам. Например, когда поиск пути вернул массив точек, а нам нужно персонажем пройти по ним.
Обычно такой код выглядит примерно так:

```

var nextPoint = points[index];
if ((nextPoint - position).sqrMagnitude <= MIN_DISTANCE_SQR) {
++index;
if (index >= points.Length) {
// Мы дошли до конца пути
return;
}
}

position = Vector3.MoveTowards(position, nextPoint, dt * speed);

```

Логика понятная: дошли до точки - берем следующую и идем к ней, и так пока не дойдем до конца.

Но в таком подходе кроется одна проблема: если персонаж проходит за кадр 1 метр, а расстояние до точки 0.5 метра, то персонаж будет проходить на самом деле меньшее расстояние, чем должен был:

```

-[]--[]--[]--[]--[]
---------------[] // Этот персонаж дойдет до конца быстрее, чем первый

```

Что делать?

На самом деле нужно использовать примерно такую логику:

```

var distance = speed * dt;
while (distance > 0f) {
var nextNodePos = points[index];
var distanceToNextNode = (nextNodePos - currentPos).magnitude;
if (distance >= distanceToNextNode) {
distance -= distanceToNextNode;
currentPos = nextNodePos;
++index;
if (index >= points.Length) break;
continue;
}
var direction = (nextNodePos - currentPos).normalized;
currentPos = direction * distance;
break;
}

```

Метод HasReached должен проверять "перешли ли мы точку или еще нет". Таким образом, мы "перебрасываем" часть длины, которую мы прошли на новую точку, а если перешли и ее, то еще раз и так пока либо не закончится этот хвост, либо мы не дойдем до конца.
Грубо говоря, если персонаж будет двигаться со скоростью 1000 метров в секунду, а контрольных точек на пути будет много (например, каждый метр), то за секунду он пройдет ровно 1000 метров, а в первом варианте намного меньше.

#unity #algorithms #movement

8 months, 2 weeks ago

```
int Method(IInterface obj) {
...
return obj.Calc();
}

public struct S1 : IInterface {…}
public struct S2 : IInterface {…}

void Update() {
Method(new S1());

Method(new S2());
}

interface IInterface {
int Calc();
}

```

Чего я только не слышу про этот код на собесах. Тут 2 вопроса:
1. Что не так с этим кодом? Может быть и все так.
2. Как исправить?

И знаете, я вот думаю этот вопрос сделать самым первым на собесе, т.к. я слышу такие ответы:
1. Я бы сделал базовую структуру...
2. Можно сделать ref IInterface
3. Можно поменять struct на class
4. Можно сделать IInterface obj, out int...
5. Поменять struct на class + хранить их в static полях, оттуда забирать когда надо
6. Придумайте свой идиотский вариант

#interview #unity

We recommend to visit
HAYZON
HAYZON
5,791,257 @hayzonn

👤 𝐅𝐨𝐮𝐧𝐝𝐞𝐫: @Tg_Syprion
🗓 ᴀᴅᴠᴇʀᴛɪsɪɴɢ: @SEO_Fam
Мои каналы: @mazzafam

Last updated 1 month, 1 week ago

Architec.Ton is a ecosystem on the TON chain with non-custodial wallet, swap, apps catalog and launchpad.

Main app: @architec_ton_bot
Our Chat: @architec_ton
EU Channel: @architecton_eu
Twitter: x.com/architec_ton
Support: @architecton_support

Last updated 1 month ago

Канал для поиска исполнителей для разных задач и организации мини конкурсов

Last updated 1 month, 3 weeks ago