question

taptastic.studios@gmail.com avatar image
taptastic.studios@gmail.com asked

Persistent turn based games?

Hello,

I am currently working on my first multiplayer turn based game (which is also my frist game in unity), so bear with me and help me out with a bit of clarification as all this is getting confusing to me.

The multiplayer functionality that I need is like this:
- one player is starting a multiplayer game, he doesnt finds an available room, so creates one
- he plays his turn (simple moves), data is sent to the server and now the user can close the application
- when second player is starting a multiplayer game he finds the game created by the first player and joins it
- he gets first players turn data and after that he can play his turn
- first player receives a notification that is now his move... etc

For the multiplayer part I started using turnbased photon + playfab, only to discover now that I may need another server for photon webhooks (?) to keep persistent game rooms.

What is the recommended why of achieving this functionality?

Thanks

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

brendan avatar image
brendan answered

Update:

After much review and testing, we need to correct this. Using Shared Group Data as a global source for anything in a title is not a valid usage model, as there's no way to scale this effectively. Data which is intended for all users to read, like Title Data and Content, is not designed to be updated frequently, and so is distributed as copied versions in across multiple servers. But for data that must be updated more frequently, such as user data, you have a single repository so that you don't have sync issues with the data - and across many users, you then have many servers (since one user's data may not live where another's does). Shared Group Data acts like User Data rather than Title Data, as it is a store for frequently-changing data that players use to interact on group level, like a Clan. But if all players in a game were to attempt to read from the same Shared Group Data, the latency introduced once your game reaches scale would rapidly become intolerable.

Therefore, until we provide a distinct asynchronous matchmaking service in PlayFab directly, the best recommendation is (as Hamza stated) to use Photon Turnbased for matchmaking players into Rooms. When saving out the room data between moves, the recommendation would be to have that data saved once, to prevent sync issues. Each player would therefore have a single Shared Group Data for tracking on open games, and the player who started the game session (the first player in the game) would be the one with the full data, you would save a reference to that player's PlayFab ID in the other players' Shared Group Data, so that they can read/write to that store via Cloud Script when they take their moves. Note that this does imply that you should only be saving the data to the Shared Group Data which is required for the game's current state (don't serialize the whole room state out as-is, since most of it likely isn't needed), but then, we will always encourage good data hygiene practices. :)

We'll be updating the Blog post and providing a sample showing this usage pattern as soon as we can, while we work to prioritize the creation of an asynchronous matchmaker in PlayFab.

2 comments
10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

sgruelingplayfab avatar image sgruelingplayfab commented ·

Is there some new Info regarding a blog post to show the principal?

I'm just implementing such a solution and don't want to work in the wrong direction.

On room created, i create a shared user group, i let the players join this group and save the state i need to verify every move there if the game ended i give out the currencies/items and delete the shared group.

Is this correct?

0 Likes 0 ·
brendan avatar image brendan ♦♦ sgruelingplayfab commented ·

If you mean that the Shared Group Data will only ever be read from or written to by the small number of players in the session, then yes, that should be fine - that's what it's designed for, when it comes to turn-based games. However, if player moves are in rapid succession, as with realtime synchronous games, then you'll want to use a custom game server to host the session, and write out the stats and data when it's done.

1 Like 1 ·
brendan avatar image
brendan answered

Since Photon Cloud isn't really designed for persistent data, the key is to use PlayFab for your storage, user accounts, economy, etc., while Photon Turnbased (in this case) can serve as your "presence" state for the game. We'll have more support for asynchronous games soon, but in the near-term, the way to do this would be (note - this is specifically designed for games where each player has to wait for the other player's move to be complete before they can go):

. Use globally defined Shared Group Data to hold the set of "open" games - those awaiting an opponent. Each game needs to have a reliably unique ID (a generated GUID, created by the player who starts the game).

. Use a Shared Group Data per player to track on the games in-progress. Note that this does mean that a player can only have 100 games active at a time.

. The game data in the player's Shared Group Data should be either the current state of the game session, or a reference to the other player's Shared Group Data (where that data is stored). In addition to the ID for the game and the game state info, you'll also want to store the PlayFab IDs of the players.

. Note: Do not add players to any Shared Group Data using AddSharedGroupMembers. These should only be written to via Cloud Script, so that you can have appropriate cheat-checking in place.


Simple version, for games in which the player must wait for the other player to complete their move before they play:

. Have a short TTL, so that the room goes away immediately after the player leaves.

. When starting a game, first grab that Shared Group Data, pick a random game, and try to create a Photon Turnbased room with that ID.

. If you can't create the room, another player already 'claimed' that game. At that point, create a new game instead.

. To create a new game, have the player make all the necessary moves for the first turn, then create the game state in that player's Shared Group Data. Finally, write the info on the open game to the "open game list" Shared Group Data.


More complicated version, for games in which players don't have to wait on each others' moves, or where you want players to be able to be in the room together:

. When starting a game, first grab that Shared Group Data, pick a random game, and try to create the Photon Turnbased room that you'll use to hold the current game state.

. If you can't create the room, try joining it.

. If you join the room, you still need to check whether the local player should be added (multiple people could be trying this with the given ID), so check the game data (from the Shared Group Data described above) and check that a) it only references one PlayFab ID, and b) the PlayFab ID of any players currently in the room match that ID.

