Локальний мультиплеєр в Unity за допомогою Unet

Всім привіт! Сьогодні хотілося б розповісти про один із способів, як можна створити локальний мультиплеєр в Unity. Дане рішення підходить для шоукейсов, тесту фіч або локального мультиплеєра. Наприклад, якщо вам хочеться бачити, що робить гравець, але не хочеться скажімо на андроїд витрачати зайві ресурси і забирати скрінкасти з допомогою ADB, то можна просто підняти сервер на якийсь машинці у вигляді копії додатку, яке працює на телефоні, і слати туди інформацію про дії гравця.


Я коротко опишу, як можна це зробити з допомогою HLAPI на Unet, але не через NetworkingManager, а трохи більше низкоуровнево. За доброю традицією докладу приклад реалізації такого клієнт-серверної взаємодії. Приклад прошу не судити строго, так як я прекрасно розумію, що архітектура даного рішення нікуди не годиться і створює купу проблем у перспективі. Моєю метою було максимально швидко (за вихідні) написати систему, в якій можна показати принцип роботи з мережею. Також скажу з якими проблемами довелося зіткнутися. В даному прикладі реалізації передбачається, що сервер так само є Unity додатком.

В інтернеті самим частим прикладом того, як зробити мультиплеер є чат, але я люблю ігри, і робити чат мені здалося нудним. Тому я вирішив розібрати те, як можна зробити мультиплеер на прикладі хрестиків-нуликів. Припустимо, що вся логіка гри, визначення переможця, зміни ходів і т.п. написані, і нам залишилося прикрутити сервер. В найпростішому випадку В хрестики-ноликах нам треба обробляти 2 повідомлення. Визначення порядку ходу (роздача айдишников) і захоплення клітини.



В цілому гра в даному прикладі працює дуже просто. Є 2 сцени. Завантажувальна і геймплейна. При завантаженні геймплейної сцени у нас генерується поле, на якому грають гравці. Там же відбувається перевірка умови перемоги, є класи відповідають за роботу UI, а так само порядок ходів і в цілому логіку гри, але нам це не особливо цікаво. Основні класи, які відповідають за сітку — це Server Client, NetManager і окремий файл для повідомлень NetMessages і визначений у ньому enum MyMsgType. Вони представляють із себе обгортку над засобами Unet. З точки зору Unet основні класи, які ми будемо використовувати — це NetworkClient, NetworkServer, MessageBase і MsgType. Що це за класи?

Найпростіші класи — це MessageBase і MsgType. Перший — це абстрактний клас, від якого треба наслідувати всі наші повідомлення, щоб пересилати їх між клієнтом і сервером. MsgType — це просто клас, який зберігає в собі константи, що відповідають за певний набір ушитих в Unet повідомлень.



NetworkServer — це одинак, який надає можливість обробляти спілкування з віддаленими клієнтами. Під капотом він використовує примірник NetworkServerSimple і по суті представляє з себе зручну обгортку над ним. Для початку нам треба запустити сервер на певному порті. Для цього необхідно викликати метод Listen(int serverPort) — цей метод запускає сервер на порте serverPort. (Нагадаю, що всі порти в діапазоні від 0 до 1023 є системними і їх не варто використовувати в якості параметра даного методу)

Відмінно сервер працює і слухає якийсь порт. Тепер потрібно, щоб він реагував на повідомлення. Для цього потрібно зареєструвати хендлер з допомогою методу RegisterHandler(short msgType, Networking.NetworkMessageDelegate handler). Даний метод приймає параметром тип повідомлення і делегат. Делегат у свою чергу повинен приймати вхідним параметром NetworkMessage. Припустимо ми хочемо, щоб в той момент, коли до нього приєднувався гравець на сервері початку завантажуватися геймплейна сцена, а так само лунали айдишники гравців. Тоді потрібно зареєструвати хендлер на відповідне повідомлення, а так само реалізувати метод, який ми будемо передавати в якості делегата для реєстрації.

Виглядає це приблизно так:

Приклад реєстрації хендлер і делегата
NetworkServer.RegisterHandler(MsgType.Connect, OnConnect);

private void OnConnect(NetworkMessage msg)
{

Debug.Log(string.Concat("Connected: ", msg.conn.address));
var connId = msg.conn.connectionId;
if (NetworkServer.connections.Count > Constants.PLAYERS_COUNT)
{
SendPlayerID(connId, -1);
}
else
{
int index = Random.Range(0, Constants.PLAYERS_IDS.Length);
SendPlayerID(connId, Constants.PLAYERS_IDS[index]);
_CurrentUser.PlayerID = Constants.PLAYERS_IDS[(index + 1) % Constants.PLAYERS_COUNT];
SceneManager.LoadScene(1);
}
}


