Another frequent question I come across in user forums is related to how to implement local high scores. The question has come up frequently enough for me to conclude that it is to the benefit 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 its proper place and removing scores that are no longer one of the top. For persisting 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. You might want to do this if they keep track of scores in different modes separately from each other (e.g.: 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 whatever 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:
[DataContract]
public class ScoreInfo : INotifyPropertyChanged
{
private string _playerName = String.Empty;
[DataMember]
public string PlayerName
{
get { return _playerName; }
set
{
if (_playerName != value)
{
_playerName = value;
OnPropertyChanged("PlayerName");
}
}
}
private int _score = 0;
[DataMember]
public int Score
{
get { return _score; }
set
{
if (_score != value)
{
_score = value;
OnPropertyChanged("Score");
}
}
}
private DateTime _scoreDate = DateTime.Now;
[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:
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);
}
}
}
private int _maxScoreCount = 10;
[DataMember]
public int MaxScoreCount
{
get { return _maxScoreCount; }
set
{
if (_maxScoreCount != value)
{
_maxScoreCount = value;
OnPropertyChanged("MaxScoreCount");
}
}
}
private string _scoreFileName = "DefaultScores";
[DataMember]
public string ScoreFileName
{
get { return _scoreFileName; }
set
{
if (_scoreFileName != value)
{
_scoreFileName = value;
OnPropertyChanged("ScoreFileName");
}
}
}
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
}
}