. If they don't, and there's only one player in the game data, you'll need some negotiation logic so that the player can query the other players (apart from the one who started the session) can determine which one of them "wins" in claiming the game session.

. That player then coordinates with the player who started the session, to update the game state, while the other players create a new game each.

. To create a new game, create the Shared Group Data for the game state info in the player-specific Shared Group Data, add the game info to the "open games list" Shared Group Data, then create the Photon Turnbased room.

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

brendan avatar image
brendan answered

Quick follow-up, as a question came up concerning how our data limits (100 Keys per Shared Group Data, in this case) would impact this design.

The way what I've described would work, you would have to have more than 100 users all searching on an open game simultaneously for the number of Keys needed in the file to be more than 100. Even making it a fairly paranoid period of 1 full second for the overlap time on searches, if users searched every 60 seconds, you would need to have around 6,000 concurrent users for this to be a risk (conservatively, that's usually around 60K DAU/600K MAU). And that's with no bucketing of the users at all - if needed, you could create separate open games lists for region, language, game mode, etc.

3 comments
10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Denzie Gray avatar image Denzie Gray commented ·

Is there still a limit in 2020?

And does User Data have this 100 key cap too?

0 Likes 0 ·
brendan avatar image brendan ♦♦ Denzie Gray commented ·

We call this out in a few places, but scheduling for feature work has virtually nothing to do with when a request is made for that work. It has to do with the priority of the work, which is directly influenced by the impact such a change could have for our community. In general, that's measured through the feedback we get from our interactions with developers, here in the forums, as well as our ticketed support system, and more direct communications.

That said, it's not clear what exactly you're asking for or what the feature is you're aiming to implement. That many individual keys in user data (or SGD) would be inefficient, and would very likely have higher costs than you'd like. Also, this particular thread has strayed info a range of only marginally-related topics from the original question. What I would recommend is posting a new question, describing the feature you're attempting to implement, so that our support team can work with you on the best approach.

0 Likes 0 ·
Denzie Gray avatar image Denzie Gray brendan ♦♦ commented ·

Ok, I will do just that - right now. Thank you for the speedy response!

0 Likes 0 ·
Hamza Lazaar avatar image
Hamza Lazaar answered

Brendan, I think you did not understand the question of the OP. This is not about building or enhancing matchmaking systems. Photon already has a good one. This is about cloud save, about loading and saving games.

The question is: how to persist "room states" between "Photon sessions"? (a session starts when client connects to Photon servers and ends when he/she disconnects from Photon servers) PhotonEngine team refers to it as "Rejoin" use case.

Photon nature is synchronous. Even Photon TurnBased started as a cheaper Photon RealTime with a lower threshold for the number of exchanged messages inside rooms. It's still relatively new and it has evolved a lot since the beginning. It's flexible enough thanks to webhooks.

With enough knowledge about webhooks, the developer knows when it is possible to load and save Photon room state (from which webhooks). An advanced webhooks related question would be "How often should the state be saved?". This can be relative to the game itself.

What's really missing is the CloudScript part that should address the following questions:
1- How to keep track of room lists?
2- Where rooms' state (games data) should be saved?

Let's try to answer those 2 questions:
1- I believe the optimal solution is to keep a list of ongoing (not ended) games per player as a SharedGroup. So each player should have a list of ongoing games in a SharedGroup, the name of the SharedGroup should be guessed from the Player's PlayFabId (e.g. {currentPlayerId}_GamesList). No player should have more than 100 games open at the same time (which is logical). No member should be added to the SharedGroup so all updates on the SharedGroup should be done from CloudScript. The keys of the SharedGroup are the gameIDs (room names). The values however is open to discussion (see answer to question 2). A game entry should be added to the SharedGroup as soon as the player joins a new Photon room. When the game is over the entry should be removed.

Now try to ask yourself this question:
We have n players and each one can know the list of the games he/she's playing (joined). This is useful as a lobby list. But where to put actual game data? We just represented the "1 to many" relationship between a player and games? "1 player --> * games". But what about "1 game --> * players"? meaning how to link single game data to the players joined to that game. If player A creates the room then leaves it before player B could even join it. How can player B access the game data left by player A? Then when player A comes back, how can he load the game data again? ==> the point is, the data of a single game should be saved somewhere accessible by both (all) players of the same game.