Тепер метод OnConnect буде викликатися при кожному конекті якогось користувача. Варто уточнити, що в даній реалізації айдишники визначають порядок ходу, тому при першому коннекті вибираються айдишники для клієнта і сервера. Інші клієнти автоматом отримують айді рівний -1, що означає, що цей клієнт є глядачем.

Простий сервер є. Тепер клієнти не завадили. Для цього скористаємося класом NetworkClient. Для того, щоб приєднатися до сервера треба просто викликати метод Connect(string serverIp, int serverPort). В якості порту ми встановлюємо порт, який слухає наш сервер, як айпі ставимо localhost, якщо ми тестуємо наше додаток на одній машині або ж айпі компа в локальній мережі, який ми використовуємо в якості сервера (Дізнатися його можна або в налаштуваннях мережі, або в консолі за допомогою команди ipconfig на тому компі, який буде виступати в ролі сервера).

Чудово, ми може подконнектиться. Раніше говорилося, що сервак у нас роздає айдишники. Значить нам треба по-перше, послати повідомлення про айдишнике, а так само зареєструвати хендлер цього повідомлення на клієнті. Як згадувалося раніше всі повідомлення треба наслідувати від MessageBase. Для початку визначимо їх (я люблю це робити в окремому файлі):

Повідомлення і його тип
public class PlayerIDMessage : MessageBase
{
public int PlayerID;
}
public enum MyMsgType : short
{
PlayerID = MsgType.Highest + 1,
}


Щоб надіслати це повідомлення викликаємо на клієнті метод Send(short msgType, Networking.MessageBase msg), який відправить повідомлення msg «типу» msgType сервера, або на сервері один з методів залежно від мети SendToAll(short msgType, Networking.MessageBase msg) або SendToClient(int connectionId, short msgType, Networking.MessageBase msg), де connectionId — це id певного клієнта.

Щоб читати наші кастомні повідомлення прийшли нам від іншої програми користуємося reader.ReadMessage(). Наприклад:

Обробка на клієнті прийшов айдишника
private void OnPlayerID(NetworkMessage msg)
{
PlayerIDMessage message = msg.reader.ReadMessage<PlayerIDMessage>();
_CurrentUser.PlayerID = message.PlayerID;
SceneManager.LoadScene(1);
}


В принципі все. Подальше використання залежить від конкретики. Створюємо потрібні нам повідомлення в залежності від геймплея і відправляємо їх. У хрестики-ноликах я так само визначив ще одне повідомлення, яке відповідає за захоплення точки. Цілком реалізацію проекту можна подивитися тут.

Про що ще хочеться сказати, та на що довелося витратити час, так це кілька речей, які вам можуть бути не очевидні, якщо ви не працювали з мережами.

1. Перевірте, що редактор юніті не заблокований вашим фаєрволом для спілкування по протоколах TCP і UDP. Одного разу я витратив на це деякий час, при тому, що дістався до фаєрвола і поставив потрібний порт в винятки, але не перевірив те, що редактор незаблокирован.

2. Передавайте в повідомленнях value-type. Reference-type будуть передавати нісенітниця, так як за адресою який ви хочете передати в іншу аппу, невідомо що лежить. (Я думаю, що це і так зрозуміло тим, хто розуміє, як працюють value і reference type)

У випадку, коли ми говоримо про локальну мережу і заздалегідь відоме залізо, немає багатьох проблем, що виникають в разі «справжнього» мультиплеєра. Практично не потрібно замислюватися про затримки, флуктуаціях, втрати пакетів та інше. Тому таке рішення може бути корисно, хоча і ці проблеми можна вирішувати з таким підходом до деякої межі. У порівнянні з більш високим рівнем абстракції в Unet, через NetworkManager, NetworkBehavior і т. п., воно надає більш зрозумілу і очевидну гнучкість (на мій погляд), якщо на клієнтах потрібно завантажувати різні сцени і т. п., а скажімо сервер використовується, як стрімінг, і показує те, що відображається на девайсі гравця, приймаючи його позицію і поворот + дублюючи відбувається на своїй стороні. В інших випадках, коли потрібне рішення швидше і ви вмієте працювати з мережею, Unet надає можливість писати на транспортному рівні.

Так само хочеться уточнити про рішення на гітхабі (не з точки зору інструментів, а з точки зору підходу до архітектури) на мій погляд, це приклад — як не варто робити. Не кажучи про саму архітектуру ігри проблема в тому, що логіка на клієнтові і на сервері вважається незалежно. При реалізації адекватного мультиплеєра з клієнт-серверною архітектурою краще, коли стан гри зберігається на сервері і реплікується на клієнтів, а клієнти посилають команди, які змінюють стан гри на сервері. Це звичайно так само залежить від багатьох факторів, але в середньому такий підхід.

Продублюю тут посилання на рішення.

Спасибі за увагу!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.