Torque 2D/StandardTutorials/Advanced/HighScores

From TDN

This page is a Work In Progress.

Though this is a WIP, it is fully functional.

Contents

Simple High Score Object

Introduction

You find yourself almost done with your game, and see the need to store "high scores"? Well, you could try using an object similiar to the one found in this tutorial.

Who this is Intended For

This tutorial is intended for

  • anyone who needs a pre-made High Score Object
  • anyone who wants to learn how to create a high score system through script
  • anyone who wants to learn about saving/loading complex SimObjects

Basics

First, we need to identify how we want to store our High Score table. For the purposes of this tutorial, we'll simply store the generic meta-data that most old-school games store, this would be Player Name, Player Score and Player Rank.

Example 1

An example of the score table would be:

RankPlayerScore
1Player B300
2Player A298
3Player C260
4Player D233
5Player B170

Now, we have a basic idea of what we intend to store and what order we intend to store it in.

SimSets

As with almost everything in TorqueScript, we need some form of object to store this data in. In most programming langauges there is some form of 'object' or 'datatype' which can store a reference or copy of other objects, usually referred to as an Array. In TorqueScript, this concept exists as well, but is referred to as a SimSet, which is an object derived from SimObject.

SimSet simply stores data about other objects, in a zero-based 'stack' (array) logic.

Basically, if you have 10 objects in a SimSet, the first object is '0' and the last is '9'. You can refer to these objects by using the getObject function of SimSet, such as %simSet.getObject(1) to retrieve the 2nd object in the 'Set'.

Hrm, seems SimSet is a logical storage mechanism for a basic High Score table, yes?

This is how we create a simple SimSet object:

%set = new SimSet();
%set.add(%obj1);
%set.add(%obj2);
%set.add(%obj3);
%set.bringToFront(%obj4);

Now, we just created a simple SimSet object, and added 4 child objects to it. We used (in this scenerio) pre-defined variables %obj1-4 and stored references to them in our new SimSet. Each call to the 'add' function added the object to the end of the 'stack'. When we called 'bringToFront' on %obj4, we actually placed the reference to %obj4 at the top of the stack, so the stack looks like this:

%set -> %obj4
%set -> %obj1
%set -> %obj2
%set -> %obj3

Now that we know how to use a SimSet, let's figure out how to use a SimSet to create a High Score Table.

ScriptObject

Since Object Oriented Programming (OOP) and Design (OOD) is common these days, and so easy to 'model', let's try to keep our High Score table stored in an easy to re-use 'object model', we'll refer to the 'class' as 'HighScores'. Since we'll most likely be wanting to store additional information about High Score Table other then the players/scores/ranks, let's create a simple ScriptObject, like so;

%highscores = new ScriptObject()
{
  class = "HighScores"
}

Now, we have a simple ScriptObject, which uses the HighScores class/namespace.

We can now create functions within this class that both override the defaults of the ScriptObject (derived once again from SimObject) to add additional logic to them, as well as store dynamic fields for additional meta-data.

Now, you may be thinking, what kind of meta-data is needed, good question. As I stated earlier, we'd like to write this code once, and re-use it in all of our games, right? Sure we do. So, our first game has a Top 10 High Score table, but our second game only has a Top 5 High Score table. With this knowledge, or "possibilities for the future" foresight, we add a "MaxScores" or "MaxCount" dynamic field to our HighScores object.

For example;

%highscores = new ScriptObject()
{
  class = "HighScores";
  MaxCount = 10;
}

Now, we have some way of knowing how many scores can be stored in this stack.

Putting it all Together

Now that we have the basic principal of ScriptObject and SimSets, let's put the two together.

For example, the following will create a ScriptObject, of type 'HighScores' and attach a SimSet to it;

%highscores = new ScriptObject()
{
  class = "HighScores";
  MaxCount = 10;
};
%highscores.Scores = new SimSet();

Now, the %highscores.Scores dynamic field is a reference to a SimSet object, which we can store our scores in.

Take for example, the following code:

// FUNCTION: NewHighScores
// PARAMS  : None
// DESC    : Create's a new HighScores object
// USE     : public
function NewHighScores()
{
   if(isObject(%this)) return %this;
   
   %obj = new ScriptObject()
   {
      class = "HighScores";
      MaxCount = 10; // DEFAULT
   };
   return %obj;
}

The 'NewHighScores' function created in the code block above create's a new 'HighScores' object and returns it, so we can use the following code;

%highscores = NewHighScores();

Now we have a reuseable function that creates a HighScores object for us. But wait, where's the SimSet? How do we add scores to the object?

Wait, wait, wait, I'm getting to that part...

Now, as I said previously, we can create functions for the HighScores class, such as an 'add' function to add scores to the object, let's look at the following example;

