Всім привіт.
Цієї весни я натрапив на проект, в якому хлопці навчилися запускати Dota 2 сервер версії 2014 року і, відповідно, грати на ньому. Я великий фанат цієї гри, і не зміг пройти повз унікальну можливість поринути у своє дитинство.
Занурився я дуже глибоко, і так вийшло, що я написав Discord бота, який відповідає практично за весь функціонал, який не підтримується в старій версії гри, а саме матчмейкінг.
До всіх нововведень із ботом лобі створювалося вручну. Збирали 10 реакцій на повідомлення і вручну збирали сервер, або хостили локальне лобі.
Моя натура програміста не витримала такої кількості ручної роботи, і за ніч я накидав найпростішу версію бота, яка автоматично піднімала сервер, коли набиралося 10 людей.
Писати відразу вирішив на nodejs, тому що не дуже люблю пітон, та й комфортніше почуваюся в цьому середовищі.
Це мій перший досвід написання бота для Discord, але виявилося все дуже просто. Офіційний модуль npm discord.js надає зручний інтерфейс для роботи з повідомленнями, збором реакцій і т.д.
Дисклеймер: усі приклади коду є «актуальними», тобто пройшли кілька ітерацій переписування ночами.
Основа матчмейкінгу - це «черга», в яку поміщаються гравці, які хочуть грати, і забираються, коли розхотіли або знайшли гру.
Такий вигляд має суть «гравця». Спочатку це був просто ID користувача в Discord, але в планах лаунчер/пошук гри з сайту, але про все по порядку.
export enum Realm {
DISCORD,
EXTERNAL,
}
export default class QueuePlayer {
constructor(public readonly realm: Realm, public readonly id: string) {}
public is(qp: QueuePlayer): boolean {
return this.realm === qp.realm && this.id === qp.id;
}
static Discord(id: string) {
return new QueuePlayer(Realm.DISCORD, id);
}
static External(id: string) {
return new QueuePlayer(Realm.EXTERNAL, id);
}
}
А ось інтерфейс черги. Тут замість "гравців" використовується абстракція у вигляді "групи". Для одиночного гравця група складається з нього самого, а для гравців у групі відповідно з усіх гравців групи.
export default interface IQueue extends EventEmitter {
inQueue: QueuePlayer[]
put(uid: Party): boolean;
remove(uid: Party): boolean;
removeAll(ids: Party[]): void;
mode: MatchmakingMode
roomSize: number;
clear(): void
}
Вирішив використати події для обміну контекстом. Підходило під кейси — за подією «знайдено гру для 10 осіб» можна і відправити в особисті повідомлення гравцям потрібне повідомлення, і виконати основну бізнес логіку — запустити таск для перевірки готовності, підготувати лобі до запуску тощо.
Для IOC я використовую InversifyJS. Маю приємний досвід роботи із цією бібліотекою. Швидко та просто!
Черг у нас на сервері кілька - додалися режими 1х1, звичайний/рейтинговий, і пара кастомок. Тому є singleton RoomService, який лежить між користувачем та пошуком гри.
constructor(
@inject(GameServers) private gameServers: GameServers,
@inject(MatchStatsService) private stats: MatchStatsService,
@inject(PartyService) private partyService: PartyService
) {
super();
this.initQueue(MatchmakingMode.RANKED);
this.initQueue(MatchmakingMode.UNRANKED);
this.initQueue(MatchmakingMode.SOLOMID);
this.initQueue(MatchmakingMode.DIRETIDE);
this.initQueue(MatchmakingMode.GREEVILING);
this.partyService.addListener(
"party-update",
(event: PartyUpdatedEvent) => {
this.queues.forEach((q) => {
if (has(q.queue, (t) => t.is(event.party))) {
// if queue has this party, we re-add party
this.leaveQueue(event.qp, q.mode)
this.enterQueue(event.qp, q.mode)
}
});
}
);
this.partyService.addListener(
"party-removed",
(event: PartyUpdatedEvent) => {
this.queues.forEach((q) => {
if (has(q.queue, (t) => t.is(event.party))) {
// if queue has this party, we re-add party
q.remove(event.party)
}
});
}
);
}
(Локшина коду для подання, як приблизно виглядають процеси)
Тут я ініціалізую чергу під кожен із реалізованих режимів гри, а також слухаю зміни «груп», щоб підкоригувати черги та уникнути деяких конфліктів.
Так, я молодець, я вставив шматки коду, які ніяк не відносяться до топіка, а тепер перейдемо вже безпосередньо до мачтмейкінгу.
Розглянемо кейс:
1) Користувач хоче пограти.
2) Для того, щоб розпочати пошук, він використовує Gateway = Discord, тобто ставить реакцію на повідомлення:
3) Цей гейтвей йде в RoomService, і каже "Користувач з дискорду хоче увійти в чергу, режим: нерейтингова гра".
4) RoomService приймає прохання гейтвея, і пхає в потрібну чергу користувача (точніше, групу користувача).
5) Черга під час кожної зміни перевіряє, чи вистачає гравців для гри. Якщо можна - емітті подія:
private onRoomFound(players: Party[]) {
this.emit("room-found", {
players,
});
}
6) RoomService, очевидно, з радістю слухає кожну чергу у трепетному очікуванні цієї події. На вхід ми отримуємо список гравців, формуємо з них віртуальну «кімнату», і, звичайно ж, емітом подія:
queue.addListener("room-found", (event: RoomFoundEvent) => {
console.log(
`Room found mode: [${mode}]. Time to get free room for these guys`
);
const room = this.getFreeRoom(mode);
room.fill(event.players);
this.onRoomFormed(room);
});
7) Ось ми й дісталися «вищої» інстанції — класу Бот. В цілому він займається зв'язком між гейтвеями (як це смішно російською виглядає я не можу) і бізнес логікою матчмейкінгу. Робот підслуховує подію, і наказує DiscordGateway надіслати всім користувачам перевірку на готовність.
8) Якщо хтось відхилив або не прийняв гру за 3 хвилини, то ми не повертаємо їх у чергу. Всіх інших повертаємо в чергу та чекаємо, коли знову набереться 10 людей. Якщо всі гравці взяли гру, починається цікава частина.
Конфігурація виділеного сервера
У нас ігри хоститься на VDS з Windows Server 2012. З цього можна зробити кілька висновків:
- На нього немає докера, що вдарило мене в серце
- Ми заощаджуємо на оренді
Стоїть завдання: з VPS на лінукс запускати процес на VDS. Написав простий сервер Flask. Так, не люблю пітон, але що вдієш — на ньому написати цей сервер швидше і простіше.
Він виконує 3 функції:
- Запуск сервера з конфігурацією – вибір карти, кількості гравців для старту гри та набір плагінів. Про плагіни зараз не писатиму — це окрема історія з літрами кави ночами впереміш зі сльозами та вирваним волоссям.
- Зупинка/перезапуск сервера у разі невдалих підключень, які ми можемо обробити лише вручну.
Тут все просто, приклади коду навіть недоречні. Скрипт на 100 рядків
Отже, коли 10 людей зібралися разом і взяли гру, запущено сервер і всі прагнуть грати, в особисті повідомлення надходить посилання на підключення до гри.
Натиснувши посилання гравця коннектит до ігрового серверу, і далі вже все. Через ~25 хвилин віртуальна «кімната» з гравцями очищується.
Заздалегідь перепрошую за нескладність статті, давно не писав сюди, та й коду надто багато, щоб виділити важливі ділянки. Локшина, коротше.
Якщо побачу інтерес до теми, то буде друга частина - в ній будуть мої муки з плагінами для srcds (Source dedicated server), і, напевно, система рейтингу та міні-dotabuff, сайт зі статистикою ігор.
Небагато посилань:
Джерело: habr.com