Last Updated: 3/15/2022, 8:31:13 AM

# Build Your Own Matchmaking Service with AccelByte Go SDK in AWS Lambda

# Overview

In this documentation, you will learn how to build and customize your own Matchmaking service using AccelByte’s Golang SDK. This guide will show you how to create a Matchmaking service where two players can play together. The implementation covered in this guide will flow as follows:

golang-matchmaking

# Prerequisites

  • Ensure that you have followed the prerequisites when creating the AWS .yaml script

  • Your .yaml script is configured

  • You have a clone of the latest AccelByte Golang SDK (opens new window) version

    IMPORTANT

    We recommend using the specific latest version of AccelByte Golang SDK when starting a new project instead of the command "latest" to avoid issues when the SDK version is updated.

    Inside the repository, you will work with the sample (opens new window) folders:

    Folder Usage
    title-matchmaking Sample code to work with the aws-lambda and client folders for matchmaking testing
    cli CLI to test the matchmaking

    Configure the repository by following AccelByte’s Golang SDK Getting Started Guide.

# Files Setup

After you’ve cloned the Golang SDK, check the following places in your repository:

  1. Open samples/title-matchmaking/aws-lambda (opens new window) to start working with the sample code for the Matchmaking Server-side SDK. We will be using the logic of AWS Lambda to call the main function directly, and the main function will then call the handler. The handler will manage the requests, logic, and responses.

    REFERENCES

    See the AWS documentation to learn how to work with the AWS Lambda function handler in Go (opens new window) or our reference in the repository (opens new window).

  2. Ensure you have the following handler code included in the main.go (opens new window) file. This code will directly call the handler that contains the logic required to start the Matchmaking service in AWS Lambda.

    lambda.Start(titleMMService.StartMatchmaking)
    

