This document provides an overview and examples of using Cloud Script, a Platform-as-a-Service company's server-side scripting solution. It recaps the basics of creating and calling scripts, and provides scenarios for validating player actions, granting rewards, messaging between players, and making external API calls. Examples show how to validate moves, grant items for completing levels, send messages stored in shared group data, and make authorized calls to third-party APIs using OAuth tokens obtained via Cloud Script. Resources for learning more about Cloud Script capabilities and examples are also listed.
6. In GitHub
• Examples of the essentials
• AsynchronousMatchmaker –
Companion to our blog post
• BasicSample – Provided with all new
titles
• Photon-Cloud-Integration – Shows
using custom auth and webhooks
• Rewards – Granting items and virtual
currency to players
October 15, 2015 5
7. BasicSample – Hello World
• Because there’s always “Hello World”!
handlers.helloWorld = function (args) {
// "currentPlayerId" is initialized to the PlayFab ID of the player logged-in on the game client.
// Cloud Script handles authenticating the player automatically.
var message = "Hello " + currentPlayerId + "!";
// You can use the "log" object to write out debugging statements. The "log" object has
// three functions corresponding to logging level: debug, info, and error.
log.info(message);
// Whatever value you return from a CloudScript handler function is passed back
// to the game client. It is set in the "Results" property of the object returned by the
// RunCloudScript API. Any log statments generated by the handler function are also included
// in the "ActionLog" field of the RunCloudScript result, so you can use them to assist in
// debugging and error handling.
return { messageValue: message };
}
October 15, 2015 6
8. [Basics] Scenario 1: Updates
October 15, 2015 7
• Updating a title is more than just the executable
• Changing items
• User data
• Save values
• User Internal Data to track player data version
• Have an “onLogin” handler
• Calls current version handler (“updateToRevN”), passing in player data version
• If not at this version, call previous version handler
• Once returned, update for latest version
• onLogin then sets player version
Yeah, don’t do that…
9. Updating the User Version
• Roll the changes into one call
handlers.playerLogin = function (args) {
var currentVersion = "7";
var versionKey = "userVersion";
var playerData = server.GetUserInternalData({
PlayFabId: currentPlayerId,
Keys: [versionKey]
});
var userVersion = playerData.Data[versionKey];
if (userVersion != currentVersion) {
UpdateToVersion7(userVersion, currentPlayerId);
}
var updateUserDataResult = server.UpdateUserInternalData({
PlayFabId: currentPlayerId,
Data: {
userVersion: currentVersion
}
});
October 15, 2015 8
log.debug("Set title version for player " + currentPlayerId + "
to " + currentVersion);
}
function UpdateToVersion7(userVersion, userPlayFabId)
{
// Here's where you'd update the player's data
// changing any items or stat values needed for this version
return null;
}
10. [Basics] Scenario 2: Player Actions
October 15, 2015 9
• For competitive games
• Resolution of player action must be server authoritative
• Player selects moves, which are sent to Cloud Script
• Evaluate moves
• Are they possible?
• Does the player have the right items?
• etc.
• On failure, write info to User Internal Data
• Use cheat tracking to have cheaters play together
11. BasicSample – Validating Player Action
• Has enough time passed?
function processPlayerMove(playerMove) {
var now = Date.now();
var playerMoveCooldownInSeconds = 15;
var playerData = server.GetUserInternalData({
PlayFabId: currentPlayerId,
Keys: ["last_move_timestamp"]
});
var lastMoveTimestampSetting = playerData.Data["last_move_timestamp"];
if (lastMoveTimestampSetting) {
var lastMoveTime = Date.parse(lastMoveTimestampSetting.Value);
var timeSinceLastMoveInSeconds = (now - lastMoveTime) / 1000;
log.debug("lastMoveTime: " + lastMoveTime + " now: " + now + " timeSinceLastMoveInSeconds: " +
timeSinceLastMoveInSeconds);
if (timeSinceLastMoveInSeconds < playerMoveCooldownInSeconds) {
log.error("Invalid move - time since last move: " + timeSinceLastMoveInSeconds + "s less than minimum of " +
playerMoveCooldownInSeconds + "s.")
return false;
}
}
October 15, 2015 10
12. BasicSample – Validating Player Action
• If so, update the statistics and the timestamp
var playerStats = server.GetUserStatistics({
PlayFabId: currentPlayerId
}).UserStatistics;
if (playerStats.movesMade)
playerStats.movesMade += 1;
else
playerStats.movesMade = 1;
server.UpdateUserStatistics({
PlayFabId: currentPlayerId,
UserStatistics: playerStats
});
server.UpdateUserInternalData({
PlayFabId: currentPlayerId,
Data: {
last_move_timestamp: new Date(now).toUTCString()
}
});
return true;
}
October 15, 2015 11
13. [Basics] Scenario 3: Rewards
October 15, 2015 12
• Determination of rewards for player actions
• Player actions are sent to Cloud Script
• Evaluate actions
• Has enough time passed?
• Are the values reasonable?
• etc.
• On failure, write info to User Internal Data
• Use cheat tracking to decide how to manage them
14. Rewards – onLevelComplete
• Optional: Define rewards in Title Internal Data
var LevelRewards =
[
["TestItem1"],
["TestItem2"],
["TestItem3"],
["TestItem1", "TestItem2"],
["TestItem2", "TestItem2"],
["TestItem3", "TestItem3"]
]
handlers.onLevelComplete = function(args)
{
var levelNum = args.level;
// Do some basic input validation
if(levelNum < 0 || levelNum >= LevelRewards.length)
{
log.info("Invalid level "+levelNum+" completed by "+currentPlayerId);
return {};
}
October 15, 2015 13
15. Rewards – onLevelComplete
• Also tracking level completion
var levelCompleteKey = "LevelCompleted"+levelNum;
// Get the user's internal data
var playerInternalData = server.GetUserInternalData(
{
PlayFabId: currentPlayerId,
Keys: [levelCompleteKey]
});
// Did they already complete this level?
if(playerInternalData.Data[levelCompleteKey])
{
log.info("Player "+currentPlayerId+" already completed level "+levelNum);
return {};
}
October 15, 2015 14
16. Rewards – onLevelComplete
• Grant the reward
var rewards = LevelRewards[levelNum];
var resultItems = null;
if(rewards)
{
// Grant reward items to player for completing the level
var itemGrantResult = server.GrantItemsToUser(
{
PlayFabId: currentPlayerId,
Annotation: "Given by completing level "+levelNum,
ItemIds: rewards
});
resultItems = itemGrantResult.ItemGrantResults;
}
October 15, 2015 15
17. Rewards – onLevelComplete
• And finally update the level tracking and return the information on the reward
// Mark the level as being completed so they can't get the reward again
var saveData = {};
saveData[levelCompleteKey] = "true";
server.UpdateUserInternalData(
{
PlayFabId: currentPlayerId,
Data: saveData
});
// Return the results of the item grant so the client can see what they got
return {
rewards: resultItems
};
}
October 15, 2015 16
18. [Basics] Scenario 4: Messaging
October 15, 2015 17
• Push Messages
• Require Server authority
• Player attacking another player’s base
• Player beat a friend’s score
• etc.
• Player-to-player messages
• Write them to Shared Group Data, using ID of PlayFabId
• Arbitrary messages, with arbitrary payloads
19. Using the PlayFab ID as the Key, Part 1
• Really, you’d expect this to work, wouldn’t you?
handlers.messageToPlayer = function (args) {
var messageGroupId = args.toPlayerId + "_messages";
server.UpdateSharedGroupData(
{
"SharedGroupId": messageGroupId, "Data" :
{
currentPlayerId : args.messageText
}
}
);
}
October 15, 2015 18
20. Using the PlayFab ID as the Key, Part 1
• But yeah, it doesn’t
{
"code": 200,
"status": "OK",
"data": {
"Data": {
"currentPlayerId": {
"Value": "Hi there!",
"LastUpdated": "2015-10-14T07:25:22.749Z",
"Permission": "Private"
}
}
}
}
October 15, 2015 19
Really, you need to watch:
https://www.destroyallsoftware.com/talks/wat
21. Using the PlayFab ID as the Key, Part 2
• Here’s how to do it
handlers.messageToPlayer = function (args) {
var messageGroupId = args.toPlayerId + "_messages";
var dataPayload = {};
var keyString = currentPlayerId;
dataPayload[keyString] = args.messageText;
server.UpdateSharedGroupData(
{
"SharedGroupId": messageGroupId, "Data" : dataPayload
}
);
}
October 15, 2015 20
22. Consuming the Message
• Space is limited – once the message is received, remove it
handlers.checkMessages = function (args) {
var messageGroupId = currentPlayerId + "_messages";
var messageList = server.GetSharedGroupData({"SharedGroupId": messageGroupId});
var dataPayload = {};
for (var key in messageList.Data)
{
if (messageList.Data.hasOwnProperty(key)) {
var message = JSON.parse(messageList.Data[key].Value);
// Take action on the key - display to user, etc.
var keyString = key;
dataPayload[keyString] = null;
}
}
server.UpdateSharedGroupData({
"SharedGroupId": messageGroupId, "Data" : dataPayload
});
}
October 15, 2015 21
23. [Basics] Scenario 5: Extra Leaderboard Columns
October 15, 2015 22
• Shared Group Data
• Storage not tied to a particular player
• Readable by any player (permission set Public)
• Data value per player
• Key is the PlayFab ID of the player
• Write extra data when updating score
• One API call to read players in display
24. [New and Improved] Scenario 5: Web API Calls
• Whether your own or a third party
• The http function of Cloud Script allows for secure calls
• Enables integration with any third-party Web API
• The question to ask is, what are your needs?
October 15, 2015 23
25. Option 1: Use Session Ticket
• Using basic authentication mechanism from PlayFab
• Use Server/AuthenticateSessionTicket to validate
handlers.externalCall = function (args) {
var url = "https://api.yourdomainhere.com/playfab/someaction";
var method = "post";
var obj = {"dataValue1":value1, "dataValue2":value2};
var contentBody = JSON.stringify(obj);
var contentType = "application/json";
var headers = {};
headers["Authorization"] = args.sessionTicket;
var response = http.request(url,method,contentBody,contentType,headers);
}
October 15, 2015 24
26. Option 2: OAuth2
• More secure solutions may require OAuth2, or similar
• Which, as a Web API, we support – no problem
• Normal process for OAuth2-type systems
• Use a locally stored secret key to request a token from the OAuth2 service
• Use that token to access secure calls
• Recommendation: Short token lifetime
October 15, 2015 25
27. Obtaining the Bearer Token
• Use a secret key to obtain a client-specific token
• Optional: Store the secret key in PlayFab, and the client never even sees it
function GetAccesToken(secretKey)
{
var url = "https://api.yourdomainhere.com/RequestToken";
var method = "post";
var contentBody = "token_type=Bearer";
var contentType = "application/x-www-form-urlencoded";
var headers = {};
headers["Authorization"] = "Basic "+secretKey;
var response = http.request(url,method,contentBody,contentType,headers);
var finalData = JSON.parse(response);
var access_token = finalData["access_token"];
return access_token;
}
October 15, 2015 26
28. Using the Bearer Token
• Once you have the Bearer Token, use it to access custom functionality
• Note: Do not store the Bearer Token for re-use – regenerate it each time
handlers.externalCall = function (args) {
var url = "https://api.yourdomainhere.com/playfab/someaction";
var method = "post";
var obj = {"dataValue1":value1, "dataValue2":value2};
var contentBody = JSON.stringify(obj);
var contentType = "application/json";
var bearerAccessToken = GetAccessToken(args.secretKey);
var headers = {};
headers["Authorization"] = "Bearer "+ bearerAccesToken;
var response = http.request(url,method,contentBody,contentType,headers);
}
October 15, 2015 27