question

brendan avatar image
brendan asked

Where to save games data ?

johntube
started a topic on Mon, 19 January 2015 at 4:44 PM

I want to ask where and how can I store data about save games that are not "player oriented" (not binded to a single player but shared between two or more). For example, if I want to save the state and some extra data (events history of players' moves and game stats) of a multiplayer match/game and load it later whether it has ended or to re-join it and resume playing.

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.

1 Answer

·
brendan avatar image
brendan answered

Best Answer
Brendan Vanous said on Thu, 05 February 2015 at 1:51 AM

Correct - registering a new player results in a login. You'll get a Session Token back, as a result.

I'd say you've got a really good handle on the overall flow. Based on how you're managing the game flow, I don't see there being any problem with the number of shared group data objects created (it doesn't really make sense to have these as title data), but I would recommend not adding the players to the objects directly. Instead, just have a default account (your own) that you add to everything, and manage it all via Cloud Script. Otherwise, a hacked client could update the data in ways you don't want them to (since they're managing state). Also, it's possible to have the data for each game in a single Key/Value pair - it's really just a matter of how you want to manage the data. The one other question is around the updates based upon different player actions. I know you said that the first player is the one to set up the object to start, but it seems like you'll want to track on whose move it is as part of the data, so that in the logic of the Cloud Script, you can make sure that the player submitting the move is valid.

Now, the one other thing to ask about is the way in which players get into games. The shared group for a player listing current games is definitely needed, but what about challenges? It sounds like players can challenge specific other players, but what about "open" challenges, where they're just looking for someone to play against? In that case, you may want to have a single shared group data object to track on open game requests, which other players can accept (though there's obviously a bit more work to be done in coordinating this). And even for the challenges which are to known individuals, I'd recommend having those all go into a list (in a shared group data object), so that you can track on the outstanding requests and expire them if they go a long time without a response.

Brendan


17 Comments
Brendan Vanous said on Tue, 20 January 2015 at 12:49 AM

We have a Shared Group Data type which could be used for this, depending on the specifics of your requirements. My recommendation would be to manage this via Cloud Script, rather than give the client the ability to write to the shared space, though. Otherwise, a hacked client could write arbitrary data there. That also allows you to control which player gets to update the file at any time, by recording who made the last move. You would have a "manifest" file on each player account - preferably also read/written by Cloud Script, for security of the data - which would have the info on all the active games. On startup of a player, you could then process that list to check for a) games that have moves, and b) games that haven't had any moves in long enough that they should be "pruned".


johntube said on Tue, 20 January 2015 at 3:03 PM

I thought PlayFab had such feature out-of-box. What I really need is the Photon-PlayFab integration and the webhooks integration precisely. Since this could take a long time to be released I thought of working with Photon and PlayFab separately by ignoring the Photon webhooks and using PlayFab API or PlayFab cloud scripts. A 2-players turn-based game data can be saved as player data for the player who created the game and accessed by both players or saved to both players (duplicate data).

Anyway even with cloud scripts I will still use the same functions available from the API, unlike Parse for example I don't have the freedom to create and update custom files. In other words there isn't a proper database for the game itself. Correct me if I'm wrong.


Brendan Vanous said on Thu, 22 January 2015 at 1:07 AM

We do indeed have Shared Group Data, which can be used for a variety of purposes. For our initial revision of shared data, we don't manage locking because this could cause issues for titles operating at scale, and we don't do conflict resolution as the "right" thing to do when two values are written to the same key is dependent upon the specifics of the game logic. We do have a backlog item to add a way for titles to select to define how to resolve conflicts for shared data, but in fact, it's easy to work around this. The way to do so is to ensure that a player write to a Key which uses his unique PlayFabId as either all, or part, of the Key itself. Since no other player could be writing to it simultaneously, that prevents conflicts. The one exception would be a player using the same account on multiple devices simultaneously, but this should be a rare enough corner case that good messaging should be sufficient to handle it.

Our Photon integration is in process, though I don't have a specific date for the release of the webhook integration yet. But you are correct that it is entirely possible to use the two systems in parallel - the obvious difference being that the webhook route would be the Photon Cloud server calling PlayFab on behalf of a specific user, versus just having the user call directly to the Cloud Script. Since the Photon Cloud servers don't host custom logic, it works out effectively the same.

For data, could you clarify what your needs are? Using Cloud Script, you can use any Server API as you pointed out, which provides access to all the data forms in our system. If you need something more akin to a full SQL database running on a server together with game-specific logic (for a twitch-type game, for instance), that would be a situation where you would need to have hosted servers, which we can also help with.

Brendan


johntube said on Thu, 22 January 2015 at 5:40 PM

