Why are UVs wrong when building for iPhone?

I am making a tile puzzle game. Each tile is made of three segments, and I am changing their colours on the fly by setting UVs in code. (The mesh is built from scratch in code too). At Awake() I am filling an array with hard-coded UV settings, and then a ChangeColor(int,int,int) method returns a Vector2 which goes straight into the mesh’s uv settings. It is working fine in the editor, but when I build to test on my iPhone 4, the UVs get screwed around. I’ve attached a picture to illustrate. I’ve tried changing the import settings on the texture, and changing the shader. Same result. Is this a known problem? Am I doing something wrong?

Here’s the code that handles the mesh:
(By the way TriPos.height is a constant - the height of an equilateral triangle)

using UnityEngine;
using System.Collections;

public class TileData : MonoBehaviour {
// Holds info on UVs for all colors, trangled or not. Only a mono so it can init on its own
	
	// Tile Prefab
	//---------------------------------------------------------
	public GameObject TilePrefab;
	public static GameObject Prefab;
	
	// Arrays to hold the information
	//---------------------------------------------------------
	private static Vector3[] verts;
	private static int[] tris;
	private static Vector2[,,] uvs; // for each color, for locked or not, for each vertex
	public static readonly int colorCount = 9;

	// Store hard coded data into arrays
	//---------------------------------------------------------
	void Awake () {
		
		// Static access to prefab
		Prefab = TilePrefab;
		
		// Set Verts
		verts = new Vector3[9];
		verts[0] = Vector3.zero;
		verts[1] = new Vector3( 0, TriPos.Height*(2f/3f), 0 );
		verts[2] = new Vector3( 0.5f, -TriPos.Height/3f, 0 );
		verts[3] = verts[0];
		verts[4] = verts[2];
		verts[5] = new Vector3( -0.5f, -TriPos.Height/3f, 0 );
		verts[6] = verts[0];
		verts[7] = verts[5];
		verts[8] = verts[1];
		
		// Set Tris
		tris = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
		
		// Set UVs for all colors, for unlocked/locked
		// ??? needs to be sorted out
		float h = TriPos.Height;
		uvs = new Vector2[colorCount,2,3];
		
		// color 0, locked
		uvs[0,1,0] = new Vector2(0.25f, h/3f);
		uvs[0,1,1] = new Vector2(0.25f, 0);
		uvs[0,1,2] = new Vector2(0,     h/2f);
		// color 1, locked
		uvs[1,1,0] = uvs[0,1,0];
		uvs[1,1,1] = uvs[0,1,2];
		uvs[1,1,2] = new Vector2(0.5f,  h/2f);
		// color 2, locked
		uvs[2,1,0] = uvs[0,1,0];
		uvs[2,1,1] = uvs[1,1,2];
		uvs[2,1,2] = uvs[0,1,1];
		
		// color 3, locked
		uvs[3,1,0] = new Vector2(0.5f,  h/6f);
		uvs[3,1,1] = uvs[0,1,1];
		uvs[3,1,2] = uvs[1,1,2];
		// color 4, locked
		uvs[4,1,0] = uvs[3,1,0];
		uvs[4,1,1] = uvs[3,1,2];
		uvs[4,1,2] = new Vector2(0.75f,  0);
		// color 5, locked
		uvs[5,1,0] = uvs[3,1,0];
		uvs[5,1,1] = uvs[4,1,2];
		uvs[5,1,2] = uvs[3,1,1];
		
		// color 6, locked
		uvs[6,1,0] = new Vector2(0.75f,  h/3f);
		uvs[6,1,1] = uvs[4,1,2];
		uvs[6,1,2] = uvs[4,1,1];
		// color 7, locked
		uvs[7,1,0] = uvs[6,1,0];
		uvs[7,1,1] = uvs[6,1,2];
		uvs[7,1,2] = new Vector2(1,  h/2f);
		// color 8, locked
		uvs[8,1,0] = uvs[6,1,0];
		uvs[8,1,1] = uvs[7,1,2];
		uvs[8,1,2] = uvs[6,1,1];
		
		// Mirror along y==h/2 line for locked
		for ( int c=0; c<colorCount; c++ ) {
			for ( int v=0; v<3; v++ ) {
				Vector2 untr = uvs[c,1,v];
				uvs[c,0,v] = new Vector2( untr.x, h - untr.y);
			}
		}
	}
	
	// Get Verts from stored arrays
	//---------------------------------------------------------
	public static Vector3[] GetVerts() {
		return verts;
	}
	
	// Get Tris from stored arrays
	//---------------------------------------------------------
	public static int[] GetTris() {
		return tris;
	}
	
	// Get UVs from stored arrays
	//---------------------------------------------------------
	public static Vector2[] GetUVs( int c0, int c1, int c2, bool locked ) {
		c0 = c0 % colorCount; c1 = c1 % colorCount; c2 = c2 % colorCount;
		int tr = locked ? 1 : 0;
		Vector2[] u = new Vector2[9];
		u[0] = uvs[c0,tr,0];
		u[1] = uvs[c0,tr,1];
		u[2] = uvs[c0,tr,2];
		u[3] = uvs[c1,tr,0];
		u[4] = uvs[c1,tr,1];
		u[5] = uvs[c1,tr,2];
		u[6] = uvs[c2,tr,0];
		u[7] = uvs[c2,tr,1];
		u[8] = uvs[c2,tr,2];
		return u;
	}
}

And here’s what the texture looks like:

9173-tile.jpg

After some diagnostics, I’ve discovered the exact cause of the problem. A 3D array of Vector2s goes awry when built for iPhone: each Vector2 seems to use 2 array spaces - the index position for Vector2.x and the one after for Vector2.y! It can be grasped more easily using this script - attach it to an Empty in an otherwise empty scene. Then run it in the editor and build for the device. I’ve attached the two outputs as screen grabs below.

