Specialized components
Matchmaking system
Match function (MMF)
20 min
overview the match function (mmf) is a core component of the ir engine's matchmaking system that implements the logic for grouping players into balanced game sessions it processes match tickets according to the criteria defined in match profiles, creating match proposals that represent potential game instances by applying game specific rules and considering factors like team sizes and player attributes, the mmf ensures that matches are fair, balanced, and aligned with the game's design this chapter explores the implementation, workflow, and integration of the match function within the matchmaking architecture core concepts match creation logic the match function implements the core logic for creating game matches ticket grouping combines compatible match tickets into potential game sessions profile interpretation applies the rules and requirements defined in match profiles team formation organizes players into balanced teams when applicable match proposal creates structured representations of potential game sessions scoring assigns quality scores to match proposals for evaluation this logic ensures that matches meet the game's requirements for player counts, team balance, and other criteria query based filtering the match function uses query services to filter and retrieve appropriate tickets pool queries retrieves tickets that match the criteria defined in profile pools filtering applies tag, range, and string filters to select compatible tickets batching processes tickets in groups to create multiple match proposals efficiency optimizes queries to handle large numbers of concurrent players this query based approach allows the match function to efficiently process the available ticket pool implementation match function service the match function is implemented as a service that responds to match requests // simplified from open match custom pods/matchfunction/mmf/matchfunction go package mmf import ( "log" "context" "open match dev/open match/pkg/matchfunction" "open match dev/open match/pkg/pb" ) // matchfunctionservice implements the grpc service for creating matches type matchfunctionservice struct { queryserviceclient pb queryserviceclient matchname string } // run is the entry point for the match function func (s matchfunctionservice) run(req pb runrequest, stream pb matchfunction runserver) error { // get the match profile from the request profile = req getprofile() log printf("mmf starting for profile %s", profile getname()) // query for tickets matching the profile's pools pooltickets, err = matchfunction querypools(stream context(), s queryserviceclient, profile getpools()) if err != nil { log printf("failed to query pools %v", err) return err } // check if we have any tickets to process if len(pooltickets) == 0 { log printf("no tickets found for profile %s", profile getname()) return nil } // create match proposals from the available tickets proposals, err = makematches(profile, pooltickets) if err != nil { log printf("failed to make matches %v", err) return err } // send the match proposals back to open match for , proposal = range proposals { if err = stream send(\&pb runresponse{proposal proposal}); err != nil { log printf("failed to send proposal %v", err) return err } } log printf("sent %d proposals for profile %s", len(proposals), profile getname()) return nil } this service receives a match profile from the request queries for tickets that match the profile's pools creates match proposals using the makematches function streams the proposals back to open match handles errors and logs the process for debugging match creation logic the makematches function implements the core logic for creating matches // simplified from open match custom pods/matchfunction/mmf/matchfunction go func makematches(p pb matchprofile, pooltickets map\[string]\[] pb ticket) (\[] pb match, error) { // default players needed per pool for a match var playersneededperpool = 1 // extract team size from profile extensions if p extensions != nil { if profiledatamsg, ok = p extensions\["profiledata"]; ok { // unmarshal the profile data profiledata = \&common profiledatamessage{} if err = profiledatamsg unmarshalto(profiledata); err == nil { // get the team size from the profile data playersneededperpool = int(profiledata getteamsize()) log printf("using team size from profile %d", playersneededperpool) } } } // list to hold match proposals var matchestopropose \[] pb match // keep creating matches until we run out of tickets for { canmakethismatch = true currentmatchtickets = \[] pb ticket{} // process each pool in the profile for poolname, ticketsinthispool = range pooltickets { // check if we have enough tickets in this pool if len(ticketsinthispool) < playersneededperpool { canmakethismatch = false break } // take the required number of tickets from this pool ticketstotake = ticketsinthispool\[0\ playersneededperpool] currentmatchtickets = append(currentmatchtickets, ticketstotake ) // remove the taken tickets from the pool pooltickets\[poolname] = ticketsinthispool\[playersneededperpool ] } // if we couldn't get enough tickets for all pools, stop making matches if !canmakethismatch { log printf("not enough tickets to make another full match") break } // create a match proposal with the gathered tickets proposal = \&pb match{ matchid fmt sprintf("profile %s time %d", p getname(), time now() unixnano()), matchprofile p getname(), matchfunction matchname, tickets currentmatchtickets, } // add the proposal to our list matchestopropose = append(matchestopropose, proposal) log printf("created match proposal with id %s", proposal matchid) } return matchestopropose, nil } this function extracts the team size from the profile's extensions iteratively creates matches until there aren't enough tickets for each match, takes the required number of tickets from each pool creates a match proposal with a unique id and the selected tickets returns all created match proposals team formation for team based games, the match function can organize players into teams // example of team formation logic func createteams(tickets \[] pb ticket, teamsize int) map\[string]\[] pb ticket { teams = make(map\[string]\[] pb ticket) // create two teams (red and blue) redteam = tickets\[0\ teamsize] blueteam = tickets\[teamsize 2 teamsize] teams\["red"] = redteam teams\["blue"] = blueteam return teams } // store team information in the match proposal func addteamstoproposal(proposal pb match, teams map\[string]\[] pb ticket) error { // create a teams message teamsmsg = \&common teamsmessage{ teams make(map\[string] common teammessage), } // add each team to the message for teamname, teamtickets = range teams { ticketids = make(\[]string, len(teamtickets)) for i, ticket = range teamtickets { ticketids\[i] = ticket id } teamsmsg teams\[teamname] = \&common teammessage{ ticketids ticketids, } } // marshal the teams message teamsany, err = anypb new(teamsmsg) if err != nil { return err } // add the teams message to the proposal extensions if proposal extensions == nil { proposal extensions = make(map\[string] anypb any) } proposal extensions\["teams"] = teamsany return nil } this logic divides the selected tickets into teams creates a structured representation of the teams stores the team information in the match proposal's extensions enables the game server to assign players to the correct teams match scoring the match function can assign quality scores to match proposals // example of match scoring logic func scorematch(proposal pb match) float64 { // start with a base score score = 100 0 // calculate average wait time var totalwaittime float64 for , ticket = range proposal tickets { // get the ticket's creation time var enterqueuetime float64 if ticket searchfields != nil && ticket searchfields doubleargs != nil { if time, ok = ticket searchfields doubleargs\["time enterqueue"]; ok { enterqueuetime = time } } // calculate wait time if enterqueuetime > 0 { waittime = float64(time now() unixnano()) enterqueuetime totalwaittime += waittime } } // adjust score based on average wait time if len(proposal tickets) > 0 { averagewaittime = totalwaittime / float64(len(proposal tickets)) // increase score for matches with longer wait times waittimebonus = math min(50 0, averagewaittime / 1000000000 0 10 0) // 10 points per second of wait, up to 50 score += waittimebonus } return score } this logic starts with a base score for the match calculates the average wait time for players in the match adjusts the score based on factors like wait time helps prioritize matches with players who have been waiting longer match function workflow the complete match function workflow follows this sequence sequencediagram participant director as director participant backend as open match backend participant mmf as match function participant query as query service participant tickets as ticket database director >>backend request matches (profile, mmf) backend >>mmf run(profile) mmf >>query querypools(profile pools) query >>tickets get tickets matching pool criteria tickets >>query return matching tickets query >>mmf return pooltickets map mmf >>mmf extract team size from profile loop until not enough tickets mmf >>mmf check if enough tickets for a match mmf >>mmf take required tickets from each pool mmf >>mmf create match proposal mmf >>mmf add proposal to list end mmf >>backend stream match proposals backend >>director return match proposals this diagram illustrates the director requests matches from the open match backend the backend calls the match function with the specified profile the match function queries for tickets matching the profile's pools the match function extracts parameters like team size from the profile the match function iteratively creates match proposals the match function streams the proposals back to the backend the backend returns the proposals to the director integration with other components the match function integrates with several other components of the matchmaking system match profile the match function interprets and applies match profiles // example of profile integration func extractprofileparameters(profile pb matchprofile) (string, int, error) { // default values mode = "default" teamsize = 1 // extract custom data from profile extensions if profile extensions != nil { if profiledatamsg, ok = profile extensions\["profiledata"]; ok { // unmarshal the profile data profiledata = \&common profiledatamessage{} if err = profiledatamsg unmarshalto(profiledata); err != nil { return mode, teamsize, err } // get parameters from the profile data mode = profiledata getmode() teamsize = int(profiledata getteamsize()) } } return mode, teamsize, nil } this integration extracts game specific parameters from the profile applies these parameters to the match creation logic ensures matches conform to the requirements defined in the profile provides flexibility for different game modes and configurations query service the match function uses the query service to retrieve tickets // example of query service integration func querytickets(ctx context context, queryclient pb queryserviceclient, pools \[] pb pool) (map\[string]\[] pb ticket, error) { // use the open match helper function to query pools pooltickets, err = matchfunction querypools(ctx, queryclient, pools) if err != nil { return nil, err } // additional processing if needed for poolname, tickets = range pooltickets { // sort tickets by wait time sort slice(tickets, func(i, j int) bool { timei = getenterqueuetime(tickets\[i]) timej = getenterqueuetime(tickets\[j]) return timei < timej // earlier time (longer wait) comes first }) } return pooltickets, nil } // helper function to get the enter queue time from a ticket func getenterqueuetime(ticket pb ticket) float64 { if ticket searchfields != nil && ticket searchfields doubleargs != nil { if time, ok = ticket searchfields doubleargs\["time enterqueue"]; ok { return time } } return 0 } this integration uses the open match query service to retrieve tickets applies additional processing like sorting by wait time organizes tickets by pool for efficient match creation handles large numbers of tickets efficiently director the match function responds to requests from the director // example of director integration in server go func start(queryserviceaddr string, port int) { // create a connection to the query service conn, err = grpc dial(queryserviceaddr, grpc withinsecure()) if err != nil { log fatalf("failed to connect to query service %v", err) } defer conn close() // create the match function service mmfservice = \&matchfunctionservice{ queryserviceclient pb newqueryserviceclient(conn), matchname "mode based mmf", } // create a grpc server server = grpc newserver() pb registermatchfunctionserver(server, mmfservice) // start the server lis, err = net listen("tcp", fmt sprintf(" %d", port)) if err != nil { log fatalf("failed to listen %v", err) } log printf("starting match function server on port %d", port) if err = server serve(lis); err != nil { log fatalf("failed to serve %v", err) } } this integration creates a grpc server that the director can call registers the match function service with open match listens for match requests from the director provides a standardized interface for match creation benefits of the match function the match function (mmf) provides several key advantages customization enables game specific matching logic tailored to the game's needs flexibility supports various game modes with different team structures scalability processes large numbers of tickets efficiently fairness creates balanced matches based on defined criteria prioritization can incorporate wait time and other factors into match quality separation of concerns isolates matching logic from other system components extensibility allows for additional matching criteria as the game evolves these benefits make the match function an essential component for creating fair and enjoyable game experiences next steps with an understanding of how the match function creates potential game sessions, the next chapter explores how the director orchestrates the overall matchmaking process next director docid\ j8dqwin wv5sbdnkti8n