PlayerPrefs, High scores.

Hello everyone,

At the moment I’m currently using two score scripts:

of Attempts script

Time taken to complete level script

On end of level, these two scripts disable and a level complete GUI.Box is shown and displays the two variables of both of the above scripts.

I’ve never used PlayerPrefs before, and getting mixed results with UnityAnswers and documentation.

What is the best way to turn these two variables at end of the level, which is shown in the GUI.Box, into a static variable (if required) to be able to save to PlayerPrefs in order to be shown in another scene/level such as the main menu level selection for each level?

Also, what might be the required mathematics to save the top 5 best scores for each level, putting the shorter completion times at the very top and the rest after?

I would prefer to save PlayerPrefs in some sort of encrypted file, but don’t believe this to be possible. I wish for the solution to be cross platform between Windows Mac.

Edit: Using UnityScript / Javascript.

Thank you.

I think I got the hang of writing PlayerPrefs.

PlayerPrefs.SetInt("lvl1attempts", PrintAttempts.numAttempts);
PlayerPrefs.SetString("lvl1time", PrintTime.textTime);

This would now be my problem, maths isn’t my strongest point.

Cheers.

You could do something like this

static string PlayerPrefName(string sceneName, string category, int rank ) {
    return string.Format("{0}-{1}.{2}", sceneName, category, rank);
}

static int[] TopAttemptsForLevel(string sceneName ) {
    int[] attempts = new int[5];
    int i =0;
    for(; i < 5; i ++)
    {
        string prefName = PlayerPrefName(sceneName, "attempts", i);
        if(!PlayerPrefs.HasKey(prefName)) break;
        else attempts[i] = PlayerPrefs.GetInt( prefName );
    }

    if( i < 5 ) System.Array.Resize( ref attempts, i );

    return attempts;
}

static void PostAttemptCount(string sceneName, int attempts){
    int[] topAttempts = TopAttemptsForLevel(sceneName);
    int i = 0;
    int postNumber =0;
    for(; i < topAttempts.Length; i++) {
        if( topAttempts[i] > attempts )
            PlayerPrefs.SetInt(PlayerPrefName(sceneName, "attempts",postNumber++), attempts);
        PlayerPrefs.SetInt(PlayerPrefName(sceneName, "attempts", postNumber++), attempts);
    }
    // check if we did not make a post and we need to add it to the end of the list
    if( postNumber == i  topAttempts.Length < 5 ){
        PlayerPrefs.SetInt(PlayerPrefName(sceneName, "attempts", postNumber++), attempts);
    }
}

This should give you a start. its kind of unoptimal, but for stuff like score posting its not as important how well it runs as say somethig that gets executed every frame.

You can use Application.loadedLevelName to get the scene name

Sorry, but I believe that’s in C#? I’m using UnityScript, It’s somewhat confusing as well and would like a few more //comments as to how it works.

I don’t know JS very well but heres a shot.

// generates a key for playerprefs
static function PlayerPrefKey( var levelName : string, var category : string, var place : int ) : string {
    return levelName + "-" + category + "-" place;// gets a string combining the parameters ( "level-category-#" )
}

// gets the top 5 attempts for level.
static function TopAttemptCounts( var levelName ) : int[] {
    // make a array of 5 ints
    var array : int[] = new int[5];
    var place : int = 0;// the current place getting out of player prefs ( start with zero )
    for(; place < 5; place ++){ 
        var key : string = PlayerPrefKey( levelName, "attempts", place );// get the key which this level, category, and place would be.
        if( !PlayerPrefs.HasKey(key) )
            break;// if there is no key for this place, we havnt completed it 5 times and we break out of this loop.
        else
            array[place] = PlayerPrefs.GetInt(key);// there is a place stored for this category and we set it to the array position.
    }
    // if place doesnt equal five its because we do not have enough records
    if( place < 5 )
        System.Array.Resize( ref array, place );// resize our array ( shrink it ) to the amount of places we've read.
    // return the array
    return array;
}