# Authorization

  1. Authorize access to AccelByte’s API using following steps:

    1. Ensure that you have created a User and a Game Client in the Admin Portal.
    2. Open the IAM Swagger page from the OAuth2 authorize API: /iam/v3/oauth/authorize (opens new window). A Request ID will be generated.
    3. Log in using the user_name and password from the Authentication API: /iam/v3/authenticate (opens new window). Input the Request ID generated in the previous step. If the request succeeds, you will receive an authorization code which will be used in the next step.
    4. Generate the user token using the authorization code from the OAuth2 Access Token Generation: /iam/v3/oauth/token (opens new window). If the request succeeds, you will receive a user token which will be used in Step 2.
    5. Generate an OAuth Client token using client_credentials from the OAuth2 Access Token Generation: /iam/v3/oauth/token (opens new window). If the request succeeds, you will receive an OAuth Client token which will be used when implementing functions related to the DSMC.
  2. Parse the user token into the object’s model with the OauthmodelTokenResposeV3 (opens new window) field below:

      type OauthmodelTokenResponseV3 struct {
    
    	// access token
    	// Required: true
    	AccessToken *string `json:"access_token"`
    
    	// bans
    	// Required: true
    	Bans []*AccountcommonJWTBanV3 `json:"bans"`
    
    	// display name
    	// Required: true
    	DisplayName *string `json:"display_name"`
    
    	// expires in
    	// Required: true
    	ExpiresIn *int32 `json:"expires_in"`
    
    	// is comply
    	IsComply bool `json:"is_comply,omitempty"`
    
    	// jflgs
    	Jflgs int32 `json:"jflgs,omitempty"`
    
    	// namespace
    	// Required: true
    	Namespace *string `json:"namespace"`
    
    	// namespace roles
    	// Required: true
    	NamespaceRoles []*AccountcommonNamespaceRole `json:"namespace_roles"`
    
    	// permissions
    	// Required: true
    	Permissions []*AccountcommonPermissionV3 `json:"permissions"`
    
    	// platform id
    	PlatformID string `json:"platform_id,omitempty"`
    
    	// platform user id
    	PlatformUserID string `json:"platform_user_id,omitempty"`
    
    	// refresh token
    	// Required: true
    	RefreshToken *string `json:"refresh_token"`
    
    	// roles
    	// Required: true
    	Roles []string `json:"roles"`
    
    	// token type
    	// Required: true
    	TokenType *string `json:"token_type"`
    
    	// user id
    	// Required: true
    	UserID *string `json:"user_id"`
    }
    

    Our user token does not contain a User ID even though there is a User ID field in the code above. In our case, the User ID will be used in the Lobby and Session browsers. Once you have filled in the above fields, store these values in the tokenConvert variable.

    // parse token
    reqToken := req.Headers["Authorization"]
    splitToken := strings.Split(reqToken, "Bearer ")
    if len(splitToken) == 1 || len(splitToken) > 2 {
    	log.Print("Token split \"Bearer\" and token authorization")
    	message := fmt.Sprintf("Invalid token.")
    	response := events.APIGatewayProxyResponse{StatusCode: http.StatusUnauthorized, Body: message}
    	return response
    }
    reqToken = splitToken[1]
    tokenConvert, err := repository.ConvertTokenToTokenResponseV3(reqToken)
    if tokenConvert == nil {
    	log.Print("Unable to convert token to response model :", err.Error())
    	response := events.APIGatewayProxyResponse{StatusCode: http.StatusUnauthorized, Body: fmt.Sprintf(err.Error())}
    	return response
    }
    

    Code: Token Parsing (opens new window)

  3. Validate the user tokens and permissions using the tokenConvert function. Once completed, validate the user token with custom permissions for the role inside the validatePermissionHandler (opens new window) function.

    // validating permission using lambda function
    func (titleMMService *TitleMatchmakingService) validatePermissionHandler(reqToken, clientId string,
    	tokenResponse *iamclientmodels.OauthmodelTokenResponseV3) (int, error) {
    	var namespaceRoles []iam.NamespaceRole
    	var permissions []iam.Permission
    
    	for _, namespaceRole := range tokenResponse.NamespaceRoles {
    		n := iam.NamespaceRole{
    			RoleID:    *namespaceRole.RoleID,
    			Namespace: *namespaceRole.Namespace,
    		}
    		namespaceRoles = append(namespaceRoles, n)
    	}
    	log.Printf("namespaceRoles : %+v", namespaceRoles)
    
    	var rangeSchedule []string
    	for _, permission := range tokenResponse.Permissions {
    		p := iam.Permission{
    			Resource:        *permission.Resource,
    			Action:          int(*permission.Action),
    			ScheduledAction: int(permission.SchedAction),
    			CronSchedule:    "",
    			RangeSchedule:   rangeSchedule,
    		}
    		permissions = append(permissions, p)
    	}
    
    	// validate token
    	validateAccessToken, err := titleMMService.IamClient.ValidateAccessToken(reqToken)
    	if err != nil {
    		log.Print("Validate access token error. Token expired.", err.Error())
    		return http.StatusBadRequest, err
    	}
    	if !validateAccessToken {
    		log.Print("Validate access token return false. ", err)
    		return http.StatusUnauthorized, err
    	} else {
    		log.Print("Access token is a valid one.")
    	}
    
    	// validate permission
    	claims := iam.JWTClaims{
    		Namespace:             *tokenResponse.Namespace,
    		DisplayName:           *tokenResponse.DisplayName,
    		Roles:                 tokenResponse.Roles,
    		AcceptedPolicyVersion: nil,
    		NamespaceRoles:        namespaceRoles,
    		Permissions:           permissions,
    		Bans:                  nil,
    		JusticeFlags:          0,
    		Scope:                 "",
    		Country:               "",
    		ClientID:              clientId,
    		IsComply:              false,
    		Claims:                iam.JWTClaims{}.Claims,
    	}
    	resource := make(map[string]string, 0)
    	resource["{namespace}"] = claims.Namespace
    	validatePermission, err := titleMMService.IamClient.ValidatePermission(
    		&claims,
    		iam.Permission{
    			Resource: "NAMESPACE:{namespace}:MATCHMAKING",
    			Action:   iam.ActionCreate,
    		},
    		resource,
    	)
    
    	if err != nil {
    		log.Print("Unable to validate permission. Error : ", err.Error())
    		return http.StatusForbidden, err
    	} else {
    		log.Print("Successful validate permission from iam client")
    	}
    
    	if !validatePermission {
    		log.Print("Insufficient permissions")
    		return http.StatusForbidden, err
    	} else {
    		log.Print("There's enough permission")
    	}
    
    	return http.StatusOK, nil
    }
    

    Permissions will be added to this role using the user token, as per the response below.

    golang-matchmaking

  4. Obtain the User ID from the subfield (opens new window) in the user token.

    claims, err := titleMMService.IamClient.ValidateAndParseClaims(reqToken)
    if claims == nil {
    	log.Print("Claim is empty. Error : ", err.Error())
    	message := "Claim is empty"
    	response := events.APIGatewayProxyResponse{StatusCode: http.StatusUnauthorized, Body: fmt.Sprintf(message)}
    	return response
    }
    if err != nil {
    	log.Print("Unable to validate and parse token. Error : ", err.Error())
    	response := events.APIGatewayProxyResponse{StatusCode: http.StatusUnauthorized, Body: fmt.Sprintf(err.Error())}
    	return response
    }
    userId := claims.Subject
    namespace := claims.Namespace
    namespaceGame := constants.NamespaceGame
    gameMode := constants.GameMode
    

    Code: Validate Token and Get User ID (opens new window)

  5. Store the valid user token in the empty interface so you can get and use the valid token.

    // store the valid token
    	errToken := tokenRepositoryImpl.Store(*tokenConvert)
    	if errToken != nil {
    		log.Print("Unable to store token :", errToken.Error())
    		message := fmt.Sprint("Unable to store token")
    		response := events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError, Body: message}
    		return response
    	}
    

    Code: Store the Valid Token (opens new window)

  6. Set the OAuth Client token for the DSMC service and input the registered Client ID in the GAME_CLIENT_ID field. While the Lobby and Session browsers need a user token, the DSMC needs a client token (opens new window) (client_credentials from a registered OAuth Client).

    // get token from game client for DSMC
    	log.Print("Config Repo Game Client Id : ", configGameImpl.GetClientId())
    	oauthService = iamServices.OAuth20Service{
    		IamService:       factory.NewIamClient(&configGameImpl),
    		ConfigRepository: &configGameImpl,
    		TokenRepository:  &tokenRepositoryGameImpl,
    	}
    	err = oauthService.GrantTokenCredentials("", "")
    	if err != nil {
    		log.Print("Unable to grant token : ", err.Error())
    		message := fmt.Sprint("Unable to grant token")
    		response := events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError, Body: message}
    		return response
    	}
    	tokenRepo, err := oauthService.TokenRepository.GetToken()
    	if err != nil {
    		log.Print("Empty error : ", err.Error())
    		message := fmt.Sprint("Unable to get token")
    		response := events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError, Body: message}
    		return response
    	}
    	if tokenRepo == nil {
    		log.Print("Empty tokenRepo.")
    		message := fmt.Sprint("Empty token repository")
    		response := events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError, Body: message}
    		return response
    	}
    

