search
LoginSignup
5

More than 1 year has passed since last update.

posted at

updated at

Goでゲームのロビー機能のようなモノを作る

はじめに

こちらは Go2 Advent Calendar 2020 9日目の記事です。

ゲームのマッチングを行うロビー機能のようなモノをGoで作ってみたので、紹介と簡単な解説をしたいと思います。
ログインしたらロビーでマッチングするまで待機し、相手が見つかったらゲーム画面に接続するというモノです。今回作った内容に、認証機能は含まれていません。

作ったもの

connect4という1対1のゲームをベースにしています。
プレイヤー名を入力してログインすると、ロビー(マッチング画面)に移動します。
プレイヤーが2名揃うとプレイ画面が表示されて、ゲームが始まります。

connect4play.gif

仕組み

次の3つのページで構成しています。
- ログインページ:プレイヤー名を入力してログインするためのページ
- ロビーページ:対戦相手のマッチング待つ待機用のページ
- ゲーム画面

ログインではクライアント-サーバ間でセッションを作ります。
サーバにプレイヤー名を登録して、クライアントにセッションIDを発行しています。
同時にWebSocketのペアを用意してプレイヤーに割り当てます。

ロビーでは、他のプレイヤーが同じWebSocketのペアに割り当てられるのを待機します。
割り当てが終わるとゲーム画面に移動します。

ゲーム画面では割り当てられたWebSocketのペアで通信してゲームを進めます。

gameroom.png

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
}

サーバは複数のhubpoolに持っていて、割り当てを要求されたら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で通知し、クライアントは通知を受け取るとゲーム画面に移動します。

今後について

ログイン時に入力したユーザ名が活用できていないのと、使い終わったhubpoolに戻す処理、終了処理が未対応なので、これらは近いうち作ろうと思います。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
5