Hallo zusammen.
In diesem Frühling bin ich auf ein Projekt gestoßen, in dem die Entwickler gelernt haben, einen Dota 2 Server der Version 2014 zu starten und entsprechend darauf zu spielen. Ich bin ein großer Fan dieses Spiels und konnte dieser einzigartigen Gelegenheit, in meine Kindheit einzutauchen, nicht widerstehen.
Ich bin richtig tief eingetaucht und so kam es, dass ich einen Discord-Bot geschrieben habe, der praktisch alle Funktionen abdeckt, die in der alten Version des Spiels nicht unterstützt werden, insbesondere das Matchmaking.
Vor den Neuerungen wurde das Lobby-Setup manuell erstellt. Wir sammelten 10 Reaktionen auf eine Nachricht und richteten manuell den Server ein oder hosteten eine lokale Lobby.

Meine Programmierer-Natur hielt eine so große Menge an Handarbeit nicht aus, und in einer Nacht skizzierte ich die einfachste Version des Bots, die den Server automatisch hochfuhr, sobald 10 Personen zusammenkamen.
Ich entschied mich gleich für nodejs, weil ich Python nicht so sehr mag und mich in dieser Umgebung wohler fühle.
Das ist meine erste Erfahrung beim Schreiben eines Bots für Discord, aber es stellte sich als sehr einfach heraus. Das offizielle npm-Modul discord.js bietet eine benutzerfreundliche Schnittstelle zum Arbeiten mit Nachrichten, dem Sammeln von Reaktionen usw.
Haftungsausschluss: Alle Codebeispiele sind „aktuell“, das heißt, sie haben mehrere Überarbeitungsrunden in nächtlichen Sessions durchlaufen.
Die Grundlage des Matchmakings ist die „Warteschlange“, in die Spieler eingereiht werden, die spielen wollen, und entfernt werden, sobald sie nicht mehr spielen möchten oder ein Spiel gefunden haben.
So sieht die Entität „Spieler“ aus. Ursprünglich war dies einfach die ID eines Nutzers in Discord, aber es sind Pläne für einen Launcher/Spiele-Suchfunktion von der Webseite vorgesehen, aber dazu später mehr.
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);
}
}
Hier ist das Interface für die Warteschlange. Anstelle von „Spielern“ wird eine Abstraktion in Form einer „Gruppe“ verwendet. Für einen einzelnen Spieler besteht die Gruppe aus ihm selbst, während für die Spieler in einer Gruppe entsprechend alle Spieler der Gruppe zählen.
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
}
Ich habe mich entschieden, Ereignisse zur Kontextübertragung zu nutzen. Es passt zu den Anwendungsfällen – bei dem Ereignis 'Spiel für 10 Personen gefunden' kann ich sowohl persönliche Nachrichten an die Spieler senden, als auch die Hauptgeschäftslogik ausführen – Tasks zur Überprüfung der Bereitschaft starten, das Lobby-Setup vorbereiten und so weiter.
Für IOC verwende ich InversifyJS. Ich habe gute Erfahrungen mit dieser Bibliothek gemacht. Schnell und einfach!
Wir haben mehrere Warteschlangen auf unserem Server – es kamen 1x1-Modalitäten, normale/rangbasierte und ein paar individuelle dazu. Daher gibt es einen Singleton RoomService, der zwischen dem Benutzer und der Spielsuche liegt.
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)
}
});
}
);
}
(Code-Beispiel zur Veranschaulichung des Ablaufs der Prozesse)
Hier initialisiere ich die Warteschlange für jeden der implementierten Spielmodi und höre auch auf Änderungen der „Gruppen“, um die Warteschlangen anzupassen und Konflikte zu vermeiden.
So, das habe ich gut gemacht, ich habe Codeabschnitte eingefügt, die nicht zum Thema gehören, aber jetzt kommen wir direkt zum Matchmaking.
Lass uns den Fall betrachten:
1) Der Benutzer möchte spielen.
2) Um die Suche zu starten, verwendet er Gateway=Discord, das heißt, er reagiert auf die Nachricht:

