Unity 5 uNet – нова мережева підсистема. Етюд з авторитарної архітектурі

image

Приклад реалізації мережевої архітектури в Unity3D з авторитарним сервером

В виду того, що:
а) старі мережеві принципи Unity3D, як кажуть, застаріли;
б) я все одно не розумів, як у мене запрацювало то колдунство.
в) настирливо з'явився новий метод і почав стрибати в очі при кожному запуску Unity.
я вирішив, що треба в мережевому обміні розібратися щільніше і усунути область «here be dragons».

Я почав пробивати стежку в одному напрямку – авторитарний сервер. Це означає, що ігрові події, від повороту до нанесення тяжких образ, не сумісних з невинністю, вирішуються і виконуються виключно сервером. Гравець має право тільки замовити у сервера виконання своїх намірів. Такий підхід дозволяє разом відсікти безліч читеров і хакерів, які реверсно инжинирят клієнтський код і втручаються в нього. Звичайно, є ще багато інших способів чітерства, але це поза рамками дослідження.
Завдання:
Сформувати етюд, який реалізує авторитарну архітектуру.
Обмеження:
У цьому нарисі я не займаюся організацією сесій з'єднань – для цього існують методи Unity3D «для чайників».
У цьому нарисі я не буду називати, які клавіші натискати. Читач, який не знає, як робити названі дії у Unity3D, насправді не є готовим читачем для даної статті.
У цьому нарисі я не буду вирішувати фізику процесу, обчислювати аеродинаміку та інше. Тут тільки порядок взаємодії мережевих учасників.
Необхідний результат:
На виході повинен бути виконуваний приклад, в якому видно, як клієнт взаємодіє з сервером, докладно і прозоро.
Для цього мені довелося виробити свою термінологію. Без термінології задачка мені ніяк не давалася.
Терміни:
Сервер – майданчик, на якій виповнюється код, що обслуговує інших людей. Зазвичай його консолі людина фізично не торкається.
Хост – майданчик, на якому виконується серверна код, проте, на ній же одночасно грає один з гравців. Спеціально цей режим не розглядається, тому розуміння такого режиму складається з суми розумінь як працює сервер і клієнт.
Клієнт – майданчик, на якій виповнюється код, підзвітний конкретного гравця. Грубо кажучи, ваш комп'ютер.
Юніт – ігрова одиниця, що представляє гравця в ігровому просторі, чар, перс, і так далі.
Архітектура Unity3D така, що зрозуміліше спробувати написати один скрипт управління одним юнітом, ніж намагатися написати три різних скрипта: для свого управління, для серверного подання моєї ігрової особистості, для чужих гравців на моєму комп'ютері. Цілком можливо написати ці три скрипта, але це спричинить тонкощі, які починають вивчати игродельство не потрібні (та і я їх поки що не переміг).
Тому один і той же модуль повинен виконуватися в трьох варіантах і копіях, у мене, на сервері і для чужого гравця на моєму комп'ютері.
«Я», «мій» – код, який сприймає мої наміри безпосередньо через консоль, джойстик і т. д. (Дисплей не обов'язковий ;-))
«Дух» – код, який виконується на сервері, і тільки він може реалізовувати ігрові рішення.
«Мій дух» – код на сервері, який транслює мої наміри в дії на ігровій арені.
«Чужий дух» – відповідно код, який реалізує чужі дії на сервері.
«Аватар» – код, який відображає дії будь-якого гравця на моєму (або чужому) екрані.
«Мій аватар» – код відображає дії саме мого Духа». «Я» сам собі є «моїм аватаром».
Ми будемо писати єдиний скрипт, який задовольняє цим ролям. Чому один і той же скрипт? Та просто тому, що ми виїжджаємо на однакових або порівнянних танках. Адже ви не змусите мене писати код для Т-34, інший для Panzer 4, третій для Sherman Mk.IVAY?!
Вибачте мене за винахід такого велосипеда, але річне ліниве гортання тим розробки мультиплееров показало, що такої термінології не існує взагалі, а спроби пояснення в існуючих раніше термінах людьми сприймаються важкувато.
Якщо вам ця термінологія неприємна, підкиньте мінусів в карму і спокійно розійдемося.
image
Ну ось. Як я говорив раніше, писати в Unity3D я вирішив єдиний скрипт для мене, мого духа і мого аватара. А заодно і «того хлопця». Як, власне повсюдно рекомендує документація Unity3D.
Фух… Готові?
Почали.
1) Відкриваємо Untiy3D, додаємо землю, наприклад, Літак. Масштабується до підлога 1000*1*1000.
2) Створюємо Material, фарбуємо його в колір RBG=64:80:40. Призначимо його як матеріал підлоги. Просто, щоб білий колір за замовчуванням нас не засліплював. Я його назвав Ground.
3) Додаємо на сцену Cube. До нього додаємо компонент RigidBody, масу якого вказуємо 30 тонн. Нормально для середнього танка. Перейменовуємо Cube в Tank. Масштаб Tank виставляємо (3:2:6). Піднімаємо його над підлогою, вказавши координату Y, що дорівнює 1,1. При цьому X і Z раджу залишити нульовими.
4) Додаємо на сцену Cube. Вказуємо масштаб (1,5:1:2). Називаємо його Turret. Координати (0:2,4:1).
5) Додаємо на сцену Cube. Вказуємо масштаб (0,2:0,2:4) і координати (0:2,5:4). Назвемо Barrel.
6) Додаємо на сцену порожній GameObject, назвемо Gun. Координати (0:2,4:2), масштаб не чіпаємо.
7) Вкладіть об'єкт Turret в об'єкт Tank. Вкладіть об'єкт Gun в об'єкт Turret. Вкладіть об'єкт Barrel в об'єкт Gun.
8) Я призначив частинах танка матеріал Khaki з кольором (64:96:32). Просто так.
В результаті ми отримаємо якусь подобу танка. Якщо ви запитаєте, навіщо треба вкладати в Gun Barrel, відповім – якщо обертати Barrel по осі X, то вісь буде не там. А якщо обертати Gun по осі X, то… в прийнятному діапазоні картинка не викликає сказу. Насправді ви будете працювати з нормальними графічними моделями, у яких центри обертання вказані правильно ще в графічному редакторі, і таких проблем у вас не повинно виникати.
image
Йдемо далі.
Ми будемо орієнтуватися на статтю docs.unity3d.com/Manual/UNetConverting.html, але тільки орієнтуватися. Ми будемо відхилятися від статті.
9) Створюємо на сцені GameObject і називаємо його NetworkCommander. До нього додаємо два компоненти: NetworkManager і NetworkmanagerHUD. Треба переконатися, що у компонента NetworkManagerHUD включена галочка ShowRuntimeUI. NetworkManager реалізує загальне управління мережевими сесіями. Для простоти гравців/розробників компонент NetworkManagerHUD малює на екрані простеньке меню, в якому закладені базові дії з мережею сервірувати гру, хостити гру, приєднатися до гри.
10) До об'єкта Tank додайте компонент NetworkIdentity. Однак не вмикайте галочку LocalPlayerAuthority. Це нам не потрібно для авторитарної архітектури, та все одно сенсу вона не має.
11) Зробіть із танка Prefab – тобто скопіюйте його зі сцени у ресурси. Назвіть TankPrefab.
12) В об'єкті NetworkCommander в розділі SpawInfo вставте TankPrefab в поле PlayerPrefab.
13) Створіть C# скрипт ArmourDrive.cs. Процедура Start() нам на даний момент не потрібна. Нам потрібна тільки Update().
14) Увімкніть простір імен UnityEngine.Networking. Це дозволить нам користуватися мережевими функціями uNet.
15) Змініть клас ArmourDrive з MonoBehaviour на NetworkBehaviour. Цей клас розширює звичайну логіку, додаючи до неї мережеві функції.
16) Створіть у класі ArmourDrive поля:
float veloMyMax = 10, veloMyCurr = 0; //змінні, які використовую я. Максимальна доступна і бажана швидкості, м/с.
float veloSvrMax = 10, veloSvrCurr = 0; //змінні, які використовує мій дух. Максимально доступна і необхідна швидкості, м/с.
float periodSvrRpc = 0.02 f; //як часто сервер шле оновлення картинки клієнтам, с.
float timeSvrRpcLast = 0; //коли останній раз сервер слав оновлення картинки


