Archive for category Unity3D

Timer Manager for Unity3D

It’s been a nearly a month since my last blog entry. Typically people disappear for long periods of time because they’ve either just begun a romantic charade or they just became insanely busy with work. Well in my case, I’m already happily married with a sweet little daughter so it’s clearly a work issue. If you haven’t guessed it yet from this site, I’m actually starting a new company named Monkey Prism. At Monkey Prism we’re in the midst of an up and coming Unity 3D iOS release. During our development cycle we were in need of a basic timer object. This example really serves as an object to learn from on how to use Time.time in your code to create a basic timer. Feel free to use this code in anyway that you like!

Here is how you call this MPTimer object.
Example 1

View Code CSHARP
//Example 1:
private MPTimer monkeyPrismTimer;
 
void Start() {
   /*
   Creates a timer object that will wait 5 seconds before calling 
   the ToggleExample method.  The false flag signifies that this object
   will not start the timer until the 
      monkeyPrismTimer.Active = true;
   has been set.
   */
   monkeyPrismTimer = new MPTimer(
      new MPTimerObject(gameObject, "ToggleExample", 5.0f), false);
}
 
public void ToggleExample() {
   Debug.Log("ToggleExample() called!");
}
 
void Update() {
   //daisy chain the timer running.  Every time it inactivates, we'll re-activate it.
   if (!monkeyPrismTimer.Active) {
      Debug.Log("Enabling the timer!");
      monkeyPrismTimer.Active = true;
   }
 
   //drive the timer
   monkeyPrismTimer.DoTimer();
}

Example 2 (auto-start the timer)

View Code CSHARP
private MPTimer monkeyPrismTimer;
 
void Start() {
   //create a 3 second timer that repeats 10 times.
   MPTimerObject mpTimerObject = new MPTimerObject(gameObject, "ToggleExample", 3.0f);
   mpTimerObject.repeatCount = 10;  // repeat it 10 times.
 
   //creates a timer object without auto-starting it.
   monkeyPrismTimer = new MPTimer(mpTimerObject, true);
}
 
public void ToggleExample() {
   Debug.Log("ToggleExample() called!");
}
 
void Update() {
   //drive the timer
   monkeyPrismTimer.DoTimer();
}

The file on my system is named: “MPTimer.cs“. Here it is! The meat and potatoes we’ve been talking about!

Download MPTimer.cs
using System;
using UnityEngine;
 
/// <summary>
/// A timer object that will be passed into the MPTimer class.
/// </summary>
public class MPTimerObject
{
	/// <summary>
	/// The target GameObject that contains the onCompleteMethod specified.
	/// </summary>
	public GameObject target;
	/// <summary>
	/// A string representing the method to call when the timer expires.
	/// </summary>
	public String onCompleteMethod;
	/// <summary>
	/// A float value in seconds representing how often you would like the timer to trigger.
	/// </summary>
	public float runForSeconds;
	/// <summary>
	/// An integer value representing the number of times you would like this timer to trigger before becoming inactive.
	/// </summary>
	public int repeatCount;
 
	public MPTimerObject() { }
 
	/// <summary>
	/// Creates a basic Monkey Prism timer object.
	/// </summary>
	/// <param name="targetCompleteMethodObject">
	/// A <see cref="GameObject"/> that is the targeted object that contains the "completeMethod" string parameter.
	/// </param>
	/// <param name="completeMethod">
	/// A <see cref="String"/> representing a method named contained within the GameObject specified.  Example:  "myMethod"
	/// </param>
	/// <param name="triggerTime">
	/// A <see cref="System.Single"/> float value in seconds.  5.0f would be 5 seconds.  This tells the timer to trigger in 5 seconds.
	/// </param>
	public MPTimerObject(GameObject targetCompleteMethodObject, String completeMethod, float triggerTime)
	{
		Init(targetCompleteMethodObject, completeMethod, triggerTime, 1);
	}
 
	private void Init(GameObject targetCompleteMethodObject, String completeMethod, float runForSeconds, int repeatCount)
	{
		this.target = targetCompleteMethodObject;
		onCompleteMethod = completeMethod;
		this.runForSeconds = runForSeconds;
		this.repeatCount = repeatCount;
	}
}
 
/// <summary>
/// A Monkey Prism timer object that allows for a user to trigger a method based on a time specified.
/// </summary>
public class MPTimer
{
	private MPTimerObject timerObject;
	private float startTime;
	private float endTime;
	private bool active;
	private int count;
 
	/// <summary>
	/// Create a new MPTimer object.
	/// </summary>
	/// <param name="mpTimerObject">
	/// A <see cref="MPTimerObject"/>
	/// </param>
	public MPTimer(MPTimerObject mpTimerObject)
	{
		Init(mpTimerObject, true);
	}
 
	/// <summary>
	/// Create a new MPTimer object.
	/// </summary>
	/// <param name="mpTimerObject">
	/// A <see cref="MPTimerObject"/>
	/// </param>
	/// <param name="activeState">
	/// A <see cref="System.Boolean"/> representing whether to start the timer immediately or to wait.  If true, the timer will kick-off immediately.
	/// </param>
	public MPTimer(MPTimerObject mpTimerObject, bool activeState)
	{
		Init(mpTimerObject, activeState);
	}
 
	private void Init(MPTimerObject mpTimerObject, bool activeState)
	{
		if (mpTimerObject == null || 
		    mpTimerObject.onCompleteMethod == null || mpTimerObject.target == null) {
			Debug.LogError("MPTimer Error: Invalid MPTimerObject specified.");
		}
		timerObject = mpTimerObject;
		Active = activeState;
	}
 
	/// <summary>
	/// The MPTimerObject get/set helper.  Setting a new timer object will inactivate the current timer.
	/// </summary>
	public MPTimerObject TimerObject
	{
		get { return timerObject; }
		set {
			active = false;
			timerObject = value;
		}
	}
 
	/// <summary>
	/// The Active get/set helper.  By setting this to "Active" you will instantly cause this method to be called.
	/// </summary>
	public bool Active
	{
		get { return active; }
		set { 
			if (value != active) 
				count = 0;
			active = value; 
			if (active) {
				startTime = Time.time;
				endTime = startTime + timerObject.runForSeconds;
				//Debug.Log("Start= " + startTime + ", End= " + endTime);
			}
		}
	}
 
