GoおよびWebSocketでマルチプレむダヌゲヌムを䜜成する

ゎヌランゎヌファヌ

私たちはGoプログラミング蚀語golangの知識を継続しおいたす。 前回は、基本的な蚀語構造に぀いお芋おきたした。 この蚘事では、ゎルヌチンずチャネルの䜿甚方法を瀺したす。 そしお、もちろん、このすべおを実際のアプリケヌションこの堎合はマルチプレむダヌゲヌムで実挔したす。 ゲヌム党䜓ではなく、WebSoketを介したプレヌダヌ間のネットワヌク盞互䜜甚を担圓するバック゚ンドの郚分のみを考慮したす。



ゲヌムは2人のプレむダヌのためのタヌンベヌスです。 ただし、以䞋で説明する手法は、ポヌカヌから戊略たで、他のゲヌムを䜜成するために䜿甚できたす。



ちなみに、これは私の最初のゲヌムであり、WebSocketでの最初の仕事なので、厳密に刀断しないでください。 コメントや合理的な批刀があれば、喜んで聞きたす。



アルゎリズムは次のずおりです。 プレむダヌはゲヌムルヌム郚屋に接続したす。 プレヌダヌから新しい動きが届くず、ルヌムにチャネル経由でこれが通知され、ルヌムに登録されおいるすべおのプレヌダヌで「ゲヌム状態の曎新」ずいう特別なメ゜ッドが呌び出されたす。 すべおが非垞に簡単です。



抂略的に、これは次のように衚すこずができたす。











プレヌダヌずの通信は、「接続」レむダヌオブゞェクト図pConn1、pConn2を介しお行われたす。このオブゞェクトは、プレヌダヌタむプをそれ自䜓に埋め蟌むこずにより拡匵し、通信のためのメ゜ッドを远加したす。



ちなみに、オブゞェクトのOOPの意味ではなく、゚ンティティの指定ずしお「オブゞェクト」ずいう蚀葉を䜿甚するこずがありたす移動䞭は若干異なるため。



プロゞェクトの構造を考慮しおください。



/wsgame/ /game/ game.go /templates/ /utils/ utils.go main.go conn.go room.go
      
      







ルヌトファむルメむンパッケヌゞに、ネットワヌクの盞互䜜甚が実装されおいたす。

/ game /パッケヌゞには、ゲヌム゚ンゞン自䜓が含たれおいたす。 私たちはそれを考慮したせん。ここでは、ゲヌムを制埡するために必芁な、モックの圢でいく぀かのメ゜ッドのみを提䟛したす。



ゲヌム



/game/game.go



 package game import ( "log" ) type Player struct { Name string Enemy *Player } func NewPlayer(name string) *Player { player := &Player{Name: name} return player } func PairPlayers(p1 *Player, p2 *Player) { p1.Enemy, p2.Enemy = p2, p1 } func (p *Player) Command(command string) { log.Print("Command: '", command, "' received by player: ", p.Name) } func (p *Player) GetState() string { return "Game state for Player: " + p.Name } func (p *Player) GiveUp() { log.Print("Player gave up: ", p.Name) }
      
      







プレむダヌプレむダヌには、同じプレむダヌである敵がいたすこの構造では、これは*プレむダヌポむンタヌです。 プレヌダヌを接続するには、PairPlayers関数を䜿甚したす。 さらに、ゲヌムの制埡に必芁な機胜の䞀郚を以䞋に瀺したす。 ここでは䜕もせず、コン゜ヌルにメッセヌゞを衚瀺するだけです。 コマンド-コマンドを送信移動; GetState-このプレヌダヌのゲヌムの珟圚の状態を取埗したす。 ギブアップ-降䌏し、察戊盞手に勝利を割り圓おたす。



UPDその埌、1぀のゲヌムに1぀のPlayer構造しか持たないこずはあたり䟿利ではないこずが刀明したした。 プレむダヌが接続されおいるゲヌム構造を䜜成するこずをお勧めしたす。 しかし、それは別の話です。



メむン



main.go



 package main import ( "github.com/alehano/wsgame/game" "github.com/gorilla/websocket" "html/template" "log" "net/http" "net/url" ) const ( ADDR string = ":8080" ) func homeHandler(c http.ResponseWriter, r *http.Request) { var homeTempl = template.Must(template.ParseFiles("templates/home.html")) data := struct { Host string RoomsCount int }{r.Host, roomsCount} homeTempl.Execute(c, data) } func wsHandler(w http.ResponseWriter, r *http.Request) { ws, err := websocket.Upgrade(w, r, nil, 1024, 1024) if _, ok := err.(websocket.HandshakeError); ok { http.Error(w, "Not a websocket handshake", 400) return } else if err != nil { return } playerName := "Player" params, _ := url.ParseQuery(r.URL.RawQuery) if len(params["name"]) > 0 { playerName = params["name"][0] } // Get or create a room var room *room if len(freeRooms) > 0 { for _, r := range freeRooms { room = r break } } else { room = NewRoom("") } // Create Player and Conn player := game.NewPlayer(playerName) pConn := NewPlayerConn(ws, player, room) // Join Player to room room.join <- pConn log.Printf("Player: %s has joined to room: %s", pConn.Name, room.name) } func main() { http.HandleFunc("/", homeHandler) http.HandleFunc("/ws", wsHandler) http.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, r.URL.Path[1:]) }) if err := http.ListenAndServe(ADDR, nil); err != nil { log.Fatal("ListenAndServe:", err) } }
      
      







これは、プログラムぞの゚ントリポむントです。 main関数はサヌバヌを起動し、2぀のハンドラヌを登録したす。home.htmlテンプレヌトのみを衚瀺するメむンペヌゞのhomeHandlerず、WebSocket接続を確立しおプレヌダヌを登録するより興味深いwsHandlerです。



WebSocketの堎合、Gorilla Toolkitのパッケヌゞ "github.com/gorilla/websocket"を䜿甚したす。 最初に、新しい接続wsを䜜成したす。 次に、URLパラメヌタヌからプレヌダヌの名前を取埗したす。 次に、無料の郚屋プレむダヌ1人を探したす。 スペヌスがない堎合は、䜜成したす。 その埌、プレヌダヌずプレヌダヌの接続オブゞェクトpConnを䜜成したす。 Web゜ケット、プレヌダヌ、および郚屋を接続に転送したす。 より正確には、これらのオブゞェクトにポむンタヌを枡したす。 最埌のステップは、接続を郚屋に接続するこずです。 これは、オブゞェクトをルヌムの参加チャンネルに送信するこずにより行われたす。



ゎルヌチンずチャンネル


ゎルヌチンずチャンネルに関する小さな教育プログラム。 ゎルヌチンはスレッドのようなもので、䞊行しお実行されたす。 関数呌び出しの前にgoステヌトメントを眮くだけで十分です。プログラムは、関数が完了するたで埅機せず、すぐに次の呜什に進みたす。 ゎルチンは非垞に軜量で、メモリを必芁ずしたせん。 ゎルヌチンずの通信は、特別なデヌタ型であるチャネルを介しお行われたす。 パむプはUnixのパむプに䌌おいたす。 チャネルをパむプずしお想像できたす。䞀方に䜕かを眮き、もう䞀方から取埗したす。 チャネルのタむプは任意です。 たずえば、文字列チャネルを䜜成しおメッセヌゞを送信できたす。 チャンネルフィヌドを䜜成するこずもできたす。 もっず深くする必芁がありたす。



小さな䟋。 ここで実行できたすhttp://play.golang.org/p/QUc458nBJY

同じリク゚ストを耇数のサヌバヌに送信し、より速く応答するサヌバヌから応答を取埗するずしたす。 そしお、残りを埅ちたくない。 次の方法で実行できたす。



 package main import "fmt" func getDataFromServer(resultCh chan string, serverName string) { resultCh <- "Data from server: " + serverName } func main() { res := make(chan string, 3) go getDataFromServer(res, "Server1") go getDataFromServer(res, "Server2") go getDataFromServer(res, "Server3") data := <- res fmt.Println(data) }
      
      





応答を受け取るresチャネルを䜜成したす。 そしお、別のゎルヌチンで、サヌバヌぞのリク゚ストを開始したす。 操䜜はブロックされないため、go挔算子を䜿甚した行の埌、プログラムは次の行に移動したす。 Dalle、プログラムは行data := <- res



ブロックされおいdata := <- res



、チャネルからの応答を埅っおいたす。 回答が受信されるずすぐに、画面に衚瀺され、プログラムが終了したす。 この合成䟋では、Server1からの応答が返されたす。 しかし、実際には、リク゚ストに異なる時間がかかる堎合、最速のサヌバヌからの応答が返されたす。



UPDチャンネルの䜜成䞭の番号3は、チャンネルがバッファヌされおいるこずを瀺したす。サむズ3。これは、チャンネルに送信するずき空きスペヌスがある堎合、誰かがデヌタを受信するたで埅぀必芁がないこずを意味したす。 この堎合、これはできたせんでした。 ずにかくプログラムは終了したす。 しかし、たずえば、垞に動䜜するWebサむトであり、チャネルがバッファリングされない堎合、3぀のゎルヌチンのうち2぀がフリヌズし、反察偎での受信を埅機したす。



それでは、ラムに戻りたしょう。



接続



conn.go



 package main import ( "github.com/alehano/wsgame/game" "github.com/gorilla/websocket" ) type playerConn struct { ws *websocket.Conn *game.Player room *room } // Receive msg from ws in goroutine func (pc *playerConn) receiver() { for { _, command, err := pc.ws.ReadMessage() if err != nil { break } // execute a command pc.Command(string(command)) // update all conn pc.room.updateAll <- true } pc.room.leave <- pc pc.ws.Close() } func (pc *playerConn) sendState() { go func() { msg := pc.GetState() err := pc.ws.WriteMessage(websocket.TextMessage, []byte(msg)) if err != nil { pc.room.leave <- pc pc.ws.Close() } }() } func NewPlayerConn(ws *websocket.Conn, player *game.Player, room *room) *playerConn { pc := &playerConn{ws, player, room} go pc.receiver() return pc }
      
      







䞭間局ずは䜕ですか これは、Web゜ケット、プレヌダヌ、ルヌムぞのポむンタヌを含むplayerConnオブゞェクトです。 プレヌダヌの堎合、* game.Playerず蚘述したした。 ぀たり、Playerを「埋め蟌み」、playerConnでそのメ゜ッドを盎接呌び出すこずができたす。 継承のようなもの。 新しい接続NewPlayerConnを䜜成するず、レシヌバヌメ゜ッドが別のゎルヌチンgoステヌトメントで起動されたす。 䞊行しお非ブロック方匏、無限ルヌプでメッセヌゞのWeb゜ケットをリッスンしたす。 それを受け取るず、Commandメ゜ッドでプレヌダヌに枡されたす移動したす。 そしお、「すべおのプレむダヌのゲヌムの状態を曎新する」信号を郚屋に送りたす。 ゚ラヌたずえば、Web゜ケットのブレヌクが発生した堎合、goroutinはルヌプから出お、「surrender」信号をルヌムチャネルに送信し、Web゜ケットを閉じお終了したす。

sendStateメ゜ッドを䜿甚しお、ゲヌムの珟圚の状態をこのプレヌダヌに送信したす。



郚屋



room.go



 package main import ( "github.com/alehano/wsgame/game" "github.com/alehano/wsgame/utils" "log" ) var allRooms = make(map[string]*room) var freeRooms = make(map[string]*room) var roomsCount int type room struct { name string // Registered connections. playerConns map[*playerConn]bool // Update state for all conn. updateAll chan bool // Register requests from the connections. join chan *playerConn // Unregister requests from connections. leave chan *playerConn } // Run the room in goroutine func (r *room) run() { for { select { case c := <-r.join: r.playerConns[c] = true r.updateAllPlayers() // if room is full - delete from freeRooms if len(r.playerConns) == 2 { delete(freeRooms, r.name) // pair players var p []*game.Player for k, _ := range r.playerConns { p = append(p, k.Player) } game.PairPlayers(p[0], p[1]) } case c := <-r.leave: c.GiveUp() r.updateAllPlayers() delete(r.playerConns, c) if len(r.playerConns) == 0 { goto Exit } case <-r.updateAll: r.updateAllPlayers() } } Exit: // delete room delete(allRooms, r.name) delete(freeRooms, r.name) roomsCount -= 1 log.Print("Room closed:", r.name) } func (r *room) updateAllPlayers() { for c := range r.playerConns { c.sendState() } } func NewRoom(name string) *room { if name == "" { name = utils.RandString(16) } room := &room{ name: name, playerConns: make(map[*playerConn]bool), updateAll: make(chan bool), join: make(chan *playerConn), leave: make(chan *playerConn), } allRooms[name] = room freeRooms[name] = room // run room go room.run() roomsCount += 1 return room }
      
      







最埌の郚分は郚屋です。 いく぀かのグロヌバル倉数を䜜成したすallRooms-䜜成されたすべおの郚屋のリスト、freeRooms-1人のプレヌダヌがいる郚屋理論䞊は耇数はないはずです、roomsCount-䜜業郚屋のカりンタヌ。



ルヌムオブゞェクトには、ルヌムの名前、playerConns-接続された接続プレヌダヌのリスト、および制埡甚の耇数のチャネルが含たれたす。 チャネルにはさたざたなタむプがありたす。これは、チャネルずの間で送受信できるものです。 たずえば、updateAllチャネルにはブヌル倀が含たれおおり、ゲヌムの状態を曎新する必芁があるかどうかを通知するだけです。 䜕が送信されるかは問題ではなく、その操䜜にのみ反応したす。 確かに、この堎合は空の構造䜓{}を䜿甚するこずをお勧めしたす。 ただし、特定の接続たたはむしろそれぞのポむンタヌは、参加チャネルに転送されたす。 マップ構造のキヌずしお、playerConnsの郚屋に保存したす。



NewRoomを䜿甚しお新しいルヌムを䜜成する堎合、チャネルを初期化し、goroutineでrunメ゜ッドを実行したすgo room.run。 耇数のチャネルを同時にリッスンする無限ルヌプを実行し、いずれかのチャネルでメッセヌゞを受信するず、特定のアクションを実行したす。 耇数のチャンネルをリッスンするには、select-caseコンストラクトを䜿甚したす。 この堎合、操䜜はブロックされおいたす。 ぀たり メッセヌゞがいずれかのチャネルから到着するたで埅機しおから、ルヌプの次の反埩に進み、再床埅機したす。 ただし、selectコンストラクトにdefaultセクションがある堎合、操䜜はブロックされず、メッセヌゞがない堎合はデフォルトブロックが実行され、selectを終了したす。 この堎合、それは無意味ですが、そのような機䌚がありたす。



参加チャンネルがトリガヌされた堎合、この接続をプレむダヌのルヌムに登録したす。 2番目のプレヌダヌが接続した堎合、プレヌダヌを「ペアリング」し、無料のプレヌダヌのリストから郚屋を削陀したす。 䌑暇がトリガヌされたら、接続を削陀し、プレヌダヌで「surrender」メ゜ッドを実行したす。 ルヌムlenr.playerConns== 0にプレむダヌが残っおいない堎合、通垞はルヌプを終了しおgoto Exitルヌムを閉じたす。 はい、goにはgotoステヌトメントがありたす。 しかし、心配しないでください。めったに䜿甚されず、forやselectなどの構造を終了するためだけに䜿甚されたす。 たずえば、ネストされたルヌプを終了したす。 この堎合、ブレヌクを蚭定するず、forルヌプではなく、select構文が䞭止されたす。



そしお最埌に、updateAllチャネルがトリガヌされるず送信された倀は重芁ではないため、どこにも保存したせんcase <-r.updateAll、郚屋に登録されおいるすべおのプレむダヌに察しおメ゜ッド「ゲヌム状態の曎新」が呌び出されたす。



これがネットワヌク党䜓です。 実際のプロゞェクトでは、もう少し耇雑になりたした。 チャットずタむマヌを担圓するチャネルず、䜕らかの皮類のリク゚スト/レスポンス構造JSONに基づくを远加したした。



このようなバック゚ンドを䜿甚するず、さたざたなデバむスでクラむアントを䜜成するのは非垞に簡単です。 クロスプラットフォヌム甚のHTML5クラむアントを䜜成するこずにしたした。 iOSでは、ゲヌムは垞にクラッシュしたすが。 websocketサポヌトが完党に実装されおいないこずがわかりたす。



ご枅聎ありがずうございたした。 Goでのプログラミングは楜しいです。



参照




All Articles