2- I have made 3 different drafts:
A- One SharedGroup per Room
Each room created from Photon should have a corresponding SharedGroup in PlayFab. The room name (gameID) is used as SharedGroup ID. The easiest way is to stringify and save room state as is in one key of the SharedGroup. If needs be (Room state exceeds 400KB which should be considered as strange) the state could be exploded into multiple keys but need to be rejoined in the same original state when loaded and passed to Photon again.
The ongoing games lists per player (from the answer of question 1) can contain anything in their values.
This needs an answer to this question: how many SharedGroups are allowed per player / title ?
B- One master SharedGroup
All room states should be stringified and saved as values in one central master SharedGroup. The keys being their respective game IDs (room names).
Here we suppose the state never exceeds 400KB.
The ongoing games lists per player (from the answer of question 1) can contain anything in their values.
This approach clearly goes against PlayFab's guidelines. The master SharedGroup can easily exceed the 100 keys threshold.
C- Room state is replicated for all players
Every time a room state need to be saved it should be saved for all players inside their ongoing games lists. So the ongoing games lists per player (from the answer of question 1) should contain the stringified room states as values.

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

taptastic.studios@gmail.com avatar image
taptastic.studios@gmail.com answered

Thank you both for your answers, it seems that things are a bit more complicated than I thought - for a newby at least.
Hamza, I think I am missing something regarding your model. I dont understand what is the role of photon , why is it still needed? Isnt it enough just to save room state after each turn?
For each game (room) i just need a small amount of data saved (think of something like an async bowling game)

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

brendan avatar image
brendan answered

You're quite welcome, though I apologize if this does seem confusing. Hamza and I are doing something of an apples and oranges comparison. Hamza is absolutely correct that the Photon Cloud has a great matchmaker, so for games where you're trying to join a session which is in progress (or where you know you'll only be joining if it existed recently), you should just use that. But if you need to be able to create a new game, and potentially not have that game be "picked" by another player from the open games list for any significant period of time, you'll need to have that game state stored elsewhere. For periods where there are few people playing (testing, launch - if you're not spending on user acquisition, and potentially non-peak periods, if you're in few regions),

If you're confident that your concurrent user load and overall session length mean that any game created always will be claimed very quickly (within the TTL of the Photon room's lifetime), by all means use their matchmaker - it will simplify your design.

Basically, what I'm specifically saying is that for persistent data over a long-term, you'll want to use a system such as I described to let players get their data, so that they can restore their game state.
Here's what we both said, in different ways:
. Each player should track his own open games
. The game state should only be stored in one place, to prevent issues with sync

Where we differ is in where that storage occurs. Since there's a limit of 100 Keys in a Shared Group Data, and of 10 times the number of players you have of total Shared Group Data objects, you can't store all active games across all players in one Shared Group Data, nor can you have a distinct Shared Group Data for every game (assuming you allow players to have more than a few active games).
So what I propose is that the game state always be stored in the active games Shared Group Data of the player who started the game. That will always be accessible by both players via Cloud Script, since the PlayFab ID of the user who created the game is part of the data you get from the "open games" listing when you're looking for a game.

To answer your question on why Photon is needed, that would be because PlayFab does not have built-in support for asynchronous matchmaking yet. We do plan to add that soon, but if you want to ship something right away, I would have to recommend using Photon to enable this in the way I've described.

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Hamza Lazaar avatar image
Hamza Lazaar answered

@Brendan

I wasn't expecting that you're trying to extend the maximum lifetime of unmatched Photon rooms! This issue could be addressed in 3 different ways:
1. pray for your game to have enough players from the start and handle unmatched rooms from client and server sides. I think 61 minutes (AsyncRandomLobby (1 hour) + Photon RoomTTL (max 6000milliseconds)) is more that enough for random matchmaking.
2. make bots!
3. reinvent the wheel by adding a first level/layer of random matchmaking handled by PlayFab and make CloudScript file longer.

Anyway, now that I got what you mean I can add to my previous answer the following:
the option B could have a better variant:
instead of a single master SharedGroup for all ongoing games all players combined: explode it into n master SharedGroups where each can represent a segment of your players/games. For instance, a SharedGroup per region, per game mode, game type, game language, etc. But still you need to narrow it down until you have n SharedGroups that has a maximum capacity of 100.

Now let's add Brendan's first approach to the list. I will describe a scenario with a single Room and 2 Actors. I will try to simplify it as much as possible.
I- PlayerA created RoomX