	/// <summary>
	/// A method used for the "Update" call in your calling library.  If this method is not called, the timer is not automatically driven.
	/// </summary>
	public void DoTimer()
	{
		if (!active) return;
 
		if (Time.time >= endTime) {
			//Debug.Log("timer fired!");
			timerObject.target.SendMessage(timerObject.onCompleteMethod);
			if (++count >= timerObject.repeatCount)
				active = false; //toggle off.
		}
	}
}

, , , , ,

No Comments

Unity3D Projects, Version Control and Git

Remember when CVS was a pretty neat version control system for one guy working out of his garage? Then many of us became enamored with SVN, which was a nice upgrade offering many additional options than your standard CVS. There are many other version control software packages on the market as well, some cost money and others are free. When it comes to the Unity3D Pro game engine there really are two version control systems that come to mind.

  • Unity3D Asset Server  – Costs $500.00 (as of June 4, 2011).  It’s an add-on component that requires additional hardware resources to run on.  It doesn’t appear that you can run this thing in the cloud (i.e. Amazon ec2, or pick your favorite virtual flavor).
  • SVN – FREE, but a real pain to setup with your Unity3D project. What folders and data do you not upload into your repository? Unity has a how-to, but it still stinks when you have those .svn folders peppered throughout your project.

As of June 4, 2011 Unity3D Pro shares these two options on their website (including the HOW-TO).  http://unity3d.com/support/documentation/Manual/ExternalVersionControlSystemSupport.html

What if there was one more option that wasn’t discussed by Unity3D? What if this option was as easy as SVN, if not easier to implement?

Enter in Git. I have personally setup my Unity3D projects using SVN in the past and after using Git, I can’t stress how great this product is for your Unity3D projects. No more nasty .svn folders incidentally getting distributed in your software or irritatingly peppered throughout. Also, branches, tags and the ominous merges are handled inherently much better in Git. As a side note, Git was created for one of the most popular open source projects on the planet earth… Linux. And it was started by the king of handling merges… Linus Torvalds himself. Not that that should steer your thinking much, but I have to admit I was slow to begin using Git. Now that I’m using Git, I’ve already converted all my latest projects over to it because I just can’t seem to live without it’s simplicity and power.

So how do I integrate Git? I’m not going to go into detail on setting up your own Git repository here. You can easily set one up over at github.com. Or for the more adventurous, you can create your own Git repository and integrate a authentication/permissions or use straight SSH. BTW, I recommend to use gitolite for authentication/permissions. Gitolite, will make integration a snap without forcing you to create a handful of accounts on your Linux server. It also doesn’t hurt that Fedora and a few other big systems use and support Gitolite.

You still have to follow some of the Unity3D directions for creating an SVN friendly Unity3D project. More specifically, you need to follow the first 3 directions they list. Well here is my recipe…

Please note, this only applies to Unity Pro Licenses.

  1. Create a new project inside Unity and lets call it InitialUnityProject. You can add any initial assets here or add them later on.
  2. Enable Meta files in Edit->Project Settings->Editor
  3. Quit Unity (We do this to assure that all the files are saved).
  4. (My addition) Create a new Git repository for your project. Setup git on your local workstation and pull the empty repository down to a folder
  5. (My addition) Copy your new Unity3D project over to that local repository project folder.
  6. (My addition) Create a .gitignore using my recipe or a modification as shown below.
  7. (My addition) Startup Unity3D and open the project from the new local repository project folder.

Tools I use:
Git
GitX

See below for a .gitignore file which is stored at the root of your local repository location. It will take care of helping you avoid storing the wrong files into your Git repository!

# Unity3D .gitignore file.
#     Store this file at the root of your local repository.
.DS_Store
Library/AssetImportState
Library/AssetServerCacheV3
Library/FailedAssetImports.txt
Library/ScriptAssemblies
Library/ScriptMapper
Library/assetDatabase3
Library/cache
Library/expandedItems
Library/metadata
Library/previews
Library/guidmapper
Temp
*.pidb
build

Remember with Git, that you are operating on your local repository until you perform the git push origin master. This means you can bundle your commits into the parent version control!

Finally, I feel like I discovered a happy medium of finding a free alternative that works better than SVN with the Unity3D game environment.

Good luck with your projects and happy game developing!

, , , , ,

27 Comments

Unity3D Joystick Script

Here we are again. 3 cups of coffee later and another script conversion. I’ve taken the time to convert the Penelope Joystick.cs JavaScript script into C-Sharp. Why would someone do this? I did it because I am getting tired of anything that is not strongly typed in Unity3D. If you are writing your engine in JavaScript, you’re probably making variables with or without typos that don’t have the scope you thought they did and worse, you might have a 6-8 hour hot date with your code when you can’t figure out why it’s crashing due to some strange “type assumptions”. Don’t get me wrong, hot dates are a blast, especially when they aren’t with your computer.

In my usual way, here is the Joystick script converted over into C-Sharp.

May 17, 2011: Oh one more thing, I’m posting this early, call it jumping the gun, I haven’t tested it yet. I’ll be testing in the next 24 hours.

View Code CSHARP
using UnityEngine;
 
/**
 * File: MPJoystick.cs
 * Author: Chris Danielson of (monkeyprism.com)
 * 
// USED TO BE: Joystick.js taken from Penelope iPhone Tutorial
//
// Joystick creates a movable joystick (via GUITexture) that 
// handles touch input, taps, and phases. Dead zones can control
// where the joystick input gets picked up and can be normalized.
//
// Optionally, you can enable the touchPad property from the editor
// to treat this Joystick as a TouchPad. A TouchPad allows the finger
// to touch down at any point and it tracks the movement relatively 
// without moving the graphic
*/
 
[RequireComponent(typeof(GUITexture))]
public class MPJoystick : MonoBehaviour
{
	class Boundary {
		public Vector2 min = Vector2.zero;
		public Vector2 max = Vector2.zero;
	}
 
	private static MPJoystick[] joysticks;					// A static collection of all joysticks
	private static bool enumeratedJoysticks = false;
	private static float tapTimeDelta = 0.3f;				// Time allowed between taps
 
	public bool touchPad;
	public Vector2 position = Vector2.zero;
	public Rect touchZone;
	public Vector2 deadZone = Vector2.zero;						// Control when position is output
	public bool normalize = false; 							// Normalize output after the dead-zone?
	public int tapCount;	
 
	private int lastFingerId = -1;								// Finger last used for this joystick
	private float tapTimeWindow;							// How much time there is left for a tap to occur
	private Vector2 fingerDownPos;
	//private float fingerDownTime;
	//private float firstDeltaTime = 0.5f;
 
	private GUITexture gui;
	private Rect defaultRect;								// Default position / extents of the joystick graphic
	private Boundary guiBoundary = new Boundary();			// Boundary for joystick graphic
	private Vector2 guiTouchOffset;						// Offset to apply to touch input
	private Vector2 guiCenter;							// Center of joystick
 
	void Start() {
		gui = (GUITexture)GetComponent(typeof(GUITexture));
 
		defaultRect = gui.pixelInset;
		defaultRect.x += transform.position.x * Screen.width;// + gui.pixelInset.x; // -  Screen.width * 0.5;
    	defaultRect.y += transform.position.y * Screen.height;// - Screen.height * 0.5;
 
		transform.position = Vector3.zero;
 
		if (touchPad) {
			// If a texture has been assigned, then use the rect ferom the gui as our touchZone
			if ( gui.texture )
				touchZone = defaultRect;
		} else {	
			guiTouchOffset.x = defaultRect.width * 0.5f;
			guiTouchOffset.y = defaultRect.height * 0.5f;
 
			// Cache the center of the GUI, since it doesn't change
			guiCenter.x = defaultRect.x + guiTouchOffset.x;
			guiCenter.y = defaultRect.y + guiTouchOffset.y;
 
			// Let's build the GUI boundary, so we can clamp joystick movement
			guiBoundary.min.x = defaultRect.x - guiTouchOffset.x;
			guiBoundary.max.x = defaultRect.x + guiTouchOffset.x;
			guiBoundary.min.y = defaultRect.y - guiTouchOffset.y;
			guiBoundary.max.y = defaultRect.y + guiTouchOffset.y;
		}
	}
 
	public Vector2 getGUICenter() {
		return guiCenter;
	}
 
	void Disable() {
		gameObject.active = false;
		//enumeratedJoysticks = false;	
	}
 
	private void ResetJoystick() {
		gui.pixelInset = defaultRect;
		lastFingerId = -1;
		position = Vector2.zero;
		fingerDownPos = Vector2.zero;	
	}
 
	private bool IsFingerDown() {
		return (lastFingerId != -1);
	}
 
	public void LatchedFinger(int fingerId) {
		// If another joystick has latched this finger, then we must release it
		if ( lastFingerId == fingerId )
			ResetJoystick();
	}
 
	void Update() {
		if (!enumeratedJoysticks) {
			// Collect all joysticks in the game, so we can relay finger latching messages
			joysticks = (MPJoystick[])FindObjectsOfType(typeof(MPJoystick));	
			enumeratedJoysticks = true;
		}
 
		int count = Input.touchCount;
 
		if ( tapTimeWindow > 0 )
			tapTimeWindow -= Time.deltaTime;
		else
			tapCount = 0;
 
		if ( count == 0 )
			ResetJoystick();
		else
		{
			for(int i = 0; i < count; i++) {
				Touch touch = Input.GetTouch(i);			
				Vector2 guiTouchPos = touch.position - guiTouchOffset;
 
				bool shouldLatchFinger = false;
				if (touchPad) {
					if (touchZone.Contains(touch.position))
						shouldLatchFinger = true;
				}
				else if (gui.HitTest(touch.position)) {
					shouldLatchFinger = true;
				}
 
				// Latch the finger if this is a new touch
				if (shouldLatchFinger && (lastFingerId == -1 || lastFingerId != touch.fingerId )) {
 
					if (touchPad) {
						//gui.color.a = 0.15;
						lastFingerId = touch.fingerId;
						//fingerDownPos = touch.position;
						//fingerDownTime = Time.time;
					}
 
					lastFingerId = touch.fingerId;
 
					// Accumulate taps if it is within the time window
					if ( tapTimeWindow > 0 )
						tapCount++;
					else {
						tapCount = 1;
						tapTimeWindow = tapTimeDelta;
					}
 
					// Tell other joysticks we've latched this finger
					//for (  j : Joystick in joysticks )
					foreach (MPJoystick j in joysticks) {
						if (j != this) 
							j.LatchedFinger( touch.fingerId );
					}		
				}				
 
				if ( lastFingerId == touch.fingerId ) {	
					// Override the tap count with what the iPhone SDK reports if it is greater
					// This is a workaround, since the iPhone SDK does not currently track taps
					// for multiple touches
					if ( touch.tapCount > tapCount )
						tapCount = touch.tapCount;
 
					if ( touchPad ) {
						// For a touchpad, let's just set the position directly based on distance from initial touchdown
						position.x = Mathf.Clamp( ( touch.position.x - fingerDownPos.x ) / ( touchZone.width / 2 ), -1, 1 );
						position.y = Mathf.Clamp( ( touch.position.y - fingerDownPos.y ) / ( touchZone.height / 2 ), -1, 1 );
					} else {					
						// Change the location of the joystick graphic to match where the touch is
						Rect r = gui.pixelInset;
						r.x =  Mathf.Clamp( guiTouchPos.x, guiBoundary.min.x, guiBoundary.max.x );
						r.y =  Mathf.Clamp( guiTouchPos.y, guiBoundary.min.y, guiBoundary.max.y );		
						gui.pixelInset = r;
					}
 
					if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled)
						ResetJoystick();					
				}
			}
		}
 
		if (!touchPad) {
			// Get a value between -1 and 1 based on the joystick graphic location
			position.x = ( gui.pixelInset.x + guiTouchOffset.x - guiCenter.x ) / guiTouchOffset.x;
			position.y = ( gui.pixelInset.y + guiTouchOffset.y - guiCenter.y ) / guiTouchOffset.y;
		}
 
		// Adjust for dead zone	
		var absoluteX = Mathf.Abs( position.x );
		var absoluteY = Mathf.Abs( position.y );
 
