ç§ãã¡ã¯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ã§ã®ããã°ã©ãã³ã°ã¯æ¥œããã§ãã
åç §ïŒ
- GitHubããã³ãã¢ã²ãŒã ã®ãã¡ã€ã«ïŒ https : //github.com/alehano/wsgame