17) Поточний скрипт буде виконуватися відразу в трьох місцях: у мене для читання моїх намірів («я»), на сервері для прийому намірів і перетворення їх у дії («мій дух»), і на компі того хлопця, для якого дії будуть малюватися («мій аватар»). Розрізнити ці три іпостасі ми можемо двома змінними.
Змінна isServer означає, що ця копія скрипт виконується на сервері і, в даному випадку є «моїм духом».
Змінна isClient означає, що ця копія скрипта виконується на клієнта, в даному випадку є «чиїмись аватаром».
Змінна isLocalPlayer означає, що це саме «мій аватар».
18) Сформуємо наміри всередині функції Update:
if (this.isLocalPlayer)
//Код виконується тільки в мене
{
//Формуємо нову вимогу по швидкості
float veloMyNew = 0;
veloMyNew += Input.GetKey(KeyCode.W) ? veloMyMax : 0;
veloMyNew += Input.GetKey(KeyCode.S) ? -veloMyMax : 0;
if (veloMyCurr != veloMyNew)
//Якщо вимога щодо швидкості змінилося, то шолом на сервер нова вимога
{
CmdDrive(veloMyCurr = veloMyNew);
}
}

19) Для того, щоб надіслати на сервер мої наміри, програміст зобов'язаний: а) почати назва функції з літер «Cmd», б) при описі функції позначити її, як виконується на сервері на вимогу клієнта. Ось її текст:
[Command(channel = 0)]
void CmdDrive(float veloSvrNew)
{
if (this.isServer)
//Мій дух приймає і перевіряє команду.
{
//Перевіряємо моя вимога на валідність.
veloSvrNew = Mathf.Clamp(veloSvrNew, -veloSvrMax, veloSvrMax);
//Встановлюємо поточне значення необхідної мною швидкості для духу.
veloSvrCurr = veloSvrNew;
//Виконувати дух пізніше.
}
}