		if (absoluteX < deadZone.x) {
			// Report the joystick as being at the center if it is within the dead zone
			position.x = 0;
		}
		else if (normalize) {
			// Rescale the output after taking the dead zone into account
			position.x = Mathf.Sign( position.x ) * ( absoluteX - deadZone.x ) / ( 1 - deadZone.x );
		}
 
		if (absoluteY < deadZone.y) {
			// Report the joystick as being at the center if it is within the dead zone
			position.y = 0;
		}
		else if (normalize) {
			// Rescale the output after taking the dead zone into account
			position.y = Mathf.Sign( position.y ) * ( absoluteY - deadZone.y ) / ( 1 - deadZone.y );
		}
 
	}
 
}

, , , , ,

8 Comments

Using Apple Property List Files with Unity3D

Persistent storage and data retrieval can be troubling without a reference in your environment to the SQLite library. In the case of Unity3D, there is no direct SQLite library wrappers offered out of the box. One can write a device specific SQLite wrapper (or purchase one) that bridges the gap into the Unity3D JavaScript and CSharp world or one can decide on a simpler method for storing and managing data. Enter Apple’s .plist or property list files. These files can be saved in binary and also in a standard text format. For our situation, we’re going to manage these files that are stored in standard text format. It is arguable that using a custom plist parser is going to be slower than using SQLite, but with a small enough subset of data I believe that plist should be an adequate solution.

The unity community has a free version of a PropertyListSerializer.js file. I copied this code outright and converted it into the strongly typed C-Sharp environment. My reasoning was simple, I found that using JavaScript wasn’t going to work with my current project for a few reasons so I decided to convert this file over into C-Sharp code. I did attempt to upload my changes to that site, but it continued to complain that I was getting blocked by the spam check functions.

Enjoy!

Edit: May 13, 2011 After testing on the actual iOS device I discovered that DTD validation is not implemented in Unity. I’ve now disabled the XmlDocument forced validation.

View Code CSHARP
/**
 * 
 * Converted By: Chris Danielson of http://MonkeyPrism.com
 * Converted Date: May 8, 2011
 * Original JavaScript Auther: capnbishop
 * 
 * Conversion of a Unity 3D Community script from JavaScript to type controlled C# Mono.Net.
 * 
 * Please note, that the original object was named PropertyListSerializer.  I renamed it to prevent any conflicts with the original script.
 * 
 * The PropertyListSerializer.js (CD: renamed now to PListManager) script is used to load and save an XML property list file to and from a hierarchical hashtable 
 * (in which the root hashtable can contain child hashtables and arrays). This can provide a convenient and dynamic means of serializing 
 * a complex hierarchy of game data into XML files.
 * When loading, the resulting hashtable can include 8 different types of values: string, integer, real, date, data, boolean, dictionary, and array. 
 * Data elements are loaded as strings. Dictionaries are loaded as hashtables. Arrays are loaded as arrays. Each value is loaded with an 
 * associating key, except for elements of an array. Thus, each child hashtable and array also have associating keys, and can be combined to 
 * create a complex hierarchy of key value pairs and arrays.
 * When saving, the resulting XML file will contain the same hierarchy of data. All data will end up being stored as a string, but with an 
 * associated value type. Strings, integers, and decimals values are stored as such. Dates are stored in ISO 8601 format. Hashtables are stored 
 * as a plist key/value dictionary, and arrays as a series of keyless values.
 * The loader passes a lot of values by reference, and performs a considerable amount of recursion. Primitive values had to be passed by reference. 
 * Unity's JavaScript only passes objects by reference, and cannot explicitly pass a primitive by reference. As such, we've had to create a 
 * special ValueObject, which is just an abstract object that holds a single value. This object is then passed by reference, and the primitive 
 * value is set to its val property.
 * This plist loader conforms to Apple's plist DOCTYPE definition: http://www.apple.com/DTDs/PropertyList-1.0.dtd
 * 
 * Original JavaScript URL:  http://www.unifycommunity.com/wiki/index.php?title=PropertyListSerializer
 * 
 * Example Saving:
 
 		Hashtable playerData = new Hashtable();
		playerData.Add("Health",100);
		playerData.Add("TestObject", 1.5f);
		ArrayList guns = new ArrayList();
		guns.Add("AK-47");
		guns.Add("Pistol");
	    playerData.Add("Guns", guns);
		Hashtable grenades = new Hashtable();
		grenades.Add("FragmentationCount", 1);
		grenades.Add("IncendiaryCount", 1);
		playerData.Add("Grenades", grenades);
 
		//save outside the current project (same folder as Assets and Library)
		String xmlFile = Application.dataPath + "/../ExampleSaveFile.plist"; 
		PListManager.SavePlistToFile(xmlFile, playerData);
 * 
 */
using System;
using System.Collections;
using System.IO;
using System.Xml;
using System.Text;
using System.Globalization;
using UnityEngine;
 
public class PListManager {
 
	public PListManager() { }
 
	private const string SUPPORTED_VERSION = "1.0";
 
	public static bool ParsePListFile(string xmlFile, ref Hashtable plist) {                       
		if (!File.Exists(xmlFile)) { 
			Debug.LogError("File doesn't exist: " + xmlFile); 
			return false; 
		}
 
		StreamReader sr = new StreamReader(xmlFile);
		string txt = sr.ReadToEnd();
		sr.Close();
 
		XmlDocument xml = new XmlDocument();
                xml.XmlResolver = null; //Disable schema/DTD validation, it's not implemented for Unity.
		xml.LoadXml(txt);
 
		XmlNode plistNode = xml.LastChild;
		if (!plistNode.Name.Equals("plist")) { 
			Debug.LogError("plist file missing <plist> nodes." + xmlFile);
			return false;
		}
 
		string plistVers = plistNode.Attributes["version"].Value;
		if (plistVers == null || !plistVers.Equals(SUPPORTED_VERSION)) { 
			Debug.LogError("This is an unsupported plist version: " + plistVers + ". Required version:a " + SUPPORTED_VERSION); 
			return false;
		}
 
		XmlNode dictNode = plistNode.FirstChild;
    	if (!dictNode.Name.Equals("dict")) { 
			Debug.LogError("Missing root dict from plist file: " + xmlFile); 
			return false; 
		}
 
		return LoadDictFromPlistNode(dictNode, ref plist);
	}
 
 
	#region LOAD_PLIST_PRIVATE_METHODS
	private static bool LoadDictFromPlistNode(XmlNode node, ref Hashtable dict) {
		if (node == null) { 
			Debug.LogError("Attempted to load a null plist dict node.");
			return false;
		}
		if (!node.Name.Equals("dict")) { 
			Debug.LogError("Attempted to load an dict from a non-array node type: " + node + ", " + node.Name); 
			return false;
		}
		if (dict == null) { 
			dict = new Hashtable();
		}
 
		int cnodeCount = node.ChildNodes.Count;
		for (int i = 0; i+1 < cnodeCount; i+=2) {
        	// Select the key and value child nodes
			XmlNode keynode = node.ChildNodes.Item(i);
			XmlNode valuenode = node.ChildNodes.Item(i+1);
 
			// If this node isn't a 'key'
        	if (keynode.Name.Equals("key")) {
				// Establish our variables to hold the key and value.
				string key = keynode.InnerText;
				ValueObject value = new ValueObject();
 
	            // Load the value node.
	            // If the value node loaded successfully, add the key/value pair to the dict hashtable.
	            if (LoadValueFromPlistNode(valuenode, ref value)) {
	                // This could be one of several different possible data types, including another dict.
	                // AddKeyValueToDict() handles this by replacing existing key values that overlap, and doing so recursively for dict values.
	                // If this not successful, post a message stating so and return false.
	                if (!AddKeyValueToDict(ref dict, key, value)) {
						Debug.LogError("Failed to add key value to dict when loading plist from dict"); 
						return false;
					}
	            } else { 
					Debug.LogError("Did not load plist value correctly for key in node: " + key + ", " + node);
					return false;
				}
	        } else { 
				Debug.LogError("The plist being loaded may be corrupt.");
				return false;
			}
 
		} //end for
 
		return true;
	}
 
	private static bool LoadValueFromPlistNode(XmlNode node, ref ValueObject value) {
		if (node == null) { 
			Debug.LogError("Attempted to load a null plist value node."); 
			return false;
		}
		if (node.Name.Equals("string")) { value.val = node.InnerText; }
	    else if (node.Name.Equals("integer")) { value.val = int.Parse(node.InnerText); }
	    else if (node.Name.Equals("real")) { value.val = float.Parse(node.InnerText); }
	    else if (node.Name.Equals("date")) { value.val = DateTime.Parse(node.InnerText, null, DateTimeStyles.None); } // Date objects are in ISO 8601 format
	    else if (node.Name.Equals("data")) { value.val = node.InnerText; } // Data objects are just loaded as a string
	    else if (node.Name.Equals("true")) { value.val = true; } // Boollean values are empty objects, simply identified with a name being "true" or "false"
	    else if (node.Name.Equals("false")) { value.val = false; }
	    // The value can be an array or dict type.  In this case, we need to recursively call the appropriate loader functions for dict and arrays.
	    // These functions will in turn return a boolean value for their success, so we can just return that.
	    // The val value also has to be instantiated, since it's being passed by reference.
	    else if (node.Name.Equals("dict")) { 
			value.val = new Hashtable();
			Hashtable htRef = (Hashtable)value.val;
			return LoadDictFromPlistNode(node, ref htRef);
		}
	    else if (node.Name.Equals("array")) {
			value.val = new ArrayList();
			ArrayList alRef = (ArrayList)value.val;
			return LoadArrayFromPlistNode(node, ref alRef);
		} else { 
			Debug.LogError("Attempted to load a value from a non value type node: " + node + ", " + node.Name);
			return false;
		}
 
	    return true;
	}
 
	private static bool LoadArrayFromPlistNode(XmlNode node, ref ArrayList array ) {
	    // If we were passed a null node object, then post an error stating so and return false
	    if (node == null) { 
			Debug.LogError("Attempted to load a null plist array node.");
			return false;
		}
	    // If we were passed a non array node, then post an error stating so and return false
	    if (!node.Name.Equals("array")) { 
			Debug.LogError("Attempted to load an array from a non-array node type: " + node + ", " + node.Name); 
			return false;
		}
 
	    // We can be passed an empty array object.  If so, initialize it
	    if (array == null) { array = new ArrayList(); }
 
	    // Itterate through the child nodes for this array object
	    int nodeCount = node.ChildNodes.Count;
	    for (int i = 0; i < nodeCount; i++) {
	        // Establish variables to hold the child node of the array, and it's value
	        XmlNode cnode = node.ChildNodes.Item(i);
	        ValueObject element = new ValueObject();
	        // Attempt to load the value from the current array node.
	        // If successful, add it as an element of the array.  If not, post and error stating so and return false.
	        if (LoadValueFromPlistNode(cnode, ref element)) { 
				array.Add(element.val); 
			} else { 
				return false; 
			}
	    }
 
	    // If we made it through the array without errors, return true
	    return true;
	}
 
