AAcademyCloud
Сервер MMORPG на Java
Безкоштовний урок-прев'ю

Архітектура ігрового сервера: з чого складається світ

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

Цей курс — на нашому власному матеріалі. Ми тримаємо живий світ Ейрін і розбираємо системи, які в ньому справді працюють.

Чим ігровий сервер відрізняється

Вебсервер Ігровий сервер
З'єднання коротке, стан у БД постійне, стан у пам'яті
Модель запит → відповідь двонаправлений потік подій
Стан у базі у пам'яті, база — резервна копія
Затримка 200 мс прийнятно 100 мс уже відчутно
Масштабування додати інстанс світ не ділиться просто так

Головне: стан живе в пам'яті. Читати позицію персонажа з бази на кожен крок неможливо — це тисячі запитів на секунду з нульовою користю. База потрібна, щоб пережити перезапуск, а не щоб працювати.

Звідси випливає решта курсу: як тримати стан консистентним у пам'яті, як його вчасно скидати на диск і що робити, коли процес усе-таки впаде.

З чого складається система

              ┌──────────────┐
   гравець ──▶│ Login-сервер │──┐
              └──────────────┘  │
                                ▼
                          ┌──────────┐
                          │  MySQL   │
                          └──────────┘
                                ▲
              ┌──────────────┐  │
   гравець ──▶│ Game-сервер  │──┘
              └──────────────┘

Login-сервер перевіряє логін і пароль, віддає список світів і видає одноразовий ключ сесії. Живе окремо, бо це єдина точка, де ходить пароль, і бо перезапуск ігрового світу не має вибивати людей з автентифікації.

Game-сервер тримає світ: персонажів, NPC, предмети, бій. Тут постійні з'єднання й ігровий цикл.

База зберігає те, що має пережити перезапуск: акаунти, персонажів, інвентар, стан кланів.

Часто додається кеш (Redis) для міжсерверних речей: онлайн-статус, черга, повідомлення між світами.

Потоки: що де виконується

Найважливіше архітектурне рішення — розділення потоків.

[Netty I/O потоки]  читають байти з сокетів, декодують пакети
        │
        ▼  черга
[Ігровий потік]     виконує логіку світу: рух, бій, тік
        │
        ▼  черга
[Пул БД]            асинхронні збереження, батчинг

Правило, порушення якого відчують усі: в ігровому потоці не можна блокувати. Жодного запиту в базу, жодного Thread.sleep, жодного мережевого виклику. Запит до MySQL на 50 мс — це 50 мс, протягом яких увесь світ стоїть.

Другий бік цього правила: ігрова логіка виконується в одному потоці (або по одному на регіон), тому здебільшого не потрібні блокування. Це навмисний обмін: відмовляємось від паралелізму логіки заради простоти й передбачуваності.

Тік

Світ живе дискретними кроками:

while (running) {
    long start = System.nanoTime();

    processIncomingPackets();   // те, що надіслали гравці
    updateMovement();           // перерахувати позиції
    updateCombat();             // удари, кулдауни, регенерація
    updateAI();                 // рішення NPC
    broadcastChanges();         // розіслати зміни

    long elapsed = (System.nanoTime() - start) / 1_000_000;
    long sleep = TICK_MS - elapsed;
    if (sleep > 0) Thread.sleep(sleep);
    else logger.warn("Тік не вклався: {} мс", elapsed);
}

Типовий тік для MMORPG — 100–200 мс. Для шутера — 15–30 мс, але там і світ менший.

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

Стек

Що використовуємо в курсі:

  • Java 21 — віртуальні потоки, records, pattern matching
  • Netty 4 — асинхронний мережевий шар
  • MySQL + HikariCP — сховище й пул з'єднань
  • Caffeine — кеш у пам'яті
  • Micrometer + Prometheus — метрики
  • JMH, async-profiler — вимірювання

Чому Java, а не C++: збирач сміття — керована проблема, а швидкість розробки й екосистема виграють. Більшість відомих емуляторів MMORPG написані саме на Java.

Що ламається першим

Порядок, у якому проєкти зазвичай упираються в стелю:

  1. Блокуючий виклик в ігровому потоці — світ завмирає на час запиту
  2. Broadcast усім замість тих, хто поруч — трафік росте квадратично
  3. Збереження на кожну дію — база лягає під навантаженням
  4. Довгі GC-паузи — світ замирає на секунду, гравці телепортуються
  5. Довіра до клієнта — читери роблять що завгодно

Кожному з цих пунктів присвячено окремий модуль.

Структура проєкту

mmorpg-server/
├── common/          спільні моделі й утиліти
├── login-server/
│   ├── net/         пакети автентифікації
│   └── auth/
├── game-server/
│   ├── net/         кодеки, обробники пакетів
│   ├── model/       персонажі, NPC, предмети
│   ├── world/       регіони, видимість, спавн
│   ├── combat/      формули, навички
│   ├── ai/
│   └── db/          DAO, черга збереження
└── data/            конфіги світу: спавни, дроп, статистика

Розділення model і db принципове: ігрова модель не має знати про SQL. Інакше кожна зміна схеми потягне за собою правки бойової логіки.

Практика

  1. Створи багатомодульний Gradle-проєкт за структурою вище.
  2. Напиши каркас ігрового циклу з фіксованим тіком і логуванням перевищень.
  3. Додай штучну затримку 300 мс усередині циклу й подивись у логах, як поїде тік.
  4. Порахуй на папері: скільки пакетів на секунду згенерує 500 гравців, якщо кожен рухається двічі на секунду, а про рух знають усі 500.

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

Підсумок

  • Стан живе в пам'яті, база — засіб пережити перезапуск
  • Login і game розділені навмисно
  • В ігровому потоці не можна блокувати — жодного I/O
  • Один потік логіки прибирає більшість гонок
  • Тік, що не вкладається, — головна метрика здоров'я світу
  • Модель не знає про SQL

Далі — мережевий шар.

Сподобався урок?

Придбайте повний курс, щоб отримати доступ до всіх уроків.

Перейти до курсу