Very satisfied so far with PlayFab. There's one subject I need advice though.
I have a simple multiplayer turn-based asynchronous game system. I need to store a json string somewhere to be accessed by exactly 2 players for each match created.
There are various methods of storing things in PlayFab, I'm not sure which one is the most appropriate.
Are shared groups appropriate for this kind of use? May it present performance issues when over-used?
There is also the option to store the data in the player that created the match. A player may have multiple games at the same time, I don't know if this is the correct purpose of this feature.
I also plan to keep the match history of each player for stats purposes. This can get large. Some players have 1k+ matches
Right now I'm storing all this in my old server. I'd like to know if it's worth/possible moving it to PlayFab and which method to use. Or should I wait for the official PlayFab turn based multiplayer support?
Answer by Brendan · Oct 26, 2016 at 01:47 AM
Either (Shared Group Data or User Read-Only/Internal Data) would work, though it's important to note that you should not add the users directly to the Shared Group Data is you use that, since that would give the users read/write access to the data directly (allowing a hacker to post whatever he wants).
Now, we do limit the number of Shared Group Data IDs in use in your title to 10 per player, and User Data (in all forms) has a limit on the number of keys - after all, nothing is unlimited, and we need to keep the service affordable for everyone. So if you want to store all the data about all the games for all time, you would need to do one of the following:
1. Set up an S3 bucket as an endpoint for the PlayStream Event Archive. That will get you 100% of all events for your game, so if you make each move (and then each game end report) a custom event, you'll have everything. Since you're using it for stats purposes, this is likely your best bet, since it will allow you to do full analysis on everything - each move made, time between moves, etc., etc.
2. Use a custom game server, so that you can write all the gameplay history to the Content service. The Content service isn't limited, since we pass on the costs for it directly (it's CloudFront).
I wouldn't recommend waiting on our turn-based service. That work has not been scheduled yet, so I can't guarantee when it will be available.
When you say I should not add the users directly, do you mean I should do it via cloud script?
I know there is a limited number of keys for the user data, but is there a size limit for each key?
I'm referring specifically to Server/AddSharedGroupMembers. Adding players that way gives them the ability to change any data from the client.
And yes, the Value for a Key in Shared Group Data is limited to 1,000 bytes.
You can increase the size limit for values in Shared Group Data via the Add-ons->Limits tab.
User Title Data is limited to 10,000 bytes by default, which you can also upgrade, but bear in mind that it's specific to the user, so you would need to either save it on each user, or save a reference to the other user's data on one of them. As with Shared Group Data, the other thing to bear in mind is that you do have a limit on the total number of keys you can write to - 10, by default - so you do need to make sure you're only keeping the most recent game sessions (though again, you can always collect the information directly, for long-term storage of all games).
"I wouldn't recommend waiting on our turn-based service. That work has not been scheduled yet, so I can't guarantee when it will be available."
when this features come?
Until something is in active development, we do not estimate completion dates. As a live service, we have to adapt to whatever conditions exist for our live titles as our highest priority, and that can move our schedule around quite a bit. We would not want anyone to take a dependency on a feature for which we aren't 100% confident of the date.
any sugestion for turn based game documantation, tutorial or sample project?
Answer by bda2206@gmail.com · Oct 30, 2016 at 12:42 AM
my work in progress node.js turn based game server
var express = require('express'); var bodyParser = require('body-parser'); var loki = require("lokijs"); var util = require('util'); var https = require('https'); var querystring = require('querystring'); var server = express(); var router = express.Router(); var games; server.use(bodyParser.urlencoded({ extended: true })); server.use(bodyParser.json()); router.get('/', function(req, res) { res.json({ message: 'hooray! welcome to our api!' }); }); var Playfab = { titleId: "", serverAPIKey : "", PublisherID : "", sdkVersion: "0.13.160328", ServerUrl : "????.playfabapi.com" } function OnUpdateSharedGroupData(res,callbackData) { console.log('OnUpdateSharedGroupData'); res.setEncoding('utf8'); res.on('data', function (chunk) { console.log('Playfab Response: ' + chunk); var result = JSON.parse(chunk); if (result){ if (result.status == "OK"){ //add to the list of games available in DB var newGame = { PlayerID : callbackData.PlayerID, SharedGroupID : callbackData.sharedGroupID, PlayerSkill : callbackData.PlayerSkill } console.log("newgame:"); games.insert(newGame); console.log("setheaders 45:"); callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'}); callbackData.rtc.end('Success'); } else{ console.log("setheaders 50:"); console.log("ERROR onUpdateUserData failed:"); callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'}); callbackData.rtc.end('Fail'); return false; } } }); } function OnAddSharedGroupToPlayersUserData(res,callbackData){ console.log('OnAddSharedGroupToPlayersUserData'); res.setEncoding('utf8'); res.on('data', function (chunk) { console.log('Playfab Response: ' + chunk); var result = JSON.parse(chunk); if (result){ if (result.status == "OK"){ //add to the list of games available in DB var newGame = { PlayerID : callbackData.PlayerID, SharedGroupID : callbackData.sharedGroupID, PlayerSkill : callbackData.PlayerSkill } console.log("newgame:" + newGame); games.insert(newGame); var requestData = '{ a json object ! }'; console.log(requestData); PlayfabExecuteClientRequest("UpdateSharedGroupData", requestData, callbackData, OnUpdateSharedGroupData); } else{ console.log("setheaders 91:"); console.log("ERROR onUpdateUserData failed:"); callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'}); callbackData.rtc.end('Fail'); return false; } } }); } function AddSharedGroupToPlayersUserData(callbackData){ console.log("AddSharedGroupToPlayersUserData"); //turn gamelist into json, then add sharedgroupID, then update users data.?? console.log(callbackData.gameList); if (callbackData.gameList == undefined){ var newGameList = callbackData.sharedGroupID; //var newGameList = JSON.stringify(gameList); console.log("was undefined,now:" + newGameList); //create a new list }else{ var newGameList = callbackData.gameList + "," + callbackData.sharedGroupID ; console.log("was defined, now:" + newGameList); } //callbackData["GameList"] = newGameList; var requestData = '{"Data" : {"GameList" : "' + newGameList + '"}}'; //console.log(requestData); PlayfabExecuteClientRequest("UpdateUserData", requestData, callbackData, OnAddSharedGroupToPlayersUserData); } function OnGetGameListToAddNewGroup(res,callbackData) { console.log('OnGetGameListToAddNewGroup'); res.setEncoding('utf8'); res.on('data', function (chunk) { console.log('Playfab Response: ' + chunk); var result = JSON.parse(chunk); if (result){ if (result.status == "OK"){ var gameList = result.data.Data.GameList.Value; console.log("gameList " + gameList) callbackData["gameList"] = gameList; AddSharedGroupToPlayersUserData(callbackData); } else{ console.log("ERROR OnGetGameListToAddNewGame chunk unparsed:" + chunk); callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'}); callbackData.rtc.end('Fail'); } } }); } function DelSharedGroupFromPlayersUserData(playerID, sessionTicket, sharedGroupID, callback){ } function OnCreateSharedGroup(res,callbackData) { //do this with the players token, prove auth is ok console.log('OnCreateSharedGroup'); res.setEncoding('utf8'); res.on('data', function (chunk) { console.log('Playfab Response: ' + chunk); var result = JSON.parse(chunk); if (result){ if (result.status == "OK"){ var sharedGroupID = result.data.SharedGroupId; callbackData["sharedGroupID"] = sharedGroupID; //GetGameListToAddNewGroup(callbackData); var requestData = '{ "Keys" : ["GameList"] }'; //var postData = querystring.stringify(requestData); //console.log(postData); PlayfabExecuteClientRequest('GetUserData', requestData, callbackData, OnGetGameListToAddNewGroup); } } }); } function OnAddSharedGroupToPlayer2sUserData(res,callbackData){ console.log('onUpdateUserData'); res.setEncoding('utf8'); res.on('data', function (chunk) { console.log('Playfab Response: ' + chunk); var result = JSON.parse(chunk); if (result){ if (result.status == "OK"){ //remove from the list of games available in DB //var results = games.findOne({ SharedGroupID: {'$eq': callbackData.SharedGroupID} }); callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'}); callbackData.rtc.end('Success'); } else{ console.log("ERROR onUpdateUserData failed:"); callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'}); callbackData.rtc.end('Fail'); return false; } } }); } function AddSharedGroupToPlayer2sUserData(callbackData){ console.log("AddSharedGroupToPlayer2sUserData"); //turn gamelist into json, then add sharedgroupID, then update users data.?? console.log(callbackData.GameList); var newGameList = ""; if (callbackData.GameList == undefined){ newGameList = callbackData.SharedGroupID; //var newGameList = JSON.stringify(gameList); console.log("was undefined" + newGameList); //create a new list }else{ newGameList = callbackData.GameList + "," + callbackData.SharedGroupID ; console.log("was defined" + newGameList); } //callbackData["GameList"] = newGameList; var requestData = '{"PlayFabId" : "'+callbackData.PlayerID +'", "Data" : {"GameList" : "' + newGameList + '"}}'; console.log(requestData); PlayfabExecuteServerRequest("UpdateUserData", requestData, callbackData, OnAddSharedGroupToPlayer2sUserData); } function OnGetGameList2ToAddNewGroup(res,callbackData) { console.log('OnGetGameList2ToAddNewGroup'); res.setEncoding('utf8'); res.on('data', function (chunk) { console.log('Playfab Response: ' + chunk); var result = JSON.parse(chunk); if (result){ if (result.status == "OK"){ var gameList = result.data.Data.GameList.Value; console.log("GameList " + gameList) callbackData["GameList"] = gameList; AddSharedGroupToPlayer2sUserData(callbackData); } else{ console.log("ERROR OnGetGameListToAddNewGame chunk unparsed:" + chunk); callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'}); callbackData.rtc.end('Fail'); } } }); } function OnUpdateSharedGroupPlayer2(res,callbackData){ //add the game to player2s list of games var requestData = '{ "PlayFabId" : "'+callbackData.PlayerID +'", "Keys" : ["GameList"] }'; //var postData = querystring.stringify(requestData); //console.log(postData); PlayfabExecuteServerRequest('GetUserData', requestData, callbackData, OnGetGameList2ToAddNewGroup) } function DeleteInvalidSharedGroupData(res,callbackData){ console.log('DeleteInvalidSharedGroupData'); res.setEncoding('utf8'); res.on('data', function (chunk) { console.log('Playfab Response: ' + chunk); var result = JSON.parse(chunk); if (result){ if (result.status == "OK"){ NewGameRequested(callbackData); } else{ console.log("ERROR DeleteInvalidSharedGroupData chunk unparsed:" + chunk); callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'}); callbackData.rtc.end('Fail'); } } }); } function OnGetSharedGroupInfo(res,callbackData){ console.log('OnGetSharedGroupInfo'); res.setEncoding('utf8'); res.on('data', function (chunk) { console.log('Playfab Response: ' + chunk); var result = JSON.parse(chunk); if (result){ if (result.status == "OK"){ //check game is valid if ( result.data.Data.Moves != null && result.data.Data.P1 != null && result.data.Data.P2 != null && result.data.Data.GameData != null){ var WhosTurn = result.data.GameData; if (WhosTurn == "WaitingForPlayer"){ //add player2 and update whosturn var requestData = '{ "SharedGroupId" :"'+ callbackData.SharedGroupID +'","Data" : {"GameData" : "'+callbackData.PlayerID +'", "Player2" :"'+callbackData.PlayerID +'"} }'; }else{ var requestData = '{ "SharedGroupId" :"'+ callbackData.SharedGroupID + '",'+ '"Data" : {"P2" :"'+ callbackData.PlayerID + '1-2-3-4-5-6-7-'+ callbackData.PlayerName+'"} }'; //"WaitingForPlayer1-2-3-4-5-6-7-Waiting For Player } PlayfabExecuteServerRequest('UpdateSharedGroupData', requestData, callbackData, OnUpdateSharedGroupPlayer2); } }else{ var requestData = '{ "SharedGroupId" :"'+ callbackData.SharedGroupID + '"}'; PlayfabExecuteServerRequest('DeleteInvalidSharedGroupData', requestData, callbackData, OnDeleteInvalidSharedGroupData); } } }); } function OnDeleteSharedGroup(res,callbackData){ callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'}); callbackData.rtc.end('Fail'); } function OnAddSharedGroupMember(res,callbackData){ console.log('OnAddSharedGroupMember'); res.setEncoding('utf8'); res.on('data', function (chunk) { console.log('Playfab Response: ' + chunk); var result = JSON.parse(chunk); if (result){ if (result.status == "OK"){ //var SharedGroupID = callbackData.SharedGroupID; //callbackData["SharedGroupID"] = SharedGroupID; //get whos turn var requestData = '{ "SharedGroupId" :"'+ callbackData.SharedGroupID +'", "Keys" : ["Moves","P1","P2","GameData"]}'; PlayfabExecuteServerRequest('GetSharedGroupData', requestData, callbackData, OnGetSharedGroupInfo); } else{ var requestData = '{ "SharedGroupId" :"'+ callbackData.SharedGroupID +'"}'; PlayfabExecuteServerRequest("DeleteSharedGroup", requestData, callbackData, OnDeleteSharedGroup); } } }); } function onGetAccountInfoForPlayer2(res,callbackData){ //used to confirm auth of player 2. console.log('onGetAccountInfoForPlayer2'); res.setEncoding('utf8'); res.on('data', function (chunk) { console.log('Playfab Response: ' + chunk); var result = JSON.parse(chunk); if (result){ if (result.status == "OK"){ var requestData = '{ "SharedGroupId" : "'+ callbackData.SharedGroupID +'","PlayFabIDs" : ["' + callbackData.PlayerID+'"] }'; PlayfabExecuteServerRequest('AddSharedGroupMembers', requestData, callbackData, OnAddSharedGroupMember); } else{ //unauth - fail message } } }); } function PlayfabExecuteServerRequest(request,requestData,callbackData,callback){ var postData = querystring.stringify(requestData); console.log("ServerRequest:" + request ); console.log(requestData); var postOptions = { host: Playfab.ServerUrl , port: '443', path: '/Server/' + request, method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', 'X-PlayFabSDK': 'JavaScriptSDK-' + Playfab.sdkVersion, 'X-SecretKey' : Playfab.serverAPIKey } }; // Set up the request var postReq = https.request(postOptions, function(res) { //console.log('postReq callback!'); callback(res,callbackData); }); // console.log('postReq callback!'); // post the data postReq.write(requestData); postReq.end(); } function PlayfabExecuteClientRequest(request,requestData,callbackData,callback){ var postData = querystring.stringify(requestData); console.log("ClientRequest:" + request ); console.log(requestData); var postOptions = { host: Playfab.ServerUrl , port: '443', path: '/Client/' + request, method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', 'X-PlayFabSDK': 'JavaScriptSDK-' + Playfab.sdkVersion, 'X-Authorization' : callbackData.Token } }; // Set up the request var postReq = https.request(postOptions, function(res) { //console.log('postReq callback!'); callback(res,callbackData); }); // console.log('postReq callback!'); // post the data postReq.write(requestData); postReq.end(); } function NewGameRequested(callbackData) { var results = games.findOne({ PlayerID: {'$ne': req.body.PlayerID} }); if (results == null){ console.log("start a game"); var requestData = ""; callbackData = { PlayerID : req.body.PlayerID, PlayerName : req.body.PlayerName, Token : req.body.Token, rtc : res //response to client } PlayfabExecuteClientRequest('CreateSharedGroup', requestData, callbackData, OnCreateSharedGroup); } else{ console.log("found a game"); console.log(results); var SharedGroupID = results.SharedGroupID; console.log(SharedGroupID); callbackData = { SharedGroupID : SharedGroupID, PlayerID : req.body.PlayerID, PlayerName : req.body.PlayerName, Token : req.body.Token, rtc : res //response to client } //delete the game from the server now to avoid double allocation! games.remove(results); var requestData = ''; PlayfabExecuteClientRequest('GetAccountInfo', requestData, callbackData, onGetAccountInfoForPlayer2); } } router.route('/newgame') .post(function(req,res){ console.log("----------------------------------------------------------------------"); console.log('post!' + util.inspect(req.body, false, null)); //validate the request.body for playerID and Token if ((req.body.PlayerID)&& (req.body.PlayerName) && (req.body.Token)&& (req.body.PlayerSkill)) { var results = games.findOne({ PlayerID: {'$ne': req.body.PlayerID} }); if (results == null){ console.log("start a game"); var requestData = ""; var callbackData = { PlayerID : req.body.PlayerID, PlayerName : req.body.PlayerName, Token : req.body.Token, rtc : res //response to client } PlayfabExecuteClientRequest('CreateSharedGroup', requestData, callbackData, OnCreateSharedGroup); } else{ console.log("found a game"); console.log(results); var SharedGroupID = results.SharedGroupID; console.log(SharedGroupID); var callbackData = { SharedGroupID : SharedGroupID, PlayerID : req.body.PlayerID, PlayerName : req.body.PlayerName, Token : req.body.Token, rtc : res //response to client } //delete the game from the server now to avoid double allocation! games.remove(results); var requestData = ''; PlayfabExecuteClientRequest('GetAccountInfo', requestData, callbackData, onGetAccountInfoForPlayer2); } } else{ res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('invalid post data'); } }); server.use('/api', router); var db = new loki('data.db', { autoload: true, autoloadCallback : loadHandler, autosave: true, autosaveInterval: 5000 }); function loadHandler() { // if database did not exist it will be empty so I will intitialize here games = db.getCollection('games'); if (games === null) { console.log('create collection'); games = db.addCollection('games'); } //coll.insert(game); server.listen(3000,function(){ console.log('request!'); }); }
Where to store photon gameStates. 1 Answer
Bigger User Data (e.g. saving recordings) 1 Answer
Should I use Playfab Party or Playfab Multiplayer Servers? 1 Answer
Where/How to save players personal data? 1 Answer
Questions on storing a list of strings on PlayFab to be accessed by all users, or similar methods 2 Answers