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

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

💼 How to create capital and increase it using cryptocurrency

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

Last updated 6 hours ago

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

Last updated 3 months ago

Новые и перспективные Web3 игры с добычей токенов.

Чат: https://t.me/Crypto_Wolf_Chat

Правила чата смотрите в описании чата.

Все свои вопросы направляйте в чат или главному модератору чата: @Exudna_118

По теме сотрудничества: @Zombini

Last updated 2 months, 2 weeks ago

hace 1 mes, 2 semanas

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

hace 1 mes, 2 semanas

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

hace 1 mes, 2 semanas
Статические лямбды

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

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

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

```

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

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

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

#basics #staticlambda

hace 7 meses

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

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

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

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

#interview #unity

hace 7 meses

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

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

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

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

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

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

#unmanaged #blittable #serialization

hace 7 meses

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

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

```

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

hace 7 meses

```
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

hace 7 meses, 1 semana

А вы знали, что юнити из коробки поддерживает дебаг пикселя по клику?)

Для URP надо включить дефайн ENABLE_SHADER_DEBUG_PRINT в проекте (для HDRP, кажется, оно и так будет работать).

Затем добавить в шейдер строчку
// \#include "Packages/com.unity.render\-pipelines.core/ShaderLibrary/ShaderDebugPrint.hlsl"

А в самом шейдере вызвать
ShaderDebugPrintMouseButtonOver(int2(input.positionSS.xy), ShaderDebugTag('S','m','o', 't'), fragData.smoothness);

И тогда при клике ЛКМ по какому-либо месту на экране, в консоль будет написано
Frame \#270497: Smot float4(0.1f, 0.2f, 0.3f, 0.4f) - ну, или что у вас там в вашем цвете.

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

Так можно дебажить свой сложный кастомный шейдер.
Особенно хорошо работает в сочетании с окном Rendering Debugger.

Пост: https://t.me/unity_cg/74386

#rendering #urp

Telegram

WellMOR in Unity CG Tech

А вы знали, что юнити из коробки поддерживает дебаг пикселя по клику?) Для URP надо включить дефайн ENABLE\_SHADER\_DEBUG\_PRINT в проекте (для HDRP, кажется, оно и так будет работать). Затем добавить в шейдер строчку // #include "Packages/com.unity.render…

hace 9 meses, 3 semanas

https://youtu.be/EGHdtjmG0Jw

Записал небольшой ролик про то как работает сейчас мой прототип RTS. Тут есть и поиск целей, и туман войны, и поиск пути. Ну и пара тысяч юнитов ?
Нет никакой оптимизации графики, на написание и тестирование логики это никак не влияет.

#mebecs #ecs #rts #prototype

YouTube

ME.BECS: Huge units count pathfinding + targets search. Editor test (without graphics optimization)

Profiling game prototype in editor https://github.com/chromealex/ME.BECS https://t.me/unsafecsharp

hace 10 meses

UnityEngine.Object == null

Мне не так давно написал один из разработчиков на проекте, который застрял на проблеме зомби объектов.

Зомби объект — жив потому что GC не может его собрать, так как в стэке есть ссылка на него, но сам объект уже Destroyed.

И дело тут не в том, что GC.Collect еще не вызвался, а в том что мы храним указатель на объект, который уже уничтожен.

Такую ситуацию очень легко получить:
?Берем любой класс наследник MonoBehaviour
? Реализуем в нем любой интерфейс
? Instantiate'им объект, сохраняем в переменную
? Во вторую переменную cast'им наш объект к интерфейсу
? Уничтожаем MonoBehaviour
Через Object.Destroy или Object.DestroyImmediate
? Пытаемся вызвать поле или метод интерфейса

Вопросы:
? Будет ли MonoBehaviour == null?
? Будет ли переменная интерфейса == null?
? Будет ли ошибка при вызове любого member'а интерфейса?

Подсказка

Ответы:
? Да
? Нет
И да и нет.
Если вызваемый мембер не часть MonoBehavior имплементации - ошибки не будет
В остальных случаях будет MissingReferenceException

Почему так?
Потому что любой UnityEngine.Object имеет 2 runtime части:
?Одна на стороне unity (написанная на C++)
?Вторая на стороне CLR (Common Language Runtime) — класс/структура C#

Это значит:
? CLRObject может смотреть уже на уничтоженный UnityObject ровно столько, сколько мы храним указатель на него
? Если UnityObject уничтожен, мы все еще можем обратиться к его CLR части и взять оттуда любые данные, которые не является частью UnityObject
? Момент сборки данного объекта GC может быть отложен на сколько угодно по времени

Это ведет к проблемам:
♦️ Утечки памяти по всему проекту.
UnityObject уничтожен и нам нужно удалить объект из коллекции, а мы не можем, потому что interface != null
♦️MissingReferenceException, в неожиданных местах со вкусом фрустрации и сложной отладки

4 возможных решения:
1️⃣ Проверять все экземпляры типа интерфейса методом:

```

bool IsNullUniversal(T instance)
{
  if (instance is Object unityObject)
    return unityObject == null;

return instance == null;
}

```

2️⃣ Для абстракции ВСЕХ монобехов использовать только abstract классы
3️⃣ Наследовать все интерфейсы от IEquatable и использовать везде метод Equals вместо ==
4️⃣ (самый быстрый, самый дерзкий)
Через UnsafeUtility читать m_CachedPtr и m_InstanceID и через рефлексию дергать метод DoesObjectWithInstanceIDExist

Почему именно так:
?UnityObject содержит перегрузку оператора == и != которая проверят что нативная (C++ часть) "жива"
?Но в CLR части == транслируется в операцию seq, которая в после IL2CPP будет выглядеть так:
((((RuntimeObject)instance) == ((RuntimeObject)NULL))? 1 : 0)

Или проще говоря в обычное сравнение 2ух указателей.

Такой проверки в случае реализации интерфейса в UnityObject не достаточно.
Потому мы получается false-negative результат при сравнение на null, который потенциально ведет к утечкам памяти

Я создал Gist в котором расписал подробно TestCase'ы и решение.
Не стесняйтесь его дополнить или предложить свой вариант в комментариях ?

Об архитектуре проектов на unity я пишу в своем канале, подписывайся❤️‍?

Специально для @unsafecsharp

We recommend to visit
HAYZON
HAYZON
6,375,969 @hayzonn

💼 How to create capital and increase it using cryptocurrency

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

Last updated 6 hours ago

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

Last updated 3 months ago

Новые и перспективные Web3 игры с добычей токенов.

Чат: https://t.me/Crypto_Wolf_Chat

Правила чата смотрите в описании чата.

Все свои вопросы направляйте в чат или главному модератору чата: @Exudna_118

По теме сотрудничества: @Zombini

Last updated 2 months, 2 weeks ago