3) Dieses Gateway geht zu RoomService und sagt: „Ein Benutzer aus Discord möchte sich in die Warteschlange einreihen, Modus: unbewertetes Spiel.“
4) RoomService akzeptiert die Anfrage des Gateways und fügt den Benutzer (genauer gesagt die Benutzergruppe) in die entsprechende Warteschlange ein.
5) Die Warteschlange überprüft bei jeder Änderung, ob genügend Spieler für das Spiel vorhanden sind. Wenn ja, emittieren wir das Ereignis:
private onRoomFound(players: Party[]) {
this.emit("room-found", {
players,
});
}
6) RoomService hört natürlich gespannt auf jede Warteschlange in freudiger Erwartung dieses Ereignisses. Wir erhalten eine Liste von Spielern, bilden daraus einen virtuellen „Raum“ und emittieren natürlich das Ereignis:
queue.addListener("room-found", (event: RoomFoundEvent) => {
console.log(
`Raum gefunden Modus: [${mode}]. Zeit, um einen freien Raum für diese Leute zu bekommen`
);
const room = this.getFreeRoom(mode);
room.fill(event.players);
this.onRoomFormed(room);
});
7) Jetzt erreichen wir die „höchste“ Instanz – die Klasse Bot. Insgesamt sorgt er für die Verbindung zwischen Gateways (wie das auf Russisch klingt, kann ich nicht fassen) und der Geschäftlogik des Matchmakings. Der Bot lauscht auf Ereignisse und befiehlt dem DiscordGateway, allen Nutzern eine Bereitschaftsprüfung zu senden.

8) Wenn jemand das Spiel innerhalb von 3 Minuten ablehnt oder nicht annimmt, bringen wir sie NICHT zurück in die Warteschlange. Alle anderen kommen zurück in die Warteschlange, und wir warten, bis wieder 10 Personen zusammenkommen. Wenn alle Spieler das Spiel angenommen haben, beginnt der spannende Teil.
Konfiguration des dedizierten Servers
Unsere Spiele laufen auf einem VDS mit Windows Server 2012. Daraus lassen sich einige Schlussfolgerungen ziehen:
- Docker ist nicht vorhanden, was mich ins Mark trifft.
- Wir sparen bei der Miete.
Die Aufgabe besteht darin, einen Prozess auf dem VDS mit einem Linux-VPS zu starten. Ich habe einen einfachen Server in Flask geschrieben. Ja, ich mag Python nicht, aber was soll man machen – diesen Server schnelle und einfacher zu schreiben.
Er erfüllt 3 Funktionen:
- Start des Servers mit der Konfiguration – Auswahl der Karte, der Anzahl der Spieler für den Spielstart und einer Reihe von Plugins. Über die Plugins möchte ich jetzt nicht schreiben – das ist eine eigene Geschichte mit Litern Kaffee in der Nacht, gemischt mit Tränen und herausgerissenen Haaren.
- Stoppen/Neustarten des Servers bei fehlgeschlagenen Verbindungen, die wir nur manuell bearbeiten können.
Hier ist alles ganz einfach, Codebeispiele sind sogar unangemessen. Ein Skript mit 100 Zeilen.
Wenn also 10 Personen zusammenkommen und das Spiel starten, wird der Server gestartet und alle sind gespannt auf das Spiel. In den persönlichen Nachrichten kommt ein Link zur Verbindung mit dem Spiel.

Beim Klicken auf den Link wird der Spieler mit dem Spielserver verbunden, und der Rest geschieht von selbst. Nach etwa 25 Minuten wird der virtuelle "Raum" mit den Spielern geleert.
Ich entschuldige mich im Voraus für die Unordnung des Artikels, ich habe hier lange nicht mehr geschrieben, und es gibt zu viel Code, um die wichtigen Teile hervorzuheben. Der Text ist also etwas wirr.
Wenn ich Interesse an dem Thema sehe, wird es einen zweiten Teil geben – darin werde ich meine Mühen mit den Plugins für srcds (Source Dedicated Server) und wahrscheinlich ein Bewertungssystem sowie ein Mini-Dotabuff, eine Webseite mit Statistiken zu den Spielen, teilen.
Einige Links:
Quelle: habr.com
