昨天我們已經初步了解了,Apex 這款遊戲的玩法與配對機制,今天我們將基於 Open-Match 配對框架,來實作看看 Apex 的配對過程。我們將透過兩種模式、多個角色、多個區間與不同級分,來簡單模擬一下,配對可能會需要注意的地方。
kubectl apply -n open-match-demo -f ./apex-open-match-demo.yml
重點在於我們在劃分 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,
},
},
},
},
},
}
我們將在 MatchFunction 實作大多數的配對細節,包含 3人一組、不同 MatchProfile 將使用不同的 func、同階級 pool 才能組隊、同隊伍不能有相同角色等等,這些邏輯全部都彙整於我們的 MMF 中。我們可以依照我們邏輯的重要性,切分成下列流程:
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
}
//下略
}
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
}
再有 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
}
確認人數、角色、級距等結果,是否符合我們的預期
{
"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
}
{
"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
}