# Creating a Lobby System

**URL:** https://heroiclabs.com/docs/nakama/guides/concepts/lobby/
**Summary:** This guide demonstrates how to implement a multiplayer lobby system using server authoritative runtime code.
**Keywords:** lobby, multiplayer lobby, lobby system, match invites, public match, private match, invite friends to match, matchmaking, multiplayer, lobby chat
**Categories:** nakama, lobby, concepts

---


# Creating a Lobby System

This guide will show you how to implement a multiplayer lobby system using server authoritative runtime code. The lobby system will allow players to create and join public and private matches, invite friends to matches, and chat with other players in the lobby.

![A fully functional lobby system with public/private matches, friend invites, lobby chat and ready up functionality]({{< fingerprint_image "/images/pages/nakama/guides/concepts/lobby/menu-screen.png" >}})

## Prerequisites

This guide assumes you are already familiar with and have [set up a local Nakama instance](/docs/nakama/getting-started/install/docker/).

## Creating the match handler

The first thing we need to do is setup a basic authoritative match handler using custom server runtime code.

{{< note "important" "Authoritative Match Handlers" >}}
To learn more about authoritative match handlers please see the [authoritative multiplayer](/docs/nakama/concepts/multiplayer/authoritative/) and [match handler API](/docs/nakama/server-framework/typescript-runtime/function-reference/match-handler/) documentation.
{{< / note >}}

