Amazon GameLift Fleet Management using the Nakama GameLift Plugin
#
This guide details the integration process between Nakama’s powerful social matchmaking system and Amazon GameLift’s session management, enabling developers to efficiently scale multiplayer games.
Follow along to learn how to configure AWS services, set up SQS and SNS queues, install and configure the Nakama-GameLift plugin, and manage game sessions. By the end, you’ll be equipped to deploy a robust multiplayer environment powered by Nakama and Amazon GameLift.
In order to facilitate integration between Nakama and Amazon GameLift, a number of components need to be setup and configured within your AWS account: an AWS GameLift Fleet with a Placement Queue, an SNS Topic, and an SQS Queue.
Additionally, it is required that each GameLift Game Session updates Nakama on certain status changes to allow it to maintain it’s own state representation of the active Amazon GameLift instances. This is required so that Nakama knows in near real-time the current state of your fleet and all of the instances within it, allowing this data to be queryable and drive your matchmaking needs.
Important!
These updates to Nakama are to be sent via two RPC endpoints (Update/Delete) exposed by the Nakama-GameLift Plugin. For further information please refer to the section titled “Notifying Nakama of game session updates”.
This guide covers just the required steps to get your headless Unity server configured to work with Nakama and GameLift. It does not cover synchronizing game state between the headless server and the client, for this please see the documentation for your chosen networking framework (e.g. Nakama, Unity Netcode for GameObjects, Mirror, etc).
Create a Plugins folder in your Unity project’s Assets folder.
Copy the .DLL files from the extracted zip’s GameLift-CSharp-ServerSDK-5.0.0/src/GameLiftServerSDK/bin/Release folder into it. (You may need to remove Newtonsoft.Json.dll if it is already in use in your project).
Where you choose to initialize GameLift depends on your game. It is recommended that you place the following code snippets somewhere within the initialization script of your game.
It is important that during the OnProcessTerminate lifecycle event, Nakama must be informed that the game session is ending via the RPC defined by the Nakama-GameLift plugin as shown in the code example below.
Create an InitializeGameLift method that will handle initializing the GameLift lifecycle events:
privatevoidInitializeGameLift(){Debug.Log("Initializing GameLift");// Pass null values here as these are configured automatically by AWS GameLift; only used when using GameLift AnywherevarserverParameters=newServerParameters(null,null,null,null,null);// Initialize the GameLift SDKvarinitOutcome=GameLiftServerAPI.InitSDK(serverParameters);if(!initOutcome.Success){Debug.LogError($"Failed to initialize GameLift: {initOutcome.Error}");return;}// Configure process callbacks (methods defined below)varprocessParameters=newProcessParameters(OnStartGameSession,OnUpdateGameSession,OnProcessTerminate,OnHealthcheck,7778,// Port depends on your networking framework (Netcode for GameObjects uses 7778)newLogParameters(newList<string>{"/local/game/logs/server.log"// Define where GameLift should grab the logs from}));// Tell GameLift the server is readyvarprocessReadyOutcome=GameLiftServerAPI.ProcessReady(processParameters);if(!processReadyOutcome.Success){Debug.LogError($"GameLift ProcessReady failed: {processReadyOutcome.Error}");}else{Debug.Log("GameLift initialized successfully");}}
Create the methods to handle the various GameLift callbacks defined above, making sure to call the appropriate Nakama RPCs to maintain game session state integrity between Nakama and Amazon GameLift.
// Called when GameLift creates a game session and sends an activation request, here you can get information about the particular game sessionprivatevoidOnStartGameSession(GameSessiongameSession){Debug.Log($"Game session started.\nIP: {gameSession.IpAddress}\nPort: {gameSession.Port}");// You must call ActivateGameSession once the server is ready to accept incoming player connectionsGameLiftServerAPI.ActivateGameSession();// It is recommended that you cache a copy of the game session so that you can reference metadata later on._gameSession=gameSession;Debug.Log("Game session activated");}// Called when a game session is updatedprivatevoidOnUpdateGameSession(UpdateGameSessionupdateGameSession){// Not used in this example}// Called before shutting down an instance hosting this serverprivatevoidOnProcessTerminate(){// Tell Nakama that the GameLift session is endingvarpayload=newDictionary<string,string>{{"id",GameLiftServerAPI.GetGameSessionId().Result}};_nakamaClient.RpcAsync(_httpKey,"delete_instance_info",payload.ToJson());// Tell GameLift this process is endingGameLiftServerAPI.ProcessEnding();}// Called periodically (every ~60 seconds) to check the health of the serverprivateboolOnHealthCheck(){// Return true here to confirm the server is healthyreturntrue;}
Call the InitializeGameLift method from the Start method of your chosen script:
Any player session created in GameLift needs to be validated by the server. This can be achieved by calling the ApprovePlayerSession GameLift API method. You should call this when a player has connected.
1
2
3
// Accept the player session in GameLift and update NakamavaracceptOutcome=GameLiftServerAPI.AcceptPlayerSession(playerSessionId);UpdateNakamaGameSessionData();
When a player connects or disconnects from the game server, or when game session meta data has changed, Nakama must be informed via the Update RPC that the Nakama-GameLift plugin exposes.
publicvoidUpdateNakamaGameSessionData(){// Class used as a payload to the Nakama Fleetmanager Update RPCprivateclassNakamaUpdateRequest{publicstringid;publicintplayer_count;publicDictionary<string,object>metadata;}// Get the total current player count from GameLiftvarreq=newDescribePlayerSessionsRequest(){GameSessionId=gameSessionId};varsessions=GameLiftServerAPI.DescribePlayerSessions(req);if(sessions.Error!=null){Debug.Log($"Error getting sessions from GameLiftServerAPI: {sessions.Error?.ErrorMessage}");}varplayerCount=0;foreach(varsinsessions.Result.PlayerSessions){if(s.Status==PlayerSessionStatus.ACTIVE||s.Status==PlayerSessionStatus.RESERVED){playerCount++;}}// Notify Nakama of the updated information about this game session.// These values can be retrieved from the Game Session object cached earlier if required.// If these values are omitted, the existing values will persist.varmetadata=newDictionary<string,object>{{"GameSessionData","<game_session_data>"},{"GameSessionName","<game_session_name>"},{"GameProperties":newDictionary<string,string>{{"key1","value1"},{"key2","value2"}}};varpayload=newNakamaUpdateRequest(){id=GameLiftServerAPI.GetGameSessionId().Result,player_count=playerCount,metadata=metadata};_nakamaClient.RpcAsync(_httpKey,"update_instance_info",payload.ToJson());}
If your headless server process exits for any reason (e.g. after a match) you should explicitly tell GameLift that the process is ending to ensure the game session ends appropriately.
1
GameLiftServerAPI.ProcessEnding();
You should also explicitly call Destroy when the application quits.
The AWS requirements for setting up the GameLift service are as follows:
An SNS Topic for publishing events to
An SQS Queue for reading events from
An S3 bucket for storing server builds
An IAM Role to allow GameLift access to the S3 bucket
An IAM Role to allow the Nakama server to authenticate and access GameLift APIs
The GameLift service including placement queue, builds, fleets, and an alias
Creating the Topic in Amazon Simple Notification Service (SNS)
#
A Topic must be created for notifications/events to be published to via Amazon Simple Notification Service (SNS). This Topic will be used by the Fleet Placement Queue to publish placement status updates. Nakama will receive these events through a Simple Queue System (SQS) subscribed to this topic.
Creating the Queue in Amazon Simple Queue Service (SQS)
#
This queue will subscribe to the SNS topic created above in order to make GameLift fleet events available to Nakama. This is necessary as SNS events are unavailable outside of AWS without first pushing them to an accessible SQS queue.
To create the SQS queue that will be read from via Nakama:
Click Subscribe to Amazon SNS Topic and choose the fleet-game-session-placement.fifo topic created earlier.
You can confirm that everything is configured correctly by verifying that the subscription to the SNS Topic shows up under the SNS Subscriptions section.
This role will now allow the AWS GameLift service to access the S3 bucket and download the headless server builds stored there when creating a new GameLift Build.
In order for Nakama to access GameLift services there needs to be an IAM User created with an access token. This access token (and secret access token) will be passed to Nakama via environment variables so that it can interact with the GameLift APIs to search for and create new game sessions.
This Policy will allow the IAM User to interact with various GameLift APIs. You can add or remove API functions from this policy depending on your needs.
Navigate to the Users section.
Create a new User called NakamaGameLiftUser.
Attach the GameLiftAPIAccess policy under the Permissions tab.
Now create access tokens for this user:
Go to the Security Credentials tab.
Under the Access Keys section click Create access key.
Follow the instructions and then note down the Access Key and Secret Access Key for later.
The first step in launching a fleet of headless game servers with GameLift is to create a Build. A Build defines the executable binary that GameLift will spin up as part of a fleet. You should specify the operating system that you expect the build to run on as well as the GameLift Server SDK version your build is using.
Navigate to the GameLift service and go to the Builds section.
Give your build a Name (e.g. my-server-1.0.0).
Specify a Version number (e.g. 1.0.0).
Choose the Operating System of your build (e.g. Linux 2).
Note that the Linux option does not support the 5.0.0 server SDK
Choose 5.0.0 for the Server SDK Version.
Under the Game Server Build option, choose your server build Zip file.
From the IAM Role dropdown choose the GameLiftS3AccessRole.
Once you have created a build you’re ready to create a fleet. A fleet is a group of game servers that can be configured to scale up and down as demand calls for it.
To create your fleet:
Navigate to the Fleets section and click Create Fleet.
Choose Managed EC2 for Compute Type.
Give your fleet a Name (e.g. build_1_0_0_fleet_1).
Keep the Binary Type as Build.
Select the Build you just created from the Build dropdown.
Leave the Additional Details section blank and click Next.
Choose a Location for your Fleet and click Next.
For Instance Type choose On Demand.
Select the most appropriate instance type for your needs (for this guide the recommendation is c4.large) and click Next .
For Launch Path enter the path to your server binary in the Zip file you uploaded (e.g. server.x86_64)
For Launch parameters enter at least the following:
Add an entry for both TCP and UDP with the following:
Port Range: enter 7000-8000
IP Address Range: enter 0.0.0.0/0
Click on Next.
Leave the Tags section blank unless you wish to add them to your fleet.
Finally click on Submit.
Once your fleet is created it will go through several stages which can be viewed via the Events tab. If any errors occur during these stages you will be notified here.
When your fleet status changes to FLEET_STATE_ACTIVE it will begin scaling up your fleet as per the scaling configuration, which by default will spin up 1 instance.
An alias is a pointer to a fleet that can be changed at any point. This allows you to dynamically change which fleet a client connects to without needing to rebuild and republish the client itself.
To create an alias:
Navigate to the Aliases section and click Create Alias.
Give your alias a Name (e.g. alias-1).
Select the Simple option for Routing Type.
Select the Fleet the alias should point to.
Click Create and take note of the Alias ID.
Creating the GameLift Game Session Placement Queue
#
In order for Nakama to be able to effectively create game sessions within Amazon GameLift, a game session placement queue must be configured. Requests made to this queue signals to Amazon GameLift that a new game session is required and allows them to be asynchronously processed. Whenever a new game session is required, Nakama will make a request to this queue, and once the request is fulfilled by Amazon GameLift, a callback will be triggered within Nakama containing the information about the newly created game session.
Have a Nakama server project up and running using the Go runtime. Follow the Introduction to Nakama Go Runtime documentation to get started, if needed.
Are familiar with hosting a Nakama instance. See our Heroic Cloud documentation for details on how to launch an instance of Nakama in the cloud.
This guide focuses on implementing the necessary functionality to communicate with AWS GameLift in order to search for and create game sessions and receive match results.
The Nakama-GameLift plugin provides an Amazon GameLift implementation of Nakama’s Fleet Manager interface and is available as an Open-Source plugin via GitHub and
To install the plugin in your Nakama project, run the following command in the project folder:
1
go get github.com/heroiclabs/nakama-gamelift/fleetmanager
Once installed, a new FleetManager instance can be created within the InitModule function.
// It is recommended that you pass in the necessary AWS configuration values via ENV vars.
// e.g.
//
// env, ok := ctx.Value(runtime.RUNTIME_CTX_ENV).(map[string]string)
// if !ok {
// return fmt.Errorf("expects env ctx value to be a map[string]string")
// }
// awsAccessKey, ok := env["GL_AWS_ACCESS_KEY"]
// if !ok {
// return runtime.NewError("missing GL_AWS_ACCESS_KEY environment variable", 3)
// }
// Create the GameLift configuration object using appropropriate string values for AWS resources.
cfg:=fleetmanager.NewGameLiftConfig(awsAccessKey,awsSecretAccessKey,awsRegion,awsAliasId,awsPlacementQueueName,awsGameLiftPlacementEventsQueueUrl)// Create a new Fleet Manager instance.
fm,err:=fleetmanager.NewGameLiftFleetManager(ctx,logger,db,initializer,nk,cfg)iferr!=nil{returnerr}// Register the Fleet Manager with Nakama.
iferr=initializer.RegisterFleetManager(fm);err!=nil{logger.WithField("error",err).Error("failed to register aws gamelift fleet manager")returnerr}
To access the fleet manager elsewhere after it has been registered:
The Fleet Manager API exposes several functions that can be used to Create, List, Retrieve, Join and Delete game sessions within Amazon GameLift. These are covered below. For full documentation please visit the nakama-gamelift repository on GitHub.
The Create function is used to create a new game session within the Amazon GameLift fleet. The process is asynchronous, so the function takes a callback as part of the signature. This callback is invoked once the creation process succeeds, times-out or fails. The callback can be used to notify any interested parties on the status of the creation process:
varcallbackruntime.FmCreateCallbackFn=func(statusruntime.FmCreateStatus,instanceInfo*runtime.InstanceInfo,sessionInfo[]*runtime.SessionInfo,metadatamap[string]any,createErrerror){// createErr is not nil only if status is != runtime.CreateSuccess.
// the original AWS Placement Event can be retrieved from `metadata` under the 'event' key.
switchstatus{caseruntime.CreateSuccess:// Create was successful, instanceInfo contains the instance information for player connection.
// sessionInfo contains Player Session info if a list of userIds is passed to the Create function.
info,_:=json.Marshal(instanceInfo)logger.Info("GameLift instance created: %s",info)// Notify any interested party.
returncaseruntime.CreateTimeout:// AWS GameLift was not able to successfully create the placed Game Session request within the timeout
// (configurable in the placement queue).
// The client should be notified to either reattempt to find an available Game Session or retry creation.
info,_:=json.Marshal(instanceInfo)logger.Info("GameLift instance created: %s",info)// Notify any interested party.
returndefault:// The request failed to be placed.
logger.WithField("error",createErr.Error()).Error("Failed to create GameLift instance")// Notify any interested party.
return}}maxPlayers:=10// Maximum number of players that will be able to connect to the Game Session
playerIds:=[]string{userId}// Optional - Reserves a Player Session for each userId. The reservation expires after 60s if the client doesn't connect.
metadata:=map[string]any// Optional - Metadata containing GameProperties or
err=fm.Create(ctx,maxPlayers,playerIds,metadata,callback)
The list function enables querying of existing game sessions and uses the same query syntax found elsewhere in Nakama:
1
2
3
4
query:="+value.playerCount:2"// Query to list Game Sessions currently containing 2 Player Sessions. An empty query will list all Game Sessions.
limit:=10// Number of results per page (does not apply if a query is != ""
cursor:=""// Pagination cursor
instances,nextCursor,err:=fm.List(ctx,query,limit,cursor)
The join function reserves a seat in an existing game session and retrieves the corresponding player session data needed for a client to connect:
1
2
3
4
5
6
id:="<game session ARN>"userIds:=[]string{userId}metadata:=map[string]string{userId:"<player data>",}// metadata is optional. It can be used to set an arbitrary string that can be retrieved from the Game Session. Each key needs to match an userId present in userIds, otherwise it is ignored.
joinInfo,err:=fm.Join(ctx,idstring,userIds[]string,metadatamap[string]string)
Delete should be used to delete an InstanceInfo data regarding a Game Session that was terminated on GameLift:
1
2
id:="<game session ARN>"err:=fm.Delete(ctx,id)
Example: Finding/Creating a Game Session via Nakama Matchmaking
#
One common use case for creating an Amazon GameLift game session via Nakama is to combine it with Nakama’s powerful social matchmaking features. In the following client/server code examples, a player will use Nakama’s matchmaking to find a match from their friends list and Nakama will then find or create an Amazon GameLift game session in the configured fleet before sending the connection details to the matched players.
On the server, register a MatchmakerMatched hook that will find or create an Amazon GameLift game session for the matched players and, when the game session is available, send the connection details to the matched players.
// Define notification codes
notificationConnectionInfo:=111notificationCreateTimeout:=112notificationCreateFailed:=113// Register the Matchmaker Matched hook to find/create a GameLift game session
iferr:=initializer.RegisterMatchmakerMatched(func(ctxcontext.Context,loggerruntime.Logger,db*sql.DB,nkruntime.NakamaModule,entries[]runtime.MatchmakerEntry)(string,error){// Define the maximum amount of players per game
maxPlayers:=10// Find existing GameLift game sessions
fm:=nk.GetFleetManager()query:=fmt.Sprintf("+values.playerCount:<=%v",maxPlayers-len(entries))// Assuming a max match size of 10, find a match that has enough player spaces for the matched players
limit:=1cursor:=""instances,_,err:=fm.List(ctx,query,limit,cursor)iferr!=nil{logger.WithField("error",err.Error()).Error("failed to list gamelift instances")return"",ErrInternalError}// If an instance was found, tell GameLift the players are joining the instance and then notify players with the connection details
iflen(instances)>0{instance:=instances[0]userIds:=make([]string,0,len(entries))for_,entry:=rangeentries{userIds=append(userIds,entry.Presence.UserId)}joinInfo,err:=fm.Join(ctx,instance.Id,userIds,nil)iferr!=nil{logger.WithField("error",err.Error()).Error("failed to join gamelift instance")return"",ErrInternalError}// Send connection details notifications to players
for_,userId:=rangeuserIds{// Get the user's GameLift session ID
sessionId:=""for_,sessionInfo:=rangejoinInfo.SessionInfo{ifsessionInfo.UserId==userId{sessionId=sessionInfo.SessionIdbreak}}subject:="connection-info"content:=map[string]interface{}{"IpAddress":joinInfo.InstanceInfo.IpAddress,"DnsName":joinInfo.InstanceInfo.DnsName,"Port":joinInfo.InstanceInfo.Port,"SessionId":sessionId,}code:=notificationConnectionInfosenderId:=""// System sender
persistent:=falsenk.NotificationSend(ctx,userId,subject,content,code,senderId,persistent)}// We don't pass a Match ID back to the user as we are not creating a Nakama match
return"",nil}// If no instance was found, ask GameLift to create a new one and, when it is available, notify the players with the connection details
// First establish the creation callback
varcallbackruntime.FmCreateCallbackFn=func(statusruntime.FmCreateStatus,instanceInfo*runtime.InstanceInfo,sessionInfo[]*runtime.SessionInfo,metadatamap[string]any,createErrerror){switchstatus{caseruntime.CreateSuccess:joinInfo,_:=json.Marshal(instanceInfo)logger.Info("GameLift instance created: %s",joinInfo)// Send connection details notifications to players
for_,userId:=rangeuserIds{// Get the user's GameLift session ID
sessionId:=""for_,sessionInfo:=rangejoinInfo.SessionInfo{ifsessionInfo.UserId==userId{sessionId=sessionInfo.SessionIdbreak}}subject:="connection-info"content:=map[string]interface{}{"IpAddress":joinInfo.InstanceInfo.IpAddress,"DnsName":joinInfo.InstanceInfo.DnsName,"Port":joinInfo.InstanceInfo.Port,"SessionId":sessionId,}code:=notificationConnectionInfosenderId:=""// System sender
persistent:=falsenk.NotificationSend(ctx,userId,subject,content,code,senderId,persistent)}returncaseruntime.CreateTimeout:logger.WithField("error",createErr.Error()).Error("Failed to create GameLift instance, timed out")// Send notification to client that game session creation timed out
for_,userId:=rangeuserIds{subject:="create-timeout"content:=map[string]interface{}{}code:=notificationCreateTimeoutsenderId:=""// System sender
persistent:=falsenk.NotificationSend(ctx,userId,subject,content,code,senderId,persistent)}default:logger.WithField("error",createErr.Error()).Error("Failed to create GameLift instance")// Send notification to client that game session couldn't be created
for_,userId:=rangeuserIds{subject:="create-timeout"content:=map[string]interface{}{}code:=notificationCreateFailedsenderId:=""// System sender
persistent:=falsenk.NotificationSend(ctx,userId,subject,content,code,senderId,persistent)}return}}// Game session metadata as described by AWS GameLift Documentation
// https://docs.aws.amazon.com/gamelift/latest/apireference/API_GameSession.html
// These properties can be queried by the Fleet Manager when listing existing game sessions.
// These properties can also be updated by calling the Update RPC from the headless server.
metadata:=map[string]interface{}{"GameSessionData":"<game_session_data>","GameSessionName":"<game_session_name>","GameProperties":map[string]string{"key1":"value1","key2":"value2",},}iferr=fm.Create(ctx,maxPlayers,userIds,metadata,callback);err!=nil{logger.WithField("error",err.Error()).Error("failed to create new fleet game session")return"",ErrInternalError}// We don't pass a Match ID back to the user as we are not creating a Nakama match
return"",nil});err!=nil{logger.Error("unable to register matchmaker matched hook: %v",err)returnerr}
On the client, use matchmaking to match with friends:
socket.ReceivedNotification+=notification=>{constintnotificationConnectionInfo=111;constintnotificationCreateTimeout=112;constintnotificationCreateFailed=113;switch(notification.Code){casenotificationConnectionInfo:vardata=notification.Content.FromJson<Dictionary<string,string>>();Debug.Log("Game session created, connection info:");Debug.Log($"IP: {data["IpAddress"]}");Debug.Log($"DnsName: {data["DnsName"]}");Debug.Log($"Port: {data["Port"]}");Debug.Log($"SessionId: {data["SessionId"]}");// Connect to the Amazon GameLift headless server with your chosen networking framework and the connection info.break;casenotificationCreateTimeout:Debug.LogError("Game session creation timed out");break;casenotificationCreateFailed:Debug.LogError("Failed to create game session");break;}};
Be sure to check the Access Policies and Permissions associated with each AWS service to make sure it has been configured to correctly have access to the relevant ARNs.
My Nakama instance is not receiving events from the SQS queue
Similarly to above, double check that the policies and permissions are correct, particularly within the SNS Topic and SQS queue.
My Nakama instance cannot interact with the AWS APIs
As above, ensure that the policies and permissions within AWS are correct.
Game sessions aren’t being created
Check the Fleet configuration to ensure scaling has been configured correctly, in particular check the max instances setting. If you are still experiencing issues, please refer to the Amazon GameLift documentation.
I cannot connect to the headless game server
Be sure to check that the appropriate Ports have been configured when creating the fleet. This will depend on your chosen networking framework, for example, Unity Netcode For GameObjects uses port 7778 by default.
If this does not resolve the issue, be sure to check the Debug logs of both the client and the server for more information.
I’m having an issue with X/Y/Z
If the issue you’re experiencing is not covered above, then there are a few things you can do to help diagnose the issue:
Check the Nakama server logs
Check the Game Session logs to look for client side issues