# Tutorials

In this section, you will learn how to implement the services needed to build a Matchmaking service. All of the required functions are available inside the matchmaking.go files.

# Create a Matchmaking Request

To create a matchmaking request (opens new window), you will need a ticket that includes a process for the request, as well as preparation for a new channel and a party.

// store userId as waiting ticket and look for all users in database
createTicketErr := titleMMService.createTicket(namespace, gameMode, userId, 1, 1)
if createTicketErr != nil {
  log.Print(createTicketErr)
  message := "Unable to create ticket."
  response := events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError, Body: message}
  return response, nil
}

# Set Up Waiting Time

The Matchmaking service will look for possible players (opens new window) and set the maximum waiting time (opens new window) for the service to find another player to send a Matchmaking request.

// possible allies
	possibleAllies := len(allTickets)
	log.Printf("There are %v tickets in database with id: %v", possibleAllies, allUsers)
	if len(allUsers) <= 1 {
		go func() {
			b := backoff.NewExponentialBackOff()
			b.MaxElapsedTime = 5 * time.Second

			checkDB := func() error {
				foundUserIds, _ := titleMMService.checkAllies(namespace, userId, gameMode)
				if foundUserIds != nil {
					allUsers = foundUserIds
				}
				return nil
			}

			err = backoff.Retry(checkDB, b)
			if err != nil {
				log.Fatalf("error after retrying: %v", err)
			}
		}()

		time.Sleep(5 * time.Second)

		log.Printf("Not enough player! There is only %v player", possibleAllies)
		message := "Not enough player"
		response := events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError, Body: message}
		return response, nil
	}

