Specialized components
Matchmaking system
Director
19 min
overview the director is the orchestration component of the ir engine's matchmaking system that coordinates the entire matchmaking process it manages the flow of match requests, initiates the creation of match proposals, and finalizes matches by assigning players to game servers by continuously monitoring the system and making decisions about when and how to create matches, the director ensures that players are efficiently grouped into appropriate game sessions this chapter explores the implementation, workflow, and responsibilities of the director within the matchmaking architecture core concepts orchestration the director orchestrates the matchmaking process coordination manages the interaction between different matchmaking components scheduling determines when to initiate match creation for different game modes decision making evaluates match proposals and decides which to fulfill assignment finalizes matches by assigning players to game servers monitoring tracks the state of the matchmaking system and adjusts as needed this orchestration ensures that the matchmaking process runs smoothly and efficiently continuous operation the director operates in a continuous cycle periodic execution runs at regular intervals to check for potential matches profile iteration processes each match profile in each cycle concurrent handling manages multiple game modes simultaneously stateless operation maintains minimal state between cycles fault tolerance handles errors and continues operation this continuous operation ensures that players are matched into games as quickly as possible implementation main loop the director's main function implements a continuous processing loop // simplified from open match custom pods/director/main go func main() { // initialize connections to open match backend conn, err = grpc dial(fmt sprintf("%s %d", envconfig backendhostname, envconfig backendport), grpc withinsecure()) if err != nil { log fatalf("failed to connect to open match backend %v", err) } defer conn close() // create a client for the backend service be = pb newbackendserviceclient(conn) // generate match profiles for all supported game modes profiles = generateprofiles(envconfig gametypes, envconfig gametypessizes) log printf("generated %d match profiles", len(profiles)) // main processing loop runs every 5 seconds for range time tick(time second 5) { var wg sync waitgroup // process each profile concurrently for , p = range profiles { wg add(1) go func(wg sync waitgroup, profile pb matchprofile) { defer wg done() // step 1 fetch match proposals for this profile matches, err = fetch(be, profile, envconfig functionhostname, envconfig functionport) if err != nil { log printf("failed to fetch matches for profile %s %v", profile getname(), err) return } // step 2 assign matches to game servers if any were found if len(matches) > 0 { log printf("got %d match proposals for profile %s", len(matches), profile getname()) if err = assign(be, profile, matches); err != nil { log printf("failed to assign matches for profile %s %v", profile getname(), err) } } }(\&wg, p) } // wait for all profile processing to complete before next cycle wg wait() } } this implementation connects to the open match backend service generates match profiles for all supported game modes runs a loop that executes every 5 seconds processes each profile concurrently in each cycle fetches match proposals for each profile assigns matches to game servers when proposals are found waits for all profile processing to complete before the next cycle fetching match proposals the fetch function requests match proposals from the match function // simplified from open match custom pods/director/main go func fetch(be pb backendserviceclient, p pb matchprofile, functionhostname string, functionport int32) (\[] pb match, error) { // prepare the request to fetch matches req = \&pb fetchmatchesrequest{ // specify the match function configuration config \&pb functionconfig{ host functionhostname, port functionport, type pb functionconfig grpc, }, // provide the match profile to use profile p, } // request match proposals from open match backend stream, err = be fetchmatches(context background(), req) if err != nil { return nil, fmt errorf("failed to fetch matches %v", err) } // collect all match proposals from the stream var proposals \[] pb match for { resp, err = stream recv() if err == io eof { break } if err != nil { return nil, fmt errorf("failed to receive match %v", err) } proposals = append(proposals, resp getmatch()) } return proposals, nil } this function prepares a request with the match function configuration and profile sends the request to the open match backend receives a stream of match proposals in response collects all proposals from the stream returns the collected proposals for further processing assigning matches the assign function finalizes matches by assigning players to game servers // simplified from open match custom pods/director/main go func assign(be pb backendserviceclient, p pb matchprofile, matches \[] pb match) error { // process each match proposal for , match = range matches { // extract ticket ids from the match ticketids = \[]string{} for , ticket = range match gettickets() { ticketids = append(ticketids, ticket id) } // in a production system, this would come from a game server allocation service // for this example, we generate a unique server address serverconnectioninfo = "gameserver " + uuid new() string() + " 7777" log printf("assigning tickets %v to server %s", ticketids, serverconnectioninfo) // prepare the assignment request req = \&pb assignticketsrequest{ assignments \[] pb assignmentgroup{{ ticketids ticketids, assignment \&pb assignment{ // connection information for the game server connection serverconnectioninfo, // additional assignment details could be added here }, }}, } // send the assignment request to open match backend if , err = be assigntickets(context background(), req); err != nil { return fmt errorf("failed to assign tickets %v", err) } } return nil } this function processes each match proposal individually extracts the ticket ids from the match determines the game server connection information prepares an assignment request with the tickets and server information sends the assignment request to the open match backend handles any errors that occur during assignment director workflow the complete director workflow follows this sequence sequencediagram participant director participant backend as open match backend participant mmf as match function participant gameserver as game server allocation loop every 5 seconds par for each match profile director >>backend fetchmatches(profile, mmfconfig) backend >>mmf run(profile) mmf >>backend stream match proposals backend >>director return match proposals alt match proposals found director >>gameserver allocate server for match gameserver >>director return server connection info director >>backend assigntickets(ticketids, serverinfo) backend >>backend record assignments end end end this diagram illustrates the director runs a cycle every 5 seconds for each match profile, it requests match proposals the backend forwards the request to the match function the match function returns proposals to the backend the backend forwards the proposals to the director if proposals are found, the director allocates a game server the director assigns the tickets to the allocated server the backend records the assignments for players to retrieve game server allocation in a production environment, the director would interact with a game server allocation service // example of game server allocation integration func allocategameserver(match pb match) (string, error) { // extract relevant information from the match profiledata = \&common profiledatamessage{} if ext, ok = match matchprofile extensions\["profiledata"]; ok { ext unmarshalto(profiledata) } // determine game mode and player count gamemode = profiledata mode playercount = len(match tickets) // request a game server from the allocation service serverrequest = \&allocation serverrequest{ gamemode gamemode, players playercount, region "us west", // could be determined dynamically } // call the allocation service serverresponse, err = allocationclient allocateserver(context background(), serverrequest) if err != nil { return "", fmt errorf("failed to allocate server %v", err) } // return the connection information return serverresponse connectionstring, nil } this integration extracts game specific information from the match determines the requirements for the game server requests a server from an allocation service returns the connection information for the assigned server integration with other components the director integrates with several other components of the matchmaking system match profile the director uses match profiles to guide the matchmaking process // example of profile integration func generateprofiles(modes \[]string, teamsizes map\[string]uint32) \[] pb matchprofile { var profiles \[] pb matchprofile // create a profile for each game mode for , mode = range modes { // create profile data with mode and team size profiledata = \&common profiledatamessage{ mode mode, teamsize teamsizes\[mode], } // marshal the profile data marshalledprofiledata, err = anypb new(profiledata) if err != nil { log printf("failed to marshal profile data for mode %s %v", mode, err) continue } // create the match profile profile = \&pb matchprofile{ name "mode based profile " + mode, pools \[] pb pool{{ name "pool mode " + mode, tagpresentfilters \[] pb tagpresentfilter{ {tag mode}, }, }}, extensions map\[string] anypb any{ "profiledata" marshalledprofiledata, }, } profiles = append(profiles, profile) } return profiles } this integration creates profiles for each supported game mode configures the profiles with appropriate parameters uses the profiles to guide the match function ensures matches are created according to game requirements match function the director interacts with the match function through the open match backend // example of match function integration func configurefetchrequest(profile pb matchprofile, mmfhost string, mmfport int32) pb fetchmatchesrequest { return \&pb fetchmatchesrequest{ config \&pb functionconfig{ host mmfhost, port mmfport, type pb functionconfig grpc, }, profile profile, } } this integration specifies the match function to use for each profile provides the profile to the match function receives match proposals from the match function processes the proposals to create actual matches match assignment the director creates match assignments for players // example of assignment integration func createassignment(ticketids \[]string, serverinfo string) pb assignticketsrequest { return \&pb assignticketsrequest{ assignments \[] pb assignmentgroup{{ ticketids ticketids, assignment \&pb assignment{ connection serverinfo, // additional assignment details extensions map\[string] anypb any{ // game specific information }, }, }}, } } this integration creates assignments with server connection information associates assignments with specific tickets provides additional game specific information enables players to retrieve their assignments benefits of the director the director provides several key advantages centralized control provides a single point of coordination for the matchmaking process continuous operation ensures matches are created as soon as possible concurrent processing handles multiple game modes efficiently flexible scheduling adapts to different game modes and player populations server allocation manages the assignment of players to game servers error handling provides resilience against component failures monitoring enables visibility into the matchmaking process these benefits make the director an essential component for creating a robust and efficient matchmaking system next steps with an understanding of how the director orchestrates the matchmaking process, the next chapter explores how players receive their match assignments and connect to game servers next match assignment docid\ xa7f6vuvhunpinpmxynrz