Игровые приложения

Игровые приложения предъявляют к сетевому взаимодействию особые требования:

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

Библиотека STOMP.js используется как клиентская реализация протокола STOMP поверх WebSocket и позволяет организовывать обмен игровыми событиями между браузером и сервером.

Игровая архитектура обычно состоит из следующих компонентов:

Игрок → STOMP.js → WebSocket → Broker → Игровой сервер

Типичный стек:

Компонент Назначение
STOMP.js Работа с STOMP-протоколом
WebSocket Постоянное двустороннее соединение
RabbitMQ / ActiveMQ Message broker
Node.js / Java / Go Игровой backend
Redis Состояние игровых комнат
PostgreSQL Хранение статистики

Подключение игрового клиента

Для игровых приложений особенно важна стабильная настройка клиента.

Установка

npm install @stomp/stompjs sockjs-client

Создание клиента

import { Client } from '@stomp/stompjs';

const client = new Client({
    brokerURL: 'ws://localhost:8080/game',

    reconnectDelay: 5000,

    heartbeatIncoming: 4000,
    heartbeatOutgoing: 4000,

    debug(str) {
        console.log(str);
    }
});

Активация подключения

client.activate();

Организация игровых каналов

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

Пример структуры

/game/global
/game/rooms/1
/game/rooms/1/chat
/game/rooms/1/state
/game/player/52
/game/matchmaking

Такое разделение позволяет:

  • уменьшать объём лишнего трафика;
  • изолировать игровые комнаты;
  • разделять игровые и служебные сообщения;
  • организовывать персональные события.

Подписка на состояние игровой комнаты

Получение обновлений состояния

client.onConn ect = () => {

    client.subscribe('/topic/game/rooms/1/state', message => {

        const state = JSON.parse(message.body);

        updateGame(state);
    });

};

Обновление интерфейса

function updateGame(state) {

    renderPlayers(state.players);

    renderObjects(state.objects);

    renderScore(state.score);
}

Передача игровых действий

Игроки постоянно отправляют действия:

  • движение;
  • атаки;
  • использование предметов;
  • взаимодействие с объектами;
  • анимационные события.

Отправка движения игрока

function movePlayer(x, y) {

    client.publish({
        destination: '/app/game/move',
        body: JSON.stringify({
            x,
            y
        })
    });

}

Отправка атаки

function attack(targetId) {

    client.publish({
        destination: '/app/game/attack',
        body: JSON.stringify({
            targetId
        })
    });

}

Синхронизация состояния

В игровых приложениях существует два подхода:

Полная синхронизация

Сервер регулярно отправляет весь state.

{
    players: [],
    bullets: [],
    enemies: [],
    score: {}
}

Преимущества:

  • простота реализации;
  • лёгкое восстановление состояния.

Недостатки:

  • большой трафик;
  • высокая нагрузка.

Синхронизация дельт

Передаются только изменения.

{
    type: 'PLAYER_MOVED',
    playerId: 12,
    x: 450,
    y: 210
}

Преимущества:

  • низкий объём сообщений;
  • высокая производительность.

Недостатки:

  • сложность восстановления;
  • необходимость контроля порядка событий.

Интерполяция движения

При сетевых задержках движение объектов может быть рывками.

Интерполяция сглаживает анимацию между обновлениями.

Пример

function interpolate(current, target, alpha) {

    return current + (target - current) * alpha;
}

Использование

player.x = interpolate(
    player.x,
    serverState.x,
    0.1
);

player.y = interpolate(
    player.y,
    serverState.y,
    0.1
);

Клиентское предсказание

В action-играх ожидание ответа сервера делает управление «тяжёлым».

Поэтому клиент заранее отображает действие локально.

Схема

Игрок нажал клавишу
↓
Клиент сразу двигает персонажа
↓
Сервер подтверждает действие
↓
Клиент корректирует позицию

Пример

function localMove(dx, dy) {

    player.x += dx;
    player.y += dy;

    render();

    client.publish({
        destination: '/app/move',
        body: JSON.stringify({ dx, dy })
    });

}

Коррекция состояния

Сервер остаётся единственным источником истины.

Если клиент предсказал состояние неверно:

function reconcile(serverState) {

    player.x = serverState.x;
    player.y = serverState.y;
}

Обработка игровых событий

Игровые события удобно разделять по типам.

Пример универсального обработчика