# Set Up Searching Notifications

A search notification function can be used to display the current progress of the Matchmaking service to players while they are waiting to be matched. The sendNotificationSearching (opens new window) function will find the WebSocket message containing the word searching and will be sent to players via the FreeFormNotificationByUserId (opens new window) function from the Lobby service, which will hit the Freeform endpoint through our Golang SDK (opens new window).

// GO-SDK lobby service
func sendNotificationSearching(namespace, userId string) error {
  message := "searching"
  topic := constants.MatchmakingNotificationTopic
  body := lobbyclientmodels.ModelFreeFormNotificationRequest{
     Message: &message,
     Topic:   &topic,
  }
  input := &notification.FreeFormNotificationByUserIDParams{
     Body:      &body,
     Namespace: namespace,
     UserID:    userId,
  }
  gameNotificationService := lobby.NotificationService{
     Client:          factory.NewLobbyClient(&configImpl),
     TokenRepository: &tokenRepositoryImpl,
  }
  //lint:ignore SA1019 Ignore the deprecation warnings
  sendNotificationSearchingErr := gameNotificationService.FreeFormNotificationByUserID(input)
  if sendNotificationSearchingErr != nil {
     log.Printf("Unable to send notification match searching to lobby. userId : %+v", userId)
     log.Print(sendNotificationSearchingErr.Error())
     return sendNotificationSearchingErr
  }

  return nil
}

# Manage Game Sessions

# Create a Game Session in the Session Browser

In this section, we will be registering the game session to the session browser (opens new window). This step will also check if there are enough players in the database. Once enough players have been found, the game session will be registered to the session browser and a Session ID will be created. This ID will be used to register the session in the DSMC.

// GO-SDK session browser service
func createSession(namespaceGame string) (*sessionbrowserclientmodels.ModelsSessionResponse, error) {
  allowJoinInProgress := true
  currentPlayer := int32(0)
  maxPlayer := int32(0)
  currentInternalPlayer := int32(0)
  maxInternalPlayer := int32(2)
  numBot := int32(0)
  username := os.Getenv("SESSION_BROWSER_USERNAME")
  password := os.Getenv("SESSION_BROWSER_PASSWORD")
  mapName := os.Getenv("SESSION_BROWSER_MAP_NAME")
  mode := os.Getenv("SESSION_BROWSER_MODE")
  sessionType := os.Getenv("SESSION_BROWSER_TYPE")
  gameVersion := os.Getenv("SESSION_BROWSER_GAME_VERSION")
  var settings interface{}
  var sessionResponse *sessionbrowserclientmodels.ModelsSessionResponse
  gameSetting := sessionbrowserclientmodels.ModelsGameSessionSetting{
     AllowJoinInProgress:   &allowJoinInProgress,
     CurrentInternalPlayer: &currentInternalPlayer,
     CurrentPlayer:         &currentPlayer,
     MapName:               &mapName,
     MaxInternalPlayer:     &maxInternalPlayer,
     MaxPlayer:             &maxPlayer,
     Mode:                  &mode,
     NumBot:                &numBot,
     Password:              &password,
     Settings:              &settings,
  }
  sessionBrowserService := sessionbrowser.SessionService{
     Client:          factory.NewSessionbrowserClient(&configImpl),
     TokenRepository: &tokenRepositoryImpl,
  }
  body := sessionbrowserclientmodels.ModelsCreateSessionRequest{
     GameSessionSetting: &gameSetting,
     GameVersion:        &gameVersion,
     Namespace:          &namespaceGame,
     SessionType:        &sessionType,
     Username:           &username,
  }
  input := &session.CreateSessionParams{
     Body:      &body,
     Namespace: namespaceGame,
  }
  createSessionResp, err := sessionBrowserService.CreateSession(input)
  if err != nil {
     log.Printf("Unable to create session. namespace : %s. Error: %v", namespaceGame, err)
     return createSessionResp, err
  }
  if createSessionResp == nil {
     log.Print("create session response is nil: ", createSessionResp)
     return nil, nil
  } else {
     createSessionResponse := &sessionbrowserclientmodels.ModelsSessionResponse{
        CreatedAt:          createSessionResp.CreatedAt,
        GameSessionSetting: createSessionResp.GameSessionSetting,
        GameVersion:        createSessionResp.GameVersion,
        Joinable:           createSessionResp.Joinable,
        Match:              createSessionResp.Match,
        Namespace:          createSessionResp.Namespace,
        Server:             createSessionResp.Server,
        SessionID:          createSessionResp.SessionID,
        SessionType:        createSessionResp.SessionType,
        UserID:             createSessionResp.UserID,
        Username:           createSessionResp.Username,
     }
     sessionResponse = createSessionResponse
  }
  return sessionResponse, nil
}

