Code Samples #

This page provides some common examples for the functionality available that can be used as templates when developing your project using the TypeScript runtime.

Match handler #

A match handler represents all server-side functions for handling game inputs and operations for authoritative multiplayer matches. See the Match Handler API and Match Runtime API reference pages to learn about the match handler functions.

This is an example of a Ping-Pong match handler. Messages received by the server are broadcast back to the peer who sent them.

 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
77
78
79
80
const matchInit = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, params: {[key: string]: string}): {state: nkruntime.MatchState, tickRate: number, label: string} {
  logger.debug('Lobby match created');

  const presences: {[userId: string]: nkruntime.Presence}

  return {
    state: { presences },
    tickRate: 1,
    label: ''
  };
};

const matchJoinAttempt = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, presence: nkruntime.Presence, metadata: {[key: string]: any }) : {state: nkruntime.MatchState, accept: boolean, rejectMessage?: string | undefined } | null {
  logger.debug('%q attempted to join Lobby match', ctx.userId);

  return {
    state,
    accept: true
  };
}

const matchJoin = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, presences: nkruntime.Presence[]) : { state: nkruntime.MatchState } | null {
  presences.forEach(function (presence) {
    state.presences[presence.userId] = presence;
    logger.debug('%q joined Lobby match', presence.userId);
  });

  return {
    state
  };
}

const matchLeave = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, presences: nkruntime.Presence[]) : { state: nkruntime.MatchState } | null {
  presences.forEach(function (presence) {
    delete (state.presences[presence.userId]);
    logger.debug('%q left Lobby match', presence.userId);
  });

  return {
    state
  };
}

const matchLoop = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, messages: nkruntime.MatchMessage[]) : { state: nkruntime.MatchState} | null {
  logger.debug('Lobby match loop executed');

  Object.keys(state.presences).forEach(function (key) {
    const presence = state.presences[key];
    logger.info('Presence %v name $v', presence.userId, presence.username);
  });

  messages.forEach(function (message) {
    logger.info('Received %v from %v', message.data, message.sender.userId);
    dispatcher.broadcastMessage(1, message.data, [message.sender], null);
  });

  return {
    state
  };
}

const matchTerminate = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, graceSeconds: number) : { state: nkruntime.MatchState} | null {
  logger.debug('Lobby match terminated');

  const message = `Server shutting down in ${graceSeconds} seconds.`;
  dispatcher.broadcastMessage(2, message, null, null);

  return {
    state
  };
}

const matchSignal = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, data: string) : { state: nkruntime.MatchState, data?: string } | null {
  logger.debug('Lobby match signal received: ' + data);

  return {
    state,
    data: "Lobby match signal received: " + data
  };
}

Context #

This example demonstrates extracting the ID of the calling user and a key stored as an environment variable:

1
2
3
4
5
6
7
8
9
// Getting the calling user ID from the context
const userId = ctx.userId;

// Getting the environment variables from the context
const env = ctx.env;
const secretKey = env["SECRET_KEY"];
if !secretKey {
  // Did not find the environment variable
}

Database handler #

This example creates a system ID - an ID that cannot be used from a client - and the custom SQL query inserting it in the users table:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let InitModule: nkruntime.InitModule =
        function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
    let systemId: string = ctx.env["SYSTEM_ID"]

    nk.sqlExec(`
INSERT INTO users (id, username)
VALUES ($1, $2)
ON CONFLICT (id) DO NOTHING
    `, { systemId, "system_id" })

    logger.Info('system id: %s', systemId)
}

RPC #

The example below registers a function with the identifier custom_rpc_func_id. This ID can then be used within client code to send an RPC message to execute the function on the server and return the result.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let customFuncRpc: nkruntime.RpcFunction =
function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) {
	logger.info('payload: %q', payload);

	// "payload" is bytes sent by the client we'll JSON decode it.
	let json = JSON.parse(payload);

	return JSON.stringify(json);
}

// Register as an after hook for the appropriate feature, this call should be in InitModule.
initializer.registerRpc("custom_rpc_func_id", customFuncRpc);

Before hook #

The code example below fetches the current user’s profile and checks the metadata, which is assumed to be JSON encoded with "{level: 12}" in it. If a user’s level is too low, an error is thrown to prevent the Friend Add message from being passed onwards in the server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let userAddFriendLevelCheck: nkruntime.BeforeHookFunction<AddFriendsRequest> =
function(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, data: nkruntime.AddFriendsRequest): nkruntime.AddFriendsRequest {
	let userId = ctx.userId;

	let users: nkruntime.User[];
	try {
			users = nk.usersGetId([ userId ]);
	} catch (error) {
			logger.error('Failed to get user: %s', error.message);
			throw error;
	}

	// Let's assume we've stored a user's level in their metadata.
	if (users[0].metadata.level < 10) {
			throw Error('Must reach level 10 before you can add friends.');
	}

	// important!
	return data;
};

// Register as an after hook for the appropriate feature, this call should be in InitModule.
initializer.registerBeforeAddFriends(userAddFriendLevelCheck);