The script stores a sequence of Vector2s in a 3d array, retrieves each just after storing (works fine), then retrieves them all after all have been stored (the Y of each has been overwritten by the X of the subsequent one on the device). It prints the Vector2s three different ways, to test if it is a formatting problem (it isn’t).

I don’t know if this occurs for 2d arrays as well, or for Vector3s as well - but it’s worth looking into. I think this is worth reporting as a bug. I fixed it for my game by splitting the 3d array of Vector2s into 2 x 3d arrays of floats (one for x, one for y). I tried a 4d array at first, but then discovered another issue, namely that 4d arrays work fine in the editor but cause fatal error on the device!

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Vec2Problem : MonoBehaviour {
	
	public List<string> msgs;
	
	// Use this for initialization
	void Start () {
		MakeV2s ();
	}
	
	private void MakeV2s() {
		
		int width = 2, height = 2, depth = 2;
		Vector2[,,] vecs = new Vector2[width,height,depth];
		float a = 0;
		float b = 0;
		
		msgs.Add ( "STORE VECTOR2s IN 3D BUILT-IN ARRAY:" );		
		for ( int x = 0; x < width; x++ ) {
			for ( int y = 0; y < height; y++ ) {
				for ( int z = 0; z < depth; z++ ) {
					
					Vector2 v = new Vector2(a,b);
					string s1 = string.Format ("RAW Vector2:        ",x,y,z);
					s1+= v.ToString () + " / " + v.ToString ("F3") + " / (" + v.x.ToString ("F3") + ", " + v.y.ToString ("F3") + ")";
					msgs.Add (s1);
					
					vecs[x,y,z] = v;
					string s2 = string.Format ("             vecs[{0},{1},{2}]:    ",x,y,z);
					s2+= vecs[x,y,z].ToString () + " / " + vecs[x,y,z].ToString ("F3") + " / (" + vecs[x,y,z].x.ToString ("F3") + ", " + vecs[x,y,z].y.ToString ("F3") + ")";
					msgs.Add (s2);
					
					Vector2 v2 = vecs[x,y,z];
					string s3 = string.Format ("             Vector2:        ",x,y,z);
					s3+= v2.ToString () + " / " + v2.ToString ("F3") + " / (" + v2.x.ToString ("F3") + ", " + v2.y.ToString ("F3") + ")";
					msgs.Add (s3);
					
//					msgs.Add ("");
					
					a += 0.279f;
					b += 0.131f;
				}
			}
		}
		
		msgs.Add ("RETRIEVAL:");
		
		for ( int x = 0; x < width; x++ ) {
			for ( int y = 0; y < height; y++ ) {
				for ( int z = 0; z < depth; z++ ) {
					string s4 = string.Format ("vecs[{0},{1},{2}]:    ",x,y,z);
					s4+= vecs[x,y,z].ToString () + " / " + vecs[x,y,z].ToString ("F3") + " / (" + vecs[x,y,z].x.ToString ("F3") + ", " + vecs[x,y,z].y.ToString ("F3") + ")";
					msgs.Add (s4);

					Vector2 v3 = vecs[x,y,z];
					string s5 = string.Format ("   Vector2:        ",x,y,z);
					s5+= v3.ToString () + " / " + v3.ToString ("F3") + " / (" + v3.x.ToString ("F3") + ", " + v3.y.ToString ("F3") + ")";
					msgs.Add (s5);

//					msgs.Add ("");					
				}
			}
		}
		
		
	}


	void OnGUI() {
		
		string msgBlock = "";
		if (msgs==null) return;
		foreach( string s in msgs ) {
			msgBlock += s + "

";
}

		if (msgBlock != "")
			GUI.Label( new Rect( 2, 2, Screen.width, Screen.height ), msgBlock );
		
	}

}

Well, this could be a race condition bacause you setup your array in Awake but all your variables and getter methods are static. So i guess you use the static functions from another class. I guess the other class reads the data before Awake has been called.

If it’s static data you could use the static constructor of the TileData class

static TileData()
{
    // init
}

But keep in mind that the static constructor will be called when this class is accessed the first time, so if this class relies on other things, make sure they are available at the time this class get initialized.

That’s why a real Singleton pattern is better :wink: A singleton will ensure that the object is created and initialized when someone tries to use it.

Anyways, you should figure out your initialization order. Keep in mind that the order in which things are called can change in the built version because the internal order of the objects might change. As a general advice: Use Awake to initialize the class itself. Use Start to initialize everything else that relies on other classes.

edit
I took another look at your picture and code and i actually don’t believe that the arrays are uninitialized because:

  • An uninitialized array would be null or empty, so trying to access an element would either raise a null reference exception or an index out of bounds exception.
  • The UVs are actually (almost) the correct UVs.

First of all the only explanation i have is that you somehow “scramble” the whole UV array. As an example, in the 2nd row 4th column the right triangle quite clearly uses the center point of the left and right “locked” triangles and the center point of the middle “unlocked” triangle. That actually doesn’t make any sense.

In addition it seems some uv positions are slightly off (beside that they are all in the wrong order :wink: ). As example 1st row 3rd column bottom triangle. The 3 coordinates are actually the right ones but a little bit off and in the wrong order. It’s rotated to the right.

Next thing is the corruption have to be inside your 3 dimensional array. That’s simply because triangles which should have received the same color look exactly the same. I can’t say that for sure since i don’t know how you actually build the mesh, maybe there’s something wrong. However it’s hard to imagine anything that would make it work in the editor and fail on the device.

I’m really interested in what actually caused this strange behaviour.