# Register a Game Session to the DSMC

After successfully creating a session, the handler will register a session in the DSMC service (opens new window) using the OAuth Client token.

func registerSessionDSMC(sessionId, gameMode, namespaceGame, partyId string,
  allUsers []string) (*dsmcclientmodels.ModelsSessionResponse, error) {

  var partyAttributes interface{}
  var matchingAllies []*dsmcclientmodels.ModelsRequestMatchingAlly
  var matchingParties []*dsmcclientmodels.ModelsRequestMatchParty
  var partyMembers []*dsmcclientmodels.ModelsRequestMatchMember

  for _, userId := range allUsers {
     partyMembers = append(partyMembers, &dsmcclientmodels.ModelsRequestMatchMember{UserID: &userId})
  }

  matchingParty := dsmcclientmodels.ModelsRequestMatchParty{
     PartyAttributes: partyAttributes,
     PartyID:         &partyId,
     PartyMembers:    partyMembers,
  }
  matchingParties = append(matchingParties, &matchingParty)

  matchingAlly := dsmcclientmodels.ModelsRequestMatchingAlly{MatchingParties: matchingParties}
  matchingAllies = append(matchingAllies, &matchingAlly)

  clientVersion := ""
  configuration := ""
  deployment := os.Getenv("DSMC_DEPLOYMENT")
  podName := ""
  region := ""

  dsmcService := dsmc.SessionService{
     Client:      factory.NewDsmcClient(&configGameImpl),
     TokenRepository: oauthService.TokenRepository,
  }
  body := dsmcclientmodels.ModelsCreateSessionRequest{
     ClientVersion:  &clientVersion,
     Configuration:  &configuration,
     Deployment:     &deployment,
     GameMode:       &gameMode,
     MatchingAllies: matchingAllies,
     Namespace:      &namespaceGame,
     PodName:        &podName,
     Region:         &region,
     SessionID:      &sessionId,
  }
  input := &dsmcSession.CreateSessionParams{
     Body:      &body,
     Namespace: namespaceGame,
  }
  registerSession, registerSessionErr := dsmcService.CreateSession(input)
  if registerSessionErr != nil {
     log.Print(registerSessionErr)
  }
  return registerSession, nil
}

Code: registerSessionDSMC (opens new window)

# Claim a Game Server

This function is used to claim a game server (opens new window) from the DSMC service.

