Skip to main content

Director

Open Match Director Edgegap's tutorial (basic)#

The Director component of Open Match is the orchestrator of everything. This component requests game proposals and assigns game server to them.

Prerequisites#

Create the main loop#

The first thing that we need for our Director is a loop that will match players every 5 seconds.

Now, in your main.go file, you can add the following code. This task will create the main loop.

package main
import (
"fmt"
"time"
)
const (
openMatchMatchFunctionHost = "basics-match-function"
openMatchMatchFunctionPort = 50502
openMatchBackendService = "open-match-backend:50505"
// Game server data
gameServerPort = "<APP_PORT>" // String | E.G. 25565
appName = "<APP_NAME>" // E.G. MySuperGame
appVersion = "<APP_VERSION>" // E.G. V1
arbitriumAPI = "<ARBITRIUM_API>" // E.G. https://staging-api.edgegap.com/
apiToken = "<API_TOKEN>" // E.G. 1111aa11aa11111a1aa11111d111a111111111a1
)
func main() {
fmt.Println("Stating Edgegap tutorial's Director!")
for range time.Tick(time.Second * 5) {
fmt.Println("Creating matches...")
}
}

You can try it by starting your app with go run ./main.go. Every 5 seconds, you should see

Creating matches...

Notes#

Don't forget to change the variables gameServerPort, appName, appVersion, arbitriumAPI, apiToken with valid data. The values should come from Arbitrium

Get Profiles#

In order to obtain match proposals, we will need profiles. We will create a method that returns a list of profiles. We will need Open Match dependency for that.

go get open-match.dev/open-match/pkg/pb

Here is the code for profiles generation

// Get all the possible profiles
func getProfiles() []*pb.MatchProfile {
var profiles []*pb.MatchProfile
modes := []string{"mode.casual", "mode.ranked", "mode.private"}
for _, mode := range modes {
profiles = append(profiles, &pb.MatchProfile{
Name: fmt.Sprintf("%s_profile", mode),
Pools: []*pb.Pool{
{
Name: "pool_" + mode,
TagPresentFilters: []*pb.TagPresentFilter{
{
Tag: mode,
},
},
},
},
},
)
}
return profiles
}

You will have to change your dependencies. They should look like this

import (
"fmt"
"time"
"open-match.dev/open-match/pkg/pb"
)

Create a connection to Open Match Backend service#

We will retrieve the match proposals by communicating to Open Match Backend service via gRPC. We need to create the connection.

For that, we need gRPC dependency

go get google.golang.org/grpc

Here is the code that creates the connection.

// Create a connection to Open Match Backend service
func newBackendConnection() pb.BackendServiceClient {
conn, err := grpc.Dial(
openMatchBackendService, grpc.WithInsecure(),
)
if err != nil {
log.Printf("Error while communicating with Open Match Backend, err: %v", err.Error())
}
return pb.NewBackendServiceClient(conn)
}

You will have to change your dependencies. They should look like this

import (
"fmt"
"time"
"log"
"google.golang.org/grpc"
"open-match.dev/open-match/pkg/pb"
)

Fetching match proposals#

The next step would be to create a function that will fetch the match proposals for a specific profile. We will make a call to Open Match Back End service.

Here is the piece of code for that.

// fetch Fetch profile's matches
func fetch(p *pb.MatchProfile, backendService pb.BackendServiceClient) ([]*pb.Match, error) {
// Making request object
req := &pb.FetchMatchesRequest{
Config: &pb.FunctionConfig{
Host: openMatchMatchFunctionHost,
Port: openMatchMatchFunctionPort,
Type: pb.FunctionConfig_GRPC,
},
Profile: p,
}
// Getting match proposals
stream, err := backendService.FetchMatches(context.Background(), req)
if err != nil {
return nil, err
}
var result []*pb.Match
for {
resp, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
result = append(result, resp.GetMatch())
}
return result, nil
}

You will have to change your dependencies. They should look like this

import (
"fmt"
"time"
"log"
"io"
"context"
"google.golang.org/grpc"
"open-match.dev/open-match/pkg/pb"
)

Assign game server to match proposals#

We need a function that will assign a game server to our match proposals. It is here that we will call Arbitrium API (CoDeMa). You could make an HTTP request by following our API call definitions; however, we will use an auto-generated Go SDK. You can learn how to generate an SDK here. In your director project, you will have to create a swagger folder and put the generated SDK inside.