My "problem" has 3 sides :

  1. Photon webhooks pending --> temporary solution : bypass it by implementing all webhooks as CloudScript functions taking into consideration the "timing" and roles of all the webhooks [explained here] to be ready to "embrace" the integration as soon as it is released. It's also preferable that the exchanged data should have the same format should from/to Photon and PlayFab.

  2. all PlayFab data is key-store values, document or column based nosql dbs would be easier for me (next point) --> I guess I have to live with it

  3. design of the persistent data distributed saving/loading "protocol" (format, parsing, conflicts resolutions, caching, optimization) --> this is my first game so God help me.

My game's characteristics are :

  • turn-based --> need to send moves info details after each turn

  • word game but with a DYNAMIC grid --> need to update the grid config after each turn

And as all games I have to save the :

  • players score (2 players in this case)

  • game progress/state (who's turn/round number, resigned, invitation sent/rejected/pending, etc.)

After investigating all the API calls related to the SharedGroupData I came up with 2 first drafts :

1) draft 1 : the SharedGroupData will contain all the data of a SINGLE game between any two opponents

  • just before the creation of a Photon room a SharedGroup is created with CreateSharedGroup --> photon room ID will be set to the SharedGroup ID, uniqueness is guaranteed by PlayFab.

the player who created the game will init the first game state data and add his opponent to the SharedGroup once joined to the PhotonGame. --> a key/value pair for the global/current game state data

  • the ID of the SharedGroup/Photon room will be saved to both players' with UpdateUserData as a key and the value will contain info about the corresponding game (status, timestamp of last update, etc.) --> this will be useful to load the LIST of all/some player games

  • each player move/turn will be added to the SharedGroupData with its own unique key (based on the player ID and the turn #) using UpdateSharedGroupData

  • the two players can update the global game state with UpdateSharedGroupData

2) draft 2 : the SharedGroupData will contain all the data of ALL the games between the SAME couple of opponents

  • key/value pairs will be single games --> the value will be relatively "big" ! (is there a limit ?) + more parsing headaches

OR

  • each key/pair refers to a piece of data about a single game like draft 1 with a small difference that a unique gameId must be generated and incorporated in that game's relative keys.

Brendan Vanous said on Fri, 23 January 2015 at 1:17 AM

Hi Hamza,

The limit on data is 400KB (the Item Size limit in DynamoDB). Given that your game is turn-based, I do believe that our service's data storage (user, title, publisher) is more than sufficient to manage the data needs. The design patterns you proposed sound good, though I would also recommend that you not add the users to the shared data object (and to be clear, the Photon room ID is guaranteed unique by Exit Games, though we do trust their code). You definitely do not want the client to be able to update any data in those objects, as they could use that ability to cheat the individual games.

Having the Cloud Script be the way to get at the data would prevent the clients from not only being able to modify the data, but being able to read it as well. Again, just for purposes of cheat prevention. Having your hosted logic be authoritative concerning what gets updated will allow you to control your user experience much more closely.

Brendan


johntube said on Fri, 23 January 2015 at 4:52 PM

Hey Brendan,

About the Photon room name/ID : it should be passed as an argument when using Photon method OpCreateRoom and if null a GUID is assigned by the Photon server. In their Memory game (Photon turn-based demo) they're generating a UID by concatinating the username of the Actor who is creating the room and a "pseudo-randomly" generated number. I'm generating my own game IDs (based on timestamps and also a PRNG), do you recommend the Photon/Mircosoft's GUID ?

The API calls I mentioned in my previous post are going to be called directly from CloudScript as server functions but I did not understand what you meant by "I would also recommend that you not add the users to the shared data object", did you mean that I should not use AddSharedGroupMembers ?

My design pattern is to implement most Photon webhooks as CloudScript functions (= server actions = JS handlers) and add few others that Photon call WebRPC (most obvious example is GetGamesList).

HL


Brendan Vanous said on Fri, 23 January 2015 at 5:24 PM

Hi Hamza,

I haven't specifically played around with the Photon GUID generator, but if it's generating anything similar to the Microsoft one, it should be reliably unique. You may want to check with the Exit Games team to be sure, if you have any concerns about that, I'd recommend emailing the Exit Games team. Let us know if you need a contact point there.

Whether or not to add players to your Shared Group Data depends upon its use. Any player who has been added can read and change the data. A hacked client could conceivably be written to make unintended changes, therefore. So if the data stored is something someone could use to either get an advantage over another player, or otherwise disrupt your game, it's better to use the user read-only data or user internal data.

Brendan


johntube said on Sun, 01 February 2015 at 7:22 PM

Hi Brendan,

Can you explain the difference (the purpose/use of each) between the following "concepts/terms" :

UserData vs. UserPublisherData and their respective (ReadOnly and Internal) variants.

What's meant by "publisher-specific" ?

