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 by MatchmakingServer 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 names request_time and elapsed_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 on enable_match_progress_callback on MatchmakingServer.

    • timeout : Time limit to finish a matchmaking. If match is not found for this duration, the request is automatically canceled and match_callback will be notified with kMRTimeout. 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 issuing StartMatchmaking().

    • player_id : The same player id used when calling StartMatchmaking().

    • 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 by MatchmakingClient::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 to MatchmakingClient::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.
}