はじめに
こちらは Go2 Advent Calendar 2020 9日目の記事です。
ゲームのマッチングを行うロビー機能のようなモノをGoで作ってみたので、紹介と簡単な解説をしたいと思います。
ログインしたらロビーでマッチングするまで待機し、相手が見つかったらゲーム画面に接続するというモノです。今回作った内容に、認証機能は含まれていません。
作ったもの
connect4という1対1のゲームをベースにしています。
プレイヤー名を入力してログインすると、ロビー(マッチング画面)に移動します。
プレイヤーが2名揃うとプレイ画面が表示されて、ゲームが始まります。
仕組み
次の3つのページで構成しています。
- ログインページ:プレイヤー名を入力してログインするためのページ
- ロビーページ:対戦相手のマッチング待つ待機用のページ
- ゲーム画面
ログインではクライアント-サーバ間でセッションを作ります。
サーバにプレイヤー名を登録して、クライアントにセッションIDを発行しています。
同時にWebSocketのペアを用意してプレイヤーに割り当てます。
ロビーでは、他のプレイヤーが同じWebSocketのペアに割り当てられるのを待機します。
割り当てが終わるとゲーム画面に移動します。
ゲーム画面では割り当てられたWebSocketのペアで通信してゲームを進めます。
func main() {
flag.Parse()
http.HandleFunc("/", serveFront) // ログインページ
http.HandleFunc("/lobby", serveLobby) // ロビーページ
http.HandleFunc("/play", servePlay) // ゲーム画面
http.HandleFunc("/login", serveLoginHandler) // Session開始とWebSocketペアの発行
http.HandleFunc("/ws", serveWebsocket) // WebSocket処理
err := http.ListenAndServe(*addr, nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
セッション管理
セッションの構造体定義は以下の通りです。
Cookieを使ってsessionIDでやり取りします。Session
の更新はManagerを介して行います。
type Session struct {
cookieName string
ID string
manager *Manager
Values map[string]interface{}
writer http.ResponseWriter
}
セッションを開始する
セッション管理はManagerで行っていて、manager.Start()
でセッションを開始します。この中ではcookieNameと*http.Request
のcookie情報を基にセッションを取得しています。このとき、既にセッションが確立していれば、既存のセッションを返します。
セッション情報がない場合は、ログインに使ったプレイヤー名をセッションに登録して保存します。このときhttp.responseWriter
にcookie情報をセットして返します。
これでクライアントにセッションIDを発行できました。
// セッションを開始
manager := sessions.NewManager()
session, err := manager.Start(w, r, cookieName)
if err != nil {
http.Error(w, "session start faild", http.StatusMethodNotAllowed)
return
}
session.Set("account", r.FormValue("account"))
if err := session.Save(); err != nil {
http.Error(w, "session save faild", http.StatusMethodNotAllowed)
return
}
WebSocket通信
WebSocket通信はgorilla/websocket
を使っています。
単一のWebSocket通信はexamplesを参考にしているので、そちらを見てください。
hub
に接続したクライアント同士がWebSocketのペアとなり、メッセージを受け取れるようになっています。
単一のWebSocketペアはhub
で管理されていますが、複数のPlayルームを用意したいので複数のWebSocketペアをManagerを作って管理します。Managerの構造体は次のようになっています。
type Manager struct {
database map[string]*Hub
pool []*Hub
count map[*Hub]int
users map[*Hub][]string
}
サーバは複数のhub
をpool
に持っていて、割り当てを要求されたらpool
の先頭から順に割り当てるという単純な作りです。hub
の定員に達したらpool
から除外していきます。今回は1対1のゲームなので、hub
の定員は2名です。
func (m *Manager) Get(key string) (*Hub, error) {
if hub, ok := m.database[key]; ok {
return hub, nil
}
if len(m.pool) <= 0 {
return nil, fmt.Errorf("no hub")
}
hub := m.pool[0] // 先頭のHubを割り当てる
m.database[key] = hub
m.count[hub]++
if m.count[hub] >= PoolMax {
m.pool = m.pool[1:] // 先頭のHubを除外する
}
m.users[hub] = append(m.users[hub], key)
return hub, nil
}
サーバはhub
が定員に達したら、マッチング完了を待機中のクライアントに対してWebSocketで通知し、クライアントは通知を受け取るとゲーム画面に移動します。
今後について
ログイン時に入力したユーザ名が活用できていないのと、使い終わったhub
をpool
に戻す処理、終了処理が未対応なので、これらは近いうち作ろうと思います。