Client pass in match 'map', then Join or Create?

I wanted to have the client pass in either an obfuscated ‘id’, like ‘3’ or ‘22’, or perhaps even a map ID, like ‘cellars’ or ‘ice_caves’… either way. I want the client to pass in a map ‘id’ for the type of match they want to join. In this RPC, I want to do some match making logic, for now it is a simple ‘map == map i want’ logic. Later I plan to add in Elo, levels, etc. Here is what I have for the RPC:

local nk = require("nakama")

local function get_match_id(context, payload)
	local json = nk.json_decode(payload)
	local limit = 10
	local authoritative = true
	local label = ""
	local min_size = tonumber(json['min'])
	local max_size = tonumber(json['max'])
	local map_id = json['map']
	local query = "+label.map:" .. map_id

	local matches = nk.match_list(limit, authoritative, label, min_size, max_size, query)

	if(#matches > 0) then
		table.sort(matches, function (a, b)
			return a.size > b.size
		end)
		current_match = matches[1]
		return current_match.match_id
	end

	if current_match == nil then
		current_match = nk.match_create(map_id, {})
		return current_match.match_id
	end
end

This breaks because it says I’m trying to modify a read only table in the ‘create’ method… I’m new to Lua, so struggling as I learn.

In turn, I have this in ‘cellars.lua’:

local match_control = {}

function match_control.match_init(context, params) -- state, tickrate, label
	local state = {
		presences = {}
	}

	local tick_rate = 10
	local label = "map=cellars"--[[move this to 'create or join game' passing in Id]]--

	return state, tick_rate, label
end

… rest of code to add the module below this ‘init’ method.

Is this the correct way to do it? Pass in the ‘name’ of the .lua file? Or would I keep the match_init universal, and pass in the map id to the match_create parameters?

How do I go from payload to RPC, to creating or joining a match, using parameters either define in the payload, or else pulled from the DB in the RPC, to the match_create?

I have some steps in the right direction, but this does not create an ‘authoritative’ game…

Here is my Unity code:

public async Task ConnectToMatchMakerLobby(string mapId)
{
    var client = refs.Get<Client>();
    var session = refs.Get<ISession>();

    Debug.Log("------->ConnectToMatchMakerLobby");
    var stringProperties = new Dictionary<string, string>
    {
        {"map", mapId}
    };
    
    var numericProperties = new Dictionary<string, double>
    {
    };
    
    Debug.Log("MapId:"+mapId);
    
    var socket = refs.Get<ISocket>();
    socket.ReceivedMatchmakerMatched += OnReceivedMatchmakerMatched;
    
    var matchmakerTicket = await socket.AddMatchmakerAsync("properties.map:"+mapId, 2, 2, stringProperties, numericProperties);
    matchTicket = matchmakerTicket.Ticket;
    refs.Bind(matchmakerTicket);
    Debug.Log("------->Matchmaker: Looking For match:"+matchTicket);
}

Here is the ‘match_rpc.lua’

local nk = require("nakama")
local function matchmaker_matched(context, matched_users)
	return nk.match_create("matchcontrol", { expected_users = matched_users })
end
nk.register_matchmaker_matched(matchmaker_matched)

Here is matchcontrol.lua

local match_control = {}

function match_control.match_init(context, params) -- state, tickrate, label
	local state = {
		presences = {}
	}

	local tick_rate = 10
	local label = "map="..params.map--[[move this to 'create or join game' passing in Id]]--

	return state, tick_rate, label
end

-- see how to send tick, etc.
function match_control.match_join_attempt(context, dispatcher, tick, state, presence, metadata)
	if (state.presences[presence.user_id] ~= nil) then
		return state, false, "User already joined."
	end
	return state, true
end

-- see how to send tick, etc.
function match_control.match_join(context, dispatcher, tick, state, presences)
	for _, presence in ipairs(presences) do
		state.presences[presence.user_id] = presence
	end
	return state
end

-- see how to send tick, etc.
function match_control.match_leave(context, dispatcher, tick, state, presences)
	for _, presence in ipairs(presences) do
		state.presences[presence.user_id] = nil
	end
	return state
end

-- see how to send tick, etc. validate inputs, positions, etc
function match_control.match_loop(context, dispatcher, tick, state, messages)
	--[[for _, p in pairs(state.presences) do
        nk.logger_info(string.format("Presence %s named %s", p.user_id, p.username))
    end
    for _, m in ipairs(messages) do
        nk.logger_info(string.format("Received %s from %s", m.data, m.sender.username))
        local decoded = nk.json_decode(m.data)
        for k, v in pairs(decoded) do
            nk.logger_info(string.format("Key %s contains value %s", k, v))
        end
        -- PONG message back to sender
        dispatcher.broadcast_message(1, message.data, { message.sender })
    end]]--
    return state
end

-- see how to send tick, etc. leave match, play animation, taking players back to menu, cleaning up state
function match_control.match_terminate(context, dispatcher, tick, state, grace_seconds)
	local message = "Server shutting down in " .. grace_seconds .. " seconds"
    -- dispatcher.broadcast_message(2, message)
    -- return nil
    return state
end

return match_control

I’m getting closer, they can see each other, but it is not an authoritative match…

Any advice or suggestions is welcome.

@Bunzaga I can’t see anything immediately wrong with your code snippets, but the picture is incomplete. You’re on the right track with a server-side matchmaker matched hook, and a match handler module. You’re also correct that using nk.match_create("matchcontrol", ... will create an authoritative match based on the matchcontrol.lua module, if it exists.

but it is not an authoritative match

Can you go into a bit more depth here? Do you mean your users connect to a match, but it’s a relayed match rather than an authoritative one? How are you determining this?

Yes, they connect, but when I look at the Nakama console, it has: Match ID “91173bff-8c0c-4040-b872-ad69743af1d7.”, Presence Count “2”, Authoritative “No”, Handler Name is blank, and Tick Rate is “-”.

I saw in the match_init, I had label = “map=”…params.map I don’t think this does anything does it? Since I’m not passing in ‘map’ into params, I am passing in expected_users=matched_users.

I saw with the matchmaking, it uses the users properties: stringProperties and numericProperties, and with the query, it uses something like

var query = "+properties.map:"+mapId

Is there such a thing as ‘match properties’, for example, if I want ‘teamACount’ and ‘teamBCount’? What I would really like, is a match making example, for authoritative, where I can have teams of a specific size, for example 3v3. These teams could be totally random users, or a combination of paired up groups, for example, one group could be 3 people, who have already teamed up ahead of time, and they want to queue for a match, they could get matched up with 3 totally random people, or a group of 2+1 rando, etc.

This is what I will create eventually, but for now I just want 3v3 random join, match does not begin until both teams have 3 players each… for authoritative (not relay).

I changed some code and it became authoritative! :smiley: So now I have a 1v1 match working. I will move on to state and input sync, etc. Then will circle back around to try handling 2v2 or 3v3 with teams/groups/etc. later.

Here is my code, for people who may want some reference later:

Unity ‘MatchService.cs’:

public async Task ConnectToMatchMakerLobby(string mapId)
{
    Debug.Log("------->ConnectToMatchMakerLobby");
    var stringProperties = new Dictionary<string, string>
    {
        {"map", mapId}
    };
    
    var numericProperties = new Dictionary<string, double>
    {
        {"team", 0} // possibly used for later ??
    };
    
    var socket = refs.Get<ISocket>();
    socket.ReceivedMatchmakerMatched += OnReceivedMatchmakerMatched;
    var query = "+properties.map:"+mapId;
    var matchmakerTicket = await socket.AddMatchmakerAsync(query, 2, 2, stringProperties, numericProperties);
    matchTicket = matchmakerTicket.Ticket;
    refs.Bind(matchmakerTicket);
}

public async Task DisconnectFromMatchMakerLobby()
{
    Debug.Log("------->DisconnectToMatchMakerLobby");
    var ticket = refs.Get<IMatchmakerTicket>();
    if (ticket != null)
    {
        var socket = refs.Get<ISocket>();
        if (socket != null)
        {
            socket.ReceivedMatchmakerMatched -= OnReceivedMatchmakerMatched;
            await socket.RemoveMatchmakerAsync(ticket);
        }
        refs.Unbind<IMatchmakerTicket>();
        Debug.Log("------->removed matchTicket");
    }
}

private async void OnReceivedMatchmakerMatched(IMatchmakerMatched matchmakerMatched)
{
    Debug.Log("------->Matchmaker: Found Match (matchmakerMatched):" + matchmakerMatched.MatchId);
    refs.Bind(matchmakerMatched);
    var socket = refs.Get<ISocket>();
    var match = await socket.JoinMatchAsync(matchmakerMatched);
    Debug.Log("------->Matchmaker: Connected to Match:" + match.Id);
    refs.Bind(match);

    socket.ReceivedMatchState += OnReceivedMatchState;
    
    Debug.Log("our session id:" + match.Self.SessionId);

    var presences = match.Presences;
    foreach (var user in presences)
    {
        Debug.Log("Other session id:" + user.SessionId);
    }

    var mainThreadDispatcher = Refs.Instance.Get<MainThreadDispatcher>();
    
    mainThreadDispatcher.OnNextUpdate(() =>
    {
        var positionData = new PositionData(UnityEngine.Random.Range(-100f, 100f), UnityEngine.Random.Range(-100f, 100f));
        var vec3 = positionData.Vec3;
        Debug.Log("----------sending state data:" + vec3.x+", "+vec3.z);
        SendState(0, positionData.Serialize(positionData));
    });
}

private void SendState(long opCode, byte[] state)
{
    var socket = refs.Get<ISocket>();
    var match = refs.Get<IMatch>();
    socket.SendMatchStateAsync(match.Id, opCode, state);
}

‘match_rpc.lua’

local nk = require("nakama")

local function matchmaker_matched(context, matched_users)
	local match_id = nk.match_create("matchcontrol", { expected_users = matched_users })
	nk.logger_info("match_id:"..match_id)
	return match_id
end

nk.register_matchmaker_matched(matchmaker_matched)

‘matchcontrol.lua’

local match_control = {}

function match_control.match_init(context, params) -- state, tickrate, label
	local match_state = {
		presences = {},
		input_buffers = {},
		delta_buffers = {},
		snapshot_buffers = {}
	}

	local tick_rate = 10
	local label = ""

	return match_state, tick_rate, label
end

-- see how to send tick, etc.
function match_control.match_join_attempt(context, dispatcher, tick, state, presence, metadata)
	if (state.presences[presence.user_id] ~= nil) then
		return state, false, "User already joined."
	end
	return state, true
end

-- see how to send tick, etc.
function match_control.match_join(context, dispatcher, tick, state, presences)
	for _, presence in ipairs(presences) do
		state.presences[presence.user_id] = presence
	end
	return state
end

-- see how to send tick, etc.
function match_control.match_leave(context, dispatcher, tick, state, presences)
	for _, presence in ipairs(presences) do
		state.presences[presence.user_id] = nil
	end
	return state
end

-- see how to send tick, etc. validate inputs, positions, etc
function match_control.match_loop(context, dispatcher, tick, state, messages)
	--[[for _, p in pairs(state.presences) do
        nk.logger_info(string.format("Presence %s named %s", p.user_id, p.username))
    end
    for _, m in ipairs(messages) do
        nk.logger_info(string.format("Received %s from %s", m.data, m.sender.username))
        local decoded = nk.json_decode(m.data)
        for k, v in pairs(decoded) do
            nk.logger_info(string.format("Key %s contains value %s", k, v))
        end
        -- PONG message back to sender
        dispatcher.broadcast_message(1, message.data, { message.sender })
    end]]--
    return state
end

-- see how to send tick, etc. leave match, play animation, taking players back to menu, cleaning up state
function match_control.match_terminate(context, dispatcher, tick, state, grace_seconds)
	local message = "Server shutting down in " .. grace_seconds .. " seconds"
    -- dispatcher.broadcast_message(2, message)
    -- return nil
    return state
end

return match_control
1 Like

@Bunzaga Did you figure out all you needed to handle authoritative multiplayer matches with the matchmaker?

Not ‘everything’ but enough to get started with 1v1 games. I want to do teams eventually, like 3v3, where users can group up ahead of time. I saw that this is being developed, that the back end is done, but you guys are working on client side over time. I’m looking forward to when Unity will have this :smiley:

1 Like