We'll start by defining the server's [tick rate](/docs/nakama/concepts/multiplayer/authoritative/#tick-rate) and how long we should keep the match active when there are no connected players.

{{< code type="server" >}}
```typescript
const tickRate = 10;
const maxEmptyTicks = tickRate * 10;// tickRate * seconds
```
{{< / code >}}

{{< code type="server" >}}
```go
const tickRate int = 10
const maxEmptyTicks int = tickRate * 10 // tickRate * seconds
```
{{< / code>}}

{{< code type="lua" >}}
```lua
local tick_rate = 10
local max_empty_ticks = tick_rate * 10 -- tick_rate * seconds
```
{{< / code >}}

Next we'll define the shape of our match state, making sure to keep track of the following information:

- `players` connected or connecting and their state
- `playerCount` of connected players
- `requiredPlayerCount` we expect in the match before it can begin
- if the game `isPrivate` or public
- the current `gameState`: either `WaitingForPlayers`, `WaitingForPlayersReady` or `InProgress`
- how many `emptyTicks` the game has been active without any players

For each player we will keep track of their `presence` and their `isReady` state.

{{< code type="server" >}}
```typescript
interface LobbyMatchState extends nkruntime.MatchState {
  players: { [userId: string]: PlayerState },
  playerCount: number,
  requiredPlayerCount: number,
  isPrivate: boolean,
  gameState: GameState,
  emptyTicks: number
}

interface PlayerState {
  presence: nkruntime.Presence,
  isReady: boolean
}

enum GameState { WaitingForPlayers, WaitingForPlayersReady, InProgress }
```
{{< / code >}}

{{< code type="server" >}}
```go
type LobbyMatch struct{}

type LobbyMatchState struct {
	Players             map[string]PlayerState
	PlayerCount         int
	RequiredPlayerCount int
	IsPrivate           bool
	GameState           GameState
	EmptyTicks          int
}

type PlayerState struct {
	Presence runtime.Presence
	IsReady  bool
}

const (
	WaitingForPlayers GameState = iota
	WaitingForPlayersReady
	InProgress
)
```
{{< / code>}}

{{< code type="lua" >}}
```lua
// Define variables for the 3 match game states
local WAITING_FOR_PLAYERS = 1
local WAITING_FOR_PLAYERS_READY = 2
local IN_PROGRESS = 3
```
{{< / code >}}

Next we'll implement some basic functionality necessary to get our authoritative match handler running. This includes handling players joining/disconnecting, ending the match if it has been empty for too long, and changing the match's game state depending on how many players are connected and ready.

### MatchInit

The `MatchInit` function takes care of the setup and configuration of our match. Here we define the match's initial state, give the match a label that will allow others to find it using the [match listing API](/docs/nakama/concepts/multiplayer/match-listing/), and then return the state, tick rate, and label.

{{< code type="server" >}}
```typescript
const MatchInit: nkruntime.MatchInitFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, params: {[key: string]: string}) {
  // Determine if the match should be private based on the passed in params
  const isPrivate = params.isPrivate === "true";

  // Define the match state
  const state: LobbyMatchState= {
    players: {},
    isPrivate,
    playerCount: 0,
    requiredPlayerCount: 2,
    gameState: GameState.WaitingForPlayers,
    emptyTicks: 0
  };

  // Update the match label to surface important information for players who are searching for a match
  const label = JSON.stringify({ isPrivate: state.isPrivate.toString(), playerCount: state.playerCount, requiredPlayerCount: state.requiredPlayerCount });

  return {
      state,
      tickRate,
      label
  };
};
```
{{< / code >}}

{{< code type="server" >}}
```go
func (m *LobbyMatch) MatchInit(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, params map[string]interface{}) (interface{}, int, string) {
	isPrivate := false

	if val, ok := params["isPrivate"]; ok {
		isPrivate = val.(bool)
	}

	state := &LobbyMatchState{
		Players:             make(map[string]*PlayerState),
		PlayerCount:         0,
		RequiredPlayerCount: 2,
		IsPrivate:           isPrivate,
		GameState:           WaitingForPlayers,
		EmptyTicks:          0,
	}

	// Create the match label
	label := map[string]interface{}{
		"isPrivate":           strconv.FormatBool(state.IsPrivate),
		"playerCount":         state.PlayerCount,
		"requiredPlayerCount": state.RequiredPlayerCount,
	}

	bytes, err := json.Marshal(label)
	if err != nil {
		logger.Error("error marshaling json")
		return nil, tickRate, ""
	}

	labelJson := string(bytes)

	return state, tickRate, labelJson
}
```
{{< / code>}}

{{< code type="server" >}}
```lua
local M = {}

function M.match_init(context, initial_state)
	local is_private = false

	if initial_state.is_private then
		is_private = initial_state.is_private
	end

	local state = {
		players = {},
		player_count = 0,
		required_player_count = 2,
		is_private = is_private,
		game_state = WAITING_FOR_PLAYERS,
		empty_ticks = 0
	}

  -- Create the match label
	local label = nk.json_encode({ ["isPrivate"] = state.is_private, ["playerCount"] = state.player_count, ["requiredPlayerCount"] = state.required_player_count })

	return state, tick_rate, label
end
```
{{< / code >}}

### MatchJoinAttempt

In the `MatchJoinAttempt` function we decide if a player can join based on whether there are enough spots left in the match (determined by the `requiredPlayerCount` state variable).

If the player is allowed to join, we reserve their spot in the match by adding their user ID to the `players` dictionary.

{{< code type="server" >}}
```typescript
const MatchJoinAttempt: nkruntime.MatchJoinAttemptFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: LobbyMatchState, presence: nkruntime.Presence, metadata: {[key: string]: any }) {
  // Accept new players unless the required amount has been fulfilled
  let accept = true;
  if (Object.keys(state.players).length >= state.requiredPlayerCount) {
    accept = false;
  }

  // Reserve the spot in the match
  state.players[presence.userId] = { presence: null, isReady: false };
  
  return {
      state,
      accept
  };
};
```
{{< / code >}}

{{< code type="server" >}}
```go
func (m *LobbyMatch) MatchJoinAttempt(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presence runtime.Presence, metadata map[string]string) (interface{}, bool, string) {
	matchState, ok := state.(*LobbyMatchState)
	if !ok {
		logger.Error("state not a valid state object")
		return nil
	}

	accept := true

	if len(matchState.Players) >= matchState.RequiredPlayerCount {
		accept = false
	}

	matchState.Players[presence.GetUserId()] = PlayerState{
		Presence: nil,
		IsReady:  false,
	}

	return matchState, accept, ""
}
```
{{< / code>}}

{{< code type="server" >}}
```lua
function M.match_join_attempt(context, dispatcher, tick, state, presence, metadata)
	-- Accept new players unless the required amount has been fulfilled
	local accept = true
	if #state.players >= state.required_player_count then
		accept = false
	end

    -- Reserve the presence in the match
    state.players[presence.user_id] = { presence = presence, is_ready = false }

    return state, accept
end
```
{{< / code >}}

### MatchJoin

The `MatchJoin` function is called when 1 or more players have joined the match. Here we update the `players` dictionary with each player's presence object and increment the match's `playerCount`.

We'll also use this as a point to check if the match is full. If it is, we can transition into the next state of the match which is the `WaitingForPlayersReady` state.

Finally we'll update the match's label to reflect the updated player counts.

{{< code type="server" >}}
```typescript
 const MatchJoin: nkruntime.MatchJoinFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: LobbyMatchState, presences: nkruntime.Presence[]) {
  // Populate the presence property for each player
  presences.forEach(function (presence) {
    state.players[presence.userId].presence = presence;
    state.playerCount++;
  });
  
  // If the match is full then update the state
  if (state.playerCount === state.requiredPlayerCount) {
    state.gameState = GameState.WaitingForPlayersReady;
  }

  // Update the match label
  const label = JSON.stringify({ isPrivate: state.isPrivate.toString(), playerCount: state.playerCount, requiredPlayerCount: state.requiredPlayerCount });
  dispatcher.matchLabelUpdate(label);

  return {
      state
  };
};
```
{{< / code >}}

{{< code type="server" >}}
```go
func (m *LobbyMatch) MatchJoin(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presences []runtime.Presence) interface{} {
	matchState, ok := state.(*LobbyMatchState)
	if !ok {
		logger.Error("state not a valid state object")
		return nil
	}

	// Populate the presence property for each player
	for i := 0; i < len(presences); i++ {
		matchState.Players[presences[i].GetUserId()].Presence = presences[i]
		matchState.PlayerCount++
	}

	// If the match is full then update the state
	if matchState.PlayerCount == matchState.RequiredPlayerCount {
		matchState.GameState = WaitingForPlayersReady
	}

	// Update the match label
	label := map[string]interface{} {
		"isPrivate": strconv.FormatBool(matchState.IsPrivate),
		"playerCount": matchState.PlayerCount,
		"requiredPlayerCount": matchState.RequiredPlayerCount
	}

	bytes, err := json.Marshal(label)
	if err != nil {
		logger.Error("error marshaling json")
		return matchState
	}

	dispatcher.MatchLabelUpdate(string(bytes))

	return matchState
}
```
{{< / code>}}

{{< code type="server" >}}
```lua
function M.match_join(context, dispatcher, tick, state, presences)
	-- Populate the presence property for each player
	for _, presence in ipairs(presences) do
		state.players[presence.user_id].presence = presence
		state.player_count = state.player_count + 1
	end

	-- If the match is full then update the state
	if state.player_count == state.required_player_count then
		state.game_state = WAITING_FOR_PLAYERS_READY
	end

	-- Update the match label
	local label = nk.json_encode({ ["isPrivate"] = state.is_private, ["playerCount"] = state.player_count, ["requiredPlayerCount"] = state.required_player_count })
	dispatcher.match_label_update(label)

	return state
end
```
{{< / code >}}

### MatchLeave

The `MatchLeave` function is where we can remove player's who have left the match (or disconnected) from the match state and perform any cleanup, such as decrementing the `playerCount` state.

{{< code type="server" >}}
```typescript
const MatchLeave: nkruntime.MatchLeaveFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: LobbyMatchState, presences: nkruntime.Presence[]) {
  // Remove the player from match state
  presences.forEach(function (presence) {
    delete(state.players[presence.userId]);
    state.playerCount--;
  });

  return {
      state
  };
};
```
{{< / code >}}

{{< code type="server" >}}
```go
func (m *LobbyMatch) MatchLeave(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presences []runtime.Presence) interface{} {
	matchState, ok := state.(*LobbyMatchState)
	if !ok {
		logger.Error("state not a valid state object")
		return nil
	}

	for i := 0; i < len(presences); i++ {
		// Remove the player from the match state
		delete(matchState.Players, presences[i].GetUserId())
		matchState.PlayerCount--
	}

	return matchState
}
```
{{< / code>}}

{{< code type="server" >}}
```lua
function M.match_leave(context, dispatcher, tick, state, presences)
    -- Removing leaving presences from match state
	for _, presence in ipairs(presences) do
		state.players[presence.user_id] = nil
		state.player_count = state.player_count - 1
	end

	return state
end
```
{{< / code >}}

### MatchLoop

Inside our `MatchLoop` function is where we will check to see if the match is currently empty. If it is, we can increase the `emptyTicks` value (or reset it if there are connected players).

Once the `emptyTicks` value meets or exceeds our previously defined `maxEmptyTicks` value, we terminate the match by returning `null`.

{{< code type="server" >}}
```typescript
const MatchLoop: nkruntime.MatchLoopFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: LobbyMatchState, messages: nkruntime.MatchMessage[]) {
   // If the match is empty, increment the empty ticks
  if (state.playerCount === 0) {
    state.emptyTicks++;
  } else {
    state.emptyTicks = 0;
  }

  // If the match has been empty for too long, end it
  if (state.emptyTicks >= maxEmptyTicks) {
    return null;
  }

  return {
      state
  };
};
```
{{< / code >}}

{{< code type="server" >}}
```go
func (m *LobbyMatch) MatchLoop(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, messages []runtime.MatchData) interface{} {
	matchState, ok := state.(*LobbyMatchState)
	if !ok {
		logger.Error("state not a valid state object")
    return nil
	}

	// If the match is empty, increment the empty ticks
	if matchState.playerCount == 0 {
		matchState.EmptyTicks++
	} else {
		matchState.EmptyTicks = 0
	}

	// If the match has been empty for too long, end it
	if matchState.EmptyTicks > maxEmptyTicks {
		return nil
	}

	return matchState
}
```
{{< / code>}}

{{< code type="server" >}}
```lua
function M.match_loop(context, dispatcher, tick, state, messages)
  -- If the match is empty, increment the empty ticks
	if state.player_count > 0 then
		state.empty_ticks = state.empty_ticks + 1
	else
		state.empty_ticks = 0
	end

  -- If the match has been empty for too long, end it
  if state.empty_ticks > max_empty_ticks then
      return nil
  end

	return state
end
```
{{< / code >}}

### MatchTerminate and MatchSignal

The `MatchTerminate` and `MatchSignal` functions are not used in this example, but we must still define them and return the match's state.

{{< code type="server" >}}
```typescript
const MatchTerminate: nkruntime.MatchTerminateFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: LobbyMatchState, graceSeconds: number) {
  return {
      state
  };
};

const MatchSignal: nkruntime.MatchSignalFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: LobbyMatchState, data: string) {
  return {
      state,
      data
  };
};
```
{{< / code >}}

{{< code type="server" >}}
```go
func (m *LobbyMatch) MatchTerminate(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, graceSeconds int) interface{} {
	return state
}

func (m *LobbyMatch) MatchSignal(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, data string) (interface{}, string) {
	return state, data
}
```
{{< / code>}}

{{< code type="server" >}}
```lua
function M.match_terminate(context, dispatcher, tick, state, grace_seconds)
	return state
end

function M.match_signal(context, dispatcher, tick, state, data)
end

return M
```
{{< / code >}}

### Registering the match handler

Last we'll register the match handler functions with the ID `"LobbyMatch"`. Any match created using this name will be bound to this match handler.

{{< code type="server" >}}
```typescript
// This happens inside the InitModule function
initializer.registerMatch<LobbyMatchState>("LobbyMatch", {
  matchInit: MatchInit,
  matchJoinAttempt: MatchJoinAttempt,
  matchJoin: MatchJoin,
  matchLeave: MatchLeave,
  matchLoop: MatchLoop,
  matchSignal: MatchSignal,
  matchTerminate: MatchTerminate
});
```
{{< / code >}}

{{< code type="server" >}}
```go
// This happens inside the InitModule function
if err := initializer.RegisterMatch("LobbyMatch", func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule) (runtime.Match, error) {
  return &LobbyMatch{}, nil
}); err != nil {
  logger.Error("unable to register: %v", err)
  return err
}
```
{{< / code>}}

{{< code type="server" >}}
```lua
-- To register the match handler in lua, name the file appropriately. e.g. LobbyHandler.lua
```
{{< / code >}}

## Registering the matchmaker handler

In order for our match handler to be used to create a match for players using the matchmaking service, we need to register a matchmaker matched handler and tell Nakama to create an instance of our lobby match. 

Here we also pass in a parameter called `isPrivate` with a value of `false`. This is used in the `MatchInit` function defined above to determine whether or not the match should be public or private.

{{< code type="server" >}}
```typescript
const OnRegisterMatchmakerMatched: nkruntime.MatchmakerMatchedFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, matches: nkruntime.MatchmakerResult[]) {
  // Create a public match and return it's match ID
  var matchId = nk.matchCreate("LobbyMatch", { isPrivate: false });
  logger.debug(`Created LobbyMatch with ID: ${matchId}`);

  return matchId;
};

// This happens inside the InitModule function
initializer.registerMatchmakerMatched(OnRegisterMatchmakerMatched);
```
{{< / code >}}

{{< code type="server" >}}
```go
// This happens inside the InitModule function
if err := initializer.RegisterMatchmakerMatched(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, entries []runtime.MatchmakerEntry) (string, error) {
	matchId, err := nk.MatchCreate(ctx, "LobbyMatch", nil)
	if err != nil {
		return "", err
	}

	return matchId, nil
}); err != nil {
	logger.Error("unable to register matchmaker matched hook: %v", err)
	return err
}
```
{{< / code>}}

{{< code type="server" >}}
```lua
nk.register_matchmaker_matched(function(context, matched_users)
    local match_id = nk.match_create("LobbyMatch", { is_private = false })
    return match_id
end)
```
{{< / code >}}

## Creating private/public matches

![Players can create a new match without waiting for others]({{< fingerprint_image "/images/pages/nakama/guides/concepts/lobby/create-private-match.png" >}})

Not all players will want to join a match using matchmaking. They may choose to create a match themselves. In order to facilitate this, we register an RPC that creates either a public or private match depending on the payload passed to it.

{{< code type="server" >}}
```typescript
const CreateMatchRpc: nkruntime.RpcFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) {
  // Assume the match will be public by default
  let isPrivate = false;

  // Get the isPrivate value from the payload if it exists
  if (payload) {
    const data = JSON.parse(payload);
    if (data.isPrivate) {
      isPrivate = data.isPrivate;
    }
  }
  
  // Create the match and return the match ID to the player
  const matchId = nk.matchCreate("LobbyMatch", { isPrivate });
  return JSON.stringify({ matchId });
};

// This happens inside the InitModule function
initializer.registerRpc("create-match", CreateMatchRpc);
```
{{< / code >}}

{{< code type="server" >}}
```go
// This happens inside the InitModule function
if err := initializer.RegisterRpc("create-match", func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
  // Assume the match will be public by default
  isPrivate := false

  // Get the isPrivate value from the payload if it exists
  var data map[string]interface{}
  if err := json.Unmarshal([]byte(payload), &data); err != nil {
    logger.Error("error unmarshaling payload: %v", err)
    return "", err
  }

  if val, ok := data["isPrivate"]; ok {
    isPrivate = val.(bool)
  }

  params := map[string]interface{}{
    "isPrivate": isPrivate,
  }

  // Create the match and return the match ID to the player
  matchId, err := nk.MatchCreate(ctx, "LobbyMatch", params)
  if err != nil {
    return "", err
  }
  
  response := map[string]interface{}{
    "matchId": matchId,
  }
  
  bytes, err := json.Marshal(response)
  if err != nil {
    logger.Error("error marshaling response: %v", err)
    return "", err
  }

  return string(bytes), nil
}); err != nil {
  logger.Error("unable to register create match rpc: %v", err)
  return err
}
```
{{< / code>}}

{{< code type="server" >}}
```lua
nk.register_rpc(function(context, payload)
  -- Assume the match will be public by default
  local is_private = false

  -- Get the isPrivate value from the payload if it exists
  local data = nk.json_decode(payload)
  if data["isPrivate"] then
      is_private = true
  end

  -- Create the match and return the match ID to the player
  local match_id = nk.match_create("LobbyMatch", { is_private = is_private })
  return nk.json_encode({ ["matchId"] = match_id })
end, "create-match")
```
{{< / code >}}

## Joining the match

Whenever a player gets a match ID, either by receiving one from the matchmaker or as a response to creating a match via an RPC, we will join the match from the client.

```csharp
match = await socket.JoinMatchAsync(matchId);
```

When we join the match, we will inspect the match's existing presences and display them on the lobby screen.

```csharp
// A function that loops through the presences and spawns a player game object for each presence
AddPresences(match.Presences);
```

We also subscribe to the match presence event so that we can update the display of players in the lobby whenever a player joins or leaves.

```csharp
socket.ReceivedMatchPresence += OnReceivedMatchPresence;

// ...

private void OnReceivedMatchPresence(IMatchPresenceEvent presenceEvent)
{
    AddPresences(presenceEvent.Joins);
    RemovePresences(presenceEvent.Leaves);
}
```

## Player "Ready" state

![Players can mark themselves as ready]({{< fingerprint_image "/images/pages/nakama/guides/concepts/lobby/private-match-ready-up.png" >}})

There is one thing currently missing from our match handler, the ability for players to signal they're ready to begin the match.

We achieve this by defining an [Op Code](/docs/nakama/concepts/multiplayer/relayed/#op-codes) that the player can send to the server which updates their `isReady` status and broadcasts it to all other connected players.

We'll also define one to indicate that the game is starting.

{{< code type="server" >}}
```typescript
const READY_OP_CODE = 1;
const GAME_STARTING_OP_CODE = 2;
```
{{< / code >}}

{{< code type="server" >}}
```go
const ReadyOpCode = 1
const GameStartingOpCode = 2
```
{{< / code>}}

{{< code type="server" >}}
```lua
local READY_OP_CODE = 1
local GAME_STARTING_OP_CODE = 2
```
{{< / code >}}

We'll add the following to the start of the `MatchLoop` function to listen for the ready Op Code and broadcast it to other connected players.

{{< code type="server" >}}
```typescript
messages.forEach(function (message) {
  // If the message is a Ready message, update the player's isReady status and broadcast it to other players
  if (message.opCode === READY_OP_CODE) {
    state.players[message.sender.userId].isReady = true;
    dispatcher.broadcastMessage(READY_OP_CODE, JSON.stringify({ userId: message.sender.userId }));

    // Check to see if all players are now ready
    var allReady = true;
    Object.keys(state.players).forEach(function (userId) {
      if (!state.players[userId].isReady) {
        allReady = false;
      }
    });

    // If all players are ready, transition to InProgress state and broadcast the game starting event
    if (allReady && Object.keys(state.players).length === state.requiredPlayerCount) {
      state.gameState = GameState.InProgress;
      dispatcher.broadcastMessage(GAME_STARTING_OP_CODE);
    }
  }
});
```
{{< / code >}}

{{< code type="server" >}}
```go
for i := 0; i < len(messages); i++ {
  // If the message is a Ready message, update the player's isReady status and broadcast it to other players
  if messages[i].GetOpCode() == READY_OP_CODE {
    matchState.Players[messages[i].GetUserId()].IsReady = true

    data := map[string]interface{} {
      "userId": messages[i].GetUserId(),
    }

    bytes, err := json.Marshal(data)
    if err != nil {
      logger.Error("error marshaling message: %v", err)
      continue
    }

    dispatcher.BroadcastMessage(READY_OP_CODE, bytes, nil, nil, true)
  }

  // Check to see if all players are ready
  allReady := true
  for _, p := range matchState.Players {
    if !p.IsReady {
      allReady = false
      break
    }
  }

  // If all players are ready, transition to InProgress state and broadcast the game starting event
  if allReady && matchState.PlayerCount == matchState.RequiredPlayerCount {
    matchState.GameState = InProgress
    dispatcher.BroadcastMessage(GAME_STARTING_OP_CODE, nil, nil, nil, true)
  }
}
```
{{< / code>}}

{{< code type="server" >}}
```lua
for _, message in ipairs(messages) do
  -- If the message is a Ready message, update the player's isReady status and broadcast it to other players
  if message.op_code == READY_OP_CODE then
    state.players[message.sender.user_id].is_ready = true
    dispatcher.broadcast_message(READY_OP_CODE, nk.json_encode({ ["userId"] = message.sender.user_id }))

    -- Check to see if all players are now ready
    local all_ready = true
    for _, player in ipairs(state.players) do
      if player.is_ready == false then
        all_ready = false
      end
    end

    -- If all players are ready, transition to InProgress state and broadcast the game starting event
    if all_ready and state.player_count == state.required_player_count then
      state.game_state = IN_PROGRESS
      dispatcher.broadcast_message(GAME_STARTING_OP_CODE)
    end
  end
end
```
{{< / code >}}

We also need to let any newly joined players know if any currently connected players have already flagged themselves as ready. We'll do this by adding the following to the `MatchJoin` function, just before the return statement.

{{< code type="server" >}}
```typescript
// For each "ready" player, let the joining players know about their status
Object.keys(state.players).forEach(function (key) {
  const player = state.players[key];

  if (player.isReady) {
    dispatcher.broadcastMessage(READY_OP_CODE, JSON.stringify({ userId: player.presence.userId }), presences);
  }
});
```
{{< / code >}}

{{< code type="server" >}}
```go
// For each "ready" player, let the joining players know about their status
for _, p := range matchState.Players {
  if p.IsReady {
    data := map[string]interface{}{
      "userId": p.Presence.GetUserId(),
    }

    bytes, err := json.Marshal(data)
    if err != nil {
      logger.Error("error marshaling message: %v", err)
      continue
    }

    dispatcher.BroadcastMessage(ReadyOpCode, bytes, presences, nil, true)
  }
}
```
{{< / code>}}

{{< code type="server" >}}
```lua
-- For each "ready" player, let the joining players know about their status
for _, player in ipairs(state.players) do
  if player.is_ready then
    dispatcher.broadcast_message(READY_OP_CODE, nk.json_encode({ ["userId"] = player.presence.user_id }))
  end
end
```
{{< / code >}}

From the client side, we need to listen for any match data being received, interpret the message based on it's Op Code, and handle it appropriately.

```csharp
socket.ReceivedMatchState += OnReceivedMatchState;

// ...

private void OnReceivedMatchState(IMatchState matchState)
{
  const long ReadyOpCode = 1;

  if (matchState.OpCode == ReadyOpCode)
  {
    var json = Encoding.UTF8.GetString(matchState.State);
    var data = json.FromJson<Dictionary<string, string>>();
    if (data.ContainsKey("userId") && PlayerEntries.ContainsKey(data["userId"]))
    {
      var userId = data["userId"];
      // Update the user interface to show that this user is ready
    }
  }
}
```

## Lobby private chat

![Players can chat privately inside the lobby]({{< fingerprint_image "/images/pages/nakama/guides/concepts/lobby/joined-public-match.png" >}})

We'll use a [room chat channel](/docs/nakama/concepts/chat/#rooms) to provide a way for players in the lobby to talk to each other before the match begins.

From the client, we'll use the current match ID to join the appropriate chat channel after they have joined the match.

```csharp
channel = await socket.JoinChatAsync(match.Id, ChannelType.Room);
```

We'll then allow players to send chat messages to this channel.

```csharp
var messageContent = new Dictionary<string, string>
{
    {"message", LobbyChatInputField.text}
};

await socket.WriteChatMessageAsync(channel, messageContent.ToJson());
```

As well as being able to send messages, we also need to listen for incoming messages. To do this we'll subscribe to the channel message event and handle it by instantiating a new chat message prefab and adding it to the chat box.

```csharp
socket.ReceivedChannelMessage += OnReceivedChannelMessage;

// ...

private void OnReceivedChannelMessage(IApiChannelMessage channelMessage)
{
  var chatMessage = Instantiate(LobbyChatMessagePrefab, LobbyChatContainer.transform).GetComponent<ChatMessage>();
  var content = JsonParser.FromJson<Dictionary<string, string>>(channelMessage.Content);
  chatMessage.Init(channelMessage.Username, content["message"]);
}
```

## Inviting friends to the match

![A match invite notification pops up in-game]({{< fingerprint_image "/images/pages/nakama/guides/concepts/lobby/private-match-invite-received.png" >}})

To give our players the ability to invite their friends to the match, we first need to show the player their friends list. We'll display this by using the list friends API.

```csharp
var result = await client.ListFriendsAsync(session, null, 100);
```

For mutual friends (state `0`) we'll display a button that allows the player to invite their friend to the current match. 

For this we'll create an RPC that takes a friend's user ID and sends them a notification with a code of `1` (we'll define this as a match invite notification) which they can listen for on the client and then show a match invite in-game. This RPC expects a payload that contains a `friendId` and a `matchId`.

{{< code type="server" >}}
```typescript
const InviteFriendRpc: nkruntime.RpcFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) {
  const data = JSON.parse(payload);
  if (!data || !data.friendId || !data.matchId) {
    logger.error('Invalid payload.');
    throw new Error('Invalid payload.');
  }

  const notification: nkruntime.NotificationRequest = {
    code: 1,
    content: { 
      username: ctx.username,
      message: 'Join me for a match',
      matchId: data.matchId
    },
    persistent: false,
    senderId: ctx.userId,
    subject: "Match Invite",
    userId: data.friendId
  };

  nk.notificationsSend([ notification ]);
};

// This happens inside the InitModule function
initializer.registerRpc("invite-friend", InviteFriendRpc);
```
{{< / code >}}

{{< code type="server" >}}
```go
if err := initializer.RegisterRpc("invite-friend", func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
  userId, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
  if !ok {
    logger.Error("unable to get user ID")
    return "", runtime.NewError("internal error", 13)
  }

  username, ok := ctx.Value(runtime.RUNTIME_CTX_USERNAME).(string)
  if !ok {
    logger.Error("unable to get user ID")
    return "", runtime.NewError("internal error", 13)
  }

  var data map[string]interface{}
  if err := json.Unmarshal([]byte(payload), &data); err != nil {
    logger.Error("error unmarshaling payload: %v", err)
    return "", runtime.NewError("internal error", 13)
  }

  friendId, ok := data["friendId"]
  if !ok {
    logger.Error("invalid payload, missing friendId")
    return "", runtime.NewError("invalid payload, missing friendId", 3)
  }

  matchId, ok := data["matchId"]
  if !ok {
    logger.Error("invalid payload, missing matchId")
    return "", runtime.NewError("invalid payload, missing matchId", 3)
  }

  content := map[string]interface{}{
    "username": username,
    "message":  "Join me for a match",
    "matchId":  matchId,
  }

  err := nk.NotificationSend(ctx, friendId.(string), "Match Invite", content, 1, userId, false)
  if err != nil {
    return "", runtime.NewError("unable to send friend invite", 13)
  }

  return "", nil
}); err != nil {
  logger.Error("unable to register create match rpc: %v", err)
  return err
}
```
{{< / code>}}

{{< code type="server" >}}
```lua
nk.register_rpc(function(context, payload)
    local data = nk.json_decode(payload)
    if data == nil or data["friendId"] == nil or data["matchId"] == nil then
        error({ "invalid payload",  3 })
    end

    local content = {
        username = context.username,
        message = "Join me for a match",
        ["matchId"] = data["matchId"]
    }

    nk.notification_send(data["friendId"], "Match Invite", content, 1, context.user_id, false)
end, "invite-friend")
```
{{< / code >}}

On the client side we can listen for incoming notifications by subscribing to the event:

```csharp
socket.ReceivedNotification += OnReceivedNotification;
```

Once a notification is received, we check to see if it is a match invite (code `1`) and if so, extract the `matchId` and open an invite panel in-game.

```csharp
private async void OnReceivedNotification(IApiNotification notification)
{
    // Notification code 1 is a friend match invite
    if (notification.Code == 1)
    {
        var content = JsonParser.FromJson<Dictionary<string, string>>(notification.Content);
        if (content.ContainsKey("matchId"))
        {
            friendInviteMatchId = content["matchId"];
            FriendInviteMessage.text = $"{content["username"]} has invited you to a match!";
            FriendInvitePopupPanel.SetActive(true);
        }
    }
}
```

## Finding public matches

![Players can find active public matches]({{< fingerprint_image "/images/pages/nakama/guides/concepts/lobby/public-match-list.png" >}})

Along with the ability to matchmake and create private matches, we also want to allow players to find currently active public matches.

We'll achieve this by using the [match listing API](/docs/nakama/concepts/multiplayer/match-listing/). Below we pass in a query parameter that explicitly requires the match's `isPrivate` value to be `false`.

```csharp
var result = await client.ListMatchesAsync(session, 0, 100, 100, true, string.Empty, "+label.isPrivate:false");
```

We'll use each match's label to display important information to the player on the results screen, including how many players are currently in the match and how many players are required.

```csharp
var matchLabelData = JsonParser.FromJson<Dictionary<string, string>>(match.Label);
label.text = $"{matchLabelData["playerCount"]}/{matchLabelData["requiredPlayerCount"]}";
```

## Starting the match

![Finally, the match transitions from the lobby and into your game!]({{< fingerprint_image "/images/pages/nakama/guides/concepts/lobby/match-in-progress.png" >}})

When the match is full and all players have indicated that they are ready, we can begin the match. We already handled the broadcasting of the `GAME_START_OP_CODE` and changing the match state to `InProgress` in the `MatchLoop` function above, so all that is left to do is listen for this message on the client and handle it appropriately.

```csharp
private void OnReceivedMatchState(IMatchState matchState)
{
  // ...

  const long StartGameOpCode = 2;

  if (matchState.OpCode == StartGameOpCode)
  {
    LobbyPanel.SetActive(false);
    GamePanel.SetActive(true);
  }
}
```