20) Тепер сервер повинен авторитарно пересунути мій танк. Це можна зробити і в згаданій функції Update(). Однак автори Unity3D рекомендують фізичні розрахунки здійснювати в межах FixdUpdate(). У нас не крута фізика, але підемо раді:
void FixedUpdate()
{
if (this.isServer)
//Код виконується тільки в духа
{
//Обробити мої команди
this.transform.Translate(0, 0, veloSvrCurr * Time.deltaTime, Space.Self);
if (timeSvrRpcLast + periodSvrRpc < Time.time)
//Якщо час, то вислати координати всім моїм аватарам
{
RpcUpdateUnitPosition(this.transform.position);
RpcUpdateUnitOrientation(this.transform.rotation);
timeSvrRpcLast = Time.time;
}
}
}

21) Назад функцій Cmd виконуються функції Rpc – вони викликаються сервером, а виконуються на клієнтських машинах. Тут теж синтаксичні вимоги: функція повинна мати позначку [ClientRpc], і назву має починатися на літеру «Rpc». Сервер нам вислав нові координати, які вийшли з нашого вимоги швидкості, і ми повинні переставити свій юніт в нове місце.
[ClientRpc(channel = 0)]
void RpcUpdateUnitPosition(Vector3 posNew)
{
if (this.isClient)
//Мої аватари копіюють стан мого духа.
{
this.transform.position = posNew;
}
}

