Player Data

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.

AttributeValue

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.

Player 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)

SaveData for Long

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.

Type Conversion

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.

Team Play

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)

LoadPlayerData/SavePlayerData

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().

ExtraBallCount

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.

Player Data in the SDK

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.

Listing Player Data

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?

Appendix A: P3SampleApp Player Data

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