// records the attempt count to player prefs, sorting existing scores with this new one.
static function RecordAttemptCount( var levelName : string, int attempts ) {
    var topAttempts : int[] = TopAttemptCount();// get the existing top attempts.
    var placeComparing : int = 0;
    var placeWriting : int = 0;
    var wrote : boolean =false;
    // compare each existing place.
    for( ; placeComparing < 5; placeComparing ++)
    {
        if( topAttempts.Length <= placeComparing )
        {
            if(!wrote)
                // we didnt have 5 records. record this and break out of the function with a return.
                PlayerPrefs.SetInt( PlayerPrefKey( levelName, "attempts", placeWriting++), attempts );
            // if wrote was true, we already put it in and do not need to write it at this point because it was better.
            return;
        }else if( topAttempts[placeComparing] > attempts ){// see if the existing place is worse than the attempt count were commiting.
            // if it is we've got to write it. before
            PlayerPrefs.SetInt( PlayerPrefKey( levelName, "attempts", placeWriting++);
            wrote = true;
        }
        // if we wrote a new score to it we must write scores that were worse afterwards ( we do not need to write scores before it which were better )
        if( wrote  placeWriting < 5 ){ // we check for < 5 to make sure we don't write 6 scores.
            PlayerPrefs.SetInt( PlayerPrefKey( levelName, "attempts", placeWriting++);
        }
    }
}

static function TopAttemptsCountCurrentLevel() : int[] { return TopAttemptsCount(Application.loadedLevelName); }
static function RecordAttemptCountCurrentLevel( int attempts ) { RecordAttemptCount( Application.loadedLevelName, attempts ); }

you would make those functions public somewhere and call them to either record a new attempt count or get the top 5 ( or less if 5 playthroughs havent happend )
PlayerPrefKey gets a string which is unique to the level, category, and rank/placing.
TopAttemptsCount would return a array of ints, where array[0] would be the best ( lowest ) amount of attempts made for the level.
RecordAttemptsCount would push in a new attempt count into the records, and insert it so that the best would be at 0.

The TopAttemptsCountCurrentLevel and RecordAttemptsCountCurrentLevel just use Application.loadedLevel as the levelName.

Hope this helps and that all compiles ok. Again i don’t know JS very well. But basically you just got to store things in some fashion you can read from them in a way you like.

I didnt do the time part of the scoring, because i don’t know what data type your using besides string and how to compare it. If you can you should probably store the time as either a float or a int ( like converting it to milliseconds, or a float of seconds ). Then you would just copy and paste those two functions, modify them to compare how you’d like and have them use “time” instead of “attempts” for the PlayerPrefKey calls.

edit:
haha thats ushually why i avoid a comments, it looks garbled and unledgable. but paste that into the text editor and it should be more readable.

I don’t believe you can call a var within () tags as it appears to be giving errors, and I’m not quite sure how to re-order it to work within the syntax while keeping the original function. If someone would be willing to show this out, it’d be quite helpful.

That aside, the script is still quite confusing but I can see what you’re trying to do with the comments, and it doesn’t look too hard to resize the array from 5 if I ever required.

The time is a string, while the amounts counter is an int. I’m looking at the string variable, displaying it on the board, and then saving it with PlayerPrefs.

private var startTime;
var textTime : String;

function Awake() {
	this.transform.position = new Vector3(0.47f,0.999f,1.0f);
	startTime = Time.time;
}

function OnGUI() {
   var guiTime = Time.time - startTime;
   var minutes = Mathf.Floor(guiTime / 60);
   var seconds = guiTime % 60;
   var milliseconds = (guiTime * 1000) % 1000;
	textTime = String.Format ("{0:00}:{1:00}:{2:000}", minutes, seconds, milliseconds);
	guiText.text = textTime;
}

The main confusion here is I’m not sure which part of the script saves the numAttempts, (number of attempts -other than the second half), the time, and then how to call them back into the menu to show these best scores. Is it possible to save both NumAttempts with the Time while only scoring the 5 best positions in the array on Time Alone?

There’s so much code with yours, I figured this would be rather simplistic. Thanks though, I’ll look into this.

I’m sorry, i’m not well versed in js. It looks like you should be able to just remove the 'var’s out of the parameter block ( between parenthesis )

static function PlayerPrefKey( levelName : String, category : String, place : int ) : String {
// gets a string combining the parameters ( "level-category-#" )
    return levelName + "-" + category + "-" + place;
}

// gets the top 5 attempts for level.
static function TopTimes( levelName : String ) : Array {
    // make a array of 5 ints
    var array : Array = Array();
    var place : int = 0;// the current place getting out of player prefs ( start with zero )
    for(; place < 5; place ++){ 
        var key : String = PlayerPrefKey( levelName, "time", place );// get the key which this level, category, and place would be.
        if( !PlayerPrefs.HasKey(key) )
            break;// if there is no key for this place, we havnt completed it 5 times and we break out of this loop.
        else
            array.Push(PlayerPrefs.GetFloat(key));// there is a place stored for this category and we set it to the array position.
    }
    
    // return the array
    return array;
}

static function GetAttempts( levelName : String ) : Array {
    // make a array of 5 ints
    var array : Array = Array();
    var place : int = 0;// the current place getting out of player prefs ( start with zero )
    for(; place < 5; place ++){ 
        var key : String = PlayerPrefKey( levelName, "attempt", place );// get the key which this level, category, and place would be.
        if( !PlayerPrefs.HasKey(key) )
            break;// if there is no key for this place, we havnt completed it 5 times and we break out of this loop.
        else
            array.Push(PlayerPrefs.GetInt(key));// there is a place stored for this category and we set it to the array position.
    }
    
    // return the array
    return array;
}

// records the attempt count to player prefs, sorting existing scores with this new one.
static function RecordComplete( levelName : String, time : float, attempts : int ) : int {
    var topTimes : Array = TopTimes(levelName);// get the existing top times.
    var attemptNumbers : Array = GetAttempts(levelName);
    var placeComparing : int = 0;
    var placeWriting : int = 0;
    var wrote : boolean =false;
    var finalPlace : int = -1;// -1 == did not place
    // compare each existing place.
    for( ; placeComparing < 5; placeComparing ++)
    {
        if(
           // if we have not yet wrote. 
           !wrote  
           // and 
           ( 
              // we're not at recording capacity
             ( topTimes.length <= placeComparing ) ||
             // or we've got a better time
             topTimes[placeComparing] > time
           ) )
        {
            // post our new time and attempts.
            PlayerPrefs.SetFloat( PlayerPrefKey( levelName, "time", placeWriting), time);
            PlayerPrefs.SetInt( PlayerPrefKey( levelName, "attempt", placeWriting), attempts);
            finalPlace = placeWriting;// save or placing off for return.
            wrote = true;
        }
        
        // if we've written/inserted our new time in we must re-write all records past it to capacity.
        if( wrote  placeWriting < 5 ){ // we check for < 5 to make sure we don't write 6 scores.
            PlayerPrefs.SetFloat( PlayerPrefKey( levelName, "time", placeWriting), topTimes[placeComparing] );
            PlayerPrefs.SetInt( PlayerPrefKey( levelName, "attempt", placeWriting), attemptNumbers[placeComparing] );
        }
        placeWriting++;
    }
    return finalPlace;// if -1 we did not place.
}

So that, when you complete a level, you call RecordComplete with the level name ( Application.loadedLevelName ), the time in seconds float, then the attempt number/count

The function returns -1 if did not place, otherwise the place starting at 0 going to 4.

Now with that done hopefully. You then can use TopTimes and GetAttempts which will get you two seperate arrays but shared indexes. so times array [0] would be the best time, and attempts array [0] would be the number of attempts.

Thanks, trying to work with it.

On a side note, is it possible to do String < String or String > String? Currently I’m receiving
“BCE0051: Operator ‘>’ cannot be used with a left hand side of type ‘String’ and a right hand side of type ‘String’.”

var OldTime = PlayerPrefs.GetString("lvl1time");

if (OldTime > PrintTime.textTime)
{
print ("It works!");
}

Yea, Don’t store time as a string to player prefs. instead store it as a float. Then in gui do the string magic