From the docs I managed to understand the 3 levels of access to User data and I'm assuming this is mainly a security measure that prevents cheating and tampering :

  1. [] = r/w by both user/client and server/admin

  2. ReadOnly = can only be read by the user/client, always r/w by server/admin

  3. Internal = not accessible by the user/client, server/admin access only

On the other hand there is the UserDataPermission (=="how others can view/access my data"):

  • Private : hidden to others, visible only to me

  • Public : visible to everyone (others can only READ and can't WRITE ?)

Thanks,

HL


Brendan Vanous said on Sun, 01 February 2015 at 8:26 PM

Sure thing - and you are correct:

Regular data - read/write by the client

ReadOnly - readable by the client

Internal - cannot be read by the client

Private - only the player can read (where applicable)

Public - any player can read (again, where applicable); no player can write to player-specific data other than the player associated with that data (for data which can be written to by multiple players, use SharedGroupData)

Now, the distinction between Publisher and regular data, it's simple - Publisher data can be accessed across multiple titles. Just let us know which Title IDs need to be able to share data, and we'll get it set up for you. Apart from that, everything else about those data types is the same as for the non-Publisher variety.

Thanks,

Brendan


johntube said on Mon, 02 February 2015 at 8:54 AM

Just one more thing related to this matter : is there a constraint of the number of SharedGroupData I can create per PublisherId or TitleId ? Because if I decide to create a SharedGroupData per game, I risk having tons of them. Right now I'm saving every game as a Key/Value in the TitleData but in order to update (modifiy only) I need to get the old value, change it then set it (extra queries overhead).

I also want to know that if I use only server API calls to handle the SharedGroupData, the LastUpdatedBy field in the SharedGroupDataRecord will be useless (empty or null) ?


Brendan Vanous said on Tue, 03 February 2015 at 1:04 AM

At the moment, no - we haven't defined a limit on the number of SharedGroupData objects. That said, it would be best to review your plans, to make sure your usage is well-optimized (not just for our service, but so that you don't have more instances of waiting on server responses in your code than you should). If you're okay with describing the overall flow here, that's great, but otherwise we can work through the details in a private thread (devrel@playfab.com).

And no, the LastUpdated field is always filled out, even for Server calls, so it's never useless.


johntube said on Wed, 04 February 2015 at 4:59 PM

Brendan here is the design pattern :

Upon user account creation I need to create a SharedGroupData with ID = f(PlayFabId, "GamesList" string), that will contain the user's games list, with CloudScript action called in the LoginCallback(s) when NewlyCreated is set to true and RegisterWithPlayFab callback. (by the way registering a new organic PlayFab user will always automatically logs him/her in ? like in the AngryBots demo, I know I'm lazy asking here instead of testing it myself)

Then when receiving specific Photon's LoadBalancingAPI Operations "ACKs" or Events I should run some CloudScripts (the timing of the RunCloudScript calls is important as I want to reduce the amount of refactoring when the webhooks integration is out). I got the webhooks definitons/descriptions from exitgames' forums here. I'm also keeping the same webhooks names as the CloudScript handlers' functions ones.

Photon vocab translation :

  • Room = match/game

  • Room name = game's UID, if not explicitly set by the client it's a GUID generated by Photon servers

  • Actor = Photon player in the room

  • ActorId = player number/index/ID in the room

  • CustomRoomProperties = the data shared between all room actors

