マッチメイキング関数入門
注釈:本資料はAI技術を用いて翻訳されています。
概要
このガイドでは、アプリテンプレートよりも一歩進んで、以下の関数をカスタマイズしてフリーフォーオール(FFA)マッチメーカーを作成します:
EnrichTicketValidateTicketMakeMatches
マッチルール
FFA MatchMakerでは、GameRulesと呼ばれるマッチルールのセットを使用します。これは管理ポータル経由でMatchPoolに作成・登録する必要があります。GameRulesは以下のようになります:
{
"alliance": {
"min_number": 1,
"max_number": 1,
"player_min_number": 5,
"player_max_number": 10
}
}
これらのルールは、このFFAに必要な最小プレイヤー数が5人で、最大プレイヤー数が10人であることを示しています。
EnrichTicket
EnrichTicketはValidateTicketの前に呼び出され、マッチチケットに追加のロジックを加えるために使用されます。この例では、後で使用するspawnLocationを表すfloat64をチケットのTicketAttributesに追加します。
EnrichTicketは、このフローの一部でネットワーク呼び出しを行って外部データを追加するのに最適な場所です。
func (g GameMatchMaker) EnrichTicket(matchTicket matchmaker.Ticket, ruleSet interface{}) (ticket matchmaker.Ticket, err error) {
rand.Seed(uint64(time.Now().UnixNano()))
var num float64
enrichMap := map[string]interface{}{}
if len(matchTicket.TicketAttributes) == 0 {
num = float64(rand.Intn(100-0+1) + 0)
enrichMap["spawnLocation"] = math.Round(num)
matchTicket.TicketAttributes = enrichMap
} else {
num = float64(rand.Intn(100-0+1) + 0)
matchTicket.TicketAttributes["spawnLocation"] = math.Round(num)
}
return matchTicket, nil
}
ValidateTicket
ValidateTicketは次に呼び出され、マッチチケットが有効かどうかを示すboolを返します。まず、マッチチケット上のplayersの数がGameRulesで定義されたPlayerMaxNumberを超えていないかをチェックします。
次に、spawnLocationがnilでないfloat64値であるかをチェックします。
func (g GameMatchMaker) ValidateTicket(matchTicket matchmaker.Ticket, matchRules interface{}) (bool, error) {
rules, ok := matchRules.(GameRules)
if !ok {
return false, errors.New("invalid rules type for game rules")
}
if len(matchTicket.Players) > rules.AllianceRule.PlayerMaxNumber {
return false, errors.New(fmt.Sprintf("too many players on the ticket, max is %d", rules.AllianceRule.PlayerMaxNumber))
}
return true, nil
}
MakeMatches
matchTicketが装飾され検証されたので、マッチを作成しましょう!マッチメーカーロジックを作成する方法は多数あります。シンプルなものもあれば、より複雑なアプローチが必要なものもあります。このマッチメーカーの全体的な目標は、作成するマッチが最小プレイヤー数を持ちながら最大数を超えず、最大数まで埋めようとすることです。
シンプルと複雑の中間に位置しながら(成長の余地を残しつつ)実現できます。これには、バケットを使用したキューシステムを作成します。各バケットのインデックスはチケット上のプレイヤー数になるため、必要なプレイヤー数を検索したいときにキューからポップできます。キューは以下のようになります。
Queue
queueはmatchmaker.Ticket型のスライスと、popおよびpush関数を含みます。pushは単にキューのチケットに追加し、popはチケットを返して削除します。各キューには同じ数のプレイヤーが各チケットにいるため、順序は重要ではありません。
type queue struct {
tickets []matchmaker.Ticket
lock sync.Mutex
}
func (q *queue) push(ticket matchmaker.Ticket) {
q.lock.Lock()
defer q.lock.Unlock()
q.tickets = append(q.tickets, ticket)
}
func (q *queue) pop() *matchmaker.Ticket {
if len(q.tickets) == 0 {
return nil
}
q.lock.Lock()
defer q.lock.Unlock()
ticket := q.tickets[0]
q.tickets = q.tickets[1:]
return &ticket
}
buildGame
MakeMatches内に配置するロジックを格納する関数が必要です。
このロジックは上記の目標を達成できる必要があります。まず、unmatchedTicketsをキューに入れます。既にキューがあるかをチェックし、ない場合はkeyがチケット上のプレイヤー数で、値がqueueであるバケットを作成し、そこにpushします。
次に、outer loopを使用して開始するrootTicketを取得します。これにはnextTicketという別の関数を使用します。この関数はキーをソートし、GameRulesのPlayerMaxNumberを超えない最大のプレイヤー数を取得します。
rootTicketが取得できたら、PlayerMaxNumberに到達するために必要なチケット数を知りたいです。差分(remainingPlayerCount)を見つけて、キュー内の残りのチケットを通過するinner loopを開始します(remainingPlayerCountで各バケットを検索)。必要なプレイヤー数を持つチケットが見つかるかを確認します。何も見つからない場合は、デクリメントして、すべてのチケットを通過し、少なくともPlayerMinNumberがあるか、PlayerMaxNumberに到達するまで続けます。
func buildGame(unmatchedTickets []matchmaker.Ticket, results chan matchmaker.Match, gameRules GameRules) {
defer close(results)
max := gameRules.AllianceRule.PlayerMaxNumber
min := gameRules.AllianceRule.PlayerMinNumber
buckets := map[int]*queue{}
for _, ticket := range unmatchedTickets {
bucket, ok := buckets[len(ticket.Players)]
if !ok {
bucket = newQueue()
buckets[len(ticket.Players)] = bucket
}
bucket.push(ticket)
}
//start outer loop
for {
rootTicket := nextTicket(buckets, max)
if rootTicket == nil {
return
}
remainingPlayerCount := max - len(rootTicket.Players)
matchedTickets := []matchmaker.Ticket{*rootTicket}
//start inner loop
for {
if remainingPlayerCount == 0 {
break
}
otherTicket := nextTicket(buckets, remainingPlayerCount)
if otherTicket == nil {
if remainingPlayerCount >= min {
break
}
return
}
matchedTickets = append(matchedTickets, *otherTicket)
remainingPlayerCount -= len(otherTicket.Players)
}
ffaTeam := mapPlayerIDs(matchedTickets)
match := matchmaker.Match{Tickets: matchedTickets,
Teams: []matchmaker.Team{{UserIDs: ffaTeam}}}
results <- match
}
}
nextTicket
func nextTicket(buckets map[int]*queue, maxPlayerCount int) *matchmaker.Ticket {
bucketKeys := maps.Keys(buckets)
sort.Ints(bucketKeys)
for i := len(bucketKeys) - 1; i >= 0; i-- {
if bucketKeys[i] > maxPlayerCount {
continue
}
ticket := buckets[bucketKeys[i]].pop()
if ticket != nil {
return ticket
}
}
return nil
}
mapPlayerIDs
func mapPlayerIDs(tickets []matchmaker.Ticket) []player.ID {
playerIDs := []player.ID{}
for _, ticket := range tickets {
for _, p := range ticket.Players {
playerIDs = append(playerIDs, p.PlayerID)
}
}
return playerIDs
}
MakeMatches 続き
ロジックがすべて整ったので、MakeMatchesの設定を完了しましょう。まず、rulesがGameRulesであることをチェックし、次にMatchPoolのデータベースからすべてのチケットを取得するGetTickets()関数を呼び出します。次にbuildGame関数を呼び出し、すべてのマッチの作成が完了したらresults goチャネルを返します。
func (g GameMatchMaker) MakeMatches(ticketProvider TicketProvider, matchRules interface{}) <-chan matchmaker.Match {
results := make(chan matchmaker.Match)
rules, ok := matchRules.(GameRules)
if !ok {
return results
}
go func() {
var unmatchedTickets []matchmaker.Ticket
tickets := ticketProvider.GetTickets()
for ticket := range tickets {
unmatchedTickets = append(unmatchedTickets, ticket)
}
go buildGame(unmatchedTickets, results, rules)
}()
return results
}