7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Go 2Advent Calendar 2020

Day 9

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

Last updated at Posted at 2020-12-08

はじめに

こちらは 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に戻す処理、終了処理が未対応なので、これらは近いうち作ろうと思います。

7
5
0

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
  3. You can use dark theme
What you can do with signing up
7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?