Nesta primavera me deparei com um projeto em que os caras aprenderam a rodar o servidor Dota 2 da versão 2014 e, consequentemente, jogar nele. Sou um grande fã deste jogo, e não poderia deixar de aproveitar a oportunidade única de mergulhar na minha infância.
Eu mergulhei muito fundo, e aconteceu que escrevi um bot Discord, que é responsável por quase todas as funcionalidades não suportadas na versão antiga do jogo, ou seja, matchmaking.
Antes de todas as inovações com o bot, o lobby foi criado manualmente. Coletamos 10 respostas a uma mensagem e montamos manualmente um servidor ou hospedamos um lobby local.
Minha natureza de programador não suportava tanto trabalho manual, e da noite para o dia eu esbocei a versão mais simples do bot, que automaticamente ativou o servidor quando 10 pessoas foram recrutadas.
Decidi escrever imediatamente em nodejs, porque realmente não gosto de python e me sinto mais confortável neste ambiente.
Esta é minha primeira experiência de escrever um bot para o Discord, mas acabou sendo muito simples. O módulo oficial npm discord.js fornece uma interface conveniente para trabalhar com mensagens, coletar reações, etc.
Isenção de responsabilidade: todos os exemplos de código estão "atualizados", o que significa que passaram por várias iterações de reescrita durante a noite.
O núcleo do matchmaking é a "fila" na qual os jogadores que querem jogar são colocados e removidos quando não querem ou não encontram um jogo.
É assim que a essência do "jogador" se parece. Inicialmente, era apenas um ID de usuário no Discord, mas os planos incluem um launcher / busca por um jogo no site, mas primeiro as coisas mais importantes.
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);
}
}
E aqui está a interface da fila. Aqui, em vez de "jogadores", uma abstração na forma de um "grupo" é usada. Para um único jogador, o grupo consiste em ele mesmo e, para os jogadores em um grupo, respectivamente, de todos os jogadores do grupo.
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
}
Decidiu usar eventos para trocar contexto. Adequado para casos - para o evento "encontrei um jogo para 10 pessoas", você pode enviar a mensagem desejada aos jogadores em mensagens privadas e executar a lógica de negócios principal - lançar uma tarefa para verificar a prontidão, preparar o lobby para o lançamento e assim por diante.
Para IOC, estou usando InversifyJS. Tenho uma experiência agradável com esta biblioteca. Rápido e fácil!
Temos várias filas no servidor - adicionamos modos 1x1, normal / classificação e alguns personalizados. Portanto, existe um único RoomService que fica entre o usuário e a busca do jogo.
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)
}
});
}
);
}
(Codifique noodles para ter uma ideia de como são os processos)
Aqui eu inicializo uma fila para cada um dos modos de jogo implementados, e também ouço as mudanças nos "grupos" para corrigir as filas e evitar alguns conflitos.
Então, estou ótimo, eu inseri trechos de código que não têm nada a ver com o tópico e agora vamos passar diretamente para a mastmaking.
Considere um caso:
1) O usuário deseja jogar.
2) Para iniciar a busca, ele usa Gateway = Discord, ou seja, coloca uma reação à mensagem:
3) Este gateway vai para RoomService e diz "O usuário da discórdia quer entrar na fila, modo: jogo não classificado."
4) RoomService aceita a solicitação do gateway e a empurra para a fila desejada do usuário (mais precisamente, o grupo de usuários).
5) A fila verifica se há jogadores suficientes para jogar a cada mudança. Se possível, emita um evento:
private onRoomFound(players: Party[]) {
this.emit("room-found", {
players,
});
}
6) A RoomService fica obviamente feliz em ouvir cada fila, ansiosa e ansiosa por este evento. Na entrada recebemos uma lista de jogadores, formamos uma "sala" virtual deles e, claro, emitimos um evento:
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) Então chegamos à instância "mais alta" - a classe Bot . Em geral, ele lida com a conexão entre os gateways (como isso parece ridículo em russo, não consigo) e a lógica de negócios da combinação. O bot escuta o evento e ordena que o DiscordGateway envie uma verificação de prontidão a todos os usuários.
8) Se alguém rejeitar ou não aceitar o jogo em 3 minutos, NÃO o devolvemos à fila. Devolvemos todos os outros à fila e esperamos que 10 pessoas sejam recrutadas novamente. Se todos os jogadores aceitarem o jogo, começa a parte divertida.
Configuração de servidor dedicado
Nossos jogos são hospedados em VDS com servidor Windows 2012. Várias conclusões podem ser tiradas disso:
- Não há docker nele, o que atingiu meu coração
- Economizamos no aluguel
A tarefa é iniciar o processo em VDS com VPS em Linux. Escreveu um servidor simples no Flask. Sim, eu não gosto de python, mas o que posso fazer - escrever este servidor nele é mais rápido e fácil.
Possui 3 funções:
- Lançamento do servidor com configuração - seleção de mapa, número de jogadores para iniciar o jogo e um conjunto de plug-ins. Não vou escrever sobre plug-ins agora - esta é uma história separada com litros de café à noite misturados com lágrimas e cabelo rasgado.
- Parar / reiniciar o servidor em caso de conexões malsucedidas, que só podemos tratar manualmente.
Tudo é simples aqui, os exemplos de código são até inadequados. Script para 100 linhas
Então, quando 10 pessoas se juntaram e aceitaram o jogo, o servidor está funcionando e todos estão ansiosos para jogar, um link para se conectar ao jogo vem em mensagens privadas.
Ao clicar no link, o jogador se conecta ao servidor do jogo e pronto. Após cerca de 25 minutos, a "sala" virtual com os jogadores é limpa.
Peço desculpas antecipadamente pela estranheza do artigo, não escrevo aqui há muito tempo e há muito código para destacar seções importantes. Macarrão, em resumo.
Se eu vir interesse no assunto, então haverá uma segunda parte - ela conterá meus tormentos com plugins para srcds (servidor dedicado de origem), e, provavelmente, um sistema de classificação e mini-dotabuff, um site com estatísticas de jogos.
Alguns links: