An iOS Port of a Unity3D Asteroids Example, Part 2: Saving Game Settings on Your Device

Building on Part 1, I added some basic game settings storage code using two C# classes. I used a great (faster than Unity’s default) PlayerPrefs class from PreviewLabs and a GameSettings class that talks to it. I found the basic idea for the GameSettings class in the first few videos of this useful 6-part tutorial.

GameSettings.cs & PlayerPrefs.cs

I added a simple PlayerPrefs.GetAll() method to the PreviewLabs.PlayerPrefs class. It simply checks if there are any prefs saved and returns a Hashtable containing them if they exist. If not, it returns null.

The GameSettings class is hooked up to a container GameObject called __GameSettings in the Hierarchy the “mainMenu” scene for easy access by other scripts in this scene.

GameSettings.cs

using UnityEngine;
using System.Collections;
using PlayerPrefs = PreviewLabs.PlayerPrefs;

public class GameSettings: MonoBehaviour {
	
	void Awake(){
		DontDestroyOnLoad(this);				
	}	

	public void SavePlayerData(int myScor, int myLives, bool myRes){ //(dynamic Obj)
		
		/* 
		 * Since GameSettings is inside Plugins folder & is compiled first, 
		 * it doesn't have a reference to playerScript.js which is in Scripts folder
		 * http://www.41post.com/1935/programming/unity3d-js-cs-or-cs-js-access
		*/
		//GameObject go = GameObject.Find("Player");
		//playerScript psClass = go.GetComponent<playerScript>(); or //Obj psClass = go.GetComponent<Obj>(); ??
		
		PlayerPrefs.SetInt("PlayerScore", myScor);	
		PlayerPrefs.SetInt("PlayerLives", myLives);		
		PlayerPrefs.SetBool("GameResumable", myRes);
		
		Debug.Log("GameSettings::SavePlayerData() called, GameResumable = " + PlayerPrefs.GetBool("GameResumable"));
	}
		
	public Hashtable LoadPlayerData(){		
	
		return PlayerPrefs.GetAll();
		
		/*	for larger data: 
							  		   * 
		foreach(String str in someCollection){
			if(PlayerPrefs.HasKey(str)) {
				PlayerPrefs.GetInt(str);
			}		 	
		}			
		or 		
		http://the.darktable.com/post/5612486609/jsonfx-json-serialization-for-the-unity-engine
		*/		
	}
	
	public void doFlush()
	{
		PlayerPrefs.Flush();
	}	
	
	public void gameOverDeletePrefs()
	{
		PlayerPrefs.DeleteAll();	
		doFlush();
	}	
}

Main Menu JS using GameSettings.cs to get data

Here’s an example of mainMenuScript.js using an instance of GameSettings.cs in Unity’s Awake() and OnGUI() functions. The main menu scene uses LoadPlayerData() to check if the game is resumable.

#pragma strict

import GameSettings;

static var BTN_WIDTH:float = 300;
static var BTN_HEIGHT:float = 120;
static var startNewGame:boolean = true;
static var gameResumable:boolean = false;

var skin1:GUISkin;
var instructionHdr:String;
var instructionText:String;
var go2:GameObject;
var gameSettings:GameSettings;

function Awake() {

	go2 = GameObject.Find('__GameSettings');				
	gameSettings = go2.GetComponent('GameSettings');		
	var gameData:Hashtable = gameSettings.LoadPlayerData();		    
	
	//if PlayerPrefs.txt is not empty and it exists, populate the gameResumable var
	
    if(gameData != null) {	    	    	        
		mainMenuScript.gameResumable = gameData["GameResumable"]; 	
	}	
		
  	instructionHdr = 'Instructions';
	instructionText = 'Tilt your phone left or right to move the ship. Touch screen to fire.';	
}

function OnGUI () {

  	GUI.skin = skin1;

	GUI.Label(Rect(Screen.width/2 - 145, Screen.height/2-310, 300, 100), instructionHdr); 	
	GUI.Label(Rect(Screen.width/2 - 145, Screen.height/2-240, 350, 200), instructionText);
	//GUI.TextArea(Rect(Screen.width/2- 145, Screen.height/2-210, 300, 150), instructionText);

	if(GUI.Button(Rect(Screen.width/2-150, Screen.height/2.35, mainMenuScript.BTN_WIDTH, mainMenuScript.BTN_HEIGHT), 'START GAME')) {

		startNewGame = true;
		Application.LoadLevel(1); //1 comes from Build Settings list of items in Scenes in Build 						
	} 

	
	if(mainMenuScript.gameResumable) {		
		if(GUI.Button(Rect(Screen.width/2-150, Screen.height/1.65, mainMenuScript.BTN_WIDTH, mainMenuScript.BTN_HEIGHT), 'RESUME GAME')) {
	
			startNewGame = false;
			Application.LoadLevel(1); //1 comes from Build Settings list of items in Scenes in Build 						
		} 
	}					
}

For now, the game is only resumable if the user quits the app before he loses 3 lives or scores at least 800 points (see playerScript.js below). If the game is resumable, the mainMenuScript.js shows an extra button – RESUME NOW, which lets you start the game with your previous score and number of lives (stored locally on your phone).

If you’re testing your game on a Mac, the Unity IDE will store the PlayerPrefs.txt file with it’s name/value pairs here:

/Users/yourUserName/Library/Caches/CompanyName/ProductName/PlayerPrefs.txt. 

Note: On some settings on OS X 10.7 Lion, /Users/yourUserName/Library directory may not be visible via Finder. It’s still accessible via Terminal though. In Finder, you can click on the “GO” menu in the top left OS-level navbar and hold down “Option” – the Library will show up as a menu item.

Unity picks up “CompanyName” from the Company Name field in “File > Build Settings > Player Settings” (in the Inspector) and your “ProductName” from Product Name field under Player Settings.

The name value pairs in PlayerPrefs.txt look like this:

PlayerLives : 2 : System.Int32 ; GameResumable : True : System.Boolean ; PlayerScore : 100 : System.Int32

If you’re wondering about the GUIskin stuff, it allows you to customize fonts & their appearance. In general, Fonts in Unity3D 3.5 seem to be as much of pain to use as they are in Flash CSx and Flash Builder. Check the Unity documentation, Unity Community forum and Unity Answers if you’re having trouble with fonts.

Player JS using GameSettings.cs to get and set data

playerScript.js is the other place where GameSettings.cs is used in this case. Don’t forget to create a container GameObject to hold GameSettings.cs in the “level1” scene:

Here we’re using both the LoadPlayerData() and SavePlayerData() methods. As I’ve mentioned above, the game is only resumable if the app receives the OnApplicationQuit() or the OnApplicationPause() messages from iOS before you lose 3 lives or score at least 800 points.

playerScript.js

#pragma strict

import GameSettings;

static var LEFT_BOUND:float = -2.9;
static var RIGHT_BOUND:float = 2.9;
static var playerScore:int = 0;						
static var playerLives:int = 3;									

var playerSpeed:int = 0.1;
var bullet:Rigidbody; //ref to bulletPrefab in level1 scene
var explosion:Transform; //ref to explosionPrefab in level1 scene
var timeDiff:float;
var delay1:float = 1.5; //two second delay.
var skin1:GUISkin; //var style1:GUIStyle; //for a single GUI object only        
var go2:GameObject;// = GameObject.Find('__GameSettings');
var gameSettings:GameSettings;// =  go2.GetComponent('GameSettings');
var firstTime:boolean = true;

function Start() {

	go2 = GameObject.Find('__GameSettings');
	gameSettings = go2.GetComponent('GameSettings');

    timeDiff = Time.time + delay1;
    
    firstTime = false;        
	
	if (!mainMenuScript.startNewGame) {
		
		//RESUME GAME
	    var gameData:Hashtable = gameSettings.LoadPlayerData();	
	    
	    if(gameData != null) {	    	    	
	        
		    playerScore = gameData["PlayerScore"]; 
			playerLives = gameData["PlayerLives"];
			mainMenuScript.gameResumable = gameData["GameResumable"]; 	
					
			Debug.Log("---RESUMING PREVIOUS GAME" ); 
						
		}
		
	} else {
	
		playerScore = 0; 
		playerLives = 3;
	}
	
	Debug.Log("playerScore = " + playerScore);
	Debug.Log("playerLives = " + playerLives);		
}

function Update() {

    var dir:Vector3 = Vector3.zero;
    var quat:Quaternion = Quaternion.AngleAxis(180, Vector3.forward);

    dir.x = Input.acceleration.x;

    if (dir.sqrMagnitude > 1)
        dir.Normalize();
   
    transform.Translate ( (dir * playerSpeed) * Time.deltaTime); 

    for (var touch : Touch in Input.touches) {
        if (touch.phase == TouchPhase.Began) {
            /*
                Replace with Object Pool
                http://abitgames.com/2011/blog/10-things-to-know-programming-unity3d-games/

            */
            var tempBullet:Rigidbody;
            tempBullet = Instantiate( bullet, transform.position, transform.rotation );             
        }
    }

    if (Time.time > timeDiff) //delay by 2 seconds before switching to lost scene
    {
        timeDiff = Time.time + delay1;    

        //you won
        if(playerScore >= 800){        	
            Application.LoadLevel(3);
        }
        
        //you lost    
        if( (playerLives <= 0) ){         	
            Application.LoadLevel(2);
        }
        
        //for now: 
        gameUnResumable();
        //with more Levels, game should be resumable unless the last level has been played? 
        //and even then, you should be able to choose which level to start from        
    } 
}

function OnGUI() {
    
    GUI.skin = skin1;

    GUI.Label(Rect(30,50,200,50), "SCORE: " + playerScript.playerScore); //, style1
    GUI.Label(Rect(Screen.width-180,50,200,50), "LIVES: " + playerLives);
}

function OnTriggerEnter(otherObject:Collider) {
    
    var o:GameObject = otherObject.gameObject;

    if(o.tag == 'enemy'){ //Enemy's "is Trigger" is checked

        o.transform.position.y = 7;
        o.transform.position.x = Random.Range( playerScript.LEFT_BOUND, playerScript.RIGHT_BOUND );

        var tempExplosion:Transform;
        tempExplosion = Instantiate(explosion, transform.position, transform.rotation);

        playerLives--;
    }
}

function OnApplicationPause() {	
	
	//to prevent a Null object warning on Awake()
	if(!firstTime){ 
		gameResumable();	
	}	
}

function OnApplicationQuit() {	
	
	gameResumable();
	
}

function gameResumable(){

	mainMenuScript.gameResumable = true; //necessary? 
	
	gameSettings.SavePlayerData(playerScore, playerLives, mainMenuScript.gameResumable);		
	gameSettings.doFlush();	
}

// you lost or won, for now, game becomes unresumable
function gameUnResumable(){

	mainMenuScript.gameResumable = false; //necessary? 	
	gameSettings.SavePlayerData(playerScore, playerLives, mainMenuScript.gameResumable);
	gameSettings.doFlush();
	
	Debug.Log("gameUnResumable() called");
}

Mixing C# and Javascript in Unity3d 3.5

Since we’re using both languages, it’s important to remember to place the secondary language scripts, in this case C#, inside a location that Unity compiles first. In other words put the .cs files inside the Plugins folder in your Project panel (in Finder it’s Assets/Plugins). If Plugins doesn’t exist, do Create > Folder in the Project panel.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s