(For more resources related to this topic, see here.)
There is still a lot of black in the background and as the game has a space theme, let's add some stars in there. The way we'll do this is to add a sphere that we can map the stars texture to, so click on Game Object | Create Other | Sphere, and position it at X: 0, Y: 0, Z: 0. We also need to set the size to X: 100, Y: 100, Z: 100. Drag the stars texture, located at Textures/stars, on to the new sphere that we created in our scene. That was simple, wasn't that? Unity has added the texture to a material that appears on the outside of our sphere while we need it to show on the inside. To fix it, we are going to reverse the triangle order, flip the normal map, and flip the UV map with C# code. Right-click on the Scripts folder and then click on Create and select C# Script. Once you click on it, a script will appear in the Scripts folder; it should already have focus and be asking you to type a name for the script, call it SkyDome. Double-click on the script in Unity and it will open in MonoDevelop. Edit the Start method, as shown in the following code:
void Start () {
// Get a reference to the mesh
MeshFilterBase MeshFilter = transform.GetComponent("MeshFilter")
as MeshFilter;
Mesh mesh = BaseMeshFilter.mesh;
// Reverse triangle winding
int[] triangles = mesh.triangles;
int numpolies = triangles.Length / 3;
for(int t = 0;t <numpolies; t++)
{
Int tribuffer = triangles[t * 3];
triangles[t * 3] = triangles[(t * 3) + 2];
triangles[(t * 3) + 2] = tribuffer;
}
// Read just uv map for inner sphere projection
Vector2[] uvs = mesh.uv;
for(int uvnum = 0; uvnum < uvs.Length; uvnum++)
{
uvs[uvnum] = new Vector2(1 - uvs[uvnum].x, uvs[uvnum].y);
}
// Read just normals for inner sphere projection
Vector3[] norms = mesh.normals;
for(int normalsnum = 0; normalsnum < norms.Length; normalsnum++)
{
[ 69 ]
norms[normalsnum] = -norms[normalsnum];
}
// Copy local built in arrays back to the mesh
mesh.uv = uvs;
mesh.triangles = triangles;
mesh.normals = norms;
}
The breakdown of the code as is follows:
Click and drag this script onto your sphere GameObject and test your scene. You should now see something like the following screenshot:
Now that the game is looking better, we can add some more content in to it. Luckily the jagged array we created earlier easily supports adding more levels. Levels can be any size, even with variable column heights per row. Double-click on the Sokoban script in the Project panel and switch over to MonoDevelop. Find levels array and modify it to be as follows:
// Create the top array, this will store the level arrays
int[][][] levels =
{
// Create the level array, this will store the row array
new int [][] {
// Create all row array, these will store column data
new int[] {1,1,1,1,1,1,1,1},
new int[] {1,0,0,1,0,0,0,1},
new int[] {1,0,3,3,0,3,0,1},
new int[] {1,0,0,1,0,1,0,1},
new int[] {1,0,0,1,3,1,0,1},
new int[] {1,0,0,2,2,2,2,1},
new int[] {1,0,0,1,0,4,1,1},
new int[] {1,1,1,1,1,1,1,1}
},
// Create a new level
new int [][] {
new int[] {1,1,1,1,0,0,0,0},
new int[] {1,0,0,1,1,1,1,1},
new int[] {1,0,2,0,0,3,0,1},
new int[] {1,0,3,0,0,2,4,1},
new int[] {1,1,1,0,0,1,1,1},
new int[] {0,0,1,1,1,1,0,0}
},
// Create a new level
new int [][] {
new int[] {1,1,1,1,1,1,1,1},
new int[] {1,4,0,1,2,2,2,1},
new int[] {1,0,0,3,3,0,0,1},
new int[] {1,0,3,0,0,0,1,1},
new int[] {1,0,0,1,1,1,1},
new int[] {1,0,0,1},
new int[] {1,1,1,1}
}
};
The preceding code has given us two extra levels, bringing the total to three. The layout of the arrays is still very visual and you can easily see the level layout just by looking at the arrays.
Our BuildLevel, CheckIfPlayerIsAttempingToMove and MovePlayer methods only work on the first level at the moment, let's update them to always use the users current level. We'll have to store which level the player is currently on and use that level at all times, incrementing the value when a level is finished. As we'll want this value to persist between plays, we'll be using the PlayerPrefs object that Unity provides for saving player data. Before we get the value, we need to check that it is actually set and exists; otherwise we could see some odd results.
Start by declaring our variable for use at the top of the Sokoban script as follows:
int currentLevel;
Next, we'll need to get the value of the current level from the PlayerPrefs object and store it in the Awake method. Add the following code to the top of your Awake method:
if (PlayerPrefs.HasKey("currentLevel")) {
currentLevel = PlayerPrefs.GetInt("currentLevel");
} else {
currentLevel = 0;
PlayerPrefs.SetInt("currentLevel", currentLevel);
}
Here we are checking if we have a value already stored in the PlayerPrefs object, if we do then use it, if we don't then set currentLevel to 0, and then save it to the PlayerPrefs object. To fix the methods mentioned earlier, click on Search | Replace. A new window will appear. Type levels[0] in the top box and levels[currentLevel] in the bottom one, and then click on All.
It's all well and good having three levels, but without a mechanism to move between them they are useless. We are going to add a check to see if the player has finished a level, if they have then increment the level counter and load the next level in the array. We only need to do the check at the end of every move; to do so every frame would be redundant.
We'll write the following method first and then explain it:
// If this method returns true then we have finished the level
boolhaveFinishedLevel () {
// Initialise the counter for how many crates are on goal
// tiles
int cratesOnGoalTiles = 0;
// Loop through all the rows in the current level
for (int i = 0; i< levels[currentLevel].Length; i++) {
// Get the tile ID for the column and pass it the switch
// statement
for (int j = 0; j < levels[currentLevel][i].Length; j++) {
switch (levels[currentLevel][i][j]) {
case 5:
// Do we have a match for a crate on goal
// tile ID? If so increment the counter
cratesOnGoalTiles++;
break;
default:
break;
}
}
}
// Check if the cratesOnGoalTiles variable is the same as the
// amountOfCrates we set when building the level
if (amountOfCrates == cratesOnGoalTiles) {
return true;
} else {
return false;
}
}
In the BuildLevel method, whenever we instantiate crate, we increment the amountOfCrates variable. We can use this variable to check if the amount of crates on goal tiles is the same as the amountOfCrates variable, if it is then we know we have finished the current level. The for loops iterate through the current level's rows and columns, and we know that 5 in the array is a crate on a goal tile. The method returns a Boolean based on whether we have finished the level or not. Now let's add the call to the method. The logical place would be inside the MovePlayer method, so go ahead and add a call to the method just after the pCol += tCol; statement.
As the method returns true or false, we're going to use it in an if statement, as shown in the following code:
// Check if we have finished the level
if (haveFinishedLevel()) {
Debug.Log("Finished");
}
The Debug.Log method will do for now, let's check if it's working. The solution for level one is on YouTube at http://www.youtube.com/watch?v=K5SMwAJrQM8&hd=1. Click on the play icon at the top-middle of the Unity screen and copy the sequence of moves in the video (or solve it yourself), when all the crates are on the goal tiles you'll see Finished in the Console panel.
The game now has some structure in the form of levels that you can complete and is easily expandable. If you wanted to take a break from the article, now would be a great time to create and add some levels to the game and maybe add some extra sound effects. All this hard work is for nothing if you can't make any money though, isn't it?
Further resources on this subject: