Блог юного Game Developer'а

Полезные материалы,туториалы,кодинг и различные лайфхаки.

Unity API из другого потока (can only be called from the main thread)

Наверняка многие из вас пытались вызывать методы из api юньки или обращаться к объектам из другого потока.
Если ещё не пробывали — попробуйте, компилятор заорёт примерно такой ошибкой — can only be called from the main thread
Вообщем-то погуглив я понял,что не я один с этим встречался,поэтому на будущее оставлю решение здесь,может кому пригодится. Совет нашел на SO,но ссылку привести не смогу,утерял.

Условие простое, хотим работать с unity api — проворачиваем свои делишки только в главном потоке,иначе никак.
Поскольку метод Update крутится в главном потоке,проверку будем производить там. Поехали...
Итак, допустим я в дополнительном потоке получаю сообщения от сервера. Мне необходимо каждое сообщение добавлять в Text UI. Если попытаюсь из дополнительного потока поработать с UI,то получу ошибку,исправляется следующим образом:

Для начала объявим в нашем скрипте очередь из делегатов и некоторые промежуточные переменные

private Queue<Action> mainThread = new Queue<Action>(); 
private string data = null;

Теперь для примера напишем метод,который будет взаимодействовать с UI.

private void OnWriteMessageChat()
    {
        if (data != null)
        {
            chatfield.text += data + "\n";
            data = null;
        }
    }

А это пускай будет тот самый метод,который принимает от сервера сообщения :

private void Communication()
                        {
data = "msg from server";
// бла бла , допустим мы уже получили сообщение от сервера и храним её в переменной data ,которую изначально объявили в теле класса.

                                mainThread.Enqueue(new Action(OnWriteMessageChat));
                                Debug.Log("Делегат добавлен в очередь");

                            }

У mainThread есть метод Enqueue,которые добавляет наши делегаты в очередь.
Остается только достать из очереди делегат и вызвать Invoke(). Для этого пригодится Update (я уже упоминал его в начале,не забыли?)

private void Update()
    {
        if(mainThread.Count > 0)
        {
            mainThread.Dequeue().Invoke();
        }

    }

Вот и всё,статья получилось довольно сжатой,но, думаю , смысл понятен.

Кастомные кнопки в инспекторе

Если пользовались различными редакторами,скриптами с AS и наверняка вы заметили,что многие программисты выносят кнопки,переключатели,ползунки в инспектор для вашего удобства.Пробовали когда-нибудь написать что-то подобное?
Впрочем,если у вас нет времени разбираться с UnityEditor,но очень хочется сделать нечто подобное —

То это готовое решение для вас — https://cloud.mail.ru/public/HjXJ/Ut6LGGwE3

Пример кода:

using UnityEngine;

namespace EasyButtons
{
    public class CustomEditorButtonsExample : MonoBehaviour
    {
        [Button("Custom Editor Example")]
        private void SayHello()
        {
            Debug.Log("Hello from custom editor");
        }
    }
}

Источник — https://github.com/madsbangh

8 мая  

Убираем лимит 30 CCU у NetDrone Engine Free

Всем привет,решил ,что такого рода статьи не отберут у меня много времени,но зато будут интересны некоторым читателям.

Предыстория:
Гулял на днях по AssetStore (далее AS), копался в категории Scripting/Network и набрел на один с виду хороший ассет — NetDrone Engine. Разумеется, я сразу забрал его к себе домой и стал декомпрессить. Поигрался со сценами и в итоге обнаружил для себя следующие фичи:

1) Надежный UDP, т.е надстройка над обычным UDP : проверка доставки сообщения,гарантия правильной последовательности и т.д

2) Каналы , комнаты , обработки всех возможных ситуаций (например отключение игрока от сервера) уже предусмотрены.

