An iOS Port of a Unity3D Asteroids Example, Part 3: Adding Level 2

Here’s an overview of a way to add Level 2 to the Asteroids example. The complete project package is here.

I’ve updated Level 1 a bit, since last time, the main differences being script & scene clean up & adding a background image to our scene. For example, instead of a win scene and a lose scene, consolidated those into the endGame scene (see screenshot below).

For an obvious visual difference between Level 1 and Level 2, I decided to swap the background graphic by updating the Material of a new GameObject called bgImg1 that holds the background image.

Create your background graphics in Photoshop, etc and drag & drop them into the Project panel. Create 2 new Materials and assign the background graphics as the textures for those materials (one for each).

playerScript.js, relevant lines:

var mats:Material[];
var bgImg1:GameObject;	
...
bgImg1 = GameObject.Find('BgImg1');
	
//set background Material based on Game Level (mats is zero based, levels start with 1):  			 			
bgImg1.renderer.material = mats[ mainMenuScript.gameLevel-1 ]; 

The mats array is first defined as a public variable in playerScript.js, which leads to it being visible in the Inspector. To assign correct Materials (w/ texture) to mats array:
1. select Player obj in Hierarchy
2. find Player’s Script component in Inspector
3. fill in Mats values – drag & drop each Material from Project to the Mats array in Inspector

IndexOutOfRangeException and UnassignedReferenceException errors for duplicated scene

In this example, I’m reusing as many assets as possible from Level 1 to Level 2. One issue that came up when reusing the Player GameObject and it’s playerScript.js script component was the IndexOutOfRangeException error for the mats[] array: IndexOutOfRangeException: Array index is out of range. playerScript.Start () (at Assets/Scripts/playerScript.js:82). I got this error because when I duplicated level1 scene to create level2, Unity didn’t automatically repopulate the mats[] array for me. I had to switch to level2, go into the Inspector for Player and manually repopulate again. This fixed the error. If you get “UnassignedReferenceException: The variable explosion of ‘playerScript’ has not been assigned.” for bullet or explosion vars from playerScript.js, it’s a similar issue – you need to manually reassign the prefabs for bullet and explosion in the Inspector.

Advertisements

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.

Unity3D for iOS Proof of Concept Checklist

Realtime on-device preview via Unity Remote extension

It lets you skip having to Build & Run to preview touch events functionality by connecting your device to the Unity’s Scene & Game views. Once installed, Unity Remote streams a (often low-res) version of your app to your iOS device via WIFI as you code.

A book called Unity iOS Game Development Beginner’s Guide has step by step instructions on publishing this utility app to your device.

Ran into one temporary stumbling block when trying to install Unity Remote 3 app to my device. This casting fix worked: Unity “error: assigning to ‘UITouch”.

iTween animation engine for Unity

iTween is basically TweenMax/TweenLite or Tweener for Unity. It works with C#, Javascript & Boo.

GUI: Game menus, textfields & other UI elements

As of version 3.5, the consensus in the dev community is that Unity’s built in GUI, GUITexture classes are not very efficient for mobile devices. Too many draw calls which affects performance is one of the main issues.

There are several alternatives that work better for mobile:

  • UIToolkit – free, open source; not compatible w/ iTween but has it’s own animation engine with fewer capabilities
  • NGUI – $95 & up
  • EZ GUI – $199
  • More in the Unity Assets Store. Most are not free.

Saving player settings

Looks like there’re two main options for saving player settings: Unity’s PlayerPrefs class or .NET 2.0 file i/o. The latter approach is described in Chapter 6 of Unity iOS Essentials*, while the former is known to suffer super slow performance for mobile devices.

Here’s a 3rd option: PreviewLabs’ PlayerPrefs class, an open source rewrite of PlayerPrefs for faster performance on iOS / Android.

JsonFX, a .NET JSON serializer script that has been modified to work with Unity iOS, Web, Desktop.

* The book was published in December, 2011. As of early 2012, Ch 6 contains some of type-o errors in script examples, not sure about the source files yet. The chapter uses .NET’s System.io, System.Collections & other packages with iOS’s .plist (custom xml) format and an open source project called PropertyListSerializer.
The book still uses “Application.dataPath” to grab the correct folder path to your app’s /Documents folder. As of Unity 3.3 the correct method to use is now Application.persistentDataPath.

Camera aspect ratio

In general:
iphone4 – aspect ratio is 3:2
ipad1/ipad2 – aspect ratio is 4:3

Go to File > Build Settings. Switch to iOS to see iOS related aspect ratios in for the Camera in the Game panel’s drop down menu.

Resolution & pixel density

So far not much clarity. There’s one approach similar to what I’ve seen in Objective-C & Titanium dev: check Screen.width via switch/if statement and customize values based on the result.

Code in C# or Javascript or Boo OR ALL THREE IN ONE PROJECT

Accessing a variable defined in C# from Javascript

If you want to create a Javascript that uses classes or variables from a C# script just place the C# script in the “Standard Assets”, “Pro Standard Assets” or “Plugins” folder and the Javascript outside of these folders….

In general the code inside the “Standard Assets”, “Pro Standard Assets” or “Plugins” folders, regardless of the language (C#, Javascript or Boo), will be compiled first and available to scripts in subsequent compilation steps.

Call JS from C# and vice versa – you can do one or the other but not both because if you’re mixing language scripts one of them has to be compiled first, so naturally it won’t yet have access to the other one.

C#/.NET version 2.0. Unity uses Mono (open source .NET) under the hood for C#.

Memory Profiling with iOS PRO license

With PRO license, on-device profiling is possible but works over WIFI, like Flash Builder / AIR app on-device debugging:

If you are using a firewall, you need to make sure that ports 54998 to 55511 are open in the firewall’s outbound rules – these are the ports used by Unity for remote profiling.

Full section at bottom of page:

IOS

Remote profiling can be enabled on iOS devices by following these steps:
Connect your iOS device to your WiFi network (local/adhoc WiFi network is used by profiler to send profiling data from device to the Unity Editor).
Check “Autoconnect Profiler” checkbox in Unity’s build settings dialog.
Attach your device to your Mac via cable and hit “Build & Run” in Unity Editor.
When app launches on device open profiler window in Unity Editor (Window->Profiler)

If you are using a firewall, you need to make sure that ports 54998 to 55511 are open in the firewall’s outbound rules – these are the ports used by Unity for remote profiling.

Note: Sometimes Unity Editor might not autoconnect to the device. In such cases profiler connection might be initiated from Profiler Window Active Profiler drop down menu by select appropriate device.

Optimizing graphics tips from Unity3D.com.