Player Data is a storage solution to save the player’s progress within the game. Each player has their own Player Data separate from all other players. Player Data is created anew for each game. It is preserved from ball to ball but it lasts only until the end of the game.
Player Data is documented here in its own section in the SDK Developer Guide.
Player Data is stored in the Player instance accessible from a Mode. Be aware that the Player will be null before the game starts, like in P3SABaseGameMode or P3SAAttractMode. These Modes have no Player Data. That’s fine because there is no Player progress to save at that point.
Player Data is implemented as a Dictionary<string, AttributeValue>.
An AttributeValue is an object that can store a single value of type int, long, float, double, bool or string. When retrieving the value, it can be converted to any of those types with ToInt(), ToLong(), ToFloat(), ToDouble(), ToBool() or ToString().
AttributeValue attrValue = …;
data.currentPlayer.SaveData(key, attrValue);
int value = data.currentPlayer.GetData(key).ToInt();
It is better to check whether a key exists in the Dictionary first:
int value = data.currentPlayer.ContainsKey(key) ? data.currentPlayer.GetData(key).ToInt() : 0;
In practice, you rarely call these methods because the Player class has easier helper methods.
Calling SaveData() on the Player instance sets the value on the AttributeValue if it already exists, or it creates a new AttributeValue and stores it in the Dictionary. SaveData() supports the same data types as AttributeValue.
data.currentPlayer.SaveData(key, value);
Calling GetData() returns the defaultValue if the AttributeValue is absent, otherwise it returns the AttributeValue’s value converted to the same type as defaultValue.
value = data.currentPlayer.GetData(key, defaultValue);
The score is stored in Player Data and it has its dedicated methods. SetScore()/GetScore() simply saves and gets the data for the key named "Score". These score methods are rarely called directly because the score is normally handled by the ScoreManager.
long score = data.currentPlayer.GetScore();
data.currentPlayer.SetScore(score);
You can remove a key from the Dictionary, though this is rarely needed.
data.currentPlayer.RemoveData(key);
There are methods to load and save the Player Data in a file. These are used by savepoints.
data.currentPlayer.LoadDataFromFile(filename);
data.currentPlayer.SaveDataToFile(filename);
So far, we have only showed how to call the methods on the currentPlayer because that’s the most common. It is also possible to access the Player Data for any player by accessing the Players List with a player index from 0 to data.Players.Count - 1.
data.Players[playerIndex].GetData(key, defaultValue);
SaveData() stores the type in the AttributeValue when the AttributeValue is first created. The type never changes afterward, even if a value of a different type is saved.
The type must be remembered because it affects how the value is converted to string.
data.currentPlayer.SaveData("MyInt", 1);
data.currentPlayer.SaveData("MyBool", true);
string myIntType = data.currentPlayer.GetData("MyInt").type; // returns "System.Int32"
string myBoolType = data.currentPlayer.GetData("MyBool").type; // returns "System.Boolean"
string myIntStr = data.currentPlayer.GetData("MyInt").ToString(); // returns "1"
string myBoolStr = data.currentPlayer.GetData("MyBool").ToString(); // returns "True"
data.currentPlayer.SaveData("MyInt", 0); // Save the same value in both
data.currentPlayer.SaveData("MyBool", 0); // Converts int to bool
string myIntStr0 = data.currentPlayer.GetData("MyInt").ToString(); // returns "0"
string myBoolStr0 = data.currentPlayer.GetData("MyBool").ToString(); // returns "False"
Type conversion works, but it’s clearer when always using the same type for a specific key.
When Team Play is enabled, Players with the same Profile are on the same team and share their progress. This is implemented by sharing the Player Data. Concretely, all Players in the team reuse the same Dictionary instance to store the Player Data. This is configured at the start of the game when the Players are created, likely using this Player method:
void SetDataDict(Dictionary<string, AttributeValue> newData)
The easiest strategy to manage Player Data is to always retrieve the value whenever it is needed. This is not a costly operation. This is the best strategy if multiple Modes access the same Player Data key.
int numAliens = data.currentPlayer.GetData("NumAliens", 3);
data.currentPlayer.SaveData("NumAliens", numAliens + 1);
Another strategy is to load the Player Data values into member variables when the Mode starts, and save the values in Player Data when the Mode stops. This is a good approach when that Player Data is managed by a single Mode. Another reason is to organize multiple Player Data values into a data structure like an array or a List for easier indexing.
This can be implemented directly in mode_started() and mode_stopped() but the SDK has built-in methods that make the intention clearer.
Code similar to this can be found in LanesMode in P3SampleApp:
public override void LoadPlayerData() { numCompletions = data.currentPlayer.GetData("NumLaneCompletions", 0); laneStates = new List<bool>(); for (int i=0; i<4; i++) { laneStates.Add(data.currentPlayer.GetData("LaneStates" + i, false)); } } public override void SavePlayerData() { for (int i=0; i<4; i++) data.currentPlayer.SaveData("LaneStates" + i.ToString(), laneStates[i]); data.currentPlayer.SaveData("NumLaneCompletions", numCompletions); } |
The method LoadPlayerData() is called by the GameMode superclass. For this to work, make sure your mode_started() method calls base.mode_started().
Similarly, the method SavePlayerData() is called by the GameMode superclass. Make sure your mode_stopped() method calls base.mode_stopped().
The Player instance holds another piece of Player Data. The Player instance has a member named extraBallCount which counts how many extra balls the player has earned and not used yet. Since this value is not in the Player Data Dictionary, it is not preserved when saving and restoring savepoints.
HudMode has the ability to show if the player has an extra ball available or not. P3SampleApp never awards an extra ball, so this will always show the Player has no extra ball in this game.
NextBallMode is responsible for checking the extraBallCount. If there is an extra ball available, the same player will be asked to shoot again.
This table lists the Player Data used by the SDK. The application is free to create more Player Data with non-conflicting keys.
Key | Type | Description |
BonusX | float | Saves ScoreManager.GetBonusX() when the ball ends, this BonusX will be reapplied when the next ball starts if the Player Data HoldBonusX is True. |
GameRestored | bool | Whether this Player Data comes from a restored savepoint. This is used to deny a replay and most high scores if the game was restored. |
HighestBonus | long | Best end of ball bonus by a single ball among balls already ended. Computed by the SDK but otherwise unused. The application can choose to use this value at end of ball or end of game bonus, for example. |
HighScoreNameEntered | string | Name entered during High Score Name Entry. This is used to show the results. |
HoldBonusX | bool | Saves ScoreManager.GetHoldBonusX() when the ball ends to make it available when the next ball starts. |
Profile | string | Profile name active for this player or "<None>" for the global profile. This is used to activate the profile when it is the player’s turn. It is also used to construct the player name. |
ReplayAchieved | bool | Whether this player earned a replay. This is used to award a replay only once per player per game. |
ReplayLevel | long | Score needed to earn a replay by this player. Defaults to the value of the GameAttribute CurrentReplayScore. This is only effective if the GameAttribute ReplaysEnabled is true. |
Score | long | Player’s score. Managed by ScoreManager. |
SingleBallScore | long | Best score by a single ball among balls already ended. Computed by the SDK but otherwise unused. The application can choose to use this value at end of ball or end of game bonus, for example. |
TeamMember | bool | Whether this player is part of a team. Default is False. This is used to deny a replay if the player is in a team. Also used to display the results. |
TeamNumber | int | If the player is a TeamMember, this is the number of the team the player is a member of. Computed but otherwise unused by the SDK. |
Technically, the SDK is unaware of BonusX and HoldBonusX in Player Data. To implement HoldBonusX, the SDK needs help from the application. ScoreManager keeps track of the BonusX and HoldBonusX for the current ball. HomeMode saves these values in Player Data when the ball ends and reapplies the BonusX in ScoreManager when the next ball starts if HoldBonusX is true. See SavePlayerData() and LoadPlayerData() in HomeMode. This functionality is important to preserve if your application supports HoldBonusX and has multiple scenes.
Note: scoreX and lastBallNumber are not used in P3SampleApp, so I’m not talking about them here.
As an experiment, it is possible to access the Player Data Dictionary and enumerate all entries with code like this:
System.Text.StringBuilder sb = new System.Text.StringBuilder(); sb.AppendLine("Player Data:"); foreach (string name in data.currentPlayer.GetDataDict().Keys.OrderBy(key => key)) { Multimorphic.P3App.Data.AttributeValue attrValue = data.currentPlayer.GetData(name); sb.AppendLine(" " + name + "=" + attrValue.ToString() + " // type=" + attrValue.type); } Multimorphic.P3App.Logging.Logger.LogError(sb.ToString()); |
See the sample output in Appendix A.
This is the list of Player Data in P3SampleApp when the Player is running under a Profile named JOHN. This was captured on ball 2 to make sure the modes populated the Player Data when they stopped (i.e. when ball 1 ended).
Player Data:
BallSaveGracePeriod=3 | // type=System.Int32 |
BallSaveTime=15 | // type=System.Int32 |
BonusX=2.00 | // type=System.Single |
HighestBonus=24000 | // type=System.Int64 |
HoldBonusX=False | // type=System.Boolean |
HomeAttempted=False | // type=System.Boolean |
HomeAttemptedOnce=False | // type=System.Boolean |
HomeCompleted=False | // type=System.Boolean |
JOHNDataVersion=1 | // type=System.Int32 |
LaneStates0=False | // type=System.Boolean |
LaneStates1=False | // type=System.Boolean |
LaneStates2=False | // type=System.Boolean |
LaneStates3=False | // type=System.Boolean |
LastBallNumber=1 | // type=System.Int32 |
MultiballBallSaveTime=10 | // type=System.Int32 |
MultiballJackpotTimeout=15.00 | // type=System.Single |
MultiballSuperJackpotTimeout=5.00 | // type=System.Single |
NumLaneCompletions=1 | // type=System.Int32 |
PlayGameIntro=False | // type=System.Boolean |
Profile=JOHN | // type=System.String |
ProfileStateSaveEnabled=True | // type=System.Boolean |
ReplayAchieved=False | // type=System.Boolean |
ReplayLevel=1000000 | // type=System.Int64 |
Score=45300 | // type=System.Int64 |
ShotCounter=0 | // type=System.Int32 |
SideTargetDifficulty=0 | // type=System.Int32 |
SideTargetStates0=False | // type=System.Boolean |
SideTargetStates1=False | // type=System.Boolean |
SideTargetStates2=False | // type=System.Boolean |
SideTargetStates3=False | // type=System.Boolean |
SideTargetStates4=True | // type=System.Boolean |
SideTargetStates5=True | // type=System.Boolean |
SideTargetStates6=True | // type=System.Boolean |
SideTargetStates7=True | // type=System.Boolean |
SingleBallScore=21300 | // type=System.Int64 |