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
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();
}
}
}
```
Получается такая матрешка, но давайте резберемся что будет для нескольких потоков:
Таким образом если какая-то операция происходит довольно редко, то такая реализация ленивой инициализации в многопоточной среде увеличит производительность.
Естественно, я не использую конструкцию lock, заменяя ее CompareExchange, но для примера привожу именно lock.
Важное уточнение: isCreated = true вашего объекта в конструкторе должен идти последним, т.е. когда мы уже можем использовать объект.
Новости 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 дефайны, чтобы отключать всю эту штуку.
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 и не будут никаким образом мешать друг другу.
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) вычитает повороты;
На самом деле пока писал этот пост несколько раз подумал о том, что хорошо бы рассказать было о матрицах для начала. Но надеюсь, что и эта информация для кого-то окажется полезной.
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;
}
```
Что хорошего?
Что плохого?
Резюмирую
Могу сказать, что я использую UITK во всех своих тулзах, т.к. это работает намного быстрее IMGUI и можно добиться красивого результата с меньшими затратами. Но именно для продакшена он не готов.
Статические лямбды
Я уже писал пост о том, что лямбды - это плохо, т.к. в основном лямбды используют ради замыканий, которые в свою очередь приводят к аллокациям, а аллокации - это ~~плохо~~ медленно 🙂
Видимо, разработчики шарпа тоже решили исправить ситуацию и добавили слово static к определению лямбды:
```
Method(static () => …);
```
Теперь можно не бояться, что мы случайно используем переменную извне и получим замыкание.
В целом это синтаксический сахар, который ровным счетом ничего не делает, кроме как запрещает передавать переменные в лямбду через замыкание. Если вы использовали лямбды правильно, то вы можете смело добавить в свои лямбды static.
Самый простой вопрос на собеседовании
И снова про собесы) Как определить попадает ли точка в радиус?
Казалось бы, простой вопрос, но большинство отваливаются на одном из этапов. Диалог получается примерно такой (есть вариации, но направление вопросов у меня всегда одно):
- Вычесть из точки центр
- Это будет вектор, что дальше?
- Взять длину и сравнить с радиусом
- Ок, а как посчитать длину?
- Взять magnitude/Vector2.Distance (тут вопрос зачем тогда первое действие делали)
- Ок, а что внутри у magnitude ну или «как в принципе считается длина вектора»? (По ощущениям тут отваливается процентов 50)
- Теорема Пифагора
- Ок, а почему тогда мы изначально не посчитали квадрат?
- …
На самом деле редко кто сразу говорит, что берем по теореме Пифагора считаем квадрат гипотенузы, возводим в квадрат радиус и сравниваем. Обычно приходится тащить информацию как в диалоге выше, и это как раз говорит о том, что где-то глубже знаний не хватает.
Кстати, когда я задаю этот вопрос, мне очень и очень стыдно, т.к. он действительно очень простой.
Но после него (если успешно прошли все ответы) я задаю вопрос «ну а если это будет эллипс?»
bool не является blittable типом
Давайте для начала разберемся что такое blittable типы и чем они отличаются от unmanaged типов.
В документации шарпа будет написано примерно следующее: blittable типы - это такие типы, которые могут содержать blittable типы :)
Это я, конечно, пошутил, но для неподготовленного человека объясню:
Любой примитив (кроме bool) или любая структура, которая содержит примитивы (кроме bool) или blittable структуры.
Unmanaged типы - это неуправляемые GC типы, т.е. структуры, которые содержат примитивы (любые).
То есть по факту получается, что unmanaged и blittable очень близки, но на самом деле сильно разные.
Blittable типы - это такие типы, которые в памяти на любом компутере будут выглядеть одинаково. Отсюда и проблема с bool, который на разных окружениях может занимать 1 (байт), 2 (шорт) или даже 4 байта (т.к. хранится в виде int).
Unmanaged же не гарантируют ровным счетом ничего подобного, да и не нужно ему это совсем.
Эта инфа будет полезна при бинарной сериализации структур.
Как перемещать персонажа по точкам правильно
Многие из вас делали перемещение персонажа по нодам. Например, когда поиск пути вернул массив точек, а нам нужно персонажем пройти по ним.
Обычно такой код выглядит примерно так:
```
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 метров, а в первом варианте намного меньше.
```
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. Придумайте свой идиотский вариант
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