function HighScores::add(%this, %player)
{
  // more to come ...
}

Now we can call the 'add' function on our highscores object, but right now, it doesn't really do anything (ok, it literally doesn't do anything).

function HighScores::add(%this, %player)
{
  if(!isObject(%this.Scores)) %this.Scores = new SimSet();
  %obj = new ScriptObject()
  {
    PlayerName = %player.PlayerName;
    PlayerScore = %player.PlayerScore;
  };
  %this.Scores.add(%obj);
}

Now our 'add' function does something, first, it checks to see if the Scores field is initialized and if it is not, creates it as a SimSet, and secondly it creates a temporary variable called %obj and stores the Players Name and Score to it, then add's the new object to the Scores SimSet.

Well, now we've got a basic set of code going, so we can do the following;

%highScores = NewHighScores();
%highScores.add($player1); // $player1 is a pre-existing object

Now take a moment, do you see any issues with our current logic?

It seems as though if we call the 'add' function a trillion times, we'll create a high score table with a trillion names and scores. This is not good, we only want to store at most, 10 scores. But wait, before we even bother with that, we also only want to store the highest scores, and we want to store them in order, don't we?

Let's look at this;

// FUNCTION: HighScores::Sort
// PARAMS  :
//   %this   -- object for reference
// DESC    : sorts the high score stack from Top to Bottom
// USE     : internal
function HighScores::Sort(%this)
{
   if(!isObject(%this.Scores)) { echo("Scores is not object"); return false; }
   %scores = HighScores::copySimSet(%this.Scores);
   %this.Scores.clear();
   while(%scores.getCount() > 0 && %trap < 100)
   {
      %high = null;
      %highx = -1;
      for(%x = 0; %x < %scores.getCount(); %x++)
      {
         if(%highx == -1) { %high = %scores.getObject(%x).PlayerScore; %highx = %x; }
         else {
            if(%scores.getObject(%x).PlayerScore >= %high)
            {
               %highx = %x;
               %high = %scores.getObject(%x).PlayerScore;
            }
         }
      }
      %this.Scores.add(%scores.getObject(%highx));
      %scores.remove(%scores.getObject(%highx));
   }
}

This code block shows some simple sorting logic, which is as follows:

First, we make a copy of the current SimSet (using HighScores::copySimSet -- found in the complete script file below)

Second, we clear the Scores SimSet, which removes all the object references from it.

Next, we start a 'while' loop, which will continue to perform the code logic within it until the condition is not 'true'. The condition, '%scores.getCount() > 0' simply says "While there are objects in the %scores SimSet, loop".

Within the loop, we initialize two variables, %high and %highx, which simply store our loops state. We also create another loop, this time a 'for' loop, and go through every object in the %scores SimSet. If %highx is -1, we take the first object and consider it the 'high score marker' and store it's score in the '%high' variable. We then goto the next object reference and check to see if the score is greater then the previous, if it is, we mark that object instead. When we finish the 'for' loop, we have a reference to what should be the highest score as well as the highest scores position in the %scores SimSet. We now add that object to the %this.Scores SimSet, and remove it from the %scores SimSet -- then loop again.

The loop continues until all the objects in the %scores SimSet are removed and added to the %this.Scores SimSet once again. As we are calling the %this.Scores.Add() function, the new object reference is implicitly added to the end of the SimSet.

If your wondering how intensive this logic is on the computer, in a Top 10 score table, 56 loops are performed to sort it properly -- average sorting time for a top 10 score table is <1s.

Now our scores are sorted, after we added it.

But what about keeping track of only the Top 10 scores?

First, before we bother wasting precious CPU cycles (or resources) on performing the above mentioned loop, inside our HighScores::add function, we should perform some initial logic and determine if the %player object being passed in even qualifies for a Top 10 score. This can be done with the following code:

// FUNCTION: HighScores::add
// PARAMS  :
//   %this   -- object for reference
//   %player -- player object
//              must contain a PlayerName and PlayerScore Field
//              fields should not equate to empty strings (!$=)
// DESC    : adds a player to the high score stack
//           and returns true/false if they have a high score
// USE     : public
function HighScores::add(%this, %player)
{
   if(isObject(%player))
   {
      if(%player.PlayerName !$= "")
      {
         if(%player.PlayerScore !$= "")
         {
            if(!isObject(%this.Scores)) %this.Scores = new SimSet();
            // players score is lower then lowest score
            // only check if current score count is equal or greater then max score count
            if(%this.Scores.getCount() >= %this.MaxCount)
            {
               if(%this.Scores.getObject(%this.MaxCount - 1).PlayerScore > %player.PlayerScore) return false;
            }

            %s = new SimObject()
            {
               playerName = %player.PlayerName;
               playerScore = %player.PlayerScore;
            };
            %this.Scores.add(%s);
            %this.Sort();
            while(%this.Scores.getCount() > %this.MaxCount)
            {
               %ref = %this.Scores.getObject(%this.Scores.getCount()-1);
               %this.Scores.remove(%ref);
               %ref.delete();
            }
            return true;
         } else { echo("no player score"); return false; }
      } else { echo("no player name"); return false; }
   } else { echo("player is not object"); return false; }
}

