大家好。
今年春天,我遇到了一個項目,其中的成員學習如何運行 Dota 2 伺服器版本 2014,並相應地在其上進行遊戲。 我是這款遊戲的忠實粉絲,我不能錯過這個沉浸在童年時光的獨特機會。
我研究得很深,碰巧我寫了一個 Discord 機器人,它負責幾乎所有舊版本遊戲不支援的功能,即配對。
在機器人進行所有創新之前,大廳是手動創建的。 我們收集了 10 個對一條訊息的反應,並手動組裝了一個伺服器,或託管了一個本地大廳。
我作為程式設計師的本性無法承受如此多的手動工作,連夜我勾勒出了最簡單的機器人版本,當有 10 個人時,它會自動提升伺服器。
我立即決定用nodejs來寫,因為我不太喜歡Python,而且我在這個環境中感覺更舒服。
這是我第一次為 Discord 編寫機器人,但事實證明它非常簡單。 官方 npm 模組discord.js 提供了一個方便的介面來處理訊息、收集反應等。
免責聲明:所有程式碼範例都是“當前的”,這意味著它們已經在晚上經歷了多次重寫迭代。
配對的基礎是一個“隊列”,想要玩的玩家會被放入其中,而當他們不想玩或找不到遊戲時就會被移除。
這就是「玩家」的本質。 最初它只是 Discord 中的一個用戶 ID,但有計劃從該網站啟動/搜尋遊戲,但首先要做的事情。
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。 我在這個圖書館的工作經驗很愉快。 又快又簡單!
我們的伺服器上有多個佇列 - 我們新增了 1x1、正常/額定和一些自訂模式。 因此,在用戶和遊戲搜尋之間存在一個單例 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 並顯示“來自 Discord 的用戶想要進入隊列,模式:未評級遊戲。”
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 個人。 如果所有玩家都接受了遊戲,那麼有趣的部分就開始了。
專用伺服器配置
我們的遊戲託管在Windows Server 2012的VDS上。由此我們可以得出幾個結論:
- 上面沒有docker,戳中了我的心
- 我們節省房租
任務是從 Linux 上的 VPS 在 VDS 上執行進程。 我在 Flask 中寫了一個簡單的伺服器。 是的,我不喜歡Python,但是你能做什麼?用它來寫這個伺服器更快更容易。
它執行 3 個功能:
- 使用組態啟動伺服器 - 選擇地圖、啟動遊戲的玩家數量以及一組外掛程式。 我現在不會寫插件 - 這是一個不同的故事,晚上喝幾公升咖啡,混合著眼淚和撕裂的頭髮。
- 連線不成功時停止/重新啟動伺服器,我們只能手動處理。
這裡一切都很簡單,程式碼範例甚至都不合適。 100行腳本
於是,當10人齊聚並接受遊戲時,伺服器啟動了,大家都躍躍欲試,私訊中就發送了連接遊戲的連結。
透過點擊鏈接,玩家連接到遊戲伺服器,然後就這樣了。 大約 25 分鐘後,玩家所在的虛擬「房間」就被清理乾淨了。
對於文章的尷尬,我提前表示歉意,我已經很久沒有在這裡寫文章了,而且程式碼太多,無法突出顯示重要部分。 簡而言之,麵條。
如果我發現對該主題感興趣,將會有第二部分- 它將包含我對srcds(來源專用伺服器)插件的折磨,並且可能還有一個評級系統和mini-dotabuff(一個包含遊戲統計數據的網站)。
一些連結:
來源: www.habr.com