II- PlayerB joined RoomX ==> "RoomX" key is added to Player2's ongoing games SharedGroup, the value can be {ActorNr:2, Actor1:<PlayerA's PlayFabId>}. (the tricky part how to get PlayerA's PlayFabId from in CloudScript context of PlayerB and in which webhook)
[...]
III- PlayerA left (without abandoning) RoomX
IV- PlayerB left (without abandoning) RoomX ==> RoomX becomes empty
V- RoomX's EmptyTTL expired ==> RoomX state is saved (add or override) into Player1's ongoing games SharedGroup with key = "RoomX" and value can be {ActorNr:1, State:<room state>}.
VI- PlayerA or PlayerB wants to rejoin RoomX:
reads value of "RoomX" key from his/her ongoing games SharedGroup from RoomCreated (Type=="Load") webhook in CloudScript:
if ActorNr == 1
return State directly
else (if ActorNr != 1)
read PlayerA's PlayFabId from "Actor1" to construct SharedGroupId of Player1's ongoing games SharedGroup
get value of "RoomX" key from Player1's ongoing games SharedGroup
return State

Now if I want to do a GetGameList like CloudScript call to get enough data to show the player his active/ongoing games. For the sake of simplicity I will suppose all game data is saved inside Photon's Room state only. Each entry in the game list will require at least: opponent information (PlayFabId at least) and LobbyProperties (those are the only properties exposed in the Room State). So from CloudScript I will need to query and go through all and every room state of the games listed as active for a player.

Best case scenario: in all games the player is the one who started/created them ActorNr==1 ==> # of API calls = 2 (RunCloudScript + GetSharedGroupData)
Worst case scenario: in all games the player has an ActorNr != 1 ==> # of API calls = 2 + n * GetSharedGroupData. n = number of active games for the player.

This is not good UNLESS we reverse the issue and anticipate it by saving extra info outside Photon room state directly inside the SharedGroup. Like saving score, turn, timestamp, etc. EACH time those properties are updated separately inside the corresponding SharedGroup value (key = room name). So we need to force the "Best case scenario". This certainly has a price in API calls of game updates inside most of webhooks.

What do you think?

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

brendan avatar image
brendan answered

The problem is that both of these things depend upon things that can't be guaranteed.

For matchmaking, if a title is just starting out and has few players, it is the case that there may be long periods where open games aren't claimed by others. And when a title is getting a bit older and the user base has dropped, it will also be the case that games may need to wait a long time. So my feeling is that the logic needs to account for this, rather than rely upon games being picked up in that period - that's the main reason I proposed the system I did.

The other reason though is the other issue. There realistically cannot be an "all ongoing games" list or set of lists. This can work with small user bases, but as the number of players grows, the complexity the developer would have to deal with in trying to find ways to bucketize players to keep the Shared Group Data objects below 100 Keys becomes intractable. That's why I'm recommending that all active games be tracked only in the Shared Group Data for the player who started the game session - so that it's in one place, to prevent sync issues, and it can be managed within the bounds of the limitations. There's no hardship to this for the player who joins the game, since the "open games" list contains all the info needed to connect to the originating player's data and stay in sync with the state of the session.

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Hamza Lazaar avatar image
Hamza Lazaar answered

"There's no hardship to this for the player who joins the game, since the "open games" list contains all the info needed to connect to the originating player's data and stay in sync with the state of the session."

I don't agree. It is not that easy neither.
I'm speaking from my own experience and after getting enough feedback from developers trying to integrate Photon TB with CloudScript.

Forget everything else I said and please give me your opinion about the scenario I described as a follow up to your suggestion (which I believe is the most appropriate way of doing things, I'm convinced) and everything related to that from my previous answer. I was waiting for your answer about this point in particular because I need to make a PoC and a live demo for all other developers trying to figure out how to use Photon TB with PlayFab.

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

brendan avatar image
brendan answered

Which part do you need feedback on, specifically? The essential problem with what I'm seeing currently is the need to have the global list of all games. That's not really an option, and bucketizing it into segments to try to have each be under 100 entries would be complicated and would potentially run into issues if it doesn't work perfectly (since you'd hit the limit and then new games would simply not be entered).

I'm not clear on what the issue is with the system I've suggested, though. You said getting the info from the "open games" list on the game would be difficult, but I'm having trouble seeing how. The open games list would have the ID of the game session (a generated GUID) and the PlayFab ID of the player who started it. The GUID would be used to create a Photon Cloud room and claim the session, while the PlayFab ID of the person who started the game would be used to query for the current game state. What is the complexity that this causes in your experience?

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Write an Answer

Hint: Notify or tag a user in this post by typing @username.

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Error rendering WebPanel (widgets/consolidation-widget.ftl): org.hibernate.hql.internal.ast.QuerySyntaxException: AvailableConsolidation is not mapped [from AvailableConsolidation up where up.node = :node]