Мультиплеер швидких іграх (частини I, II)



Пропоную вашій увазі переклад статті Fast-Paced Multiplayer (Part I): Introduction.

Розробка гри — саме по собі непросте заняття. Але мультиплеєрні ігри створюють абсолютно нові проблеми, які потребують вирішення. Забавно, що у наших проблем всього дві причини: людська натура і закони фізики. Закони фізики привнесуть проблеми з області теорії відносності, а людська натура не дасть нам довіряти повідомленнями з клієнта.

Якщо ви вже знайомі з базовими ідеями, що стоять за розрахованими на багато іграми — сміливо пропускайте першу частину і переходите до другої.

Частина I
Проблема чітерства
Вся наша головний біль починається з чітерства.

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

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

Є багато речей, які можна зробити, щоб запобігти чітерство. Але найголовніший принцип(і напевно самий глибокий) дуже простий: не довіряй гравцеві. Завжди чекайте найгіршого — що гравець намагатися вас обдурити.

Авторитарний сервер і наївний клієнт
Цей принцип веде нас до простого, на перший погляд, рішення — вся ігрова логіка крутиться на головному сервері, під вашим контролем, а клієнт лише демонструє поточний стан сервера і відправляє йому команди (натискання клавіш і т. д.). Зазвичай це називають авторитарним сервером, тому що він єдиний, хто вміє моделювати світ.

Звичайно, сервер може бути зламаний, але ця тема виходить за рамки цієї серії статей. Тим не менше, використання авторитарного сервера пердотвращает широкий спектр читів. Наприклад, ви не можете довіряти клієнтові рівень життів гравця. Зламаний клієнт може змінити локальну інформацію і повідомити що у гравця 100000% життів, але сервер знає життів всього 10% і якщо гравці атакують, він помре незалежно від того, що про це думає клієнт.

Так само не можна вірити гравцеві, коли він повідомляє про його позиції у світі. Якщо ви довіритеся, зламаний клієнт може повідомити сервера:

— Я на (10, 10)
А секундою пізніше:

— Я на (20, 10)

При цьому можливо він «пройшов» через стіну або рухається швидше ніж йому належить.

А ось правильна парадигма. Сервер знає що гравець знаходиться в позиції (10, 10); клієнт каже: «Я хочу посунутися на одиницю вправо». Сервер оновлює позицію гравця (11, 10), виконуючи всі необхідні перевірки, а потім відповідає гравцеві: «Ви (11, 10)»:





Підсумуємо: ігрове стан управляється тільки сервером. Клієнти надсилають свої дії на сервері, а сервер періодично оновлює свій стан і відправляє його на клієнтів, які, в свою чергу відображають його користувачам.

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

Давайте поговоримо про фізику. Припустимо що ви перебуваєте в Сан-Франциско і підключаєтеся до сервера в Нью-Йорку. Це приблизно 4000 кілометрів. Так як ніщо не може рухатися швидше швидкості світла, в кращому випадку сигнал дійде за 13 мілісекунд. Але дуже малоймовірно, що у вас буде така хороша зв'язок. В реальному світі інформація не йде прямим шляхом, причому не зі швидкістю світла.
Так що давайте припустимо, що це займає 50 мс. І це практично найкращий сценарій. А що якщо ви підключаєтеся до сервера в Токіо? А що якщо лінія зв'язку перевантажена? У таких випадках затримки доходять до половини секунди.

Повернемося до нашого прикладу. Нехай клієнт відправляє повідомлення:

— Я натиснув на стрілку вправо.

Сервер отримує запит через 50 мс і відразу відправляє назад оновлене стан.

— Ви на (11, 10)

Це повідомлення дійде до користувача через 50 мс.

З точки зору гравця, він натиснув на стрілку, потім 0.1 секунди нічого не відбувалося, а потім персонаж нарешті посунувся на одиницю вправо. Цей лад між командою та її результатом може здатися незначним, але він помітний. І вже звичайно лад в півсекунди був би не просто помітним, а зробив би гру абсолютно неиграбельной.



Резюмуючи
Ігри по мережі неймовірно веселі, але привносять абсолютно новий клас проблем і перешкод. Авторитарна архітектура гарна проти читеров, але наївна реалізація зробить гру неотзывчивой для користувачів.

Надалі ми досліджуємо можливість створення системи, що базується на авторитарній сервері, але з мінімальними затримками для гравців, роблячи їх не відрізнятись від одиночних ігор.


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



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

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

Ми можемо використовувати це якщо гра досить детермениртована (тобто результат визначений командами і попереднім станом).

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



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

Так що замість того щоб відправляти команду і чекати поки прийде нове ігрове стан щоб отрендерить його, ми можемо відправити команду і почати рендери результат як якщо б команда вже була виконана. І, зрозуміло, треба чекати від сервера результату — «цього» стану гри, яка здебільшого буде збігатися з локальним станом.



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

Проблеми синхронізації
У попередньому прикладі я обережно підібрав числа щоб все добре працювало. Давайте трохи змінимо сценарій. Лаг становитиме 250 мс, а анімація пересування на одну одиницю триватиме 100 мс. А ще давайте гравець двічі швидко натиснути на стрілку вправо.

При використанні поточного підходу ось що відбудеться:



Ми зіткнулися з цікавою проблемою на t = 250 мс, коли нас прийшов новий стан. Клієнт передбачив x = 12, але сервер каже що x = 11. Так як сервер авторитарний, клієнт повинен пересунути персонажа назад на x = 11. Але пізніше, на t = 350, сервер каже що x = 12, так що персонаж знову стрибає, але на цей раз вперед.

З точки зору гравця, він натиснув на стрілку вправо двічі, так що персонаж перемістився на дві одиниці вправо, постояв там 50 мс, стрибнув на одиницю вліво, постояв там 100 мс і стрибнув на одиницю вправо. Звичайно, це абсолютно неприпустимо.



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

Але вирішити цю проблему не так вже й складно. Давайте будемо додавати до кожного запиту від клієнта його номер. А сервер при відповіді буде додавати номер останнього обробленого запиту. І давайте зберігати на клієнті копію всіх команд, які надсилаються на сервер.



Отже, на t = 250 клієнтові приходить «x = 11, остання команда #1». Клієнт видаляє всі команди до #1 включно, але залишає копію #2, про яку ще не знає сервер. Він застосовує отримане від сервера стан (x = 11), а потім застосовує enter, який ще не видно сервера. У даному разі #2 «вправо на 1 одиницю». Кінцевий результат x = 12, що відповідає істині.

Далі, на t=350, від сервера приходить новий стан: x = 12, остання команда #2». Клієнт видаляє всі копії команд до #2 включно, а потім застосовує стан x=12(нічого не змінилося). Так як більше немає неопрацьованих команд, на цьому все закінчується, з коректним результатом.



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

Але не варто насправді оновлювати життя персонажа, поки сервер не надішле оновлене стан.

Із-за складнощів ігрового стану, яке не завжди легко відкотити, можливо не варто вбивати персонажа навіть якщо рівень його життя впав нижче нуля. Якщо інший гравець вилікувався прямо перед вашою атакою, але сервер ще про це не повідомив?

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

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

Резюмуючи
При використанні авторитарного сервера, ви повинні надати гравцеві ілюзію чуйності, хоча насправді ви чекаєте поки сервер насправді обробить enter. Для цього клієнт симулює результат всіх команд. Коли приходить оновлення від сервера, стан оновлюється виходячи з поточного стану сервера і необроблених їм команд.
Джерело: Хабрахабр

0 коментарів

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