func claimServer(namespaceGame string, sessionID *string) error {
  dsmcService := dsmc.SessionService{
     Client:      factory.NewDsmcClient(&configGameImpl),
     TokenRepository: oauthService.TokenRepository,
  }
  body := dsmcclientmodels.ModelsClaimSessionRequest{SessionID: sessionID}
  input := &dsmcSession.ClaimServerParams{
     Body:      &body,
     Namespace: namespaceGame,
  }
  claimServerErr := dsmcService.ClaimServer(input)
  if claimServerErr != nil {
     log.Print(claimServerErr)
  }

  return nil
}

Code: claimServer (opens new window)

# Get Server Information

This function retrieves the server’s information (opens new window) based on a specific session and displays this information in the Matchmaking log.

// GO-SDK DSMC service
func getServer(namespaceGame, sessionID string) (*dsmcclientmodels.ModelsSessionResponse, error) {
  dsmcService := dsmc.SessionService{
     Client:      factory.NewDsmcClient(&configGameImpl),
     TokenRepository: oauthService.TokenRepository,
  }
  input := &dsmcSession.GetSessionParams{
     Namespace: namespaceGame,
     SessionID: sessionID,
  }
  getSession, getSessionErr := dsmcService.GetSession(input)
  if getSessionErr != nil {
     log.Print(getSessionErr)
  }
  if getSession == nil {
     log.Print("get session server from DSMC service is nil")
  }

  return getSession, nil
}

# Add Players to the Game Session

This function adds players to the server in the Session browser (opens new window). In this function, we use a loop in our code to add players and match allies in the Matchmaking session.

// GO-SDK session browser service
func addPlayer(namespaceGame, userId, sessionId string) (*sessionbrowserclientmodels.ModelsAddPlayerResponse, error) {
  asSpectators := false
  body := sessionbrowserclientmodels.ModelsAddPlayerRequest{
     AsSpectator: &asSpectators,
     UserID:      &userId,
  }
  input := &session.AddPlayerToSessionParams{
     Body:      &body,
     Namespace: namespaceGame,
     SessionID: sessionId,
  }
  sessionBrowserService := sessionbrowser.SessionService{
     Client:          factory.NewSessionbrowserClient(&configImpl),
     TokenRepository: &tokenRepositoryImpl,
  }
  addPlayerResp, addPlayerErr := sessionBrowserService.AddPlayerToSession(input)
  if addPlayerErr != nil {
     log.Printf("Unable to add player to session id %v. namespace : %s. Error: %v", sessionId, namespaceGame, addPlayerErr)
     return addPlayerResp, addPlayerErr
  }
  if addPlayerResp == nil {
     log.Print("add player response is nil: ", addPlayerResp)
     return nil, nil
  }

  log.Printf("Successfully add player. userId: %v. sessionId: %v, namespace: %v", userId, sessionId, namespaceGame)
  return addPlayerResp, nil
}

# Get Session Update

After adding players to the Matchmaking session, create a function to obtain Matchmaking session updates (opens new window) by a specific Session ID. This function will find when the session was created, its joinable status, and other session parameters.

// GO-SDK session browser service
func getSessionUpdate(namespaceGame, sessionId string) (*sessionbrowserclientmodels.ModelsSessionResponse, error) {
  sessionBrowserService := sessionbrowser.SessionService{
     Client:          factory.NewSessionbrowserClient(&configImpl),
     TokenRepository: &tokenRepositoryImpl,
  }
  input := &session.GetSessionParams{
     Namespace: namespaceGame,
     SessionID: sessionId,
  }
  getSession, getSessionErr := sessionBrowserService.GetSession(input)
  if getSessionErr != nil {
     log.Print(getSessionErr)
     return getSession, getSessionErr
  }
  if getSession == nil {
     log.Print("get session response is nil: ", getSession)
  } else {
     getSessionResponse := &sessionbrowserclientmodels.ModelsSessionResponse{
        CreatedAt:          getSession.CreatedAt,
        GameSessionSetting: getSession.GameSessionSetting,
        GameVersion:        getSession.GameVersion,
        Joinable:           getSession.Joinable,
        Match:              getSession.Match,
        Namespace:          getSession.Namespace,
        Server:             getSession.Server,
        SessionID:          getSession.SessionID,
        SessionType:        getSession.SessionType,
        UserID:             getSession.UserID,
        Username:           getSession.Username,
     }
     getSession = getSessionResponse
  }

  log.Printf("Successfully get session update : %+v", *getSession)
  return getSession, nil
}

