This guide details the integration process between Nakama’s powerful social matchmaking system and Edgegap’s session management, enabling developers to efficiently scale multiplayer games.

Prerequisites #

Nakama Version
The Nakama-Edgegap plugin has been tested with Nakama 3.25 and above.

Before proceeding ensure that you have:

Creating the Headless Unity server #

This guide covers just the required steps to get your headless Unity server configured to work with Nakama and Edgegap. 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).

Installing the Nakama SDK in Unity #

To install the Nakama SDK into the Unity project:

  1. Download the latest Nakama Unity SDK package.
  2. Double-click the Nakama.unitypackage and install it in your Unity project.

Installing the Edgegap Server Nakama Plugin #

  1. Open your Unity project,
  2. Select toolbar option Window -> Package Manager,
  3. Click the + icon and select Add package from git URL…,
  4. Input the following URL https://github.com/edgegap/edgegap-server-nakama-plugin-unity.git,
  5. Click Add and wait for the Unity Package Manager to complete the installation.

Import Simple Example #

  1. Find this package in Unity Package Manager window.
  2. Open the Samples tab.
  3. Click on Import next to Simple Handler Example.
  4. Locate sample files in your project Assets/Samples/Edgegap Server Nakama Plugin/{version}/Simple Handler Example.
  5. Create an Empty GameObject in your scene and attach SimpleHandlerExample.cs script.

This script will call the methods AddUser and RemoveUser whenever a player connects or disconnects to manage allocations on Nakama.

After these steps are done, your project is ready to be launched on Edgegap, for that go to Tools -> Edgegap Hosting.

Login and follow steps 1-5, since we are doing automatic deployment via Nakama, step 6 can be skipped.

Edgegap Hosting Unity Tool
Edgegap Hosting Unity Tool

Now follow the next section to setup the Nakama Server and integrate it with Edgegap to deploy servers automatically.

Troubleshooting #

Visual Studio shows type or namespace name could not be found for Edgegap namespace.

  1. In your Unity Editor, navigate to Edit / Preferences / External Tools / Generate .csproj files.
  2. Make sure you have enabled Git packages.
  3. Click Regenerate project files.

Nakama Initialization #

To add the Edgegap Fleet Manager to your Nakama go plugin project, use the following command:

1
go get github.com/edgegap/nakama-edgegap

This will add it to your project’s go.mod dependencies.

Nakama Setup #

You must set up the following Environment Variables inside your Nakama’s cluster:

1
2
3
4
5
6
EDGEGAP_API_URL=https://api.edgegap.com
EDGEGAP_API_TOKEN=<The Edgegap's API Token (keep the 'token' in the API Token)>
EDGEGAP_APPLICATION=<The Edgegap's Application Name to use to deploy>
EDGEGAP_VERSION=<The Edgegap's Version Name to use to deploy>
EDGEGAP_PORT_NAME=<The Edgegap's Application Port Name to send to game client>
NAKAMA_ACCESS_URL=<Nakama API Url, for Heroic Cloud, it will be provided when you create your instance>

You can copy the local.yml.example to local.yml and fill it out to start with your local cluster

Make sure the NAKAMA_ACCESS_URL is prefixed with https://.

Optional Values with default

1
2
3
EDGEGAP_POLLING_INTERVAL=<Interval where Nakama will sync with Edgegap API in case of mistmach (default:15m ) >
NAKAMA_CLEANUP_INTERVAL=<Interval where Nakama will check reservations expiration (default:1m )
NAKAMA_RESERVATION_MAX_DURATION=<Max Duration of a reservations before it expires (default:30s )

Using the Nakama’s Storage Index and basic struct Instance Info, we store extra information in the metadata for Edgegap using 2 list. 1 list to holds seats reservations 1 list to holds active connections

Using Max Players field we can now create the field AvailableSeats that will be in sync with that ( MaxPlayers-Reservations-Connections=AvailableSeats)

Usage #

