Подробная инструкция по правильной разработке мультиплеер игр для браузера с помощью socket.io на NodeJS и React. Шаг за шагом – от структуры каталогов к развертыванию проекта. Также, на базе этой архитектуры был создан всем известный проект Agario, а также игра Sigmally.
Начинать с уникальной идеи это очень важно. Но правильно заданное направление при начале работы над проектом играет ключевую роль.
«Будущее принадлежит тому, кто учится новым навыкам и изобретательно объединяет их» (Роберт Грин).
Еще одно пособие?
Здесь надо внести ясность. Есть множество гайдов, которые показывают основные подходы в использовании socket.io на примере создания чат-приложений. В данной статья фокусировка будет на том как начать работать над масштабируемым проектом на базе socket.io, технологически это будет выходить за рамки обычных чат-приложений.
В этой статье мы раскроем все важные аспекты работы с кодом, а блок информации о UI/UX вынесем в отдельную статью. Если UI выглядит не так уж и привлекательно, отнеситесь с пониманием к этому гайду.
Для чего нужен socket.io?
Socket.io является библиотекой которая использует протокол под названием WebSocket. WebSockets дает возможность обмениваться данным от сервера к клиенту и обратно в один момент времени.
Причина выбора WebSockets вместо HTTP?
В онлайн мультиплеер играх нужно, чтобы на сервер отправлялась пакеты с клиента с нужной информацией и сервер сразу же отправлял или транслировал эту информацию. Такой результат невозможно получить с HTTP, поскольку, для получения информации клиент должен направить соответствующее обращение к серверу.
Что в нашем понимании «правильная разработка»?
Правильная разработка это работа с кодом, который позже может быть масштабируемым. Правильная разработка это также и одновременное избежание многих хлопот с меньшими проектами. Она рассматривает простые подходы, которые применяются в работе где поставлены задачи с более сложной модульной организацией.
Что это за проект?
Это пособие расскажет вам как разработать мультиплеер игру на базе socket.io.
Этим проектом является Симулятор Футбольного Драфта
Суть игры.
Это пошаговая мультиплеер игра, куда игроки заходят и создают собственную комнату. Остальные игроки позже подтягиваются к комнатам. После этого начинается игра. Все пользователи будут перемешаны в случайном порядке, а первый юзер получает право вытащить любого футбольного игрока. Будет предоставлен целый список футболистов (разрешается пересмотреть все параметры футболиста) из которого нужно выбрать понравившегося за определенный промежуток времени. После того как первый игрок выбрал себе футболиста, право выбора переходит к следующему игроку. Процесс построен так, что ход будет переходить от игрока к игроку до тех пор пока каждый не соберет полностью футбольную команду.
Теперь давайте рассмотрим инфраструктуру проекта.
Структура серверной части
Структура игры
Предыдущая диаграмма наглядно показывает, как все устроено.
НТТР и WebSockets в этой инструкции работают на NodeJS. Мы будем использовать Redis DB поскольку socket.io с ним совместим, а также потому, что операции чтения и записи проходят быстрее, чем хранение информации в оперативке. MongoDB был выбран так как отлично справляется с задачей длительного хранения данных. Все информация об игре после завершения каждого раунда будет сохраняться в MongoDB. Здесь также хранятся данные для верификации, если игрок захочет создать учетную запись (проект предлагает опцию регистрации).
WebCrawler был реализован с использованием Python3, а так же Scrapy. Информация о футбольных игроках была стянута с sofifa.com. База хранит в себе 20к футболистов с детальными данными о каждом.
Структура проекта (socket.io/ExpressJS/MongoDB)
NodeJS не заставляет вас придерживаться организации кода. Это позволяет иметь много гибкости в процессе разработки, но если вы допустите грубую ошибку, то это приведет к проблемам в сопровождении и дальнейшем развитии проекта. Если вы работаете с Node и sockets, то такая файловая оптимизация будет оптимальной.
.{src}
├── controller
│ ├── authController.js # Handles authentication requests
│ ├── searchController.js # Handles search queries
│ ├── userController.js # Handles user profile operations
│ └── …
│
├── database
│ ├── db.js # Initialize DB connection
│ └── …
│
├── middlewares
│ ├── authenticated.js # Decode and verify JWT token
│ ├── error.js # Common Error Handler
│ ├── logger.js # Control logging levels
│ └── …
│
├── models
│ ├── roomsModels.js # DB model for rooms
│ ├── usersModel.js # DB model for users
│ └── …
│
├── schema
│ ├── rooms.js # DB Schema for rooms
│ ├── users.js # DB Schema for users
│ └── …
│
├── socker
│ ├── roomManager.js # Socket listeners/emitters handle
│ ├── sockerController.js # Control socket connections
│ └── …
│
├── app.js # Entry file for the project
├── env.js # Store environment variables
├── routes.js # All routes initializer
└── …
Проект требует того чтобы серверная часть была разделена на 2 разных каталога. Если вам нужно пропустить или стереть определенный модуль, то это будет сделать крайне просто, также как добавить новый каталог.
Большинство подкаталогов так же применяются для node проектов, в связи с этим детально рассматриваться они не будут. Задача комментариев для каталога это давать общее понимание о том что там хранится.
Сейчас мы рассмотрим папку socker /. В ней содержится код socket.io.
Точка входа для socket.io (App.js)
import { socker } from ‘./socker’;
import express from ‘express’;
import http from ‘http’;
import { API_PORT, host } from ‘./env’;
const app = express();
const server = new http.Server(app);
socker(server);
app.listen(API_PORT, () => {
logger.info(`Api listening on port ${Number(API_PORT)}!`);
});
server.listen(Number(API_PORT) + 1, () => {
logger.info(`Socker listening on port ${Number(API_PORT) + 1}!`);
logger.info(`Api and socker whitelisted for ${host}`);
});
Тут созданы 2 сервера, первый это app – который слушает обращения НТТР, а второй server, который слушает WebSockets. Рекомендуется использовать уникальные порты, для избежания конфликтов.
Вас могли заинтересовать 1 и 8 строка со значением “socker”.
Для чего нужен socker?
Socker нужен для того чтобы выполнять функцию алиаса. Она коннектит Server к engine.io на http.Server. Если упростить, то она соединяет socket.io с выбранным сервером.
import socketio from ‘socket.io’;
export default server => {
const io = socketio.listen(server, {…options});
io.on(‘connection’, socket => {
logger.info(‘Client Connected’);
});
return io;
};
Но предыдущий код объясняет не достаточно деталей. • Как происходит взаимодействие с пользователями которые уже подключены?
- Где создается namespaces?
- Где создаются комнаты и каналы?
- Где создается игра?
Создание namespaces, и для чего они?
Namespaces это набор соединенных между собой сокетов в определенной области, которая называется так как назван путь, например /classic-mode , /football-draft , /pokemon-draft. Благодаря этому можно сократить объем потребляемых ресурсов (TCP-соединений) и отладить ошибки в приложении, вводя отдельные каналы связи под каждую комнату.
Как создать комнату и канал.
Внутри namespaces можно создать отдельный канал или комнату. Это позже даст возможность создавать соединения, где sockets сможет выполнять join/leave.
Чтобы игроки могли создавать новые комнаты либо подключаться к существующим мы используем channels.
Пример подключения к комнате
import socketio from ‘socket.io’;
const io = socketio.listen(app);
const roomId = ‘#8BHJL’
io.on(‘connection’, async socket => {
// join() allows to join a room/channel
// Here, `await` is used; as socketio join operation uses mix of async and sync operations
await socket.join(roomId);
logger.info(‘Client Connected’);
});
Функция join() проверяет создан ли нужный roomId. В случае если комната не создана, то она создастся и к ней будет добавлен игрок. Если комната создана, тогда все пользователи будут подключаться сразу к комнате.
Финальный пример, который заключает суть использования namespaces и channels:
import socketio from ‘socket.io’;
import Room from ‘./roomManager’;
export default server => {
const io = socketio.listen(server, {
path: ‘/classic-mode’,
});
logger.info(‘Started listening!’);
// Creating a new namespace
const classicMode = io.of(‘/classic-mode’);
classicMode.on(‘connection’, async socket => {
// Receive parameters passed from socket client
const { username, roomId, password, action } = socket.handshake.query;
// Initilaise a the room for connecting socket
const room = new Room({ io: classicMode, socket, username, roomId, password, action });
const joinedRoom = await room.init(username);
logger.info(‘Client Connected’);
// Listeners opened by server
if (joinedRoom) {
room.showPlayers();
room.isReady();
room.shiftTurn();
}
room.onDisconnect();
});
return io;
};
Это все, о чем хотелось рассказать в данном гайде. Та архитектура кода которую мы показали отлично справляется со своими задачами на проектах небольшого масштаба. Если вам нужно быстро создать прототип, то можете не придерживаться схемы проекта. Если есть такая потребность то не бойтесь упрощать проект.
Что будет если проект постоянно увеличивается? Имеющаяся архитектура может плохо справляться с задачами. Решением будет создание подпапок под необходимые сервисы и компоненты ( user-authentication , __tests__ , analytics , etc.). Создание что-то наподобие микросервисов, которые будут выполняться отдельно, что вам даст возможность балансировать и масштабировать процессы под большими нагрузка.