Player Data is a storage solution to save the player’s progress within the game. Each player has his own Player Data separate from all other players. Player Data is created new for each game. It is preserved from ball to ball but it lasts only till the end of the game.
Player Data is documented here in the SDK Guide file:///C:/P3/P3_SDK_V0.8/P3SampleApp/Documentation/html/_player_data.html
Player Data is stored in the Player instance accessible from a Mode. Beware the Player will be null before the game is started, like in P3SABaseGameMode or P3SAAttractMode. These Modes have no Player Data. That’s fine because there is no Player progress to save at that time anyway.
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 convert 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 if 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. The type of value can be int, float, double, bool or string. See the section below for type long.
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 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)
The method Player.SaveData(string key, long dataToSave) is missing in P3_SDK_V0.8. If you call SaveData() with a long, it is converted to a float by the C# implicit conversion rules and then to string. The GetData() method can handle a long and will convert the string back to the original long. You will not see the difference unless the value is out of range, or you convert to string.
long myLong = 123;
data.currentPlayer.SaveData("MyLong", myLong);
string myLongAsString = data.currentPlayer.GetData("MyLong", "unknown"); // returns "123.00"
string myLong2 = data.currentPlayer.GetData("MyLong", 0L); // returns 123
The score value is a long and it has the same problem. This is more important because the maximum long you can store accurately is 10,000,000. You can store bigger long values, but you start to lose accuracy.
long score = 10000001;
data.currentPlayer.SetScore(score);
string scoreString = data.currentPlayer.GetData("Score").ToString(); // returns 10000000.00
long storedScore = data.currentPlayer.GetScore(); // returns 10000000, lost 1
Multimorphic promises to add the missing SaveData() method in the next SDK release and I expect that will also fix the score.
SaveData() stores the type in the AttributeValue when it creates it initially. The type never changes afterwards even if saving a value with a different type.
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"
Be careful with type conversion. There is a bug when converting with data.currentPlayer.SaveData(key, value). This works when calling data.currentPlayer.GetData(key).Set(value) .
data.currentPlayer.SaveData("MyBool", 1); // Convert int to bool with SaveData() is buggy
string myBoolStrBug = data.currentPlayer.GetData("MyBool").ToString(); // returns "False"???
data.currentPlayer.GetData("MyBool").Set(1); // Convert int to bool with Set() works
string myBoolStrOk = data.currentPlayer.GetData("MyBool").ToString(); // returns "True"
It might be best to avoid type conversion with Player Data and always use the same type for a specific key.
When Team Play is enabled, Players with the same Profile are in 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, probably with 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 in 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 might be 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[i] = data.currentPlayer.GetData("LaneStates" + i, false); } } public override void SavePlayerData() { data.currentPlayer.SaveData("NumLaneCompletions", numCompletions); for (int i=0; i<4; i++) { data.currentPlayer.SaveData("LaneStates" + i, laneStates[i]); } } |
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 to check 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 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 in an end of ball or end of game bonus for example. |
HighScoreNameEntered | string | Name entered when prompted for a 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 compute 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 in an 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. **This value is broken in P3_SDK_V0.8, instead use the profile name if you need to index the teams** |
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 is mostly dead code in HomeMode, so I’m not talking about it here.
See Appendix A for a list of Player Data specific to P3SampleApp.
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 Appendix A for sample output.
The first thing you will notice is the GameAttributes also appear in Player Data. This feature is unfortunate and should not be relied upon. Player Data does not track changes to the settings during the game or between savepoint save and restore. Always read the GameAttributes with data.GetGameAttributeValue() instead.
I noticed only the Profile specific GameAttributes are copied to Player Data when the Player is in a Profile. Could it be this feature was an early attempt to implement Profile specific GameAttributes?
This is the list of Player Data in P3SampleApp when the Player is running in 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).
Notice how HighestBonus appears as a System.Single in P3_SDK_V0.8. Without more information, you would think HighestBonus is a float when it is in fact a long. Same thing for the Score (and a few others).
This output was edited to form two columns to make it easier to read.
Player Data:
BallSaveGracePeriod=3 | // type=System.Int32 |
BallSaveTime=15 | // type=System.Int32 |
BonusX=1.00 | // type=System.Single |
Evt_LeftRampInc=0 | // type=System.Int32 |
Evt_RightRampInc=0 | // type=System.Int32 |
HighestBonus=5,000.00 | // type=System.Single |
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 |
ModeLitOnBallStart=True | // type=System.Boolean |
MyLong=123.00 | // type=System.Single |
NumLaneCompletions=0 | // type=System.Int32 |
PlayGameIntro=False | // type=System.Boolean |
Profile=John | // type=System.String |
ProfileStateSaveEnabled=True | // type=System.Boolean |
ReplayAchieved=False | // type=System.Boolean |
ReplayLevel=8,000,000.00 | // type=System.Single |
Score=29,900.00 | // type=System.Single |
SideTargetComplete=True | // type=System.Boolean |
SideTargetDifficulty=0 | // type=System.Int32 |
SideTargetStates0=True | // type=System.Boolean |
SideTargetStates1=True | // type=System.Boolean |
SideTargetStates2=True | // type=System.Boolean |
SideTargetStates3=True | // type=System.Boolean |
SideTargetStates4=True | // type=System.Boolean |
SideTargetStates5=True | // type=System.Boolean |
SideTargetStates6=True | // type=System.Boolean |
SideTargetStates7=True | // type=System.Boolean |
SingleBallScore=24,900.00 | // type=System.Single |
UseOtherFlipperButtons=False | // type=System.Boolean |
UserProfileEditingEnabled=True | // type=System.Boolean |
UseSecondaryFlipperButtons=False | // type=System.Boolean |