// assign Deploy a game server with CoDeMa API and assign its IP to match's tickets
func assign(matches []*pb.Match, backendService pb.BackendServiceClient) error {
for _, match := range matches {
// Getting Tickets ID and players IP
ticketIDs := []string{}
IPS := []string{}
for _, t := range match.GetTickets() {
ticketIDs = append(ticketIDs, t.Id)
IPS = append(IPS, string(t.Extensions["playerIp"].Value))
}
// Game server port
gamePort := gameServerPort
// Deploying game server and getting ip
conn, err := getServerIP(IPS, gamePort)
if err != nil {
return fmt.Errorf("AssignTickets failed for match %v, got %w", match.GetMatchId(), err)
}
// Assign game server to tickets
req := &pb.AssignTicketsRequest{
Assignments: []*pb.AssignmentGroup{
{
TicketIds: ticketIDs,
Assignment: &pb.Assignment{
Connection: conn,
},
},
},
}
if _, err := backendService.AssignTickets(context.Background(), req); err != nil {
return fmt.Errorf("AssignTickets failed for match %v, got %w", match.GetMatchId(), err)
}
log.Printf("Assigned server %v to match %v", conn, match.GetMatchId())
}
return nil
}
// getServerIP Deploy the Game server and fetch its IP when the server is ready
func getServerIP(ips []string, gamePort string) (string, error) {
// Creating Arbitrium deploy request
payload := swagger.DeployModel{
AppName: appName,
VersionName: appVersion,
IpList: ips,
}
// Creating API Client to communicate with arbitrium
configuration := swagger.NewConfiguration()
configuration.BasePath = arbitriumAPI
client := swagger.NewAPIClient(configuration)
auth := context.WithValue(context.Background(), swagger.ContextAPIKey, swagger.APIKey{
Key: apiToken,
Prefix: "token",
})
// Deploying
request, _, err := client.V1Api.Deploy(auth, payload)
if err != nil {
log.Printf("Could not deploy game server, err: %v", err.Error())
return "", err
}
timeout := 30.0
start := time.Now()
now := time.Now()
status := ""
var response swagger.Status
// Waiting for the server to be ready
for status != "Status.READY" && now.Sub(start).Seconds() <= timeout {
response, _, err = client.V1Api.GetDeploymentRequestStatus(auth, request.RequestId)
if err != nil {
log.Printf("Error while fetching status, err: %v", err.Error())
}
status = response.CurrentStatus
}
if now.Sub(start).Seconds() > timeout {
return "", errors.New("Timeout while waiting for deployment")
}
return fmt.Sprintf("%s:%d", response.PublicIp, response.Ports[gamePort].External), nil
}

You will have to change your dependencies. They should look like this

import (
"fmt"
"time"
"log"
"io"
"context"
"errors"
// <MODULE_NAME> is found in go.mod file after instruction module <MODULE_NAME>
"<MODULE_NAME>/swagger"
"google.golang.org/grpc"
"open-match.dev/open-match/pkg/pb"
)

Putting everything together#

Now it's time to put all the pieces together. Our main function should look like this.

func main() {
fmt.Println("Stating Edgegap tutorial's Director!")
backendService := newBackendConnection()
for range time.Tick(time.Second * 5) {
fmt.Println("Creating matches...")
var wg sync.WaitGroup
for _, p := range getProfiles() {
wg.Add(1)
go func(wg *sync.WaitGroup, profile *pb.MatchProfile) {
defer wg.Done()
matches, err := fetch(profile, backendService)
if err != nil {
log.Printf("Failed to fetch matches for profile %v, got %s", profile.GetName(), err.Error())
return
}
log.Printf("Generated %v matches for profile %v", len(matches), profile.GetName())
if err := assign(matches, backendService); err != nil {
log.Printf("Failed to assign servers to matches, got %s", err.Error())
return
}
}(&wg, p)
}
wg.Wait()
}
}

You will have to change your dependencies. They should look like this

import (
"fmt"
"time"
"log"
"io"
"context"
"errors"
"sync"
// <MODULE_NAME> is found in go.mod file after instruction module <MODULE_NAME>
"<MODULE_NAME>/swagger"
"google.golang.org/grpc"
"open-match.dev/open-match/pkg/pb"
)

It will be hard to test this service in a standalone context. We will see it in action when all the services (Front end, Director, and Match Function) will be ready.

Containerize your service#

Containerization is powerful. You can learn more about it on Edgegap's documentation or Docker. Our container, we will use golang:1.15 for its base image.

Create a file named Dockerfile and write this inside.

FROM golang:1.15
COPY . /go/src/app
WORKDIR /go/src/app
RUN go get -d -v /go/src/app
RUN go install -v /go/src/app
WORKDIR /go/bin
# To see your build name, go in your go.mod file and look at your module name.
# The first line should be -> module A/.../<BUILD_NAME>
CMD ["<BUILD_NAME>"]

You can now build your image using. (If you use Edgegap's harbor, <REPOSITORY_MAME> is harbor.edgegap.com/<YOUR_PROJECT>)

docker build -t <REPOSITORY_MAME>/edgegap-om-tuto-director:tuto .

You can run it using

docker run -it --name om-tuto-director <REPOSITORY_MAME>/edgegap-om-tuto-director:tuto

Now push your image on your Docker repository. (You may have to use Docker login)

WARNING: If you push your image on a public repository, anybody could pull your image and access the token in the apiToken variable. If you push on a public repository, you should delete your token after this tutorial.

docker push <REPOSITORY_MAME>/edgegap-om-tuto-director:tuto

Conclusion#

You now have a basic Open Match Director service. This is not production-ready. However, you will learn the key function of Openmatch. You also created a Docker image and pushed it to your repository.