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 a game server to them.

Prerequisites#

  • Install Golang 1.16^ on your computer

Create Your project#

Every Open Match service should be in its own project. Create a new folder named director for the OpenMatch director component.

In this folder, run the following command.

go mod init director

And create a file named main.go where the core logic of the component will be.

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.

main.go (Override All)
package main
import (
"fmt"
"time"
)
const (
openMatchMatchFunctionHost = "basics-match-function"
openMatchMatchFunctionPort = 50502
openMatchBackendService = "open-match-backend:50505"
// Game server data
gameServerPort = "<APP_PORT>" // Port Name (Port number if you port is not named) | 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...

Don't forget to change the variables gameServerPort, appName, appVersion, arbitriumAPI, apiToken with valid data. The values should come from Arbitrium. It should be an application that you own.

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

main.go (Add to file)
// 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

main.go (Replace)
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.

main.go (Add to file)
// 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

main.go (Replace)
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.

main.go (Add to file)
// 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

main.go (Replace)
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).

Using Arbitrium deployment#

This section shows you how to integrate Arbitrium's deployment into your Director. If you use Arbitrium's session, you should go to the next section.

Info : 

We assume that you already experimented with Arbitrium's deployment and know how they work. If you don't, we recommend you to see this section

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.

main.go (Add to file)
// 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.DeploymentsApi.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()
status := ""
var response swagger.Status
// Waiting for the server to be ready
for status != "Status.READY" && time.Since(start).Seconds() <= timeout {
response, _, err = client.DeploymentsApi.DeploymentStatusGet(auth, request.RequestId)
if err != nil {
log.Printf("Error while fetching status, err: %v", err.Error())
}
status = response.CurrentStatus
time.Sleep(1 * time.Second) // let's wait a bit
}
if time.Since(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

main.go (Replace)
import (
"fmt"
"time"
"log"
"io"
"context"
"errors"
"director/swagger"
"google.golang.org/grpc"
"open-match.dev/open-match/pkg/pb"
)

Using Arbitrium session#

This section shows you how to integrate Arbitrium's session into your Director. If you use Arbitrium's deployment, you should go to the previous section.

Info : 

We assume that you already experimented with Arbitrium's deployment and know how they work. If you don't, we recommend you to see this section

Warning : 

If your App Version (session kind) IS NOT in auto-deploy, you will need an instance running to link your session.

If your App Version (session kind) IS in auto-deploy, it will deploy a server automatically. It could end up in multiple instances running.

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.

main.go (Add to file)
// 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.SessionModel{
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.SessionsApi.SessionPost(auth, payload)
if err != nil {
log.Printf("Could not deploy game server, err: %v", err.Error())
return "", err
}
timeout := 30.0
start := time.Now()
ready := false
var response swagger.SessionGet
// Waiting for the server to be ready
for !ready && time.Since(start).Seconds() <= timeout {
response, _, err = client.SessionsApi.GetSession(auth, request.SessionId)
if err != nil {
log.Printf("Error while fetching status, err: %v", err.Error())
}
ready = response.Ready
time.Sleep(1 * time.Second) // let's wait a bit
}
if time.Since(start).Seconds() > timeout {
return "", errors.New("Timeout while waiting for deployment")
}
return fmt.Sprintf("%s:%d", response.Deployment.PublicIp, response.Deployment.Ports[gamePort].External), nil
}

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

main.go (Replace)
import (
"fmt"
"time"
"log"
"io"
"context"
"errors"
"director/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.

main.go (Replace)
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

main.go (Replace)
import (
"fmt"
"time"
"log"
"io"
"context"
"errors"
"sync"
"director/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.16 for its base image.

Create a file named Dockerfile and write this inside.

FROM golang:1.16
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
CMD ["director"]

You can now build your image using.

docker build -t edgegap-om-tuto-director:tuto .

You can run it using this command, but without all the other services, it will not work.

docker run -it --name om-tuto-director 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 Open Match Director. You also created a Docker image that can be used later.

Warning : The code in this tutorial is only an introduction. Unfortunately, there’s no mechanism to clean your deployments. If they do not deploy correctly, the tickets won’t be assigned. In the next loop, the Director will try to deploy a new server.