	private static bool AddKeyValueToDict(ref Hashtable dict, string key, ValueObject value) {
	    // Make sure that we have values that we can work with.
	    if (dict == null || key == null || key.Length < 1 || value == null) { 
			Debug.LogError("Attempted to AddKeyValueToDict() with null objects.");
			return false;
		}
	    // If the hashtable doesn't already contain the key, they we can just go ahead and add it.
	    if (!dict.ContainsKey(key)) { 
			dict.Add(key, value.val);
			return true;
		}
	    // At this point, the dict contains already contains the key we're trying to add.
	    // If the value for this key is of a different type between the dict and the new value, then we have a type mismatch.
	    // Post an error stating so, but go ahead and overwrite the existing key value.
		if (value.val.GetType() != dict[key].GetType()) {
	        Debug.LogWarning("Value type mismatch for overlapping key (will replace old value with new one): " + value.val + ", " + dict[key] + ", " + key);
	        dict[key] = value.val;
	    }
    	// If the value for this key is a hashtable, then we need to recursively add the key values of each hashtable.
    	else if (value.val.GetType() == typeof(Hashtable)) {
	        // Itterate through the elements of the value's hashtable.
			Hashtable htTmp = (Hashtable)value.val;
	        foreach (object element in htTmp) {
	            // Recursively attempt to add/repalce the elements of the value hashtable to the dict's value hashtable.
	            // If this fails, post a message stating so and return false.
				Hashtable htRef = (Hashtable)dict[key];
	            if (!AddKeyValueToDict(ref htRef, (string)element, new ValueObject(htTmp[element]))) {
	                Debug.LogError("Failed to add key value to dict: " + element + ", " + htTmp[element] + ", " + dict[key]);
	                return false;
	            }
	        }
    	}
	    // If the value is an array, then there's really no way we can tell which elements to overwrite, because this is done based on the congruent keys.
	    // Thus, we'll just add the elements of the array to the existing array.
	    else if (value.val.GetType() == typeof(ArrayList)) {
			ArrayList alTmp = (ArrayList)value.val;
			ArrayList alAddTmp = (ArrayList)dict[key];
			foreach (object element in alTmp) {
				alAddTmp.Add(element);
			}
	    }
	    // If the key value is not an array or a hashtable, then it's a primitive value that we can easily write over.
	    else { 
			dict[key] = value.val;
		}
 
    	return true;
	}
	#endregion
 
 
	public static bool SavePlistToFile (String xmlFile, Hashtable plist) {
	    // If the hashtable is null, then there's apparently an issue; fail out.
	    if (plist == null) { 
			Debug.LogError("Passed a null plist hashtable to SavePlistToFile.");
			return false;
		}
 
	    // Create the base xml document that we will use to write the data
	    XmlDocument xml = new XmlDocument();
	    xml.XmlResolver = null; //Disable schema/DTD validation, it's not implemented for Unity.
	    // Create the root XML declaration
	    // This, and the DOCTYPE, below, are standard parts of a XML property list file
	    XmlDeclaration xmldecl = xml.CreateXmlDeclaration("1.0", "UTF-8", null);
	    xml.PrependChild(xmldecl);
 
	    // Create the DOCTYPE
	    XmlDocumentType doctype = xml.CreateDocumentType("plist", "-//Apple//DTD PLIST 1.0//EN", "http://www.apple.com/DTDs/PropertyList-1.0.dtd", null);
	    xml.AppendChild(doctype);
 
	    // Create the root plist node, with a version number attribute.
	    // Every plist file has this as the root element.  We're using version 1.0 of the plist scheme
	    XmlNode plistNode = xml.CreateNode(XmlNodeType.Element, "plist", null);
	    XmlAttribute plistVers = (XmlAttribute)xml.CreateNode(XmlNodeType.Attribute, "version", null);
	    plistVers.Value = "1.0";
	    plistNode.Attributes.Append(plistVers);
	    xml.AppendChild(plistNode);
 
	    // Now that we've created the base for the XML file, we can add all of our information to it.
	    // Pass the plist data and the root dict node to SaveDictToPlistNode, which will write the plist data to the dict node.
	    // This function will itterate through the hashtable hierarchy and call itself recursively for child hashtables.
	    if (!SaveDictToPlistNode(plistNode, plist)) {
	        // If for some reason we failed, post an error and return false.
	        Debug.LogError("Failed to save plist data to root dict node: " + plist);
	        return false;
	    } else { // We were successful
	        // Create a StreamWriter and write the XML file to disk.
	        // (do not append and UTF-8 are default, but we're defining it explicitly just in case)
	        StreamWriter sw = new StreamWriter(xmlFile, false, System.Text.Encoding.UTF8);
	        xml.Save(sw);
	        sw.Close();
	    }
 
	    // We're done here.  If there were any failures, they would have returned false.
	    // Return true to indicate success.
	    return true;
	}
 
	#region SAVE_PLIST_PRIVATE_METHODS
 
	private static bool SaveDictToPlistNode(XmlNode node, Hashtable dict) {
	    // If we were passed a null object, return false
	    if (node == null) {
			Debug.LogError("Attempted to save a null plist dict node.");
			return false;
		}
 
	    XmlNode dictNode = node.OwnerDocument.CreateNode(XmlNodeType.Element, "dict", null);
	    node.AppendChild(dictNode);
 
	    // We could be passed an null hashtable.  This isn't necessarily an error.
	    if (dict == null) { 
			Debug.LogWarning("Attemped to save a null dict: " + dict); 
			return true;
		}
 
	    // Iterate through the keys in the hashtable
	    //for (var key in dict.Keys) {
		foreach (object key in dict.Keys) {
	        // Since plists are key value pairs, save the key to the plist as a new XML element
	        XmlElement keyNode = node.OwnerDocument.CreateElement("key");
	        keyNode.InnerText = (string)key;
	        dictNode.AppendChild(keyNode);
 
	        // The name of the value element is based on the datatype of the value.  We need to serialize it accordingly.  Pass the XML node and the hash value to SaveValueToPlistNode to handle this.
	        if (!SaveValueToPlistNode(dictNode, dict[key])) {
	            // If SaveValueToPlistNode() returns false, that means there was an error.  Return false to indicate this up the line.
	            Debug.LogError("Failed to save value to plist node: " + key);
	            return false;
	        }
	    }
 
	    // If we got this far then all is well.  Return true to indicate success.
	    return true;
	}
 
