Matchmaking¶
This feature is to group players possible to play together.
The interface of Matchmaking is in /usr/include/funapi/service/matchmaking.h.
iFun Engine’s matchmaking consists of two components: MatchmakingClient
and MatchmakingServer
.
MatchmakingClient
is to request for a new match or for joining an existing match. Though the name is suffixed with Client
, it is the server component. It’s named like that because the game server works as a client in terms of matchmaking.
MatchmakingServer
is used at the server handling matchmaking requests. It evaluates players to figure out possible matches, creates a new match, or have the requesting player join an existing match.
MatchmakingClient class¶
The MatchmakingClient
class looks like this:
class MatchmakingClient {
public:
typedef Uuid MatchId;
typedef int64_t Type;
// Per-player information in a match
struct Player {
// Player identifier
string id;
// Player context.
// It's populated by a match request
// and passed to StartMatchmaking().
Json context;
// Server ID hosting the player.
Rpc::PeerId location;
};
// Information for a single match.
struct Match {
explicit Match(Type _type);
explicit Match(const MatchId &_match_id, Type _type);
// Match identifier.
const MatchId match_id;
// Match type to distinguish matches.
// It's of integer and so user can define own match type
const Type type;
// Player list in the match.
std::vector<Player> players;
// Match context instance.
// On the call of join/leave callbacks of the MatchmakingServer component
// the callback can manipulate the context data.
Json context;
};
// This result type is passed to a MatchCallback.
enum MatchResult {
kMRSuccess = 0,
kMRAlreadyRequested,
kMRTimeout,
kMRError = 1000
};
// This cancel type is passed to a CancelCallback.
enum CancelResult {
kCRSuccess = 0,
kCRNoRequest,
kCRError = 1000
};
// This callback is invoked on the completion of matchmaking.
typedef function<void(const string & /*player_id*/,
const Match & /*match*/,
MatchResult /*result*/)> MatchCallback;
// This callback is invoked once match status gets changed.
// This feature works only if `enable_match_progress_callback`
// in MatchmakingServer is set to true.
typedef function<void(const string & /*player_id*/,
const MatchId & /*match_id*/,
const string & /*player_id_joined*/,
const string & /*player_id_left*/,
const Json & /*match_context*/)> ProgressCallback;
// This callback is invoked on the cancelation of matchmaking.
typedef function<void(const string & /*player_id*/,
CancelResult /*result*/)> CancelCallback;
static const WallClock::Duration kNullTimeout;
static const ProgressCallback kNullProgressCallback;
// Initiates a matchmaking requestM
static void StartMatchmaking(const Type &type, const string &player_id,
const Json &player_context, const MatchCallback &match_callback,
const ProgressCallback &progress_callback = kNullProgressCallback,
const WallClock::Duration &timeout = kNullTimeout);
// Cancels StartMatchmaking() in progress.
static void CancelMatchmaking(const Type &type, const string &player_id,
const CancelCallback &cancel_callback);
};
Description on MatchmakingClient methods:
MatchmakingClient::StartMatchmaking(type, player_id,player_context, match_callback, progress_callback, timeout)
Initiates a matchmaking request.
type
: Any integer value is possible. It’s used byMatchmakingServer
to distinguish matchmaking requests.player_id
: Identifier to distinguish the requesting player.player_context
: Any information in JSON on the requesting player is possible. This is to determine a match best suited to the requesting player. Field namesrequest_time
andelapsed_time
cannot be used.match_callback
: Callback to be notified when matchmaking steps are completed.progress_callback
: Callback to be notified when matchmaking status has changes. To use the feature, you should turn onenable_match_progress_callback
onMatchmakingServer
.timeout
: Time limit to finish a matchmaking. If match is not found for this duration, the request is automatically canceled andmatch_callback
will be notified withkMRTimeout
. Pass kNullTimeout to disable the timeout feature.
MatchmakingClient::CancelMatchmaking(type, player_id, callback)
Cancels a matchmaking request initiated by
StartMatchmaking()
. If the matchmaking has already completed before this function call,kCRNoRequest
will be passed to the callback.type
: The same type used when issuingStartMatchmaking()
.player_id
: The same player id used when callingStartMatchmaking()
.callback
: Callback to be invoked when the cancelation request finishes.
MatchmakingServer class¶
To make the game server work as a matchmaking server, you need to include the MatchmakingServer
component in your MANIFEST.json.
If you are planning to run a dedicated MatchmakingServer
separated from the regular game server, you may want to make a matchmaking flavor (see Flavors: Identifying servers according to their role) and to put only MatchmakingServer
in the flavor’s MANIFEST.json.
MatchmakingServer
looks like this:
class MatchmakingServer {
public:
// MatchmakingServer uses shares type definitions with MatchmakingClient.
typedef MatchmakingClient::MatchId MatchId;
typedef MatchmakingClient::Type Type;
typedef MatchmakingClient::Player Player;
typedef MatchmakingClient::Match Match;
typedef MatchmakingClient::MatchResult MatchResult;
typedef MatchmakingClient::CancelResult CancelResult;
// Indicates a matchmaking status.
// This is used by JoinCallback to determine if match is completed.
enum MatchState {
// Not enough members.
kMatchNeedMorePlayer = 0,
// Match is completed.
kMatchComplete
};
// Functor to check if the given player can join the incomplete match.
typedef function<bool(const Player & /*player*/,
const Match & /*match*/)> MatchChecker;
// Functor to check if the match is completed.
typedef function<MatchState(const Match & /*match*/)> CompletionChecker;
// Callback to be notified when the given player has joined the match.
// That is, it will be invoked only if MatchChecker returned true.
// The callback typically updates the match's context in JSON to reflect current match state.
typedef function<void(const Player & /*player*/,
Match * /*match*/)> JoinCallback;
// Callback to be notified when the given player has left the match.
// The player might have called CancelMatchmaking().
// Or it's also possible that iFun Engine has re-organized existing matches
// to finish some of incomplete matches. (This feature is enabled only if
// "enable_dynamic_match" in MANIFEST.json is set to true.)
typedef function<void(const Player & /*player*/,
Match * /*match*/)> LeaveCallback;
// Starts the matchmaking server.
static void Start(const MatchChecker &match_checker,
const CompletionChecker &completion_checker,
const JoinCallback &join_cb, const LeaveCallback &leave_cb);
};
Description on the MatchmakingServer’s methods:
MatchmakingServer::Start(match_checker, completion_checker, join_callback, leave_callback)
Starts the matchmaking server. The server component’s Install() method is a good place to call the function. (i.e.,
{ProjectName}::Install()
)match_checker
Checker functor to determine if the player can join the match. To check criteria (e.g., level, gold, etc), it may evaluate the player’s context and the match’s context.
Return: true if the player can join the match. false, otherwise.
Argument:
player
: Player in question.Argument:
match
: Match instance to evaluate. Match instance carries a context to help match join evaluation.
Note
JSON contexts of the player and the players in the match also carries an attribute named
elapsed_time
indicating how long the player has remained unmatched. Please refer to Matchmaking examples.completion_checker
Checker functor to determine if the match is completed. It’s invoked after
match_checker
returns true and the match gets additional member.Return:
kMatchNeedMorePlayer
if the match is not completed, yet.kMatchComplete
, otherwise.Argument:
match
: Match instance to evaluate.
join_callback
Invoked once a match gets additional member. That is it’s called after
match_checker
returns true and the match size increases.Argument:
player
: The player who recently has joined the match.Argument:
match
: Pointer to the match instance.
Note
JSON contexts of the player and the players in the match also carries an attribute named
elapsed_time
indicating how long the player has remained unmatched. Please refer to Matchmaking examples.leave_callback
Invoked once a player has left the match. It’s typical to do steps to undo what
join_callback
did. It can be invoked when the player has canceled the matchmaking byMatchmakingClient::CancelMatchmaking()
or when iFun Engine has re-organize incomplete matches.enable_dynamic_match
in MANIFEST.json must be set to true to use the dynamic re-reorganization.Argument:
player
: The player who has left the match.Argument:
match
: Pointer to the match instance that has lost a member.
Note
If enable_dynamic_match
of MatchmakingServer
in MANIFEST.json
is set to true, the checkers and callbacks could be invoked more frequently. This is because iFun Engine tries to re-organize incomplete matches for fast matchmaking, which is an expected behavior.
Matchmaking examples¶
Both MatchmakingServer
and MatchmakingClient
rely on the RpcService
component.
So, you should set rpc_enabled
of RpcService
to true. (see Distribution parameters)
Example 1¶
Below shows an example of 1-on-1 or 2-on-2 matchmaking if the level differences of two players is within 5. It also perform matchmaking regardless of level difference, if any player is waiting for more than 7 seconds.
MatchmakingClient¶
Include the MatchmakingClient
component in your MANIFEST.json
.
...
"MatchmakingClient": {
},
...
Now, you can issue matchmaking requests like this:
// Custom enum values to be passed to MatchmakingClient::StartMatchmaking()
// and MatchmakingClient::CancelMatchmaking().
// You can freely define your own.
enum MatchType {
// 1-on-1 match
kMatch1Vs1 = 0,
// 2-on-2 match
kMatch2Vs2
};
// This will be invoked once a match request is processed.
void OnMatched(const string &player_id, const MatchmakingClient::Match &match,
MatchmakingClient::MatchResult result) {
if (result == MatchmakingClient::kMRAlreadyRequested) {
// Already requested.
...
return;
} (result == MatchmakingClient::kMRTimeout) {
// Match was not made in a given time.
...
return;
} else if (result == MatchmakingClient::kMRError) {
// Unexpected error happened.
...
return;
}
BOOST_ASSERT(result == MatchmakingClient::kMRSuccess);
// OK. Match has been made.
// Suppose MatchmakingServer has populated the match context to
// tell about the teams information.
MatchmakingClient::MatchId match_id = match.match_id;
string team_a_player1 = match.context["TEAM_A"][0].GetString();
string team_a_player2 = match.context["TEAM_A"][1].GetString();
string team_b_player1 = match.context["TEAM_B"][0].GetString();
string team_b_player2 = match.context["TEAM_B"][1].GetString();
// We now have player IDs.
// We need to let each player start. (omitted)
...
}
// This will be invoked when a matchmaking cancelation request is processed.
void OnCancelled(const string &player_id, MatchmakingClient::CancelResult result) {
if (result == MatchmakingClient::kCRNoRequest) {
// No outstanding matchmaking request of the given type.
...
return;
} else if (result == MatchmakingClient::kCRError) {
// Unexpected error happened.
...
return;
}
BOOST_ASSERT(result == MatchmakingClient::kCRSuccess);
// Matchmaking has been stopped.
// Some additional steps like letting the player know the result should be taken here.
// (omitted)
...
}
// This is a client-to-server message handler.
// Suppose this handler will be invoked on the receipt of 2-on-2 matchmaking
// request from the client.
void OnMatchmakingRequested(const Ptr<Session> &session, const Json &message) {
// Sets the player variable using either user id or character id.
string player_id = ...;
// Say, we consider the player's level for matchmaking.
// Sets the player level information in the form of JSON context.
Json context;
context["LEVEL"] = ...;
// If we'd like to time out a matchmaking request,
// we can set a timeout value here.
WallClock::Duration timeout = MatchmakingClient::kNullTimeout;
if (enable_match_timeout) {
// In this example, timeout of 10 seconds.
timeout = WallClock::FromMsec(10 * 1000);
}
// If we'd like a matchmaking progress report,
// we register a progress callback, too.
// This feature works only if enable_match_progress_callback is turned on in MANIFEST.json.
MatchmakingClient::ProgressCallback prog_cb =
MatchmakingClient::kNullProgressCallback;
if (enable_match_progress) {
prog_cb = [](const string &player_id,
MatchmakingClient::MatchId &match_id,
const string &player_id_joined,
const string &player_id_left,
const Json &match_context) {
// 1. If player_id_joined is not empty,
// it means iFun Engine has put player_id and player_id_joined in the same matching group.
// 2. If player_id_left is not empty,
// it means match where both player_id_left and player were has lost player_id_left.
};
}
// Issue a matchmaking request.
// OnMatched will be invoked on matchrequest is completed. (either successful of failed.)
MatchmakingClient::StartMatchmaking(kMatch2Vs2, player_id, context,
OnMatched, prog_cb, timeout);
// We may need additional steps like sending a message to the client...
...
}
// This is a client-to-server message handler.
// Suppose this handler will be invoked when the client request
// to stop a 2-on-2 matchmaking request in progress.
void OnCancelRequested(const Ptr<Session> &session, const Json &message) {
// Sets the player variable using either user id or character id.
string player_id = ...;
// Requests to stop a matchmaking request.
// OnCancelled will be notified when the cancellation request is processed.
MatchmakingClient::CancelMatchmaking(kMatch2Vs2, player_id, OnCancelled);
}
MatchmakingServer¶
Include the MatchmakingServer
component in your MANIFEST.json
.
...
"MatchmakingServer": {
"enable_dynamic_match": true,
"enable_match_progress_callback": false
},
...
enable_dynamic_match
: If set to true, iFun Engine periodically tries to re-organize incomplete matches.enable_match_progress_callback
: If set to true,progress_cb
passed toMatchmakingClient::StartMatchmaking()
will be called to receive a progress report on each match.
Important
It’s not allowed to use both enable_dynamic_match
and enable_match_progress_callback
at the same time.
You can handle matchmaking requests like this:
// Match type same as one in the MatchmakingClient example.
enum MatchType {
kMatch1Vs1 = 0,
kMatch2Vs2
};
// Suppose this is the server component's Install() method.
static bool Install(const ArgumentMap &arguments) {
...
// Kick-starts the MatchmakingServer component.
MatchmakingServer::Start(CheckMatch, CheckCompletion, OnJoined, OnLeft);
...
}
// This functor checks if the player can join the match in question.
bool CheckMatch(const MatchmakingServer::Player &player,
const MatchmakingServer::Match &match) {
// We will focus on kMatch2Vs2 in this example for brevity.
if (match.type == kMatch1Vs1) {
// (omitted)
...
} else if (match.type == kMatch2Vs2) {
// Suppose we'd like a player not to wait for more than 7 seconds.
// So, if there's any player waiting for more than 7 seconds,
// we will assign the player to the match regardless of level.
// iFun Engine adds an extra field "elapsed_time" in the player context.
int64_t max_elapsed_time_in_sec =
player.context["elapsed_time"].GetInteger();
for (size_t i = 0; i < match.players.size(); ++i) {
max_elapsed_time_in_sec =
std::max(elapsed_time_in_sec,
match.players[i].context["elapsed_time"].GetInteger());
}
if (max_elapsed_time_in_sec > 7) {
// Someone is waiting for more than 7 seconds.
// Tries to complete the match ignoring the level requirement.
return true;
}
// Suppose players with a level difference of 5 or more cannot play together.
// Get the player level.
// Please remember that MatchmakingClient adds the information
// in the player context. So, we will access it.
int64_t player_level = player.context["LEVEL"].GetInteger();
// Do the same job to the players already in the match.
for (size_t i = 0; i < match.players.size(); ++i) {
int64_t member_level = match.players[i].context["LEVEL"].GetInteger();
// If anyone in the match has a significant level difference,
// we ignore this match and try another one.
if (abs(player_level - member_level) > 5) {
return false;
}
}
// (Put your check criteria here..)
// OK. This match can host the player. Returns true.
return true;
}
}
// Checks if the match in question is completed or not.
MatchmakingServer::MatchState CheckCompletion(
const MatchmakingServer::Player &player) {
// We will focus on kMatch2Vs2 in this example for brevity.
if (match->type == kMatch1Vs1) {
// (omitted)
...
} else if (match->type == kMatch2Vs2) {
if (match->players.size() == 4) {
// Completed if the match has 4 members.
return kMatchmakingServer::kMatchComplete;
}
// As with CheckMatch() above, match.players[i].context includes
// elapsed_time. So, we can tweak the matchmaking rule to force the
// match to start with AIs if someone is waiting too long.
}
return MatchmakingServer::kMatchNeedMorePlayer;
}
// This callback will be invoked once CheckMatch() above returns true.
// We will manipulate the match context to reflect the current state.
// For example, we will assign the player to team A or B.
void OnJoined(const MatchmakingServer::Player &player,
MatchmakingServer::Match *match) {
// We will focus on kMatch2Vs2 in this example for brevity.
if (match->type == kMatch1Vs1) {
// (omitted)
...
} else if (match->type == kMatch2Vs2) {
// If match does not have a context, yet, we will create one.
if (match->context.IsNull()) {
match->context.SetObject();
match->context["TEAM_A"].SetArray();
match->context["TEAM_B"].SetArray();
}
// Assigns the player to a team with less members.
if (match->context["TEAM_A"].Size() < match->context["TEAM_B"].Size()) {
match->context["TEAM_A"].PushBack(player.id);
} else {
match->context["TEAM_B"].PushBack(player.id);
}
}
}
// This callback will be invoked when the player has left the match.
// We will manipulate the match context to reflect the current state.
// For example, we will remove the player from its team.
// match->context 의 팀 정보에서 해당 player 를 삭제합니다.
void OnLeft(const MatchmakingServer::Player &player,
MatchmakingServer::Match *match) {
// We will focus on kMatch2Vs2 in this example for brevity.
if (match->type == kMatch1Vs1) {
// (omitted)
...
} else if (match->type == kMatch2Vs2) {
// Removes the player from its team.
std::vector<Json *> teams;
teams.push_back(&(match->context["TEAM_A"]));
teams.push_back(&(match->context["TEAM_B"]));
BOOST_FOREACH(Json *member_list, teams) {
Json::ValueIterator itr = member_list->Begin();
while (itr != member_list->End()) {
if (player.id == itr->GetString()) {
member_list->RemoveElement(itr);
return;
}
++itr;
}
}
}
// Once this callback returns, the CancelCallback passed
// to MatchmakingClient::CancelMatchmaking() will be invoked,
// if the player's leaving is per request. (vs. iFun Engine's dynamic re-organization)
}
Example 2¶
Below shows an example of per-stage matchmaking. Different stage has a different matchmaking rules in this example, and it tries to perform matchmaking with next stage players if player is waiting for more than a given time.
MatchmakingClient¶
//////////////////////////////////////////////////////////////////////////////
// Shared among the game server (running MatchmakingClient) and MatchmakingServer
//////////////////////////////////////////////////////////////////////////////
enum Stage {
kStage1 = 1,
kStage2,
kStage3,
kStageEnd
};
//////////////////////////////////////////////////////////////////////////////
// game server
//////////////////////////////////////////////////////////////////////////////
void OnMatchmakingCompleted(const Ptr<Session> &session, const int64_t &stage,
const string &player_id,
const MatchmakingClient::Match &match,
MatchmakingClient::MatchResult result);
// Start matchmakin.
// Player information in the context variable is only for illustration purposes.
// You may want to customize it.
void StartMatchmaking(const Ptr<Session> &session, int64_t stage) {
string player_id = "example";
Json player_context;
player_context["player_id"] = player_id;
player_context["player_level"] = 10;
player_context["character_level"] = 60;
player_context["play_count"] = 100;
LOG(INFO) << "start matchmaking: id=" << player_id << ", stage=" << stage;
// Timeout of 10 seconds.
MatchmakingClient::StartMatchmaking(stage, player_id, player_context,
bind(&OnMatchmakingCompleted,
session, stage, _1, _2, _3),
MatchmakingClient::kNullProgressCallback,
WallClock::FromSec(10));
}
// Matchmaking message handler.
// It will start matchmaking from the stage 1.
void OnMatchmakingRequested(const Ptr<Session> &session, const Json &message) {
StartMatchmaking(session, kStage1);
}
// Callback that handles a matchmaking result.
void OnMatchmakingCompleted(const Ptr<Session> &session, const int64_t &stage,
const string &player_id,
const MatchmakingClient::Match &match,
MatchmakingClient::MatchResult result) {
if (result == MatchmakingClient::kMRError) {
// Unexpected error.
LOG(ERROR) << "matchmaking error.";
return;
} else if (result == MatchmakingClient::kMRAlreadyRequested) {
// Already sent a matchmaking request.
LOG(WARNING) << "matchmaking already requested.";
return;
} else if (result == MatchmakingClient::kMRTimeout) {
// Timed-out. We'll send another request and try a next stage.
int64_t next_stage = stage + 1;
if (next_stage == kStageEnd) {
LOG(WARNING) << "no stage to try again.";
return;
}
StartMatchmaking(session, next_stage);
return;
}
LOG(INFO) << "matchmaking completed!";
// Found a match. We need to make the players start.
// match.players contain information on the players.
}
Matchmaking Server¶
//////////////////////////////////////////////////////////////////////////////
// Shared among the game server (running MatchmakingClient) and MatchmakingServer
//////////////////////////////////////////////////////////////////////////////
enum Stage {
kStage1 = 1,
kStage2,
kStage3,
kStageEnd
};
//////////////////////////////////////////////////////////////////////////////
// matchmaking server
//////////////////////////////////////////////////////////////////////////////
// Suppose this is the Install() of the server running the MatchmakingServer component.
static bool Install(const ArgumentMap &arguments) {
// You must call the MatchmakingServer::Start().
MatchmakingServer::Start(MatchChecker, CompletionChecker, JoinCb, LeaveCb);
}
// Checks if the player1 can join the match.
bool MatchChecker(const MatchmakingServer::Player &player1,
const MatchmakingServer::Match &match) {
BOOST_ASSERT(match.players.size() > 0);
const MatchmakingServer::Player &player2 = match.players.front();
if (match.type == kStage1) {
// Checks based on the player's level.
int64_t player_level1 = player1.context["player_level"].GetInteger();
int64_t player_level2 = player2.context["player_level"].GetInteger();
if (abs(player_level1 - player_level2) <= 3) {
return true;
}
} else if (match.type == kStage2) {
// Checks based on the character's level.
// (Supposed, our game has a notion of character level separated from player level)
int64_t char_level1 = player1.context["character_level"].GetInteger();
int64_t char_level2 = player2.context["character_level"].GetInteger();
if (abs(char_level1 - char_level2) <= 10) {
return true;
}
} else if (match.type == kStage3) {
// Checks based on the play count.
int64_t play_count1 = player1.context["character_level"].GetInteger();
int64_t play_count2 = player2.context["character_level"].GetInteger();
if (abs(play_count1 - play_count2) <= 30) {
return true;
}
} else {
BOOST_ASSERT(false);
}
return false;
}
// Checks if matchmaking is completed.
MatchmakingServer::MatchState CompletionChecker(const MatchmakingServer::Match &match) {
// We are assuming 1-on-1 in this example.
// So, 2 players complete the matchmaking.
if (match.players.size() == 2) {
return MatchmakingServer::kMatchComplete;
}
return MatchmakingServer::kMatchNeedMorePlayer;
}
// Callback that will be notified when the player joins the match.
// You can store match-related information into match->context (in JSON).
void JoinCb(const MatchmakingServer::Player &player,
MatchmakingServer::Match *match) {
// do nothing.
}
// Callback that will be notified when the player leaves the incompleted match.
// (e.g., the player has called MatchmakingClient::Cancel())
// If your JoinCb() infused information in the context, you should remove it here.
void LeaveCb(const MatchmakingServer::Player&player,
MatchmakingServer::Match *match) {
// do nothing.
}