From your main.go where the InitModule global function is, you need to register the Fleet Manager

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Register the Fleet Manager
efm, err := fleetmanager.NewEdgegapFleetManager(ctx, logger, db, nk, initializer)
if err != nil {
    return err
}

if err = initializer.RegisterFleetManager(efm); err != nil {
    logger.WithField("error", err).Error("failed to register Edgegap fleet manager")
    return err
}

Run the following command to start a local cluster:

1
docker compose up --build -d

Run the following command to stop it

1
docker compose down

Server Placement #

Game clients only interact with Edgegap APIs through Nakama RPCs, defaulting to Nakama authentication method of your choice. Edgegap’s Server Placement utilizing Server Score strategy uses public IP addresses of participating players to choose the optimal server location. To store the player IP address and pass it to Edgegap when looking for server, store player’s public IP in their Profile’s Metadata as PlayerIP.

In your main.go, during the Init you can add the Registration of the Authentication of the type you implemented

1
2
3
4
    // Register Authentication Methods
    if err := initializer.RegisterAfterAuthenticateCustom(fleetmanager.OnAuthenticateUpdateCustom); err != nil {
        return err
    }

This will automatically store in Profile’s Metadata the PlayerIP

Dedicated Game Server -> Nakama Instance #

When using this integration, every Deployment (Dedicated Game Server) made through Edgegap’s platform will have many Environment Variables injected.

It’s the responsibility of the Dedicated Game Server to fire lifecycle events when specific actions are triggered to communicate back to Nakama’s cluster changes regarding the Instance (Nakama’s reference to an Edgegap Deployment) and facilitate Player connections to the Dedicated Game Server.

Unity Server Plugin #

Automate all server responsibilities (instance and connection event reporting) by using our Edgegap Server Nakama Plugin for Unity.

Injected Environment Variables #

The following Environment Variables will be available in the Dedicated Game Server:

  • NAKAMA_CONNECTION_EVENT_URL (url to send connection events of the players)
  • NAKAMA_INSTANCE_EVENT_URL (url to send instance event actions)
  • NAKAMA_INSTANCE_METADATA (contains create metadata JSON)

Connection Events #

Using NAKAMA_CONNECTION_EVENT_URL you must send Player Connection events to the Nakama Instance with the following body:

1
2
3
4
5
6
{
  "instance_id": "<instance_id>",
  "connections": [
    "<user_id>"
  ]
}

connections is the list of active user IDs connected to the Dedicated Game Server. We recommend collecting updates over a short period of time (~5 seconds) and updating the full list of connections in a batch request. Contents of this request will overwrite any existing list of connections for the specified instance.

Instance Events #

Using NAKAMA_INSTANCE_EVENT_URL you must send Instance events to the Nakama Instance with the following body:

1
2
3
4
5
6
{
  "instance_id": "<instance_id>",
  "action": "[READY|ERROR|STOP]",
  "message": "",
  "metadata": {}
}

action must be one of the following:

  • READY will mark the instance as ready and trigger Nakama callback event to notify players,
  • ERROR will mark the instance in error and trigger Nakama callback event to notify players,
  • STOP will call Edgegap’s API to stop the running deployment, which will be removed from Nakama once Edgegap confirms termination.

message can be used optionally to provide extra Instance status information (e.g. to communicate Errors).

metadata can be used optionally to merge additional custom key-value information available in Dedicated Game Server to the metadata of the Instance.

Game Client -> Nakama (optional rpc) #

We included a Client RPC route to do basic operations on Instance - listing, creating, and joining. Consider this an optional starter code sample. For production/live use cases, we recommend using a matchmaker for added security and flexibility.

Create Instance #

RPC - instance_create

1
2
3
4
5
{
  "max_players": 2,
  "user_ids": [],
  "metadata": {}
}

max_players to -1 for unlimited. Use with caution, we recommend performing a benchmark for server resource usage impact.

