Player Data

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.

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

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

Type Conversion

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.

Team Play

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)

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

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 for checking 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 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.

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 the sample output in Appendix A.

Savepoints

One thing you'll notice in Appendix A is that the GameAttributes also appear in Player Data. When a Player instance is created, the current GameAttributes are copied into Player Data. Within the application, Player Data shadows the current GameAttributes and takes precedence.

This is how the SDK implements savepoints. When a savepoint is resumed, Player Data is restored, which restores both the player's progress and the GameAttributes values that were active when the savepoint was created.

Player Data can be unexpectedly overwritten if a key collides with a GameAttribute of the same name. This can be avoided through careful testing, as demonstrated by P3SampleApp, but that approach is not future-proof if the SDK or a module driver changes. One solution is to use a prefix for all application-specific Player Data keys. The SDK and module drivers currently use undecorated names for GameAttributes, so this convention guarantees that name collisions cannot occur.

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