Unity: Всё, что вы не знали о разработке – Telegram
Unity: Всё, что вы не знали о разработке
1.74K subscribers
40 photos
101 links
Авторский канал о разработке в Unity от Alex Silaev (CTO в Zillion Whales). Mushroom Wars 2 моих рук дело.
Рассказываю об интересный кейсах, делюсь лайфхаками, решениями.
Download Telegram
Всем привет!
Меня зовут Алекс Силаев и последние 15 лет я занимаюсь разработкой в Unity. До этого была backend-разработка, БД и прочие интересные штуки.

В данный момент занимаю позицию CTO в компании Zillion Whales (основной проект Mushroom Wars).

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

Основная тема - C# и Unity.

Мой github: http://github.com/chromealex
👍7🐳2
А знаете ли?
В C# строки хранятся в памяти в единственном экземпляре. Называется интернирование. Код
if (str1 == “something” || str2 == “something”)
не будет создавать 2 строки, а будет использовать ссылку на один и тот же объект (да, строка - это Reference Type).

#strings
👍18🥱6👎3🤯3💩1
А знаете ли?
Чтобы атомарно изменить значение переменной можно использовать lock, но это один из самых долгих способов. Гораздо быстрее использовать Interlocked методы. В некоторых случаях (в хот частях) лучше вообще обходиться без синхронизаций между потоками.

#threading
👍7💩2🥱1
Для сравнения Unity Object с null можно использовать конструкцию if (obj is null) вместо if (obj == null). А еще если obj - это Unity Object, то оператор == перегружен и проверяет не только фактический null на стороне C#, но и объект на стороне C++. Тот же эффект достигается при использовании ReferenceEquals.

#code
👍14🥱2💩1
Вы можете написать метод GetEnumerator в любой структуре или классе, это позволит использовать конструкцию foreach. Но есть несколько моментов, которые стоит понимать:
* Результат метода должен вернуть структуру или объект, в котором есть метод MoveNext и свойство Current;
* При использовании интерфейса IEnumerable (например, в List<>) при любом использовании foreach или GetEnumerator значение будет запаковано (boxing) и избежать этого уже никак не выйдет.

#code #foreach
👍6💩1🥱1
🥕Ну и задачка, пишите ответы в комменты;)

Что означает конструкция a = *(val1 + val2);?

1. Это не будет компилироваться;
2. Это же простое умножение, можно записать как a = a * (val1 + val2);
3. Это будет работать только в unsafe, двигаем val на index (на сколько - зависит от типа val), получаем данные по указателю;
4. Встречал такое, но точно не знаю как это работает;
5. Свой вариант.

#unsafe
👍8
Можно вызывать internal методы из других assembly, но для этого нужно разрешить другим assembly видеть эти методы. Для этого нужно написать аттрибут InternalsVisibleTo с указанием имени assembly, которая будет видеть internal-методы.
[assembly: InternalsVisibleTo("Friend.Assembly.Name")]

#assembly
👍17🥱3💩1🤨1
🥕Задачка
Вы наверняка знаете, что можно сделать static constructor. Для этого нужно просто написать static MyClass() и сделать там свои грязные делишки.
Но иногда хочется получить static destructor у этого же класса.
На практике я с таким столкнулся, когда в конструкторе я выделял память, которую нужно было где-то удалять.
Вопрос знатокам: каким образом вы бы решили такую проблему? Попробуйте не гуглить;) Своим решением поделюсь чуть позже.
👍5😢1
При сериализации структур в бинарь можно использовать подход *(T*)ptr = value, где ptr - указатель на массив byte, а value - данные, которые мы хотим туда записать. Если вы используете managed массив, то не забудьте использовать fixed.

#unsafe
👍3
Всем привет! Я хотел бы проводить общие созвоны раз в неделю, чтобы пообщаться, рассказать о каких-нибудь проблемах, найти решение.
В какое время и в какой день вам было бы удобно это?
В комментах будет опрос про время.
Final Results
8%
Понедельник
8%
Вторник
4%
Среда
4%
Четверг
9%
Пятница
47%
Суббота
21%
Воскресенье
Метод Array.Resize не проверяет размер массива в сторону уменьшения. То есть всегда будет выделен массив необходимого размера на выходе.