The CloudScript actions binded to the webhooks :

  • GameCreate : In case this is a room/game creation when no random room is available or when challenging a friend (not a resume or 1st rejoin after all/both players left the room), a SharedGroupData is created (its UID can be either the gameId or f("gameId") or generated by PlayFab server) The UID of the SharedGroupData will be added to the games list of the player (the SharedGroupData mentioned above) like this : Key = "gameId", Value = {actorId:"1", "opponentId":"opponent's PlayFabId",etc.}. There is no need to add a timestamp here as in the SharedGroupRecordData there is already a LastUpdated field. There is no contention here also as only the player who creates the game saves it (the one with actorId = 1).

  • GameJoin : the same thing as GameCreate but the actorId is 2 instead of 1.

  • GameEvent : called after each player's turn/move. This will UpdateSharedGroup data of the respective game. No contention here as each player will add/update his own/related Keys.

  • GameProperties : this is called when the shared game properties are updated. Right now I'm using Photon's CustomRoomProperties as grid configuration which should be updated after each round. By round I mean when both players ended their turn = finished sending their respective moves. I didn't decide yet how this should be implemented but I don't want to bother you with that as the logic of the game from the beginning is "client based"/"distributed" and the CloudScript is there only to help save persistent data to the cloud and retrieve it later. I might even get rid of this and use GameEvent only with separate event codes (I'm using custom event codes with Photon anyway) and not having "any shared data between players" and syncing local data manually instead of handling it to Photon.

My concern is about the limit of SharedGroupData I can create as my game with this design will require AT LEAST/FOR THE MOMENT :

  • one ShareddGroupData per player --> games list container

  • one SharedGroupData per game --> game data container

So what do you think, should I use a SharedGroupData per game or use TitleData only ("all game data in a key/value entry" or "multiple key/values per game") ?

On the other hand, instead of using Photon's WebRPC way, calling CloudScript directly is better to get games list and games data (multiple calls to GetSharedGroupData from server side and then returning JSON).

Thanks for your time, I really do appreciate your efforts and I'm happy about how quickly you reply.

H.L.


Brendan Vanous said on Thu, 05 February 2015 at 1:51 AM

Correct - registering a new player results in a login. You'll get a Session Token back, as a result.

I'd say you've got a really good handle on the overall flow. Based on how you're managing the game flow, I don't see there being any problem with the number of shared group data objects created (it doesn't really make sense to have these as title data), but I would recommend not adding the players to the objects directly. Instead, just have a default account (your own) that you add to everything, and manage it all via Cloud Script. Otherwise, a hacked client could update the data in ways you don't want them to (since they're managing state). Also, it's possible to have the data for each game in a single Key/Value pair - it's really just a matter of how you want to manage the data. The one other question is around the updates based upon different player actions. I know you said that the first player is the one to set up the object to start, but it seems like you'll want to track on whose move it is as part of the data, so that in the logic of the Cloud Script, you can make sure that the player submitting the move is valid.

Now, the one other thing to ask about is the way in which players get into games. The shared group for a player listing current games is definitely needed, but what about challenges? It sounds like players can challenge specific other players, but what about "open" challenges, where they're just looking for someone to play against? In that case, you may want to have a single shared group data object to track on open game requests, which other players can accept (though there's obviously a bit more work to be done in coordinating this). And even for the challenges which are to known individuals, I'd recommend having those all go into a list (in a shared group data object), so that you can track on the outstanding requests and expire them if they go a long time without a response.

Brendan


johntube said on Thu, 05 February 2015 at 6:00 PM

I'm not sure I will need to add an admin PlayFab account as a member of all created SharedGroups. I don't see the benefit of doing so, unless I'm willing to make my own custom dashboard/"control panel" (UI/Desktop based) to access all game titles in one place by logging in with that admin account.

Indeed, I need to handle game invitations and the destroyed "Photon open rooms" (games waiting for 2nd players to join that need to be saved). Photon is having a built-in way of handling Player TTL and Room TTL but it's not persistent. Photon would be perfect if all clients stay connected forever !

And yeah I do have an optimized way of checking who's turn it is in client code, I will use the same mechanism in the CloudScript and I can make use of the LastUpdated fields too.

I'm envying all future PlayFab-Photon customers that will find everything ready out-of-the-box ! Especially if more official/unofficial docs/tutorials will be available...and maybe I need to start a blog just about my own experience to help others.

H.L.


Brendan Vanous said on Thu, 05 February 2015 at 7:33 PM

Thanks! We'll absolutely be adding more docs, tutorials, sample scripts, etc. as we proceed, but sharing your own experiences working with these is definitely encouraged. And particularly when it comes to Cloud Script, if there are functions you believe would be useful to others, feel free to post them, or submit them to our GitHub.

The one caution with shared group data, if you choose not to have any users associated is to ensure that you're consistent in that usage model. Bear in mind that if users are added to a shared group and then removed, when there are no longer any associated users, the data object will be deleted. If it's created by a server API call, it will have no users to start, so as long as none are added (and then removed), you shouldn't have a problem.

Brendan


richjoslin said on Wed, 23 September 2015 at 7:54 PM

I'm reviving this zombie thread to see if SharedGroupData is still the way to go for persisting and accessing game turn based multiplayer data via Cloud Script. It looks like it - the client matchmaking API calls are not available in the server API (I was hoping to customize the output of GetCurrentGames using Cloud Script).


Brendan Vanous said on Wed, 23 September 2015 at 11:47 PM

Hi Rich,

To be fair, we respond to every thread where there are open questions. If there aren't open questions, we don't keep posting to them, but I wouldn't call that a "zombie" thread - more a "closed" thread, really (but that may just be my own terminology preference). That said, if at any time you feel you need more info on any subject, please do post back to any thread, and we'll answer any questions.

The client matchmaking calls are specific to games with custom game servers, such as a Call of Duty game session server. Our recommendation for asynchronous games would be to follow the design pattern described in our blog post on the subject (and the linked GitHub project, with the sample code): https://playfab.com/blog/creating-turn-based-asynchronous-matchmaker-without-dedicated-server/. So yes, Shared Group Data is the way to go for asynchronous gameplay.

Brendan

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.