If user_ids is empty, the requesting user’s ID will be used.

Get Instance #

RPC - instance_get

1
2
3
{
  "instance_id": "<instance_id>"
}

List Instance #

RPC - instance_list

1
2
3
4
5
{
  "query": "",
  "limit": 100,
  "cursor": ""
}

query can be used to search instance with available seats.

Example to list all instances READY with at least 1 seat available.

1
2
3
4
5
{
  "query": "+value.metadata.edgegap.available_seats:>=1 +value.status:READY",
  "limit": 100,
  "cursor": ""
}

Join Instance #

RPC - instance_join

1
2
3
4
{
  "instance_id": "<instance_id>",
  "user_ids": []
}

If user_ids is empty, the requesting user’s ID will be used.

Matchmaker #

You can create your own integration using Nakama’s Matchmaker, see our starter code sample:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// OnMatchmakerMatched When a match is created via matchmaker, collect the Users and create a instance
func OnMatchmakerMatched(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, entries []runtime.MatchmakerEntry) (string, error) {
	maxPlayers := len(entries)
	userIds := make([]string, 0, len(entries))
	for _, entry := range entries {
		userIds = append(userIds, entry.GetPresence().GetUserId())
	}
	
	var callback runtime.FmCreateCallbackFn = func(status runtime.FmCreateStatus, instanceInfo *runtime.InstanceInfo, sessionInfo []*runtime.SessionInfo, metadata map[string]any, createErr error) {
		switch status {
		case runtime.CreateSuccess:
			logger.Info("Edgegap instance created: %s", instanceInfo.Id)

			content := map[string]interface{}{
				"IpAddress":  instanceInfo.ConnectionInfo.IpAddress,
				"DnsName":    instanceInfo.ConnectionInfo.DnsName,
				"Port":       instanceInfo.ConnectionInfo.Port,
				"InstanceId": instanceInfo.Id,
			}
			// Send connection details notifications to players
			for _, userId := range userIds {
				subject := "connection-info"

				code := notificationConnectionInfo
				err := nk.NotificationSend(ctx, userId, subject, content, code, "", false)
				if err != nil {
					logger.WithField("error", err.Error()).Error("Failed to send notification")
				}
			}
			return
		case runtime.CreateTimeout:
			logger.WithField("error", createErr.Error()).Error("Failed to create Edgegap instance, timed out")

			// Send notification to client that instance session creation timed out
			for _, userId := range userIds {
				subject := "create-timeout"
				content := map[string]interface{}{}
				code := notificationCreateTimeout
				err := nk.NotificationSend(ctx, userId, subject, content, code, "", false)
				if err != nil {
					logger.WithField("error", err.Error()).Error("Failed to send notification")
				}
			}
		default:
			logger.WithField("error", createErr.Error()).Error("Failed to create Edgegap instance")

			// Send notification to client that instance session couldn't be created
			for _, userId := range userIds {
				subject := "create-failed"
				content := map[string]interface{}{}
				code := notificationCreateFailed
				err := nk.NotificationSend(ctx, userId, subject, content, code, "", false)
				if err != nil {
					logger.WithField("error", err.Error()).Error("Failed to send notification")
				}
			}
			return
		}
	}
	
	efm := nk.GetFleetManager()
	err := efm.Create(ctx, maxPlayers, userIds, nil, nil, callback)

	reply := instanceCreateReply{
		Message: "Instance Created",
		Ok:      true,
	}

	replyString, err := json.Marshal(reply)
	if err != nil {
		logger.WithField("error", err.Error()).Error("failed to marshal instance create reply")
		return "", ErrInternalError
	}

	return string(replyString), err
}

Register OnMatchmakerMatched callback like this in the InitModule function in your main.go:

1
2
3
4
5
err = initializer.RegisterMatchmakerMatched(OnMatchmakerMatched)
if err != nil {
    logger.WithField("error", err).Error("failed to register Matchmaker matched with fleet manager")
    return err
}