ããã«ã¡ã¯ãéèãœãªã¥ãŒã·ã§ã³äºæ¥éšã®å«ã§ãã ã·ãªãŒãºã®æåã®èšäºïŒ Part1 ïŒã§ã¯ã Kubernetes ã®åŒ·åãªæ©èœã掻çšããããã«EKSïŒElastic Kubernetes ServiceïŒãã©ã®ããã«èšå®ãããã«ã€ããŠè©³ãã説æããŸããã EKSã®èšå®ãæåããåŸãã²ãŒã ã®ã€ã³ãã©ã§ãã䜿ãããAgonesãšOpen Matchãã€ã³ã¹ããŒã«ããŸããã ãŸããå
¬åŒãã¢ã§ãã¹ããè¡ããã€ã³ã¹ããŒã«ãæ£ããè¡ãããããšã確èªããŸããã Kubernetes ã«åºã¥ãAgonesãšOpen Matchãšãã2ã€ã® ã³ã³ããŒãã³ã ã«ã€ããŠãçè§£ããŠããªãæ¹ãããã£ããããããããŸããã ãã®ãããPart2ã§ã¯ããŸãAgonesãšOpen Matchã®åºæ¬çãªæŠå¿µãç°¡åã«ç޹ä»ããŸãã æ¬¡ã«ãå®è·µã§ãããã³ã°ã·ã¹ãã ã®ãã¢ãäœæããã©ã®ããã«AgonesãšOpen Matchãçµã¿åãããŠå¹ççã§æè»ãªDedicated Serverã®ç®¡çãšãããã³ã°ãå®çŸãããã瀺ããŸãã Agonesã®çŽ¹ä» OpenMatchã®çŽ¹ä» OpenMatchã®ãããã¡ã€ã«ãŒãäœæããäžè¬çãªãã㌠OpenMatchãšAgonesã®çµ±å å®è·µïŒã²ãŒã ãããã³ã°ã·ã¹ãã ã®ãã¢äœæ ãããã³ã°ã«ãŒã«ã®å®çŸ© äºåæºå ãããã³ã°é¢æ°ã®äœæ GameFrontend Director Match Profilesã®äœæ Agones Allocator Serviceã«ããOpenMatchã®çµ±å Agonesã®Allocateæ©èœã®ããã±ãŒãžå Allocator Serviceããã±ãŒãžãOpenMatchã«çµ±å MatchFunction ãããã€ãšåäœç¢ºèª ããŒã«ã«ã«ãããŠDockerImageã®ã³ã³ãã€ã« DockerImageãAmazon ECRã«ã¢ããããŒã ã¢ãžã¥ãŒã«ãEKSã«ããã〠åäœç¢ºèª çµããã« åè Agonesã®çŽ¹ä» Agonesã¯ã Google Cloudãš Ubisoft ãå
±åã§éçºãããŠã Ubisoft å
ã®å€§èŠæš¡ãª ãã«ããã¬ã€ã€ãŒ ãªã³ã©ã€ã³ã²ãŒã (MMO)ã§å©çšãããŠãããœãªã¥ãŒã·ã§ã³ã§ãã ãŸããããã°ã©ãã³ã°ã OSS ã§å
¬éãããŠããçºå®å
šã«ç¬èªãããã¯ãŒã¯å
ã§åäœãããããšãå¯èœã§ãã Agonesã¯ã Kubernetes ã®ç¹æ§ã掻çšããŠã²ãŒã ãµãŒããŒãå¹ççã«ããããŠã¹ã±ãŒã©ãã«ã«éçšããã³ç®¡çããæ¹æ³ãæäŸããŸãã Agonesã®äž»èŠãª ã³ã³ããŒãã³ã ã«ã¯ãGameServerãFleetããããŸãã Agonesã§ã¯ãéçºè
ã¯ç°¡å㪠Kubernetes ã®ã³ãã³ããçšããŠGameServerãäœæããã³ç®¡çããããšãå¯èœã§ãããã«ããã²ãŒã ãµãŒããŒã®ç®¡çã®è€éãã倧å¹
ã«äœæžãããŸãã Agonesãã²ãŒã ãµãŒããŒã®ã©ã€ããµã€ã¯ã«ã管çããäžã§ã以äžã®6ã€ã®ã¹ããŒãžãå®çŸ©ããŠããŸãã Agonesã¯ã²ãŒã ãµãŒããŒã®ã¹ããŒãžã«å¿ããŠãé©åãªåŠçãå®è¡ããŸãã Scheduled ïŒäºå®ïŒïŒGameServerãã¹ã±ãžã¥ãŒã«ãããNodeã«å²ãåœãŠããã Requested ïŒèŠæ±ïŒïŒ Kubernetes ã®PodãäœæãããGameServerãäœæããã Starting ïŒèµ·åäžïŒïŒGameServerãèµ·åãããã¬ã€ã€ãŒãã²ãŒã ã«æ¥ç¶ã§ããç¶æ
ã«ãªãåã®æºåç¶æ
Ready ïŒæºåå®äºïŒïŒGameServerãã¢ã¯ãã£ãç¶æ
ã§ããã¬ã€ã€ãŒãæ¥ç¶ã§ãã Allocated ïŒå²ãåœãŠæžã¿ïŒïŒãã¬ã€ã€ãŒãGameServerã«æ¥ç¶ãããªãœãŒã¹ã確ä¿ãããŠãã Shutdown ïŒã·ã£ããããŠã³ïŒïŒãã¹ãŠã®ãã¬ã€ã€ãŒãåæãããGameServerãã·ã£ããããŠã³ãã OpenMatchã®çŽ¹ä» OpenMatchã¯ãFrontend API ãBackend API ãQuery API ãFunctionãªã©ãè€æ°ã® ã³ã³ããŒãã³ã ããæãç«ã£ãŠããŸãã ãããã® ã³ã³ããŒãã³ã ã¯ãããããç¬èªã®åœ¹å²ãæãããªããå調ããŠåãããããã³ã°ã·ã¹ãã ãæ§ç¯ããŸãã OpenMatchã®ãããã³ã°ãããŒã¯ä»¥äžã®ãšããã§ãã ãã¬ã€ã€ãŒãFrontend API ã«ãããã³ã°ãªã¯ ãšã¹ ããéä¿¡ãã Frontend API ã¯ãã®ãªã¯ ãšã¹ ããå
éšã®ç¶æ
ã§ã¹ãã¢ã«ä¿åãã ãããã³ã°é¢æ°ãQuery API ã䜿çšããŠç¶æ
ã¹ãã¢ããæ¡ä»¶ã«åããã¬ã€ã€ãŒãåãåãããã ãããã³ã°é¢æ°ãBackend API ã«ãããã³ã°çµæãè¿ã Backend API ããã¬ã€ã€ãŒã«ãããã³ã°çµæãè¿ã OpenMatchã®ãããã¡ ã€ã« ãŒãäœæããäžè¬çãªãã㌠OpenMatchã®ãããã¡ ã€ã« ãŒãäœæããã«ã¯äž»ã«äžã€ã®ã¹ãããããããŸãã ãããã³ã°ã«ãŒã«ãå®çŸ©ãã ãããã³ã°é¢æ°ãäœæãã ãããã¡ ã€ã« ãŒã®èšå®ããã³éçšãè¡ã ãŸãããããã³ã°ã«ãŒã«ãå®çŸ©ããŸãã ãã®ã«ãŒã«ã¯ãããã³ã°ããžãã¯ãåæ ãããã®ã§ããã¬ã€ã€ãŒã®ã¬ãã«ãå°åãã¹ãã«ãªã©ãå«ããŸãã æ¬¡ã«ããããã³ã°é¢æ°ãäœæããŸãã ãã®é¢æ°ã¯Query API ã䜿çšããŠãããã³ã°ã«ãŒã«ã«åèŽãããã¬ã€ã€ãŒãåãåããããã®ãããã³ã°çµæãBackend API ã«è¿ããŸãã æåŸã«ããããã¡ ã€ã« ãŒã®èšå®ãšéçšãè¡ããŸããOpenMatchã¯å€ãã®èšå®ãªãã·ã§ã³ãæäŸããŠããããããã¯ããŒãºã«å¿ããŠèšå®ã§ããŸãã OpenMatchãšAgonesã®çµ±å OpenMatchãšAgonesã®çµ±åã¯ãå¹ççãªã²ãŒã ãããã³ã°ã·ã¹ãã ãæ§ç¯ããäžã§ã®éèŠãªéšåã§ãããäž»ã«äºã€ã®ããã»ã¹ãé¢äžããŠããŸãã OpenMatchã®ãããã³ã°é¢æ°ããAgonesã®GameServerãåŒã³ã ããšãã GameServerã®ã©ã€ããµã€ã¯ã«ã管çãããšãã OpenMatchã¯ãã¬ã€ã€ãŒã®ãããã³ã°ãæ
åœããäžæ¹Agonesã¯GameServerã®ã©ã€ããµã€ã¯ã«ã®ç®¡çãæ
åœããŸãã ãããäºã€ã®çµã¿åããã«ããããã¬ã€ã€ãŒã®ããŒãºã«å¿ããŠGameServerãåçã«äœæããã³å²ãåœãŠãããšãã§ããŸãã OpenMatchã®ãããã³ã°é¢æ°å
ã§ãAgones SDK ãéããŠæ°ããGameServerãäœæã§ããŸãã ããããã»ãšãã©ã®å Žåæ°ããGameServerãäœæããã ãã§ã¯ãªãæ¢åã®GameServerãã¹ã±ãžã¥ãŒã«ããå²ãåœãŠãããšãããéèŠã§ãã GameServerã®ããã©ãŒãã³ã¹ãè² è·ãå°ççãªäœçœ®ãªã©ãè©äŸ¡ããæé©ãªGameServerãèŠã€ããå¿
èŠããããŸãã é©åãªGameServerãèŠã€ãããããã®ã¢ãã¬ã¹ããã¬ã€ã€ãŒã«è¿ããŸãããã¬ã€ã€ãŒã¯ãã®ã¢ãã¬ã¹ã䜿çšããŠGame Serverã«æ¥ç¶ãã²ãŒã äœéšãå§ããŸãã ããã§çµããã§ã¯ãããŸããããGameServerã®ç¶æ
ãç£èŠããå¿
èŠã«å¿ããŠèª¿æŽããå¿
èŠããããŸãã äŸãã°ãGameServerã®è² è·ãé«ãããå Žåãæ°ããGameServerãäœæããŠè² è·ã忣ã§ããŸãã äžæ¹ãGameServerã®ãã¬ã€ã€ãŒæ°ãæžå°ããå Žåããããã·ã£ããããŠã³ããŠãªãœãŒã¹ãç¯çŽããããšãå¯èœã§ãã å®è·µïŒã²ãŒã ãããã³ã°ã·ã¹ãã ã®ãã¢äœæ å
ã«ç޹ä»ããOpenMatch ãããã¡ãŒã«ãŒ ã®äœæããã»ã¹ã«åŸã£ãŠããã¢ãäœæãå§ããŸãã ãããã³ã°ã«ãŒã«ã®å®çŸ© ãã®ãã¢ã§ã¯ããŠãŒã¶ãŒã®ã¹ãã«ã¬ãã«ãšã¬ã€ãã³ã·ãåºã«ã¹ã³ã¢ãç®åºããåäžãªãŒãžã§ã³å
ã§ã¹ã³ã¢ãè¿ããŠãŒã¶ãŒããããã³ã°ãããšããã«ãŒã«ãå®è£
ããŸãã ããããã®ãããã³ã°ã«ãŒã ã¯4人ã®ãã¬ã€ã€ãŒã§æ§æãããã¹ã³ã¢ãè¿ããŠãŒã¶ãŒå士ã¯äžç·ã«ãªããŸãã 以äžã§ã¯ããã®ãããã³ã°ã®è©³çްãæé ããããŠé©çšãã ã¢ã«ãŽãªãºã ã«ã€ããŠå
·äœçã«èª¬æããŸãã ãã±ãã詳现 Ticket Details ãã±ããã¯ä»¥äžå³ã®GameFrontendã«ãã£ãŠäœæãããOpenMatchã®Frontendã«ããã·ã¥ãããæ
å ±ã§ãã ãã±ããã«ã¯ãã¬ã€ã€ãŒã«é¢ããæ
å ±ãå«ãŸããŠããããããã³ã°ã®éã«äœ¿çšãããŸãã 以äžãGameFrontendãããDirectorãããMatchFunctionãç« ã®å®è£
ã§å©çšãããŸãã ä»åã®ãã¢ã§ã¯ããã±ãã詳现ã«ã¯ä»¥äžã®èŠçŽ ãå«ãŸããŠããŸãã ã¿ã° tag ïŒã¿ã°ã䜿ã£ãŠãã±ãããåé¡ããããšãå¯èœã§ãããã«ãããããã³ã°ã·ã¹ãã ã¯ããå¹ççã«å¯Ÿå¿ãããã¥ãŒãèŠã€ããããšãã§ãã ä»åã¯ã²ãŒã ã¢ãŒã Game Mode ãšããèšèšãåæã«å®è£
ãããããã¿ã°ã¯mode.sessionãšãã ãªãŒãžã§ã³ Region ïŒããã¯ãã¬ã€ã€ãŒãããå°ççãªå°åã瀺ããŠããããä»åã¯ap-northeast-1ãap-northeast-3ãšãã ã¹ãã«ã¬ãã« Skill Level ïŒããã¯ãã¬ã€ã€ãŒã®ã¹ãã«ã¬ãã«ã瀺ããŠããã0.0ãã2.0ã®ç¯å²ã§èšå®ããã ã¬ã€ãã³ã· Latency ïŒããã¯ãã¬ã€ã€ãŒã®ãããã¯ãŒã¯é
å»¶ã瀺ããŠãã â»ã»ãšãã©ã®äººã¯0ã«è¿ãã§ããããããã¯ãŒã¯ã®ä¿¡é Œæ§ãã·ãã¥ã¬ãŒãããããã«ãäžéšã®äººã¯ç¡é倧ã«èšå®ãããŠãã ãããã³ã°æ©èœã®åºæº MatchFunction Criteria ä»åã®ãã¢ã§ã¯ããããã³ã°ã®åºæºã以äžã«å®çŸ©ããŸãã 以äžãDirectorãããMatchFunctionãç« ã®å®è£
ã§å©çšãããŸãã ãŸãã¯ãã¬ã€ã€ãŒã®å°ççãªå°åãšã²ãŒã ã¢ãŒããåºæºã«ããã±ããããŒã«ãäœæãã æ¬¡ã«ãåãã¬ã€ã€ãŒã«å¯Ÿã㊠score = skill - (latency / 1000.0) ã® ã¢ã«ãŽãªãºã ã䜿çšããŠã¹ã³ã¢ãç®åºãã ãããŠãã¹ã³ã¢ã«åºã¥ããŠã«ãŒã ã«ãã¬ã€ã€ãŒãé
眮ãã é«ã¹ãã«ãäœã¬ã€ãã³ã·ã®ãŠãŒã¶ãŒã¯åãã«ãŒã ã«å²ãåœãŠããã 1ã€ã®ãããã«åå ã§ãããã¬ã€ã€ãŒã®äžéã¯ã4人ãšå®ããããŠãã ãã£ã¬ã¯ã¿ãŒãããã¡ã€ã« Director Profiles ãã£ã¬ã¯ã¿ãŒãããã¡ã€ã«ã¯ãã£ã¬ã¯ã¿ãŒãçæãããªããžã§ã¯ãã§ããããã®ãªã¯ ãšã¹ ãã«äœ¿çšãããŸãã 以äžãDirectorãç« ã®å®è£
ã§å©çšãããŸãã ä»åã®ãã¢ã§ã¯ããã£ã¬ã¯ã¿ãŒã¯5ç§ããšã«ãããã¡ã€ã«ãçæãããããããªã¯ ãšã¹ ãããã ãŸãããã£ã¬ã¯ã¿ãŒã¯ãã¬ã€ã€ãŒã®å°ççãªå°åã«å¿ããŠã察å¿ããå°ççãªå°åã®GameServerããã¬ã€ã€ãŒã«å²ãåœãŠã äºåæºå OpenMatchã® ãªããžã㪠ãããŒã«ã«ç°å¢ã«ã¯ããŒã³ãã ããŒã¹ãšãªãã³ãŒãã¯ãtutorials/matchmaker101ã®ãã¹ã«ååšãã git clone https://github.com/googleforgames/open-match.git Golang ã«ããå®è£
ã®ãããé©å㪠IDE ãèšå®ãã ãã®èšäºã§ã¯ã Visual Studio Code ã«Go plugin(v.39.0)ãã€ã³ã¹ããŒã«ããç°å¢ã§éçºãé²ãã Dockerç°å¢ãæºåãã 察象ã®ç°å¢ã§Dockerãã»ããã¢ããããã«ã¯ã Dockerã®ã€ã³ã¹ããŒã« ã®ããã¥ã¡ã³ããåç
§ããŠãã ãã ãããã³ã°é¢æ°ã®äœæ Openmatchå
¬åŒããã¥ã¡ã³ãã® Tutorial ãããŒã¹ã«ãã¹ããã ãã€ã¹ ãããã§GameFrontendãDirectorãMatchFunctionãšãã£ã ã³ã³ããŒãã³ã ãèšèšã»äœæããŸãã â»Tutorialã¯Openmatchã® ãã¬ãŒã ã¯ãŒã¯ ããã°ã©ã ã§ããããã³ã°ããžãã¯ã¯äžèšã®ã«ãŒã«ã«åŸã£ãŠç¬èªã«å®è£
ããå¿
èŠããããŸãã 以äžã®å³ã¯ãOpenmatch ã®å
šäœç㪠ã¢ãŒããã¯ã㣠ã§ãèµ€ãå²ãŸããéšåã¯ç¬èªã«å®è£
ãã¹ãéšåã§ãã GameFrontend ãã±ããçæé¢æ°ãå®è£
ãã â»ããŒã¹ã³ãŒãïŒ https://github.com/googleforgames/open-match/blob/main/tutorials/matchmaker101/frontend/ticket.go ããããã³ã°ã«ãŒã«ã®å®çŸ©ãç« ã§å®çŸ©ãããã±ãã詳现 TicketãDetails ã®ã«ãŒã«ã«åŸã£ãŠã mode.session ãšããååã®ã¿ã°ãå®çŸ©ããŠã次ã«ã©ã³ãã ã«ã¹ãã«ãšã¬ã€ãã³ã·ã®å€ãèšå®ããŸãã ãªãŒãžã§ã³ã®èšå®ã«ã€ããŠã¯ãå
·äœçãªãŠãŒã¶ãŒãã©ãããæ¥ç¶ãããã¯ç¢ºå®ããŠããªããããå€éšããå€ãååŸããããã«å®çŸ©ããŸãããã®æ
å ±ã¯ã¯ã©ã€ã¢ã³ãããååŸããå¿
èŠããããŸãã # ticket.go import( "open-match.dev/open-match/pkg/pb" // å¿
èŠãªããã±ãŒãžè¿œå "math/rand" "time" ) func makeTicket(region string) *pb.Ticket { modes := []string{"mode.session"} ticket := &pb.Ticket{ SearchFields: &pb.SearchFields{ Tags: modes, DoubleArgs: CreateDoubleArgs(), StringArgs: map[string]string{ "region": region, }, }, } return ticket } func CreateDoubleArgs() map[string]float64 { rand.Seed(time.Now().UTC().UnixNano()) skill := 2 * rand.Float64() latency := 50.0 * rand.ExpFloat64() return map[string]float64{ "skill": skill, "latency": latency, } } echo ãã¬ãŒã ã¯ãŒã¯ ã䜿çšããŠFrontendã®APIServerãå®è£
ãã â»ããŒã¹ã³ãŒãïŒ https://github.com/googleforgames/open-match/blob/main/tutorials/matchmaker101/frontend/main.go ãã® API Serverã®URLã¯ïŒ GET /play/:region ã§ãregionãã©ã¡ãŒã¿ãæã£ãŠããŸãã # frontend/main.go import( "github.com/labstack/echo" ) type matchResponce struct { IP string `json:"ip"` Port string `json:"port"` Skill string `json:"skill"` Latency string `json:"latency"` Region string `json:"region"` } var fe pb.FrontendServiceClient var matchRes = &matchResponce{} func main() { //ïŒïŒããŒã¹ã³ãŒããçç¥ããïŒ // fe = pb.NewFrontendServiceClient(conn)以éã®ã³ãŒããã³ã¡ã³ãã¢ãŠã e := echo.New() e.GET("/play/:region", handleGetMatch) e.Start(":80") } func handleGetMatch(c echo.Context) error { // Create Ticket. region := c.Param("region") req := &pb.CreateTicketRequest{ Ticket: makeTicket(region), } matchRes.Skill = fmt.Sprintf("%f", req.Ticket.SearchFields.DoubleArgs["skill"]) matchRes.Latency = fmt.Sprintf("%f", req.Ticket.SearchFields.DoubleArgs["latency"]) matchRes.Region = req.Ticket.SearchFields.StringArgs["region"] resp, err := fe.CreateTicket(context.Background(), req) if err != nil { log.Fatalf("Failed to CreateTicket, got %v", err) return c.JSON(http.StatusInternalServerError, matchRes) } // Polling TicketAssignment. deleteOnAssign(fe, resp) return c.JSON(http.StatusOK, matchRes) } func deleteOnAssign(fe pb.FrontendServiceClient, t *pb.Ticket) { //ïŒïŒããŒã¹ã³ãŒããçç¥ããïŒ if got.GetAssignment() != nil { log.Printf("Ticket %v got assignment %v", got.GetId(), got.GetAssignment()) conn := got.GetAssignment().Connection slice := strings.Split(conn, ":") matchRes.IP = slice[0] matchRes.Port = slice[1] break } } Director Match Profilesã®äœæ â»ããŒã¹ã³ãŒãïŒ https://github.com/googleforgames/open-match/blob/main/tutorials/matchmaker101/director/profile.go ããããã³ã°ã«ãŒã«ã®å®çŸ©ãç« ã§å®çŸ©ãã ãããã³ã°æ©èœã®åºæº MatchFunction Criteria ã®ãã±ããããŒã«äœæã«ãŒã«ã«åŸã£ãŠã TagPresentFilters ããã³ StringEqualsFilter ãå®çŸ©ããŸãã ããããã³ã°ã«ãŒã«ã®å®çŸ©ãç« ã§å®çŸ©ãããã£ã¬ã¯ã¿ãŒãããã¡ã€ã« Director Profiles ã®ã²ãŒã ãµãŒããŒéžæã«ãŒã«ã«åŸã£ãŠã profile.Extensions ãå®çŸ©ããŸãã # profile.go type AllocatorFilterExtension struct { Labels map[string]string `json:"labels"` Fields map[string]string `json:"fields"` } func generateProfiles() []*pb.MatchProfile { var profiles []*pb.MatchProfile regions := []string{"ap-northeast-1", "ap-northeast-3"} for _, region := range regions { profile := &pb.MatchProfile{ Name: fmt.Sprintf("profile_%s", region), Pools: []*pb.Pool{ { Name: "pool_mode_" + region, TagPresentFilters: []*pb.TagPresentFilter{ {Tag: "mode.session"}, }, StringEqualsFilters: []*pb.StringEqualsFilter{ {StringArg: "region", Value: region}, }, }, }, } // build filter extensions filter := AllocatorFilterExtension{ Labels: map[string]string{ "region": region, }, Fields: map[string]string{ "status.state": "Ready", }, } // to protobuf Struct labelsStruct := &structpb.Struct{Fields: make(map[string]*structpb.Value)} for key, value := range filter.Labels { labelsStruct.Fields[key] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: value}} } fieldsStruct := &structpb.Struct{Fields: make(map[string]*structpb.Value)} for key, value := range filter.Fields { fieldsStruct.Fields[key] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: value}} } // put data to the protobuf Struct filterStruct := &structpb.Struct{Fields: map[string]*structpb.Value{ "labels": {Kind: &structpb.Value_StructValue{StructValue: labelsStruct}}, "fields": {Kind: &structpb.Value_StructValue{StructValue: fieldsStruct}}, }} // to google.protobuf.Any object filterAny, err := ptypes.MarshalAny(filterStruct) if err != nil { panic(err) } profile.Extensions = map[string]*any.Any{ "allocator_filter": filterAny, } profiles = append(profiles, profile) } return profiles } Agones Allocator Serviceã«ããOpenMatchã®çµ±å Agonesãçµ±åãããããã³ã°çµæã«å¯Ÿå¿ããGameServerã¢ãã¬ã¹ãå²ãåœãŠãŸãã ãã¬ã€ã€ãŒã¯ãã®ã¢ãã¬ã¹ãéããŠå¯Ÿå¿ããGameServerã«æ¥ç¶ããŸãã Agones Allocateæ©èœã®å®è£
ã¯ç¬èªã®ãã®ã§ãããOpenMatchã® ãã¥ãŒããªã¢ã« ã«ã¯åºç€ãšãªãã³ãŒããååšããŸããã ãã®ãããdirectorãã©ã«ãã®äžã«æ°èŠãã¡ã€ã«ãšããŠallocator_director.goãäœæããŸãã Agonesã®Allocateæ©èœã®ããã±ãŒãžå AgonesãæäŸãã Allocator Service ã䜿ã£ãŠå¯Ÿå¿ããGameServerãååŸããŸãã ããã©ã«ãã®ã¯ã©ã€ã¢ã³ãèšŒææžãååŸãã Allocator Service ã¯mTLSèªèšŒã¢ãŒãã䜿çšããŠãããããã«ããèšŒææžã䜿çšããŠãµãŒãã¹ã«æ¥ç¶ããããšã¯å¿
é ã«ãªããŸãã èšŒææžã¯æ¢ã«helmã€ã³ã¹ããŒã«æã«äœæãããŠããŸãã ç¬èªã®èšŒææžã䜿çšããããšãå¯èœã§ãå
·äœçãªèšå®ã¯ ãã¡ã ãåç
§ããŠãã ããã äžèšã®ã³ãã³ããå®è¡ããŠããã©ã«ãã®ã¯ã©ã€ã¢ã³ãèšŒææžãååŸãã â»çè
㯠Mac ã®ç°å¢ã§ã³ãã³ããå®è¡ããŠããŸãã Linux ã®ç°å¢ã§ããã°ã base64 -D ã®ä»£ããã« base64 -d ã³ãã³ãã䜿çšããŠãã ããã # MACã®ã³ãã³ã kubectl get secret allocator-client.default -n default -ojsonpath="{.data.tls\.crt}" | base64 -D > "client.crt" kubectl get secret allocator-client.default -n default -ojsonpath="{.data.tls\.key}" | base64 -D > "client.key" kubectl get secret allocator-tls-ca -n agones-system -ojsonpath="{.data.tls-ca\.crt}" | base64 -D > "tls-ca.crt" ååŸããã¯ã©ã€ã¢ã³ãèšŒææžãé
眮ããŸãã äžèšã§ããŠã³ããŒãããèšŒææžãç¹å®ã®ãã¹ã«ä¿åããPathãšããŠå®çŸ©ããŸãã ããã§ã¯ã allocator/certfile ãã£ã¬ã¯ã ãªã«ä¿åããããšãæ³å®ããŠããŸãã # agones_allocator.go const ( KeyFilePath = "allocator/certfile/client.key" CertFilePath = "allocator/certfile/client.crt" CaCertFilePath = "allocator/certfile/tls-ca.crt" ) Allocator Serviceã®ã¯ã©ã€ã¢ã³ããäœæãã å€éšããAgonesã®Allocateæ©èœãåŒã³ã ãå¿
èŠãããå Žåã NewAgonesAllocatorClient ãåŒã³ã ããšã¯ã©ã€ã¢ã³ããçæãããŸãã â»ã泚æïŒ ã³ãŒããé·ããªããããªãããã«ããšã©ãŒåŠçã«é¢é£ããã³ãŒãã¯åé€ããŠããŸãã # agones_allocator.go type AgonesAllocatorClientConfig struct { KeyFile string CertFile string CaCertFile string AllocatorServiceHost string AllocatorServicePort int Namespace string MultiCluster bool } type AgonesAllocatorClient struct { Config *AgonesAllocatorClientConfig DialOpts grpc.DialOption } func NewAgonesAllocatorClient() (*AgonesAllocatorClient, error) { config := &AgonesAllocatorClientConfig{ KeyFile: KeyFilePath, CertFile: CertFilePath, CaCertFile: CaCertFilePath, AllocatorServiceHost: AllocatorServiceHost, AllocatorServicePort: AllocatorServicePort, Namespace: "default", MultiCluster: false, } cert, err = ioutil.ReadFile(config.CertFile) key, err = ioutil.ReadFile(config.KeyFile) ca, err = ioutil.ReadFile(config.CaCertFile) dialOpts, err := createRemoteClusterDialOption(cert, key, ca) return &AgonesAllocatorClient{ Config: config, DialOpts: dialOpts, }, nil } func createRemoteClusterDialOption(clientCert, clientKey, caCert []byte) (grpc.DialOption, error) { cert, err := tls.X509KeyPair(clientCert, clientKey) tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true} if len(caCert) != 0 { tlsConfig.RootCAs = x509.NewCertPool() if !tlsConfig.RootCAs.AppendCertsFromPEM(caCert) { return nil, errors.New("only PEM format is accepted for server CA") } } return grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), nil } Allocateã®ã¡ã€ã³é¢æ°ãå®è£
ãã ãã®é¢æ°ã§ã¯ããŸãgrpcïŒ grpc.Dial ïŒ ãããã³ã« ã䜿çšã㊠Allocator Service ã«æ¥ç¶ããŸãã æ¬¡ã«ãGameServerã®éžæã«ãŒã«( assignmentGroup.Assignment.Extensions )ãååŸããŸãã ãã®ã«ãŒã«ã«åºã¥ããŠå¯Ÿå¿ããGameServerãååŸããæçµçã«åAssignmentã® address ãã£ãŒã«ãã«ã¢ãã¬ã¹ãä»äžããŸãã â»ã泚æïŒ ã³ãŒããé·ããªããããªãããã«ããšã©ãŒåŠçã«é¢é£ããã³ãŒãã¯åé€ããŠããŸãã # agones_allocator.go func (c *AgonesAllocatorClient) Allocate(req *pb.AssignTicketsRequest) error { conn, err := grpc.Dial(fmt.Sprintf("%s:%d", c.Config.AllocatorServiceHost, c.Config.AllocatorServicePort), c.DialOpts) defer conn.Close() grpcClient := pb_agones.NewAllocationServiceClient(conn) for _, assignmentGroup := range req.Assignments { filterAny := assignmentGroup.Assignment.Extensions["allocator_filter"] filter := &structpb.Struct{} if err := ptypes.UnmarshalAny(filterAny, filter); err != nil { panic(err) } request := &pb_agones.AllocationRequest{ Namespace: c.Config.Namespace, GameServerSelectors: []*pb_agones.GameServerSelector{ { MatchLabels: filter.Fields["labels"], }, }, MultiClusterSetting: &pb_agones.MultiClusterSetting{ Enabled: c.Config.MultiCluster, }, } resp, err := grpcClient.Allocate(context.Background(), request) if len(resp.GetPorts()) > 0 { address := fmt.Sprintf("%s:%d", resp.Address, resp.Ports[0].Port) assignmentGroup.Assignment.Connection = address } } return nil } Allocator Serviceããã±ãŒãžãOpenMatchã«çµ±å äžèšã®OpenMatchã® ã¢ãŒããã¯ã㣠å³ã«åºã¥ããAgonesãµãŒãã¹ãåŒã³åºããŠGameServerãååŸãã ã³ã³ããŒãã³ã ã¯Directorã§ãã ãã®ãããåŒã³åºãã³ãŒããDirectorã«çµ±åããå¿
èŠããããŸãã â»ããŒã¹ã³ãŒãïŒ https://github.com/suecideTech/try-openmatch-agones/blob/master/OpenMatch/mod_matchmaker101/director/main.go GameServerã®assgin颿°ãä¿®æ£ããŸãã ããã§ã¯ãäžèšã§äœæãã Allocator Service ããã±ãŒãžã䜿çšããŠå®éã®GameServerã¢ãã¬ã¹ãååŸããŸãã å
ã®ã³ãŒãã§ã¯ GameServerAllocations ã䜿ã£ãŠGameServerã¢ãã¬ã¹ãååŸããŠããŸãããããã¯Agonesãæšå¥šããŠããæ¹æ³ã§ã¯ãªãããŸãåŸæã®æ¡åŒµã«ãé©ããŠããŸããã ãã®ãããå
¬åŒã«æšå¥šãããŠãã Allocator Service ãã©ããããŠGameServerãååŸããããã«ããŸããã # director/main.go func assign(be pb.BackendServiceClient, matches []*pb.Match) error { for _, match := range matches { ticketIDs := []string{} for _, t := range match.GetTickets() { ticketIDs = append(ticketIDs, t.Id) } aloReq := &pb.AssignTicketsRequest{ Assignments: []*pb.AssignmentGroup{ { TicketIds: ticketIDs, Assignment: &pb.Assignment{ Extensions: match.Extensions, }, }, }, } client, err := allocator.NewAgonesAllocatorClient() client.Allocate(aloReq) if _, err := be.AssignTickets(context.Background(), aloReq); err != nil { return fmt.Errorf("AssignTickets failed for match %v, got %w", match.GetMatchId(), err) } log.Printf("Assigned server %v to match %v", conn, match.GetMatchId()) } return nil } MatchFunction ãŠãŒã¶ãŒãããã³ã°ã«ãŒã«ãå®è£
ããŸãã â»ããŒã¹ã³ãŒãïŒ https://github.com/suecideTech/try-openmatch-agones/blob/master/OpenMatch/mod_matchmaker101/matchfunction/mmf/matchfunction.go ããããã³ã°ã«ãŒã«ã®å®çŸ©ãç« ã§å®çŸ©ãã ãããã³ã°æ©èœã®åºæº MatchFunction Criteria ã®ãŠãŒã¶ãŒãããã³ã°ã«ãŒã«ã«åŸã£ãŠããŸããŠãŒã¶ãŒã®ã¹ã³ã¢ãèšç®ããã¹ã³ã¢ã®å€§ããã«åºã¥ããŠ4人éšå±ãå²ãåœãŠãŸãã # matchfunction.go const ( matchName = "basic-matchfunction" ticketsPerPoolPerMatch = 4 ) func (s *MatchFunctionService) Run(req *pb.RunRequest, stream pb.MatchFunction_RunServer) error { //ïŒïŒããŒã¹ã³ãŒããçç¥ããïŒ //poolTickets, err := matchfunction.QueryPools(stream.Context(), s.queryServiceClient, req.GetProfile().GetPools()) p := req.GetProfile() tickets, err := matchfunction.QueryPool(stream.Context(), s.queryServiceClient, p.GetPools()[0]) //ïŒïŒããŒã¹ã³ãŒããçç¥ããïŒ idPrefix := fmt.Sprintf("profile-%v-time-%v", p.GetName(), time.Now().Format("2006-01-02T15:04:05.00")) proposals, err := makeMatches(req.GetProfile(), idPrefix, tickets) //ïŒïŒããŒã¹ã³ãŒããçç¥ããïŒ } func (s *MatchFunctionService) makeMatches(ticketsPerPoolPerMatch int, profile *pb.MatchProfile, idPrefix string, tickets []*pb.Ticket) ([]*pb.Match, error) { if len(tickets) < ticketsPerPoolPerMatch { return nil, nil } ticketScores := make(map[string]float64) for _, ticket := range tickets { ticketScores[ticket.Id] = score(ticket.SearchFields.DoubleArgs["skill"], ticket.SearchFields.DoubleArgs["latency"]) } sort.Slice(tickets, func(i, j int) bool { return ticketScores[tickets[i].Id] > ticketScores[tickets[j].Id] }) var matches []*pb.Match count := 0 for len(tickets) >= ticketsPerPoolPerMatch { matchTickets := tickets[:ticketsPerPoolPerMatch] tickets = tickets[ticketsPerPoolPerMatch:] var matchScore float64 for _, ticket := range matchTickets { matchScore += ticketScores[ticket.Id] } eval, err := anypb.New(&pb.DefaultEvaluationCriteria{Score: matchScore}) if err != nil { log.Printf("Failed to marshal DefaultEvaluationCriteria into anypb: %v", err) return nil, fmt.Errorf("Failed to marshal DefaultEvaluationCriteria into anypb: %w", err) } newExtensions := map[string]*anypb.Any{"evaluation_input": eval} newExtensions for k, v := range origExtensions { newExtensions[k] = v } matches = append(matches, &pb.Match{ MatchId: fmt.Sprintf("%s-%d", idPrefix, count), MatchProfile: profile.GetName(), MatchFunction: matchName, Tickets: matchTickets, Extensions: newExtensions, }) count++ } return matches, nil } func score(skill, latency float64) float64 { return skill - (latency / 1000.0) } ãããŸã§ã§ããããã³ã°æ©èœãšGameServerã®ã±ãžã¥ãŒãªã³ã°æ©èœã®å®è£
ãå®äºããŸããã æ¬¡ã«ãäœæããã¢ãžã¥ãŒã«ãããããEKSã«ã¢ããããŒããããã¢ãšããŠãã¹ãããŸãã å
·äœçã«å®æãããã¢ã® ã¢ãŒããã¯ã㣠ã¯ã以äžã®å³ã®éãã§ãã ãããã€ãšåäœç¢ºèª ããŒã«ã«ã«ãããŠDockerImageã® ã³ã³ãã€ã« GameFrontend # OpenMatch/mod_matchmaker101/frontend/Dockerfile docker build -t localimage/mod_frontend:0.1 . Director # OpenMatch/mod_matchmaker101/director/Dockerfile docker build -t localimage/mod_director:0.1 . MatchFunction # OpenMatch/mod_matchmaker101/matchfunction/Dockerfile docker build -t localimage/mod_matchfunction:0.1 . DockerImageã Amazon ECRã«ã¢ããããŒã EKSã§DockerImageãååŸããéãããŒã«ã«ã®ã€ã¡ãŒãžã«ã¢ã¯ã»ã¹ã§ããªããããã€ã¡ãŒãžã Amazon ECRãµãŒãã¹ã«ã¢ããããŒãããå¿
èŠããããŸãã â» Amazon ECRã¯ã€ã¡ãŒãžãä¿ç®¡ããããã®å°çšã¬ããžããªã§ãDocker Hubãªã©ãšåæ§ã®ãµãŒãã¹ããããŸãã Amazon ECRã§ãã©ã€ããŒãã€ã¡ãŒãž ãªããžã㪠ãäœæããå
·äœçãªæ¹æ³ã«ã€ããŠã¯ã ECRã§ãã©ã€ããŒããªããžããªãäœæãã ãåç
§ããŠãã ããã 以äžã®åç§°ã® Amazon ECRãã©ã€ããŒãã€ã¡ãŒãž ãªããžã㪠ãããããäœæãã Frontendã® ãªããžã㪠åïŒmod_frontend Directorã® ãªããžã㪠åïŒmod_director MatchFunctionã® ãªããžã㪠åïŒmod_matchfunction ããŒã«ã«ã®ã€ã¡ãŒãžã Amazon ECRã«ã¢ããããŒããã # Frontend docker tag localimage/mod_frontend:0.1 {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_frontend:0.1 docker push {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_frontend:0.1 # Director docker tag localimage/mod_director:0.1 {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_director:0.1 docker push {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_director:0.1 # MatchFunction: docker tag localimage/mod_matchfunction:0.1 {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_matchfunction:0.1 docker push {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_matchfunction:0.1 ã¢ãžã¥ãŒã«ãEKSã«ããã〠ããã〠yaml ãã¡ã€ã«å
ã®imageã¢ãã¬ã¹ããäžèšã§äœæãã Amazon ECRã®ã¢ãã¬ã¹ã«å€æŽããŸãã # Frontend: ## yaml file path ## OpenMatch/mod_matchmaker101/frontend/frontend.yaml image: {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_frontend:0.1 # Director ## yaml file path ## OpenMatch/mod_matchmaker101/director/director.yaml image: {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_director:0.1 # MatchFunction: ## yaml file path ## OpenMatch/mod_matchmaker101/matchfunction/matchfunction.yaml image: {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_matchfunction:0.1 å Yaml ãã¡ã€ã«ã以äžã®ããã«ä¿®æ£ããŸãã # Frontend.yaml ## yaml file path ## OpenMatch/mod_matchmaker101/frontend/frontend.yaml ## KindãDeploymentã«å€æŽããHTTP LBãµãŒãã¹ã远å ããŸã apiVersion: v1 kind: Service metadata: name: frontend-endpoint annotations: service.alpha.kubernetes.io/app-protocols: '{"http":"HTTP"}' labels: app: frontend spec: type: NodePort selector: app: frontend ports: - port: 80 protocol: TCP name: http targetPort: frontend --- apiVersion: apps/v1 kind: Deployment metadata: name: frontend namespace: default labels: app: frontend spec: replicas: 1 selector: matchLabels: app: frontend template: metadata: labels: app: frontend spec: containers: - name: frontend image: {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_frontend:0.1 imagePullPolicy: Always ports: - name: frontend containerPort: 80 # Director.yaml ## yaml file path ## OpenMatch/mod_matchmaker101/director/director.yaml apiVersion: v1 kind: Pod metadata: name: director namespace: openmatch-poc spec: containers: - name: director image: {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_director:0.1 imagePullPolicy: Always hostname: director # MatchFunction.yaml ## yaml file path ## OpenMatch/mod_matchmaker101/matchfunction/matchfunction.yaml apiVersion: v1 kind: Pod metadata: name: matchfunction namespace: openmatch-poc labels: app: openmatch component: matchfunction spec: containers: - name: matchfunction image: {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/mod_MatchFunction:0.1 imagePullPolicy: Always ports: - name: grpc containerPort: 50502 --- kind: Service apiVersion: v1 metadata: name: matchfunction namespace: openmatch-poc labels: app: openmatch component: matchfunction spec: selector: app: openmatch component: matchfunction clusterIP: None type: ClusterIP ports: - name: grpc protocol: TCP port: 50502 ããããã® yaml ãã¡ã€ã«ãEKSã«é©çšãããããã³ã°ã·ã¹ãã ããããã€ããŸãã # GameFrontend kubectl apply -f frontend.yaml # Director kubectl apply -f director.yaml # MatchFunction kubectl apply -f matchfunction.yaml åäœç¢ºèª 以äžã®å³ã«ç€ºãããã«ã frontend.yaml ã§äœæãããServiceã«æ¥ç¶ãããããã³ã°ã·ã¹ãã ããã¹ãããŸãã ãã® frontend-endpoint ãµãŒãã¹ã«ããŒã«ã«ã§ãã¢ã¯ã»ã¹ã§ããããã«ããããã Kubernetes ã®PortForwaderingæ©èœã䜿çšããŸãã FrontendãµãŒãã¹ãããŒã«ã«ã« ãããã³ã° ãã äžå³ã®â€ãšãªã¢ã§ãã # Frontend ãµãŒãã¹ãããŒã«ã«ã®8081ããŒãã«ãããã³ã°ããŸã kubectl port-forward services/frontend-ednpoint 8081:80 8ã€ã®æ°ããã¿ãŒããã«ãäœæããŠã8åã®ãã¬ã€ã€ãŒãFrontendãµãŒãã¹ã«æ¥ç¶ããã®ãã·ãã¥ã¬ãŒããã äžå³ã®â ãšãªã¢ã§4åïŒap-northeast-1ïŒ+4åïŒap-northeast-3ïŒã®ãã¬ã€ã€ãŒãæ¥ç¶ããã®ãã·ãã¥ã¬ãŒãããŸãã # Get: /Frontend/play/regionname ## 4åïŒap-northeast-1ïŒ curl 127.0.0.1:8081/play/ap-northeast-1 ## 4åïŒap-northeast-3ïŒ curl 127.0.0.1:8081/play/ap-northeast-3 ãšã©ãŒã®æç¡ã確èªããããã«ãmatchfunction/director/frontendã¢ãžã¥ãŒã«ã®ãã°æ
å ±ãåºåãã äžå³ã®â¡ãâ£ãšãªã¢ã§ãã # matchfuntion log kubectl logs --tail 4 matchfunction -n openmatch-poc # director log kubectl logs --tail 4 director -n openmatch-poc # frontend log kubectl logs --tail 4 deployments/frontend äžèšã®æé ã«åŸã£ãŠã8åã®ãã¬ã€ã€ãŒããããã³ã°ãéå§ããã·ãã¥ã¬ãŒã·ã§ã³ãè¡ããŸãã 確èªãã€ã³ãã¯æ¬¡ã®éãã§ãã â¡ãâ£ãšãªã¢ã®ãã°ã«ã¯ãšã©ãŒåºåããããŸããã â ã®8åã®ã¯ã©ã€ã¢ã³ãå
šå¡ãIPãPortæ
å ±ãæ£åžžã«ååŸããŸãã ãŸããåã®4åã®ãã¬ã€ã€ãŒã¯åãã°ã«ãŒãã«ããããããããããã® IP:Port ã¢ãã¬ã¹ã¯åãã§ãã åŸã®4åã®ãã¬ã€ã€ãŒãåãã°ã«ãŒãã«ãããããããã® IP:Port ã¢ãã¬ã¹ãåãã§ãã â¥ãšãªã¢ã§ã¯ãGameServerã®ã¹ããŒã¿ã¹ã確èªãã2å°ã®ãµãŒããŒãAllocatedç¶æ
ã«ããããšã確èªããŸãã çµããã« ãããŸã§ã«ãAgonesãšOpen Matchã䜿çšããŠé«å¯çšæ§ãšæ¡åŒµæ§ãã¹ã±ãžã¥ãŒãªã³ã°ãå¯èœãªãããã³ã°ã·ã¹ãã ãæ§ç¯ããŸããã æ¬¡ã«ã Part3 ã§ã¯UnrealEngineã䜿çšããŠGameClientãéçºãããã®ãããã³ã°ã·ã¹ãã ã«æ¥ç¶ããæ¹æ³ã説æããŸãã ããã«ããããããã³ã°æ©èœãæã¡ãAgonesã䜿çšããŠDedicated Serverãã¹ã±ãžã¥ãŒãªã³ã°ãã ãã«ããã¬ã€ ã²ãŒã ã®éçºãå®äºããŸãã åŒãç¶ãããæ¥œãã¿ã«ããŠãã ããïŒ çŸåšISID㯠web3é åã®ã°ã«ãŒã暪æçµç¹ ãç«ã¡äžããWeb3ããã³ ã¡ã¿ããŒã¹ é åã®R&Dãè¡ã£ãŠãããŸãïŒã«ããŽãªãŒã3DCGãã®èšäºã¯ ãã¡ã ïŒã ããæ¬é åã«ãèå³ã®ããæ¹ããäžç·ã«ãã£ã¬ã³ãžããŠããããæ¹ã¯ããã²ãæ°è»œã«ãé£çµ¡ãã ããïŒ ç§ãã¡ãšåãããŒã ã§åããŠããã仲éããæ¯éãåŸ
ã¡ããŠãããŸãïŒ ISIDæ¡çšããŒãžïŒWeb3/ã¡ã¿ããŒã¹/AIïŒ åè https://agones.dev/site/docs/overview/ https://open-match.dev/site/docs/ å·çïŒ @chen.sun ãã¬ãã¥ãŒïŒ @yamashita.yuki ïŒ Shodo ã§å·çãããŸãã ïŒ