# Set Up a Match Notification

This function sends a notification (opens new window) to the player when they successfully find a match.

// GO-SDK lobby service
func sendNotificationFound(
  namespace,
  IP string,
  port int32,
  allUsers []string) (bool, error) {
  topic := constants.MatchmakingNotificationTopic
  gameNotificationService := lobby.NotificationService{
     Client:          factory.NewLobbyClient(&configImpl),
     TokenRepository: &tokenRepositoryImpl,
  }
  messageIPPort := fmt.Sprintf("found %v %v", IP, port)
  body := lobbyclientmodels.ModelFreeFormNotificationRequest{
     Message: &messageIPPort,
     Topic:   &topic,
  }
  for _, userIdToSend := range allUsers {
     input := &notification.FreeFormNotificationByUserIDParams{
        Body:      &body,
        Namespace: namespace,
        UserID:    userIdToSend,
     }
     sendNotificationMatchFoundErr := gameNotificationService.FreeFormNotificationByUserID(input)
     if sendNotificationMatchFoundErr != nil {
        log.Print(sendNotificationMatchFoundErr)
        return false, sendNotificationMatchFoundErr
     }
     log.Printf("Match found! Successfully send notification to userId : %+v", userIdToSend)
  }

  return true, nil
}

# Testing the Matchmaking

In this section, you will learn how to run and test your Matchmaking service locally.

  1. Inside the AccelByte Golang SDK repository, open the samples/title-matchmaking/Client (opens new window) folder.

  2. To test the Matchmaking service, prepare two test player emails and passwords.

  3. Open the terminal and go to the title-matchmaking repo directory.

  4. In your terminal, run the following command and fill in the following environment variables:

    • Input the Client ID, Client Secret, and BaseUrl with the value set in your OAuth Client in the Admin Portal.
    • Define the Create Matchmaking API URL with the AWS Lambda API Gateway. For testing purposes, you can use API Gateway's TestInvoke (opens new window) feature.
    $ export APP_CLIENT_ID=<user_secret_id>
    $ export APP_CLIENT_SECRET=""
    $ export JUSTICE_BASE_URL="<iam_url>"
    $ export CREATE_MATCHMAKING_ENDPOINT="<endpoint>"
    

    Once completed, press Enter.

    TIP

    If you need to check the environment value at a later stage, use the following command:

    $ echo $APP_CLIENT_ID
    $ echo $APP_CLIENT_SECRET
    $ echo $JUSTICE_BASE_URL
    
  5. To run the matchmaking, use the command go run main.go to bring up the matchmaking command.

    Commands :
    # PoC Matchmaking
    1: Login
    2: Create Matchmaking
    3: User info
    4: Logout
    
  6. Enter command 1 to log the player into the matchmaking. Enter the player’s email and password.

    golang-matchmaking

  7. Enter command 2 to bring players online for the matchmaking. In order to make a successful match, both players must be online. This command will call the Lambda function and listen to the notification service to search for a match. A notification will be sent when a match is found.

    golang-matchmaking

  8. The returned response contains the IP and port:

    golang-matchmaking

    Below is an example of what your screen will look like from the allies’ side once a match has been found. Both players must be online for the match to succeed; otherwise, an error notification will be sent.

    golang-matchmaking

# Deploying the Matchmaking Function into AWS Lambda

To upload the Matchmaking function that you created to AWS Lambda, first ensure that you have filled in all of the required fields in the AWS SAM Template and have tested the SAM locally both in AWS SAM and Client.

  1. In your terminal, go to the root directory aws-lambda (opens new window).

  2. Build the executable binary file in the main.go by running the command:

    go build main.go
    
  3. Once the build is complete, zip the executable binary file to main.zip.

  4. Upload the main.zip file into the configured AWS Lambda (opens new window) console.

  5. Once complete, call the Lambda API Gateway URL (opens new window) using a user token.