client.subscribe('/topic/game/events', message => {

    const event = JSON.parse(message.body);

    switch (event.type) {

        case 'PLAYER_JOIN':
            handlePlayerJoin(event);
            break;

        case 'PLAYER_LEAVE':
            handlePlayerLeave(event);
            break;

        case 'SHOT':
            handleShot(event);
            break;

        case 'DAMAGE':
            handleDamage(event);
            break;

    }

});

Игровой чат

Чат обычно работает через отдельный канал.

Подписка

client.subscribe('/topic/chat/1', message => {

    const data = JSON.parse(message.body);

    appendMessage(data);

});

Отправка сообщения

function sendMessage(text) {

    client.publish({
        destination: '/app/chat',
        body: JSON.stringify({
            text
        })
    });

}

Matchmaking

Подбор игроков удобно реализовывать через отдельную очередь.

Поиск матча

client.publish({
    destination: '/app/matchmaking/search',
    body: JSON.stringify({
        rank: 1200
    })
});

Получение результата

client.subscribe('/user/queue/match', message => {

    const match = JSON.parse(message.body);

    connectToRoom(match.roomId);

});

Игровые комнаты

Каждая игровая комната изолируется собственным набором каналов.

Пример

/topic/room/15/state
/topic/room/15/events
/topic/room/15/chat

Управление подключениями

Игроки могут:

  • терять соединение;
  • обновлять страницу;
  • переключать вкладки;
  • временно уходить в офлайн.

Автоматическое переподключение

const client = new Client({

    brokerURL: 'ws://localhost:8080/game',

    reconnectDelay: 3000

});

Восстановление игрового состояния

После реконнекта клиенту требуется повторная синхронизация.

Пример

client.onConn ect = () => {

    client.publish({
        destination: '/app/game/resync'
    });

};

Heartbeat-механизм

Heartbeat позволяет определять «мёртвые» соединения.

const client = new Client({

    heartbeatIncoming: 5000,
    heartbeatOutgoing: 5000

});

Если heartbeat не приходит:

  • соединение считается потерянным;
  • запускается reconnect;
  • игрок помечается disconnected.

Оптимизация сетевого трафика

Игровые приложения особенно чувствительны к объёму сообщений.

Основные техники

Уменьшение частоты обновлений

Вместо:

60 FPS = 60 сообщений/сек

используют:

20 обновлений/сек

Передача только изменений

Плохо:

{
    players: [...],
    enemies: [...],
    map: [...]
}

Хорошо:

{
    type: 'MOVE',
    id: 15,
    x: 120,
    y: 300
}

Бинарные данные

JSON создаёт дополнительный overhead.

В высоконагруженных играх используют:

  • MessagePack;
  • Protocol Buffers;
  • CBOR.

Частота игровых обновлений

Tick-rate влияет на плавность игры.

Tick rate Назначение
10 Пошаговые игры
20 Казуальные multiplayer
30 Большинство realtime игр
60 Соревновательные action-игры

Серверная авторитетность

Сервер обязательно проверяет:

  • допустимость перемещения;
  • скорость движения;
  • урон;
  • попадания;
  • использование предметов.

Пример проверки

if (speed > MAX_SPEED) {

    rejectMove();

}

Это предотвращает:

  • speedhack;
  • teleport cheat;
  • подмену координат;
  • поддельные события.

Работа с большим количеством игроков

При высокой нагрузке требуется:

  • шардирование комнат;
  • распределение брокеров;
  • горизонтальное масштабирование;
  • балансировка подключений.

Масштабирование через RabbitMQ

Игровые события могут распределяться между несколькими серверами.

Схема

Игроки
↓
Load Balancer
↓
WebSocket Gateway
↓
RabbitMQ
↓
Game Servers

Ограничение сообщений

Игроки могут генерировать спам.

Rate limiting

const lastMessage = {};

function canSend(playerId) {

    const now = Date.now();

    if (!lastMessage[playerId]) {
        lastMessage[playerId] = now;
        return true;
    }

    if (now - lastMessage[playerId] < 100) {
        return false;
    }

    lastMessage[playerId] = now;

    return true;
}

Защита игровых каналов

Игровые системы требуют аутентификации.

Передача JWT

const client = new Client({

    connectHeaders: {
        Authorization: 'Bearer TOKEN'
    }

});

Персональные сообщения

STOMP поддерживает user-очереди.

Пример

client.subscribe('/user/queue/inventory', message => {

    const inventory = JSON.parse(message.body);

    updateInventory(inventory);

});

Игровой инвентарь