3) В комплекте сервер , написанный на 2 языках (C++,C#)

Затем начался разглядывать серверный код, и наткнулся на тот самый лимит в 30 одновременных коннектов.

Переменная iMaxUser отвечает как раз установление максимального кол-ва юзеров.

Метод Initialize вызывается из библиотеки libnetdroneunity_nocrypt_ccu30.dll

Если заглянем внутрь dll (используя дизассемблер dnSpy) ,то поймем,что со своей стороны мы можем лишь уменьшить это значение, но никак не увеличить

На этом можно было бы закончить статью, казалось бы,нужно лишь отредактировать код в dnSpy и заменить оригинальную dll на нашу (модифицированную), но нет, при компиляции нас ждёт нечто подобное:

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

Погнали
Задействуем ildasm (Дизассемблер) и ilasm (Ассемблер). Две эти утилиты уже присутствуют на вашем ПК (если вы устанавливали Visual Studio)

Воспользуйтесь поиском на вашем ПК,чтобы отыскать ildasm.exe и ilasm.exe

Теперь запускаем ildasm.exe и кидаем в открывшееся окошко нашу dll. В опциях выбираем Дамп, и ничего не меняя сохраняем в произвольную папку. На выходе получим 2 файла : имя.il , имя.res

Теперь открываем имя.il с помощью блокнота и ищем интересующую нас переменную

Заменяем на своё значение и сохраняем.

Теперь, если вы нашли вторую утилиту ilasm.exe ,то скопируйте путь до папки, в которой она лежит и выполните команду в cmd

Команда : cd <путь до папки>

Ну и последний штрих — запускаем ilasm с параметрами, вот так — ilasm /dll C:\StudyServer\newIL\code.il /output=C:\StudyServer\newIL\code.dll

где C:\StudyServer\newIL\code.il — путь к нашему il файлу иC:\StudyServer\newIL\code.dll — путь к dll файлу,который создастся после отработки ilasm .

Если всё прошло ОК , то вы должны увидеть такое сообщение:

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

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

UPD: Забыл написать,что в ConfigEx.cs на 98 строке также необходимо поменять значение аргумента у метода SetMaxConnection(30) на своё

UPD: если вы криворукий балван,то вот ссылка на модифицированную dll — https://cloud.mail.ru/public/D4L2/UhtYbhi2j

Архитектура сессионных(комнатных) сетевых игр

Сессионные игры — игры,в которых каждый отдельно взятый игрок может создать своё лобби,пригласить или подождать подключения других игроков и начать играть с подключенными к лобби игроками.

Вопрос:
Пишу небольшую мультиплеерную настольную игру. Каждый «стол» игры планируется в отдельном потоке. Возник вопрос по организации приема данных от клиентов (соединение UDP): Все потоки слушают 1 порт и каждый ловит из общей массы нужную для стола инфу или же назначать порт для каждого отдельного стола избавляя от функции сортировки данных?

Ответ:
Порты и потоки — это очень ценный и ограниченный ресурс. Портов всего 65536 (и не все из них можно юзать), а потоков вообще нежелательно иметь больше чем процессорных ядер (упрощенно говоря). Что если к серверу подключится 100 клиентов? А если 1000? Дальше такой вопрос: если для каждой игровой сессии выделять отдельный порт, то откуда клиент будет знать на какой порт ему стучаться? Значит первоначальное соединение всё равно будет происходить через один фиксированный порт? Но тогда получается, что всё равно нужен механизм сортировки пакетов от разных клиентов. То есть его не избежать. Но и бояться этого не надо. Клиент просто должен каждый раз передавать ID сессии, а сервер легко найдет по нему нужную (это одна строчка кода).

Правильный подход будет приблизительно таким. Состояние каждой игровой сессии (стола) должно храниться в некоем объекте. При этом, никакого потока у сессии нет, просто хранится её состояние (ну типа чья очередь ходить, у кого сколько очков, кто где стоит) и всё. И таких вот объектов целый массив, по количеству столов. Один единственный поток слушает один единственный порт. Когда клиент обращается к серверу, он передает ID своей сессии. Сервер принимает пакет, и по ID находит в массиве нужную сессию. Вот тут, он запускает новый поток, и в этом потоке обрабатывает полученные данные. В результате обработки состояние сессии изменяется, клиенту при необходимости отправляется какой-то ответ, а поток обработки сразу после этого завершается. То есть суть в том, что новый поток запускается лишь тогда, когда нужно отреагировать на действия клиента, и не живет дольше чем надо.

Что это дает? Если у нас будет 1000 клиентов, то маловероятно, что все они одновременно сделают ход. Ну максимум 100 из них могут походить. Таким образом, в каждый момент времени у нас не 1000 запущенных потоков, а в 10 раз меньше (на практике будет еще меньше). В случае чего, можно легко добавить механизм, который будет ограничивать максимально допустимое количество потоков (скажем, не более 16), а «лишние» запросы выстраивать в очередь.
Возможно вы делаете любительский проект и там не будет ста клиентов одновременно. Тогда, если вы не хотите усложнять, то можете вообще не создавать новые потоки на каждый клиентский запрос, а обрабатывать всё в одном. Конечно клиентам придется ждать пока сервер ответит им по очереди. Но ИМХО это решение всё равно будет более верным с архитектурной точки зрения, чем держать долгоживущий поток на каждую сессию. Навряд ли время ожидания будет заметно человеку при количестве клиентов 10-20.

7 мая   games   room   udp