If, If, If, If, Huh?

First, we check to see if %player is even an object, if it is not, we return false. Next, we check to see if the player has a name, if not, we return false. Then we check to see if the player has a score, if not, we return false.

Now, we check to see if the %this.Scores SimSet has been created, if not, we create it.

Now, here comes part of our Top 10 check, we look at the %this.Scores.getCount() value and see if it's equal to or greater then our %this.MaxCount field we created. If it is, then we check to see if the %player object passed in has a score greater then the lowest score in the %this.Scores SimSet, if it does not, the %player object does not qualify for a Top 10 score and we, yet again, return false.

If the player qualifies for a Top 10 score, then we create a new ScriptObject and store our player meta-data in it. Then we add the object to the %this.Scores SimSet, and call our Sort function from earlier.

After our %this.Scores SimSet has been sorted, we then proceed to create a 'while' loop that removes all the scores off the bottom, so we keep the table at '10' -- the reason we do this as a while loop is so that we can dynamically switch between storing Top 10 and Top 5 -- allowing the player of the game, for example, to have a 'save at most X scores' option in the game config, perhaps?

Saving

To be written ... Complete Script File has Save function

Loading

To be written ... Complete Script File has Load function

Summary

Now that we have a basic HighScores object, what can we do with it?

Simple -- store High Scores in it!

Complete Script File

Below is the completed "HighScores" object. Feel free to copy/paste the code below into your favorite editor (such as Torsion), and save it as "highScores.cs" and just simply exec() it in your game code and use it.


Currently, a GUI to display the scores has not yet been developed, and as such, the HighScores::debug() is useful to look at your score table quickly and see if everything works ok --



The following code is in a beta-stage and is subject to change as the "TGB High Scores System" progresses.


// FUNCTION: NewHighScores
// PARAMS  : None
// DESC    : Create's a new HighScores object
// USE     : public
function NewHighScores()
{
   if(isObject(%this)) return %this;
   
   %obj = new ScriptObject()
   {
      class = "HighScores";
      MaxCount = 10; // DEFAULT
   };
   return %obj;
}

// FUNCTION: HighScores::setMaximumScores
// PARAMS  :
//   %this   -- object for reference
//   %count  -- maximum number of scores to store
// DESC    : sets the maximum number of scores to store
// USE     : public
function HighScores::setMaximumScores(%this, %count)
{
   %this.MaxCount = %count;
}

// FUNCTION: HighScores::getMaximumScores
// PARAMS  :
//   %this   -- object for reference
// DESC    : returns the number of maximum scores stored
// USE     : public
function HighScores::getMaximumScores(%this)
{
   return %this.MaxCount;
}

// FUNCTION: HighScores::add
// PARAMS  :
//   %this   -- object for reference
//   %player -- player object
//              must contain a PlayerName and PlayerScore Field
//              fields should not equate to empty strings (!$=)
// DESC    : adds a player to the high score stack
//           and returns true/false if they have a high score
// USE     : public
function HighScores::add(%this, %player)
{
   if(isObject(%player))
   {
      if(%player.PlayerName !$= "")
      {
         if(%player.PlayerScore !$= "")
         {
            if(!isObject(%this.Scores)) %this.Scores = new SimSet();
            // players score is lower then lowest score
            // only check if current score count is equal or greater then max score count
            if(%this.Scores.getCount() >= %this.MaxCount)
            {
               if(%this.Scores.getObject(%this.MaxCount - 1).PlayerScore > %player.PlayerScore) return false;
            }

            %s = new SimObject()
            {
               playerName = %player.PlayerName;
               playerScore = %player.PlayerScore;
            };
            %this.Scores.add(%s);
            //%this.Sort();
            return true;
         } else { echo("no player score"); return false; }
      } else { echo("no player name"); return false; }
   } else { echo("player is not object"); return false; }
}

