iT邦幫忙

2021 iThome 鐵人賽

DAY 28
0
Software Development

徵坦補! 新手可! Open-Match 配對框架系列 第 28

Day28 Apex 模擬配對實作

昨天我們已經初步了解了,Apex 這款遊戲的玩法與配對機制,今天我們將基於 Open-Match 配對框架,來實作看看 Apex 的配對過程。我們將透過兩種模式、多個角色、多個區間與不同級分,來簡單模擬一下,配對可能會需要注意的地方。

部署範例

配對目標 (MatchProfile

重點在於我們在劃分 MatchProfile 與其 Pools 的過程,同時也是設定了我們想要的配對目標 ,藉由細分 MatchProfile 的內容,可以讓我們獲得更多不同類別的匹配池 Pools

  • 一般場

    • 完全不分階級等級
    req := &pb.FetchMatchesRequest{
    			Config: &pb.FunctionConfig{
    				Host: "om-function.open-match-demo.svc.cluster.local",
    				Port: 50502,
    				Type: pb.FunctionConfig_GRPC,
    			},
    			Profile: &pb.MatchProfile{
    				Name: "3v3_normal_battle_royale",
    				Pools: []*pb.Pool{
    					{
    						Name: "3v3_normal_battle_royale",
    						StringEqualsFilters: []*pb.StringEqualsFilter{
    							{
    								StringArg: "mode",
    								Value:     "3v3_normal_battle_royale",
    							},
    						},
    					},
    				},
    			},
    		}
    
  • 排位場

    • 高級場
    • 中級場
    • 新手場
    req := &pb.FetchMatchesRequest{
    			Config: &pb.FunctionConfig{
    				Host: "om-function.open-match-demo.svc.cluster.local",
    				Port: 50502,
    				Type: pb.FunctionConfig_GRPC,
    			},
    			Profile: &pb.MatchProfile{
    				Name: "3v3_rank_battle_royale",
    				Pools: []*pb.Pool{
    					{
    						Name: "3v3_rank_low",
    						StringEqualsFilters: []*pb.StringEqualsFilter{
    							{
    								StringArg: "mode",
    								Value:     "3v3_rank_battle_royale",
    							},
    						},
    						DoubleRangeFilters: []*pb.DoubleRangeFilter{
    							{
    								DoubleArg: "score",
    								Min:       0,
    								Max:       3500,
    							},
    						},
    					},
    					{
    						Name: "3v3_rank_mid",
    						StringEqualsFilters: []*pb.StringEqualsFilter{
    							{
    								StringArg: "mode",
    								Value:     "3v3_rank_battle_royale",
    							},
    						},
    						DoubleRangeFilters: []*pb.DoubleRangeFilter{
    							{
    								DoubleArg: "score",
    								Min:       3400,
    								Max:       7300,
    							},
    						},
    					},
    					{
    						Name: "3v3_rank_high",
    						StringEqualsFilters: []*pb.StringEqualsFilter{
    							{
    								StringArg: "mode",
    								Value:     "3v3_rank_battle_royale",
    							},
    						},
    						DoubleRangeFilters: []*pb.DoubleRangeFilter{
    							{
    								DoubleArg: "score",
    								Min:       7200,
    								Max:       15000,
    							},
    						},
    					},
    				},
    			},
    		}
    

配對邏輯 (MMF

我們將在 MatchFunction 實作大多數的配對細節,包含 3人一組、不同 MatchProfile 將使用不同的 func、同階級 pool 才能組隊、同隊伍不能有相同角色等等,這些邏輯全部都彙整於我們的 MMF 中。我們可以依照我們邏輯的重要性,切分成下列流程:

依 MatchProfile 進行主模式切分

func makeMatches(p *pb.MatchProfile, poolTickets map[string][]*pb.Ticket) ([]*pb.Match, error) {
	var matches []*pb.Match
	
	//一般場
	nm, err := normalMatch(p, poolTickets)
	if err != nil {
		log.Println(err)
		return matches, err
	}

	matches = append(matches, nm...)

	//牌位場
	rm, err := rankMatch(p, poolTickets)
	if err != nil {
		log.Println(err)
		return matches, err
	}

	matches = append(matches, rm...)

	return matches, nil
}

func normalMatch(p *pb.MatchProfile, poolTickets map[string][]*pb.Ticket) ([]*pb.Match, error) {
	var matches []*pb.Match
	if p.Name != "3v3_normal_battle_royale" {
		return nil, nil
	}

	//下略
}

func rankMatch(p *pb.MatchProfile, poolTickets map[string][]*pb.Ticket) ([]*pb.Match, error) {
	var matches []*pb.Match

	if p.Name != "3v3_rank_battle_royale" {
		return matches, nil
	}
	//下略
}

再依 Pool 撈取近似性質玩家

func rankTeam(p *pb.MatchProfile, poolName string, poolTickets map[string][]*pb.Ticket) ([]*pb.Match, error) {
	matches := []*pb.Match{}
	team := &pb.Match{}
	roleInTeam := []string{}
	count := 0

	if tickets, ok := poolTickets[poolName]; ok {
		for j := range tickets {
			if len(team.Tickets) < 3 {
				//check deduplicated role
				if stringInArr(tickets[j].SearchFields.StringArgs["role"], roleInTeam) {
					continue
				}

				team.Tickets = append(team.Tickets, tickets[j])
				roleInTeam = append(roleInTeam, tickets[j].SearchFields.StringArgs["role"])

				if len(team.Tickets) == 3 {
					// Compute the match quality/score
					matchQuality := computeQuality(team.Tickets)
					evaluationInput, err := ptypes.MarshalAny(&pb.DefaultEvaluationCriteria{
						Score: matchQuality,
					})
					if err != nil {
						return nil, err
					}

					team.MatchId = fmt.Sprintf("profile-%v-time-%v-%d", poolName, time.Now().Format("2006-01-02T15:04:05.00"), rankMatchIDCreator.Generate().Int64()+int64(count))
					team.MatchFunction = rankMatchName
					team.MatchProfile = p.GetName()
					team.Extensions = map[string]*any.Any{
						"evaluation_input": evaluationInput,
					}

					matches = append(matches, team)
					team = &pb.Match{}
					roleInTeam = []string{}
					count++
				}
			}
		}
	}

	return matches, nil
}

提供 Match Quality Value

再有 overlapping 的情況下,計算出配對品質,提供 evaluator 選擇出最適合的配對

func computeQuality(tickets []*pb.Ticket) float64 {
	quality := 0.0
	high := 0.0
	low := tickets[0].SearchFields.DoubleArgs["score"]
	for _, ticket := range tickets {
		if high < ticket.SearchFields.DoubleArgs["score"] {
			high = ticket.SearchFields.DoubleArgs["score"]
		}
		if low > ticket.SearchFields.DoubleArgs["score"] {
			low = ticket.SearchFields.DoubleArgs["score"]
		}
	}
	quality = high - low

	return quality
}

Result

確認人數、角色、級距等結果,是否符合我們的預期

Normal

{
  "director": {
    "Status": "Sleeping",
    "LatestMatches": [
      {
        "match_id": "profile-3v3_normal_battle_royale-time-2021-10-06T06:24:20.67-1445636026520702976",
        "match_profile": "3v3_normal_battle_royale",
        "match_function": "3v3_normal_battle_royale_matchfunction",
        "tickets": [
          {
            "id": "c5ek1emjgom43kqfsacg",
            "search_fields": {
              "double_args": {
                "avg_dmg": 66,
                "avg_kd": 0.47,
                "level": 44,
                "rank": 2,
                "score": 1515,
                "team_member_count": 0,
                "win_streak": 0
              },
              "string_args": {
                "black_list": "[]",
                "mode": "3v3_normal_battle_royale",
                "role": "caustic",
                "server": "Taiwan_GCE2",
                "team_member": "[]",
                "user_id": "1445635647426859008"
              }
            },
            "create_time": {
              "seconds": 1633501370,
              "nanos": 449469400
            }
          },
          {
            "id": "c5ek246jgom43kqfsam0",
            "search_fields": {
              "double_args": {
                "avg_dmg": 35,
                "avg_kd": 0.27,
                "level": 51,
                "rank": 1,
                "score": 894,
                "team_member_count": 0,
                "win_streak": 0
              },
              "string_args": {
                "black_list": "[]",
                "mode": "3v3_normal_battle_royale",
                "role": "bang",
                "server": "Taiwan_GCE2",
                "team_member": "[]",
                "user_id": "1445636007352668160"
              }
            },
            "create_time": {
              "seconds": 1633501456,
              "nanos": 182299100
            }
          },
          {
            "id": "c5ek246jgom43kqfsamg",
            "search_fields": {
              "double_args": {
                "avg_dmg": 101,
                "avg_kd": 0,
                "level": 41,
                "rank": 1,
                "score": 892,
                "team_member_count": 0,
                "win_streak": 0
              },
              "string_args": {
                "black_list": "[]",
                "mode": "3v3_normal_battle_royale",
                "role": "valk",
                "server": "Taiwan_GCE2",
                "team_member": "[]",
                "user_id": "1445636007067455488"
              }
            },
            "create_time": {
              "seconds": 1633501456,
              "nanos": 182849200
            }
          }
        ],
        "extensions": {
          "evaluation_input": {
            "type_url": "type.googleapis.com/openmatch.DefaultEvaluationCriteria",
            "value": "CQAAAAAAeINA"
          }
        }
      }
    ]
  },
  "uptime": 1169
}

Rank

{
  "director": {
    "Status": "Sleeping",
    "LatestMatches": [
      {
        "match_id": "profile-3v3_rank_low-time-2021-10-06T06:22:38.73-1445635598345179136",
        "match_profile": "3v3_rank_battle_royale",
        "match_function": "3v3_rank_battle_royale_matchfunction",
        "tickets": [
          {
            "id": "c5ek196jgom43kqfsaa0",
            "search_fields": {
              "double_args": {
                "avg_dmg": 66,
                "avg_kd": 0.06,
                "level": 132,
                "rank": 0,
                "score": 814,
                "team_member_count": 0,
                "win_streak": 0
              },
              "string_args": {
                "black_list": "[]",
                "mode": "3v3_rank_battle_royale",
                "role": "crypto",
                "server": "Taiwan_GCE2",
                "team_member": "[]",
                "user_id": "1445635553034047488"
              }
            },
            "create_time": {
              "seconds": 1633501348,
              "nanos": 82612800
            }
          },
          {
            "id": "c5ek14ujgom43kqfsa7g",
            "search_fields": {
              "double_args": {
                "avg_dmg": 172,
                "avg_kd": 0.39,
                "level": 443,
                "rank": 2,
                "score": 1597,
                "team_member_count": 0,
                "win_streak": 0
              },
              "string_args": {
                "black_list": "[]",
                "mode": "3v3_rank_battle_royale",
                "role": "caustic",
                "server": "Taiwan_GCE2",
                "team_member": "[]",
                "user_id": "1445635483068862464"
              }
            },
            "create_time": {
              "seconds": 1633501331,
              "nanos": 316996500
            }
          },
          {
            "id": "c5ek1aejgom43kqfsac0",
            "search_fields": {
              "double_args": {
                "avg_dmg": 57,
                "avg_kd": 0.24,
                "level": 352,
                "rank": 1,
                "score": 345,
                "team_member_count": 0,
                "win_streak": 0
              },
              "string_args": {
                "black_list": "[]",
                "mode": "3v3_rank_battle_royale",
                "role": "gibby",
                "server": "Taiwan_GCE2",
                "team_member": "[]",
                "user_id": "1445635577381982208"
              }
            },
            "create_time": {
              "seconds": 1633501353,
              "nanos": 782109500
            }
          }
        ],
        "extensions": {
          "evaluation_input": {
            "type_url": "type.googleapis.com/openmatch.DefaultEvaluationCriteria",
            "value": "CQAAAAAAkJNA"
          }
        }
      }
    ]
  },
  "uptime": 1068
}

完整範例


上一篇
Day27 Apex 配對機制分析
下一篇
Day29 總結篇
系列文
徵坦補! 新手可! Open-Match 配對框架30

尚未有邦友留言

立即登入留言