Another frequent question I come across in the user forums is related to how some one implements local high scores. The question has come up frequently enough for me to conclude that its to the benifit of the community to have an implementation available that can be used in Silverlight or XNA that is ready to be used with very little setup.
So I’ve made a solution for others to use. By default the component will keep track of up to 10 high scores and will take care of loading and saving itself. If you add a score the component will take care of ensuring the score is in it’s proper place and removing scores that are nolonger one of the top. For persisting the score information I’ve made use of the DataSaver<T> code from a previous blog post. I hope others will find the solution easy to use.
To get started with using the component add a reference to my component to your project. You’ll want to instantiate HighScoreList
passing an optional file name that it will use to save score information. It’s possible to keep track of more than one high score list as long as your instances have different file names. One might want to do this if they keep track of scores in different modes separately from each other (Ex: a score list for Difficult mode, a score list for Easy mode, and so on).
HighScoreList _highScoreList = new HighScoreList("MyScores");
Upon instantiation the component will take care of loading any previous high scores without you doing anything more.
To add a score create a new instance of ScoreInfo
and populate its PlayerName
and Score
fields. (There is also a ScoreDate
field that automatically gets populated with the current date and time). Then use the AddScore(ScoreInfo)
method on the HighScoreList
instance to add it to the score list.
ScoreInfo scoreInfo = new ScoreInfo(){PlayerName = "Jack", Score = 1048576};
_highScoreList.AddScore(scoreInfo);
And that’s it, there’s nothing more for you to do. When you make that call the score gets added to the high score list, scores that are no longer in the top 10 (or what ever you set the limit to be) will fall off the list, and the list will automatically be persisted back to IsolatedStorage so that it is available the next time your game runs. Easy, right?
As a test project I’ve created a Silverlight application that allows you to enter new scores and see the behaviour of the component.
The main bits of the source code are below. First the ScoreInfo
class which is nothing more than a serializable collection of three properties
/// <summary>
/// ScoreInfo contains information on a single score
/// </summary>
[DataContract]
public class ScoreInfo : INotifyPropertyChanged
{
// PlayerName - generated from ObservableField snippet - Joel Ivory Johnson
private string _playerName = String.Empty;
/// <summary>
/// The name of the player that made this score
/// </summary>
[DataMember]
public string PlayerName
{
get { return _playerName; }
set
{
if (_playerName != value)
{
_playerName = value;
OnPropertyChanged("PlayerName");
}
}
}
//-----
// Score - generated from ObservableField snippet - Joel Ivory Johnson
private int _score = 0;
/// <summary>
/// The score that the player made
/// </summary>
[DataMember]
public int Score
{
get { return _score; }
set
{
if (_score != value)
{
_score = value;
OnPropertyChanged("Score");
}
}
}
//-----
// ScoreDate - generated from ObservableField snippet - Joel Ivory Johnson
private DateTime _scoreDate = DateTime.Now;
/// <summary>
/// The date and time that the player made the score. If this field is not
/// assigned a value it will automatically be assigned with the date and time
/// that the score isntance was created
/// </summary>
[DataMember]
public DateTime ScoreDate
{
get { return _scoreDate; }
set
{
if (_scoreDate != value)
{
_scoreDate = value;
OnPropertyChanged("ScoreDate");
}
}
}
//-----
protected void OnPropertyChanged(String propertyName)
{
if(PropertyChanged!=null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
And then the HighScoreList
class, which is a collection class that contains the .
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.Serialization; namespace J2i.Net.ScoreKeeper { public class HighScoreList : ObservableCollection<ScoreInfo>, INotifyPropertyChanged { static DataSaver<HighScoreList> MyDataSaver = new DataSaver<HighScoreList>(); public HighScoreList() { } public HighScoreList(string fileName):this() { this.ScoreFileName = fileName; HighScoreList temp = MyDataSaver.LoadMyData(fileName); if(temp!=null) { foreach(var item in temp) { Add(item); } } } // MaxScoreCount - generated from ObservableField snippet - Joel Ivory Johnson private int _maxScoreCount = 10; [DataMember] public int MaxScoreCount { get { return _maxScoreCount; } set { if (_maxScoreCount != value) { _maxScoreCount = value; OnPropertyChanged("MaxScoreCount"); } } } //----- // ScoreFileName - generated from ObservableField snippet - Joel Ivory Johnson private string _scoreFileName = "DefaultScores"; [DataMember] public string ScoreFileName { get { return _scoreFileName; } set { if (_scoreFileName != value) { _scoreFileName = value; OnPropertyChanged("ScoreFileName"); } } } //----- // AutoSave - generated from ObservableField snippet - Joel Ivory Johnson private bool _autoSave = true; [DataMember] public bool AutoSave { get { return _autoSave; } set { if (_autoSave != value) { _autoSave = value; OnPropertyChanged("AutoSave"); } } } //----- static int ScoreComparer(ScoreInfo a, ScoreInfo b) { return b.Score - a.Score; } public void SortAndDrop() { List<ScoreInfo> temp = new List<ScoreInfo>(this.Count); foreach(var item in this) { temp.Add(item); } if (temp.Count > MaxScoreCount) { temp.RemoveRange(MaxScoreCount - 1, (temp.Count) - (MaxScoreCount)); } temp.Sort(ScoreComparer); this.Clear(); temp.ForEach((o)=>Add(o)); } public void Save() { if(String.IsNullOrEmpty(ScoreFileName)) throw new ArgumentException("A file name wasn't provided"); MyDataSaver.SaveMyData(this, ScoreFileName); } public void AddScore(ScoreInfo score) { this.Add(score); SortAndDrop(); if(AutoSave) Save(); } protected void OnPropertyChanged(String propertyName) { if(PropertyChanged!=null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; #endregion } }
CodeProject