// FUNCTION: HighScores::Sort
// PARAMS  :
//   %this   -- object for reference
// DESC    : sorts the high score stack from Top to Bottom
// USE     : internal
function HighScores::Sort(%this)
{
   if(!isObject(%this.Scores)) { echo("Scores is not object"); return false; }
   %scores = HighScores::copySimSet(%this.Scores);
   %this.Scores.clear();
   echo("Scores.getCount() : " @ %this.Scores.getCount());
   echo("%scores.getCount(): " @ %scores.getCount());
   while(%scores.getCount() > 0 && %trap < 100)
   {
      %high = null;
      %highx = -1;
      for(%x = 0; %x < %scores.getCount(); %x++)
      {
         if(%highx == -1) { %high = %scores.getObject(%x).PlayerScore; %highx = %x; }
         else {
            if(%scores.getObject(%x).PlayerScore >= %high)
            {
               %highx = %x;
               %high = %scores.getObject(%x).PlayerScore;
            }
         }
      }
      %this.Scores.add(%scores.getObject(%highx));
      %scores.remove(%scores.getObject(%highx));
   }
}

// FUNCTION: HighScores::getScore
// PARAMS  :
//   %this   -- object for reference
//   %rank   -- the high score rank to return
// DESC    : returns the score of %rank
// USE     : public
function HighScores::getScore(%this, %rank)
{
   if(!isObject(%this.Scores)) return -1;
   if(%this.Scores.getCount() < %rank - 1) return -1;
   return %this.Scores.getObject(%rank).PlayerScore;
}

// FUNCTION: HighScores::getPlayer
// PARAMS  :
//   %this   -- object for reference
//   %rank   -- rank of player object to return
// DESC    : returns player object with %rank score
// USE     : public
function HighScores::getPlayer(%this, %rank)
{
   if(!isObject(%this.Scores)) return -1;
   if(%this.Scores.getCount() < %rank - 1) return -1;
   return %this.Scores.getObject(%rank).PlayerName;
}

// FUNCTION: HighScores::getPlayerScore
// PARAMS  :
//   %this   -- object for reference
//   %rank   -- rank of score to return
// DESC    : returns score of player at %rank
// USE     : public
function HighScores::getPlayerScore(%this, %rank)
{
   if(!isObject(%this.Scores)) return -1;
   if(%this.Scores.getCount() < %rank - 1) return -1;
   return %this.Scores.getObject(%rank);
}

// FUNCTION: HighScores::getRank
// PARAMS  :
//   %this   -- object for reference
//   %player -- player to locate
// DESC    : returns rank for %player
// USE     : public
function HighScores::getRank(%this, %player)
{
   if(!isObject(%this.Scores)) return -1;
   if(%player.PlayerName $= "") return -1;
   for(%x = 0; %x < %this.Scores.getCount(); %x++)
   {
      if(%this.Scores.getObject(%x).PlayerName $= %player.PlayerName) return %x; // rank
   }
}

// FUNCTION: HighScores::save
// PARAMS  :
//   %this   -- object for reference
//   %path   -- path to save as
// DESC    : saves high score object to %path
// USE     : public
function HighScores::save(%this, %path)
{
   if(!isObject(%this.Scores)) return false;

   %save = new SimSet(HighScoreSavedSet);
   %save.add(%this);
   for(%x=0;%x<%this.Scores.getCount();%x++)
   {
      %save.add(%this.Scores.getObject(%x));
   }
   return %save.save(%path);
}

// FUNCTION: HighScores::load
// PARAMS  :
//   %path   -- path to save file
// DESC    : returns a HighScores object
//           previously saved to %path
// USE     : public
function HighScores::load(%path)
{
   if(isObject(%path)) return; // call HighScores::load('path');
   %obj = NewHighScores();
   exec(%path);
   %load = HighScoreSavedSet;
   %obj.MaxCount = %load.getObject(0).MaxCount;
   %load.remove(%load.getObject(0));
   %obj.Scores = new SimSet();
   for(%x=0;%x<%load.getCount();%x++)
   {
      %obj.Scores.add(%load.getObject(%x));
   }
   %load.clear();
   HighScoreSavedSet.delete();
   if(isObject(%load)) echo("%load is still an object");
   if(isObject(HighScoreSavedSet)) echo("HighScoreSavedSet is still an object");
   return %obj;
}

// FUNCTION: HighScores::copySimSet
// PARAMS  :
//   %set  : set to copy
// DESC    : returns a copy of %set
// USE     : internal
function HighScores::copySimSet(%set)
{
   if(!%set.IsMemberOfClass("SimSet")) return false;
   %obj = new SimSet();
   for(%x = 0; %x < %set.getCount(); %x++)
   {
      %obj.add(%set.getObject(%x));
   }
   return %obj;
}

// FUNCTION: HighScores::debug
// PARAMS  : none
// DESC    : echo's high score table to console
// USE     : internal
function HighScores::debug(%this)
{
   if(!isObject(%this.Scores))
   {
      echo("No Scores in Stack");
      return;
   }
   for(%x=0;%x<%this.Scores.getCount();%x++)
   {
      echo("  Player: " @ %this.Scores.getObject(%x).PlayerName);
      echo("  Scores: " @ %this.Scores.getObject(%x).PlayerScore);
      echo(" ");
   }
}