	private static bool SaveValueToPlistNode(XmlNode node, object value) {
	    // The node passed will be the parent node to the new value node.
	    XmlNode valNode;
	    System.Type type = value.GetType();
	    // Identify the data type for the value and serialize it accordingly
	    if (type == typeof(String)) { 
			valNode = node.OwnerDocument.CreateElement("string"); 
		}
	    else if (type == typeof(Int16) || 
		         type == typeof(Int32) ||
		         type == typeof(Int64)) { valNode = node.OwnerDocument.CreateElement("integer"); }
	    else if (type == typeof(Single) || 
		         type == typeof(Double) ||
		         type == typeof(Decimal)) { valNode = node.OwnerDocument.CreateElement("real"); }
	    else if (type == typeof(DateTime)) {
	        // Dates need to be stored in ISO 8601 format
	        valNode = node.OwnerDocument.CreateElement("date");
			DateTime dt = (DateTime)value;
	        valNode.InnerText = dt.ToUniversalTime().ToString("o");
	        node.AppendChild(valNode);
	        return true;
	    }
	    else if (type == typeof(bool)) {
	        // Boolean values are empty elements, simply being stored as an elemement with a name of true or false
	        if ((bool)value == true) { valNode = node.OwnerDocument.CreateElement("true"); }
	        else { valNode = node.OwnerDocument.CreateElement("false"); }
	        node.AppendChild(valNode);
	        return true;
	    }
	    // Hashtables and arrays require special functions to save their values in an itterative and recursive manner.
	    // The functions will return true/false to indicate success/failure, so pass those on.
	    else if (type == typeof(Hashtable))    { 
			return SaveDictToPlistNode(node, (Hashtable)value); 
		}
	    else if (type == typeof(ArrayList)) { return SaveArrayToPlistNode(node, (ArrayList)value); }
	    // Anything that doesn't fit the defined data types will just be stored as "data", which is effectively a string.
	    else { 
			valNode = node.OwnerDocument.CreateElement("data");
		}
 
	    // Some of the values (strings, numbers, data) basically get stored as a string.  The rest will store their values in their special format and return true for success.  If we made it this far, then the value in valNode must be stored as a string.
	    if (valNode != null) valNode.InnerText = value.ToString();
	    node.AppendChild(valNode);
 
	    // We're done.  Return true for success.
	    return true;
	}
 
	private static bool SaveArrayToPlistNode (XmlNode node, ArrayList array) {
	    // Create the value node as an "array" element.
	    XmlElement arrayNode = node.OwnerDocument.CreateElement("array");
	    node.AppendChild(arrayNode);
 
	    // Each element in the array can be any data type.  Itterate through the array and send each element to SaveValueToPlistNode(), where it can be stored accordingly based on its data type.
	    foreach (object element in array) {
	        // If SaveValueToPlistNode() returns false, then there was a problem.  Return false in that case.
	        if (!SaveValueToPlistNode(arrayNode, element)) { return false; }
	    }
    	return true;
	}
 
	#endregion
 
} //end PListManager class
 
class ValueObject { 
	public object val;
	public ValueObject() {}
	public ValueObject(object aVal) { val = aVal; }
}

, , , , , ,

21 Comments

Unity3D File Based Assetbundles on the iPhone

Let me start off by saying that Unity3D is a really great game engine. Let me also say that I wish the documentation was as great as the engine is. Sadly, it’s difficult to figure out some of the more powerful features of Unity3D without significant trial and error.

Please note, that I have read that the usage of Assetbundles on the iPhone can be memory intensive and may or may not fit your needs per your project. I am just putting this information here so that others can see how to do this. Pro Tip: What this means to you? Profile the heck out of your code, pre-assetbundles and after integrating assetbundles.

Assumptions:
1. You know or are familiar Unity3D 3.3.
2. You can code in C#.
3. You’re an ace at creating prefabs in Unity.
4. You’re using Unity3D 3.3 Pro and iOS Pro.

OK, if you’re still feeling good, let’s bounce into this how-to.

Create the Project

Open up a new project in Unity3D and set it up for “iPhone” iOS. Feel free to name it whatever passes your way.

In the project hierarchy, let’s go ahead and create a simple directory structure as follows:

/Example Project/
/Example Project/iphone/
/Example Project/iphone/objects/
/Example Project/iphone/materials/

Hit save after doing this and save your default scene and name it whatever you want.  In my case, I named it “test”.

Though, this is a near complete looking example (image to the right), the basic structure should look like the image to the right here.

Add at least one prefab to your project. I have added two prefabs here, “box” and “ABCD”. Both are just tests and contain a cube prefab with a simple texture applied. Make sure that the materials referenced by your prefabs have the following attributes. 1. They are named the same as the prefab. (See the image above!). 2. They are stored in the materials folder underneath the iphone folder.

UnityEditor Coding Time

When working on assetbundles, the first thing you have to do is generate them. Unfortunately, Unity3D does not have anything built in for generating these files except for a UnityEditor API. The UnityEditor script can only be run from inside the Unity3D editor tool. It typically appears as a menu item after the script has been successfully compiled. So if you can code, then you’re in good shape.

Let’s create the UnityEditor script. Create a new C-Sharp script in a /Plugins/ folder (create it if it doesn’t already exist) at the root of your project and name it MPCreateAssetBundle. Copy and paste the following into it. Please note: some of this code was borrowed from Unity’s own Character Customization tutorial on their website.

View Code CSHARP
using System;
using System.IO;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using Object=UnityEngine.Object;
 
public class MPCreateAssetBundle : MonoBehaviour {
 
	private static String[] mpAssetDir = new String[]{"iphone", "ipad", "macosx", "pc"};// , "universal"}; <-- not needed, use the /Resources/ folder.
 