Инвентарь удобно синхронизировать отдельными событиями.

Получение предмета

{
    type: 'ITEM_ADD',
    item: {
        id: 15,
        name: 'Sword'
    }
}

Потеря предмета

{
    type: 'ITEM_REMOVE',
    itemId: 15
}

Система лобби

Лобби обычно содержит:

  • список игроков;
  • статус готовности;
  • выбор персонажа;
  • настройки матча.

Подписка на лобби

client.subscribe('/topic/lobby/5', message => {

    const lobby = JSON.parse(message.body);

    renderLobby(lobby);

});

Обработка ошибок

Ошибка STOMP

client.onStompEr ror = frame => {

    console.error(frame.headers.message);

};

Потеря соединения

client.onWebSocketCl ose = () => {

    showDisconnectedScreen();

};

Игровые уведомления

Через STOMP удобно отправлять:

  • системные сообщения;
  • игровые события;
  • достижения;
  • приглашения;
  • результаты матчей.

Пример

client.subscribe('/user/queue/notifications', message => {

    const notification = JSON.parse(message.body);

    showNotification(notification);

});

Репликация игровых объектов

Каждый объект может иметь:

{
    id: 52,
    type: 'enemy',
    x: 120,
    y: 350,
    hp: 80
}

Сервер отправляет:

  • создание;
  • обновление;
  • удаление объектов.

Система событий

Событийная архитектура особенно эффективна для игр.

Типы событий

Событие Назначение
PLAYER_MOVE Перемещение
PLAYER_ATTACK Атака
OBJECT_SPAWN Создание объекта
DAMAGE Получение урона
CHAT_MESSAGE Сообщение
MATCH_END Завершение матча

Оптимизация рендера

Сетевые обновления не должны напрямую вызывать тяжёлый render.

Очередь событий

const queue = [];

client.subscribe('/topic/game', message => {

    queue.push(JSON.parse(message.body));

});

Обработка в игровом цикле

function gameLoop() {

    while (queue.length > 0) {

        const event = queue.shift();

        processEvent(event);
    }

    render();

    requestAnimationFrame(gameLoop);
}

Система spectator mode

Наблюдатели получают состояние игры без участия.

Подключение наблюдателя

client.subscribe('/topic/match/12/spectator', message => {

    const state = JSON.parse(message.body);

    renderSpectator(state);

});

Турнирные системы

STOMP.js подходит для:

  • турнирных сеток;
  • realtime-таблиц;
  • обновления рейтингов;
  • live-статистики.

Система достижений

Получение достижения

client.subscribe('/user/queue/achievements', message => {

    const achievement = JSON.parse(message.body);

    unlockAchievement(achievement);

});

Игровая аналитика

Через брокер сообщений удобно собирать:

  • статистику матчей;
  • действия игроков;
  • время сессий;
  • экономику игры;
  • показатели retention.

Интеграция с Phaser

Пример интеграции с игровым движком Phaser.

Обновление игрока

client.subscribe('/topic/player/move', message => {

    const data = JSON.parse(message.body);

    players[data.id].setPosition(
        data.x,
        data.y
    );

});

Интеграция с PixiJS

С библиотекой PixiJS STOMP.js часто используется для multiplayer-rendering.

client.subscribe('/topic/enemies', message => {

    const enemy = JSON.parse(message.body);

    updateEnemy(enemy);

});

Профилирование производительности

В игровых приложениях необходимо контролировать:

  • RTT;
  • packet loss;
  • FPS;
  • задержки обработки;
  • размер сообщений.

Измерение latency

const start = Date.now();

client.publish({
    destination: '/app/ping',
    body: ''
});

client.subscribe('/user/queue/pong', () => {

    console.log(Date.now() - start);

});

Архитектурные ограничения STOMP.js в играх

STOMP.js хорошо подходит для:

  • карточных игр;
  • шахмат;
  • MMO-интерфейсов;
  • realtime-стратегий;
  • casual multiplayer;
  • lobby-систем;
  • совместных приложений.

Однако для ultra-low-latency игр могут использоваться:

  • UDP;
  • WebRTC DataChannel;
  • собственные бинарные протоколы;
  • специализированные game-серверы.

STOMP поверх WebSocket всё же остаётся удобным решением благодаря:

  • простоте разработки;
  • масштабируемости;
  • поддержке брокеров;
  • событийной архитектуре;
  • удобной маршрутизации сообщений;
  • устойчивой интеграции с backend-инфраструктурой.