#code
👍7🤡4
Если у вас в проекте есть много skinnedmesh анимаций (например, у вас по лесу бегает много животных), то их анимации можно запечь в текстуру, откуда читать шейдером. Такие анимации будут работать довольно с сильной погрешностью, но для объектов окружения этого может быть вполне достаточно. Такое решение намного производительнее, т.к. работает с одной текстурой и укладывается в один DrawCall.

#animations #rendering #shaders
👍10👌1🤡1😨1
Партиклы можно использовать в качестве рендера своих спрайтов. Для этого нужно вызвать var count = particleSystem.GetParticles(particlesArr);
изменить массив и вызвать
particleSystem.SetParticles(particlesArr, count);
Таким образом можно контролировать тысячи частиц.

На практике мы с таким столкнулись, когда нам нужно было отрисовать 4 тысячи юнитов на экране телефона, при этом каждый юнит имел по 4 спрайта, т.е. 16к спрайтов на одном экране старенького андроида в 2015м году. Тогда юнити еще толком не умела нормально контролировать партиклы и пришлось писать свои партиклы на плюсах, что повлекло за собой боль с поддержкой этого кода под все платформы (а это были PS4/XBOXONE/Switch/iOS/Android/PC(x86/x64).
И да, в итоге юнити доделали партиклы и мы с радостью избавились от этого кода, но осадочек то остался 😉

#particles #code #rendering
👍13🗿21
🥕Задачка
Представьте игру, где вы управляете большой толпой человечков, отдавая приказы кликом мышки. При этом игра сетевая, т.е. у каждого игрока большая толпа этих человечков. Например, возьмем, 1000 человечков у каждого игрока. Для простоты человечки ходят по прямой и не сталкиваются друг с другом. На карте есть препятствия, которые нужно обходить. Одним кликом вы контролируете сразу всех или выделенную часть.
Вопросы:
1. Каким образом будет устроен поиск пути?
2. Каким образом вы будете синхронизировать позиции человечков между игроками?
👍1🤔1
Как устроен GOAP (или Goal-Oriented Action Planning)?
Все мы знаем деревья поведений или FSM. FSM дает возможность приходить к конкретной цели, основываясь на параметрах, приобретенных при выполнении конкретных состояний, но при этом вы можете находиться в конкретном одном состоянии в определенный момент времени.
В деревьях поведений же мы не ограничены одним состоянием в определенный момент, но сейчас не о них.
Есть еще Utility based AI, но о нем нужно рассказывать отдельно.

GOAP не имеет связей. Это важно. Совсем.
Простыми словами GOAP представляет собой массив простых событий, которые выполняются, если обеспечены их входные параметры, а на выходе дают необходимые эффекты. А имея такие вводные, можно без труда построить граф для достижения необходимого действия.

Как видно из названия, Goal-Oriented или ориентрир - это цель, мы ориентируемся на достижения цели. То есть мы говорим персонажу "будь сыт", а он сам добудет еду и поест. Ну или "построй дом", а он сам добудет необходимые материалы и построит дом.
Пожалуй, на этом примере можно и остановиться.
Допустим, у вас есть 3 простых действия:
1. Собирать камень
2. Рубить дерево
3. Строить дом

Строить дом - это действие, которое как результат выдает "дом построен", но на вход ему необходимо 2 камня и 4 дерева. Для того чтобы добыть 2 камня нужно иметь кирку, а чтобы добыть 4 дерева - нужен топор. Допустим, что топор и кирка у нас уже есть. Мы идем рубить дерево и добывать камень. И повторяем эти действия столько раз, пока не наберется необходимое количество.
Таким образом GOAP - это граф, который строится динамически, когда мы просим дать нам какой-то результат. Он всегда строится исходя из кратчайшего пути, т.е. если 4 дерева будет лежать на складе, то персонаж пойдет именно туда, т.к. рубить ничего не нужно.

А что вы используете в своих проектах?

#algorithms
👍24
Атомарные операции.
int i = 123; // всегда атомарно
long j = 234L; // атомарно на x64, но кому сейчас надо x32?
i++; // никогда не атомарно, т.к. мы читаем данные, увеличиваем, а потом записываем

#threading
👍9🥱7🤔1
Реализация lock, которую я использую в ME.BECS
public 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