昨天我们已经初步了解了,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
}
>>: day21 开分支,浅谈kotlin paging3 with flow
第二天,首先我打算先把Mongo DB环境建起来 为了方便,就用docker在local部属 Mon...
可以 ASP.NET Core 网站部署的环境相当多,包含 IIS, Nginx, App serv...
0x1 API 需求 在发出建立订单 - 取得虚拟帐号的请求後,若付款完成会呼叫 BackendUR...
这几天我们把 Coroutine 神秘的面纱好像掀开了一点,知道他是用来解决非同步程序的问题,也我们...
💡 开始使用 Git 之前,我们需要先设定使用者名称及电子邮件地址。 为什麽需要设定用户名称及 E-...