After hook #

The example code below writes a record to a user’s storage when they add a friend. Any data returned by the function will be discarded.

 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
// The AddFriends function does not return a payload, hence why the outPayload argument is null.
let afterAddFriends: nkruntime.AfterHookFunction<null, AddFriendsRequest> =
function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, outPayload: null, inPayload: nkruntime.AddFriendsRequest) {
	let userId = ctx.userId;
	if (!userId) {
			throw Error('Missing user ID.');
	}

	let userIds = inPayload.ids;
	let storageObj: nkruntime.StorageWriteRequest = {
			collection: 'rewards',
			key: 'reward',
			userId: userId,
			value: { userIds },
	};

	try {
			nk.storageWrite([ storageObj ]);
	} catch (error) {
			logger.error('Error writing storage object: %s', error.message);
			throw error;
	};

	return null; // Can be omitted, will return `undefined` implicitly
};

// Register as an after hook for the appropriate feature, this call should be in InitModule.
initializer.registerAfterAddFriends(afterAddFriends);

Example module #

As a fun example let’s use the Pokéapi and build a helpful module.

 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
const apiBaseUrl = 'https://pokeapi.co/api/v2';

let InitModule: nkruntime.InitModule =
function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
	initializer.registerRpc('get_pokemon', getPokemon);
};

function lookupPokemon(nk: nkruntime.Nakama, name: string) {
	let url = apiBaseUrl + '/pokemon/' + name;
	let headers = { 'Accept': 'application/json' };
	let response = nk.httpRequest(url, 'get', headers);

	return JSON.parse(response.body);
}

let getPokemon: nkruntime.RpcFunction =
function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) {
	// We'll assume payload was sent as JSON and decode it.
	let json = JSON.parse(payload);
	let pokemon;

	try {
		pokemon = lookupPokemon(nk, json['PokemonName']);
	} catch (error) {
		logger.error('An error occurred looking up pokemon: %s', error.message);
		throw error;
	}

	let result = {
		name: pokemon.name,
		height: pokemon.height,
		weight: pokemon.weight,
		image: pokemon.sprites.front_default,
	}

	return JSON.stringify(result);
}

You can now make an RPC call for a Pokémon from a client:

Client
1
2
3
curl "http://127.0.0.1:7350/v2/rpc/get_pokemon" \
  -H 'authorization: Bearer <session token>'
  -d '"{\"PokemonName\": \"dragonite\"}"'
Client
1
2
3
4
const payload = { "PokemonName": "dragonite"};
const rpcid = "get_pokemon";
const pokemonInfo = await client.rpc(session, rpcid, payload);
console.log("Retrieved pokemon info: %o", pokemonInfo);
Client
1
2
3
4
var payload = JsonWriter.ToJson(new { PokemonName = "dragonite" });
var rpcid = "get_pokemon";
var pokemonInfo = await client.RpcAsync(session, rpcid, payload);
System.Console.WriteLine("Retrieved pokemon info: {0}", pokemonInfo);
Client
1
2
3
4
5
6
7
8
auto successCallback = [](const NRpc& rpc)
{
  	std::cout << "Retrieved pokemon info: " << rpc.payload << std::endl;
};

string payload = "{ \"PokemonName\": \"dragonite\" }";
string rpcid = "get_pokemon";
client->rpc(session, rpcid, payload, successCallback);
Client
1
2
3
4
5
6
Map<String, String> payloadData = new HashMap<>();
payloadData.put("PokemonName", "dragonite");
String payload = new Gson().toJson(payloadData, payloadData.getClass());
String rpcid = "get_pokemon";
Rpc pokemonInfo = client.rpc(session, rpcid, payload);
System.out.format("Retrieved pokemon info: %s", pokemonInfo.getPayload());
Client
1
2
3
4
5
6
7
var payload = {"PokemonName": "dragonite"}
var rpc_id = "get_pokemon"
var pokemon_info : NakamaAPI.ApiRpc = yield(client.rpc_async(session, rpc_id, JSON.print(payload)), "completed")
if pokemon_info.is_exception():
	print("An error occurred: %s" % pokemon_info)
	return
print("Retrieved pokemon info: %s" % [parse_json(pokemon_info.payload)])
Client
1
2
3
4
5
6
7
var payload = {"PokemonName": "dragonite"}
var rpc_id = "get_pokemon"
var pokemon_info : NakamaAPI.ApiRpc = await client.rpc_async(session, rpc_id, JSON.stringify(payload))
if pokemon_info.is_exception():
	print("An error occurred: %s" % pokemon_info)
	return
print("Retrieved pokemon info: %s" % [JSON.parse_string(pokemon_info.payload)])
Client
1
2
3
4
5
6
7
8
POST /v2/rpc/get_pokemon
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "PokemonName": "dragonite"
}
Client
1
2
3
4
local payload = { PokemonName = "dragonite"}
local rpcid = "get_pokemon"
local pokemon_info = client.rpc_func(rpcid, json.encode(payload)) 
pprint("Retrieved pokemon info:", pokemon_info)

Related Pages