Тут можна було б вставити гладке наближення до цих координатах щоб уникнути ривків танка на дисплеї, але ми не будемо на це відволікатися.
22) Оскільки танк може при лінійному русі випадково повернутися (на купину наїхати і т. д.), нам треба нову орієнтацію танка так само виставити
[ClientRpc(channel = 0)]
void RpcUpdateUnitOrientation(Кватерніонів oriNew)
{
if (this.isClient)
//Мої аватари копіюють стан мого духа.
{
this.transform.rotation = oriNew;
}
}

23) Цей скрипт треба додати до префабу танка
24) Тепер ми майже готові стартувати гру. Але є проблема – звичайно зручніше працювати з ігровим об'єктом на сцені, конфігурувати, розвивати його. Але при старті мережевої гри цього об'єкта на сцені бути не повинно. Зазвичай на youtube різні вчителі перед запуском гри перуть зі сцени цей об'єкт. Але мені зручніше його вимикати, а не видаляти. Вимкніть об'єкт Tank на сцені або видаліть (переконайтеся, що префаб існує!)
25) Тепер ми точно готові запускати гру. Давайте скористаємося режимом «Хост», щоб створити і увійти в гру швидко без проблем з запуском додаткових копій. Стартуйте і натисніть в діалозі, який надає NetworkManagerHud, кнопку LAN (Host).
26) Оп-па. Танк потонув у землі вежу. Це тому, що NetworkManager за замовчуванням спавнит гравця на нульовій висоті, так, що днище танка вже знаходиться під землею. З респавном гравців ми можемо розібратися пізніше, а зараз в педагогічних цілях давайте дозволимо собі одну милицю. Тому, що ми розбираємо зараз не респавн, а мережевий рух. Пишемо:
void Awake()
{
//Милицю: підняти гравця на 110см вище, щоб не провалювався в ландшафт
//TODO розібратися з координатами респавнинга гравців.
this.transform.position = new Vector3(0, 1.1 f, 0);
}

27) Запускаємо гру знову і знову вибираємо режим Host. Все нормально, танк при натисканні клавіш рухається вперед і назад.
image
28) Тепер давайте зберемо Standalone, щоб ми могли запустити кілька копій гри. Викликаємо Build Settings і Player options. Обов'язково включаємо галочку Run in Background – це дозволить нам на одному комп'ютері запустити кілька примірників без того, щоб більшість з них впало в сплячку. Так само давайте вимкнемо Default is Full Screen і для наочності виставимо дрібне дозвіл 400*300. Оскільки нам поки не потрібно, вимикаємо Display Resolution Dialog в Disabled. Далі знову запускаємо Build і створюємо виконуваний файл.
29) Ми можемо запустити виконувані файли кілька разів.
30) Ми можемо включити їх в будь-якій комбінації – хост і два клієнта, або сервер та два клієнта. Ось приклад сервера і двох клієнтів
image
Результат
Ми можемо переконатися, що обидва танки їздять нормально, гладкість синхронізації для такого етюду більш ніж прийнятна.
Але ви можете сказати «Стривайте! Адже реалізовано рух тільки вперед-назад! А де ж поворот танки і вежі?!»
На це у мене відповідь такий:
Поворот танки, і гармати вежі можна реалізувати абсолютно аналогічно руху вперед-назад.
Чому я реалізував мережевий обмін саме так, і навіщо я вываливаю цю саморобку на Хабр?
Ось саме тому, що туторіали по нової мережевої системі uNet поки що жалюгідні, мізерні в кількості, а методи, які вони наставляють, не можуть повернути частини танка, тільки весь танк
А тут ми розглянули (я сподіваюся) зрозумілу логіку роботи мережі, яка дійсна і для руху частин танка, і для диму, і для ушкоджень, і, загалом-то, самих пострілів.
Такі справи.

Джерело: Хабрахабр

0 коментарів

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