0

Problem

I'm using Firebase Realtime Database (for Unity) to manage the server side for a turn based game but I have a problem with my matchmaking... a high download usage.

Every online game have 2 base states to avoid more than 2 players join a game: Created and Joined

  • Created: A player try to join a game, if can't find one a new game will be Created
  • Joined: A player try to join a game, if find one change the state to from Created to Joined

I'm using RunTransaction to prevent more than 2 players from joining a game, but I checked that the latest data was not fetched from the database because of the local cache, adding keepSynced over my matches-{lang} child node will always have the latest data but naturally this produces a high download usage.

private DatabaseReference DatabaseReference()
{
    return FirebaseDatabase.DefaultInstance.RootReference.Child(MatchesLocation(LanguageManager.Manager.GetPlayerLanguageCode()));
}

private DatabaseReference DatabaseReferenceLangMatch(Language language)
{
    return FirebaseDatabase.DefaultInstance.RootReference.Child(MatchesLocation(LanguageManager.Manager.GetLanguageCode(language)));
}

public void ManageKeepSyncedMatches(Language lang)
{
    DatabaseReferenceLangMatch(Language.English).KeepSynced(lang == Language.English);
}

public void JoinMatchTransaction(GameMatchOnline gameMatchOnline, UnityAction<string, bool> callback)
{
    JoinTransactionAbort joinResult = JoinTransactionAbort.None;

    DatabaseReference matchesListRef = DatabaseReference();
    Dictionary<string, object> joinerDict = gameMatchOnline.ToJoinDictionary();

    matchesListRef.Child(gameMatchOnline.matchId).RunTransaction(matchData =>
    {
        Dictionary<string, object> matchDict = matchData.Value as Dictionary<string, object>;
        if (matchDict == null)
        {
            joinResult = JoinTransactionAbort.Null;
            return TransactionResult.Success(null);
        }

        if (!matchDict.ContainsKey("state"))
        {
            joinResult = JoinTransactionAbort.Error;
            return TransactionResult.Abort();
        }
        GameMatchOnline.State state = (GameMatchOnline.State)System.Convert.ToInt32(matchDict["state"]);

        if (state != GameMatchOnline.State.Created)
        {
            joinResult = JoinTransactionAbort.Error;
            return TransactionResult.Abort();
        }

        joinResult = JoinTransactionAbort.None;

        matchDict.Add("joinerInfo", joinerDict["joinerInfo"]);
        matchDict["state"] = joinerDict["state"];
        matchData.Value = matchDict;

        return TransactionResult.Success(matchData);

    }).ContinueWith(task =>
    {
        // Fail
        if (task.IsFaulted || task.IsCanceled)
        {
            UnityThread.executeInUpdate(() =>
            {
                if (joinResult == JoinTransactionAbort.Error)
                {
                    callback(null, false);
                }
            });
        }
        // Can Join match
        else if (task.IsCompleted)
        {
            UnityThread.executeInUpdate(() =>
            {
                if (joinResult == JoinTransactionAbort.None)
                {
                    AddListenerResultsValueChanged(gameMatchOnline.matchId, gameMatchOnline.joinerInfo.userId, gameMatchOnline.isPrivate, gameMatchOnline.language);
                    callback(gameMatchOnline.matchId, true);
                }
                else
                {
                    callback(null, false);
                }
            });
        }
    });
}

Question

  • Removing keepSynced players will have locally cached information for matches-{lang}, can I trust that by doing this there will be no more than 2 players per game? *Transactions are supposed to avoid this kind of problem.
  • Is there a way to avoid the local cache for a request and thus always get the updated data?
  • Could the best solution be to move the games to another node to reduce the size of the matches-{lang} node?

Thanks!

4

1 回答 1

1

Removing "keepSynced" players will have locally cached information for "matches", can I trust that by doing this there will be no more than 2 players per game? *Transactions are supposed to avoid this kind of problem.

With KeepSynced off, Transactions will still hit the local cache then hit the internet. It'll probably save you some bandwidth since it's a lazy access (that's assuming you don't do something like "get all matches"), and you'll be able to make the guarantees you need. Whether or not you use KeepSynced, you should be prepared for your transaction to run multiple times (and against null data if the local cache is empty).

Is there a way to avoid the local cache for a request and thus always get the updated data?

Correction

It looks like I got this a little backwards, see this answer for more details. It will return the cached value and request an updated one. Subsequent calls will get a new value when it's available. You should always try to use ValueChanged when possible.

old answer:

You _can_ just say `GetValueAsync`, which has to bypass the cache since it will only fire once. You really should use ValueChanged listeners to listen for changes and Transactions to change data if you can to keep everything up to date and to avoid data races.

Could the best solution be to move the games to another node to reduce the size of the "matches" node?

Generally the fewer people hitting a shared resource, the better your performance. If you haven't already, check out the Loteria post to see how a team created a realtime game on Realtime Database that was resilient enough to be a Google Doodle.

The TLDR is that rather than a player being responsible for creating or finding a game, players looking for games are written to a queue. When a player is added to the matchmaking queue, a Cloud Function trigger fires which does the work of hooking the users up. A client knows that they're in a game by putting a ValueChanged listener onto a player entry in the database and waiting for a game to be written into it.

The game is further kept low latency with some manual sharding logic. They performed some profiling to see how much traffic a single database could handle, then write some quick (manual - since it was a one day thing) scaling logic to distribute players to one of a number of databases.

I hope that all helps!

--Patrick

于 2020-04-21T15:57:03.280 回答