	[MenuItem("Custom/Monkey Prism/Create All Assetbundles #&i")]
	public static void Execute() {
		Debug.Log("Creating Assetbundles");
		bool blnFound = false;
		String currentDir = null;
		foreach (Object o in Selection.GetFiltered(typeof(Object), SelectionMode.DeepAssets)) {
			blnFound = true;
 
			if (o.name.Contains("@")) continue; //animations!
			String assetPath = AssetDatabase.GetAssetPath(o);
			if (!File.Exists(assetPath)) {
				currentDir = assetPath;
				continue; //Files only.
			}
			//Only check those directories that we have specified in the mpAssetDir
			Debug.Log(assetPath);
 
			String currentBuildType = null;
			foreach (String s in mpAssetDir) {
				if (assetPath.Contains("/"+s+"/")) {
					currentBuildType = s;
					break;
				}
			}
			if (currentBuildType == null) continue; //if the directory is not found to be one from the mpAssetDir bail out.
 
			string assetBundleName = null, genericName = null;
 
			List<Object> toinclude = new List<Object>();
 
			//Generate pre-fabs for everything in the finished pre-fab directory.
 
			if (o.GetType() == typeof(GameObject)) {
				Debug.Log("GameObject " + currentDir);
 
				String d = CharacterRoot((GameObject)o);
				d += "materials/";
				Debug.Log(d);
				List<Material> materials = CollectAll<Material>(d);
				Debug.Log("materials count=" + materials.Count);
 
				genericName = o.name.ToLower();
				assetBundleName = currentBuildType + "-prefab-" + genericName;
				//Package up the prefabs in the iPhone directory.
				toinclude.Add(o);
				//Do we need to add in a material?  I think so.
				foreach (Material m in materials) {
					Debug.Log("Material Name=" + m.name);
                    if (m.name.Contains(genericName)) {
						toinclude.Add(m);
						Debug.Log("Added a new material!");
					}
				} //end foreach
 
			} 
 
			if (assetBundleName == null) continue;
 
 
			// Create a directory to store the generated assetbundles.
            if (!Directory.Exists(AssetbundlePath))
                Directory.CreateDirectory(AssetbundlePath);
 
            // Delete existing assetbundles for current object
            string[] existingAssetbundles = Directory.GetFiles(AssetbundlePath);
            foreach (string bundle in existingAssetbundles) {
                if (bundle.EndsWith(".assetbundle") && bundle.Contains("/assetbundles/" + assetBundleName))
                    File.Delete(bundle);
            }
 
			//Directories expected.
			Debug.Log("currentBuildType = " + currentBuildType);
 
			//path = AssetbundlePath + bundleName + ".assetbundle";
			if (toinclude.Count > 0) {
				String path = AssetbundlePath + assetBundleName + ".assetbundle";
				Debug.Log(path);
				if (currentBuildType.Equals(mpAssetDir[0]) || currentBuildType.Equals(mpAssetDir[1])) //iPhone & iPad
					BuildPipeline.BuildAssetBundle(null, toinclude.ToArray(), path, BuildAssetBundleOptions.CollectDependencies, BuildTarget.iPhone);
				else //TODO: might need to condition further and might want to use an enum with the conditional.
					BuildPipeline.BuildAssetBundle(null, toinclude.ToArray(), path, BuildAssetBundleOptions.CollectDependencies);
			}
 
 
		} //end foreach
 
		if (!blnFound) {
			Debug.Log("no objects were found for building assets with.");	
		}
	}
 
	public static string AssetbundlePath
    {
        get { return "assetbundles" + Path.DirectorySeparatorChar; }
    }
 
	// This method loads all files at a certain path and
    // returns a list of specific assets.
    public static List<T> CollectAll<T>(string path) where T : Object
    {
        List<T> l = new List<T>();
        string[] files = Directory.GetFiles(path);
 
        foreach (string file in files)
        {
            if (file.Contains(".meta")) continue;
            T asset = (T) AssetDatabase.LoadAssetAtPath(file, typeof(T));
            if (asset == null) throw new Exception("Asset is not " + typeof(T) + ": " + file);
            l.Add(asset);
        }
        return l;
    }	
	// Returns the path to the directory that holds the specified FBX.
    static string CharacterRoot(GameObject character)
    {
        string root = AssetDatabase.GetAssetPath(character);
        return root.Substring(0, root.LastIndexOf('/') + 1);
    }
 
}

Once this is completed, you’ll want to do two things.
1. Select (by moving your mouse over and left-clicking on it once) the iphone folder under “Example Project”.
2. Run the script as shown in the image:

So let’s back up for a second and look at what we just did. We created a project, created some folders within the project, placed prefabs into the iphone folder and created a UnityEditor script. Upon running this script, a new directory named “assetbundles” would be created.

This directory is not going to be visible to you until you open finder (on Mac) or explorer (on Windows) as it is located on the file system. (see the image above). Inside this folder you should see your prefab(s) named now as: iphone-prefab-<my prefab name>.assetbundle. If you are seeing this, congratulations, you’ve now created your own assetbundles uniquely per prefab.

Generic GameObject and a Simple Script

If you’ve come this far, great work! We’re nearly done.
Let’s create a new GameObject under the Hierarchy.

Now we’re going to write a simple script to instantiate the assetbundles we assembled earlier. Create a new C-Sharp script in the /Plugins/ folder and name it “Example”. Copy and paste the following code into it:

View Code CSHARP
using System;
using UnityEngine;
using System.Collections;
 
public class Example : MonoBehaviour {
	private WWW www;
	private AssetBundleRequest gameObjectRequest;	
	// Use this for initialization
	IEnumerator Start () {
        //TODO: change the name of the file below!!!
		String strPath = "file://" + Application.dataPath + "/../assetbundles/iphone-prefab-box.assetbundle";
		Debug.Log(strPath);
		www = new WWW(strPath);
		yield return www;
        /* TODO: change the name "Box" to match the name of your prefab that you compiled into the assetbundle on step 1. */
		AssetBundleRequest request = www.assetBundle.LoadAsync("Box", typeof(GameObject));
		yield return request;
		Instantiate(request.asset, new Vector3(3f, 0f, 0f), Quaternion.identity);
		Instantiate(request.asset, new Vector3(0f, 0f, 0f), Quaternion.identity);
		Instantiate(request.asset, new Vector3(-3f, 0f, 0f), Quaternion.identity);
		www.assetBundle.Unload(true);
	}
        void Update() { }
}

NOTE: Make sure to change the TODO line in the code to match your file name! If you don’t do this, it might not work!

We’re nearly done! Drag your Example.cs script onto the GameObject you created earlier. Here is how my project looks:

Build Time

We’re now going to build this codebase so that you can run it on your iOS device for testing. I’m going to skip the painful steps of creating a code signing certificate and all that jazz. We’re instead going to focus on how to get the actual assetbundle onto your iOS device in an area that will be accessible to the “Application.dataPath” call in your code!

1. Build the Unity project for iOS.
2. XCode should automatically open.
3. Within XCode you are going to need to tell it about your assetbundles folder! Open up finder or explorer and locate the assetbundles folder that was generated by the UnityEditor script we discussed earlier.
4. Drag and drop the assetbundles folder into XCode at the root of the iOS project. (You should see a prompt as follows)

5. Click “Finish”.

This will then look something like this:

6. Build the code using XCode and run it on your iOS device.

If everything works properly, you should see one of the prefabs instantiate on your iOS screen. That is all there is to it! I think you’ll find that this isn’t as bad as one might imagine, but it does require some serious fishing around in code examples and heavily crawling the Unity forums. The only way I figured this out was by trial and error, heavy amounts of Debug.Log calls and 3 pots of coffee. I’m now going to go and grind my teeth on my level editor.

Edit: I have one of my original forum posts here as well regarding this issue. Unity3D Forum

, , , , , , ,

19 Comments