Create a Jigsaw Board from an Existing Image

Create a Jigsaw Board from an Existing Image

In this tutorial, we will learn how to create a Jigsaw board from an existing image.

This tutorial is the third tutorial from a larger tutorial on Creating a Jigsaw Puzzle Game in Unity.

In Part 2, Create a Jigsaw Tile from an Existing Image, we learnt about creating a Jigsaw tile from an existing image using the Bezier curve.

Find the GitHub repository of this tutorial at https://github.com/shamim-akhtar/jigsaw-puzzle.

Click to Play the WebGL version on your browser (mobile devices may not be able to play)

Contact me

Part 3: Create a Jigsaw Board from an Existing Image

Now that we know how to create a Jigsaw tile using the Bezier curve from an existing image, we will apply that knowledge to create an entire Jigsaw board from an image. Before we get into the details on the main topic, let’s start a scene and do some basic setup to test and display our features as we move along.

The Scene

Please create a new scene and name it JigsawBoardGen_Viz. This scene will be our sample scene to test out the various features that we will implement in the following sections.

  • Add an Empty GameObject to the scene. Call the game object as BoardGen.
  • Select BoardGen from the Hierarchy window, go to Inspector and add a New Script component. Name this script as BoardGen.
  • Double-click and open BoardGen.cs in Visual Studio or your favourite IDE.

Before we create the Jigsaw tiles, we first want to display the image we will use to create our Jigsaw. Our objective is to supply the name of the image as an input in Unity Editor. We will then load the image and do the following operations:

  • Add 20-pixel padding,
  • Create two game objects with SpriteRenderers,
  • One of these two game objects will display an opaque version of the input picture (a = 1.0f), and
  • The second game object will display the transparent (or Ghost) version of the input picture.

Let’s start writing some code for our BoardGen.cs. Add the following variables.

[SerializeField] [Tooltip("The image for the Jigsaw puzzle")] string ImageFilename; // The opaque sprite. Sprite mBaseSpriteOpaque; // The transparent (or Ghost sprite) Sprite mBaseSpriteTransparent; // The game object that holds the opaque sprite. // This should be SetActive to false by default. GameObject mGameObjectOpaque; // The game object that holds the transparent sprite. GameObject mGameObjectTransparent;
Code language: C# (cs)

Add a function that will read the image from the filename, create a new texture, add 20-pixel padding and create the opaque sprite.

Sprite LoadBaseTexture() { Texture2D tex = SpriteUtils.LoadTexture(ImageFilename); if (!tex.isReadable) { Debug.Log("Error: Texture is not readable"); return null; } if (tex.width % Tile.TileSize != 0 || tex.height % Tile.TileSize != 0) { Debug.Log("Error: Image must be of size that is multiple of <" + Tile.TileSize + ">"); return null; } // Add padding to the image. Texture2D newTex = new Texture2D( tex.width + Tile.Padding * 2, tex.height + Tile.Padding * 2, TextureFormat.ARGB32, false); // Set the default colour as white for (int x = 0; x < newTex.width; ++x) { for (int y = 0; y < newTex.height; ++y) { newTex.SetPixel(x, y, Color.white); } } // Copy the colours. for (int x = 0; x < tex.width; ++x) { for (int y = 0; y < tex.height; ++y) { Color color = tex.GetPixel(x, y); color.a = 1.0f; newTex.SetPixel(x + Tile.Padding, y + Tile.Padding, color); } } newTex.Apply(); Sprite sprite = SpriteUtils.CreateSpriteFromTexture2D( newTex, 0, 0, newTex.width, newTex.height); return sprite; }
Code language: C# (cs)

Add another function that will create the transparent image based on the opaque image.

Sprite CreateTransparentView() { Texture2D tex = mBaseSpriteOpaque.texture; // Add padding to the image. Texture2D newTex = new Texture2D( tex.width, tex.height, TextureFormat.ARGB32, false); //for (int x = Tile.Padding; x < Tile.Padding + Tile.TileSize; ++x) for (int x = 0; x < newTex.width; ++x) { //for (int y = Tile.Padding; y < Tile.Padding + Tile.TileSize; ++y) for (int y = 0; y < newTex.height; ++y) { Color c = tex.GetPixel(x, y); if (x > Tile.Padding && x < (newTex.width - Tile.Padding) && y > Tile.Padding && y < newTex.height - Tile.Padding) { c.a = 0.2f; } newTex.SetPixel(x, y, c); } } newTex.Apply(); Sprite sprite = SpriteUtils.CreateSpriteFromTexture2D( newTex, 0, 0, newTex.width, newTex.height); return sprite; }
Code language: C# (cs)

Use the above two functions and create two game objects. We will do that in the Start method.

void Start() { mBaseSpriteOpaque = LoadBaseTexture(); mGameObjectOpaque = new GameObject(); mGameObjectOpaque.name = ImageFilename + "_Opaque"; mGameObjectOpaque.AddComponent<SpriteRenderer>().sprite = mBaseSpriteOpaque; mGameObjectOpaque.GetComponent<SpriteRenderer>().sortingLayerName = "Opaque"; mBaseSpriteTransparent = CreateTransparentView(); mGameObjectTransparent = new GameObject(); mGameObjectTransparent.name = ImageFilename + "_Transparent"; mGameObjectTransparent.AddComponent<SpriteRenderer>().sprite = mBaseSpriteTransparent; mGameObjectTransparent.GetComponent<SpriteRenderer>().sortingLayerName = "Transparent"; }
Code language: C# (cs)

Download image01_8_5.jpg and place it in the Assets/Resources/Images folder. Select this image and set the properties as shown below. Make sure it is Read/Write enabled.

Go to Unity Editor, select BoardGen game object from the Hierarchy and set the Image Filename field to Images/image01_8_5 as shown below.

Select the Main Camera game object from the Hierarchy and set the following values in the Inspector.

Click Play. You should see the following.

If you disable the Images/image01_8_5_Opaque, you should see the transparent (Ghost) image as shown below.

Create Jigsaw Tiles

We will now create all the Jigsaw tiles for the given image. To do so, we will need to keep the following in mind.

We will need to ensure that the image pixel size is of the multiple of 100. For example, we have an image size of 800 by 500. Based on the size of the image, we will determine the number of tiles we can have on the Jigsaw board.

In BoardGen.cs, let’s add two variables that will store rows and columns.

public int NumTilesX { get; private set; } public int NumTilesY { get; private set; }
Code language: C# (cs)

We also add two two-dimensional arrays of Tile and GameObject. These two arrays will hold the tiles and the game object created for each tile, respectively.

Tile[,] mTiles = null; GameObject[,] mTileGameObjects = null;
Code language: C# (cs)

Next, we create a new method called CreateJigsawTiles.

In this function, we will implement the code to create the Jigsaw tiles. But before we proceed with implementing this function, let’s make some changes to our Tile.cs class.

Open Tile.cs file and

Add in the following two variables.

Then, in the constructor, we comment on the following section of code.

//if (tex.width != tileSizeWithPadding || tex.height != tileSizeWithPadding) //{ // Debug.Log("Unsupported texture dimension for Jigsaw tile"); // return; //}
Code language: JSON / JSON with Comments (json)

Finally, we add a static helper method that will create a GameObject for a Tile. We will name this function as CreateGameObjectFromTile.

public static GameObject CreateGameObjectFromTile(Tile tile) { // Create a game object for the tile. GameObject obj = new GameObject(); // Give a name that is recognizable for the GameObject. obj.name = "TileGameObj_" + tile.xIndex.ToString() + "_" + tile.yIndex.ToString(); // Set the position of this GameObject. // We will use the xIndex and yIndex to find the actual // position of the tile. We can get this position by multiplying // xIndex by TileSize and yIndex by TileSize. obj.transform.position = new Vector3(tile.xIndex * TileSize, tile.yIndex * TileSize, 0.0f); // Create a SpriteRenderer. SpriteRenderer spriteRenderer = obj.AddComponent<SpriteRenderer>(); // Set the sprite created with the FinalCut // texture of the tile to the SpriteRenderer spriteRenderer.sprite = SpriteUtils.CreateSpriteFromTexture2D( tile.FinalCut, 0, 0, Padding * 2 + TileSize, Padding * 2 + TileSize); // Add a box colliders so that we can handle // picking/selection of the Tiles. BoxCollider2D box = obj.AddComponent<BoxCollider2D>(); // add the TileMovement script component. TileMovement tm = obj.AddComponent<TileMovement>(); tm.tile = tile; return obj; }
Code language: C# (cs)

The comments within the code above are self-explanatory. However, You may see that we have used a TileMovement component added to the game object towards the bottom of the function. We have not implemented that yet. We are going to implement it now.

TileMovement, as the name suggests, is a script that allows handling of the tile selection and movement by holding and dragging the left mouse button.

Go ahead and create a new C# script and name it TileMovement. Double-click and open the file in your IDE and add the following code.

public class TileMovement : MonoBehaviour { public Tile tile { get; set; } private Vector3 GetCorrectPosition() { return new Vector3(tile.xIndex * 100.0f, tile.yIndex * 100.0f, 0.0f); } private Vector3 mOffset = new Vector3(0.0f, 0.0f, 0.0f); // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } void OnMouseDown() { if (EventSystem.current.IsPointerOverGameObject()) { return; } mOffset = transform.position - Camera.main.ScreenToWorldPoint( new Vector3(Input.mousePosition.x, Input.mousePosition.y, 0.0f)); } void OnMouseDrag() { if (EventSystem.current.IsPointerOverGameObject()) { return; } Vector3 curScreenPoint = new Vector3(Input.mousePosition.x, Input.mousePosition.y, 0.0f); Vector3 curPosition = Camera.main.ScreenToWorldPoint(curScreenPoint) + mOffset; transform.position = curPosition; } void OnMouseUp() { if (EventSystem.current.IsPointerOverGameObject()) { return; } float dist = (transform.position - GetCorrectPosition()).magnitude; if (dist < 20.0f) { transform.position = GetCorrectPosition(); } } }
Code language: C# (cs)

In the OnMouseUp method above, you might have noticed that we are doing a distance calculation. Basically, we are just trying to snap the tile to the correct location within a range of 20-pixel units. You can play with different values.

float dist = (transform.position - GetCorrectPosition()).magnitude; if (dist < 20.0f) { transform.position = GetCorrectPosition(); }
Code language: C# (cs)

Now, we move on to BoardGen.cs file and continue with the CreateJigsawTiles function.

void CreateJigsawTiles() { Texture2D baseTexture = mBaseSpriteOpaque.sprite.texture; NumTilesX = baseTexture.width / Tile.TileSize; NumTilesY = baseTexture.height / Tile.TileSize;
Code language: C# (cs)

First, we calculate the number of columns and rows into the variables NumTileX and NumTileY.

mTiles = new Tile[NumTilesX, NumTilesY]; mTileGameObjects = new GameObject[NumTilesX, NumTilesY];
Code language: JavaScript (javascript)

Then we initialize the two two-dimensional arrays of mTiles and mTileGameObjects.

After that, we enter the nested loop to create our Tiles. Remember that:

  • For all tiles on the first column will have the LEFT operation as NONE.
  • For all tiles on the last column will have the RIGHT operation as NONE.
  • For all tiles on the top column will have the UP operation as NONE.
  • For all tiles on the bottom column will have the DOWN operation as NONE.
  • For all other tiles, we need to check the tile on the left for its LEFT operation and the tile below for its DOWN operation. See the figure below for an illustration.

With this as our guideline, we implement the CreateJigsawTiles function

void CreateJigsawTiles() { Texture2D baseTexture = mBaseSpriteOpaque.texture; NumTilesX = baseTexture.width / Tile.TileSize; NumTilesY = baseTexture.height / Tile.TileSize; mTiles = new Tile[NumTilesX, NumTilesY]; mTileGameObjects = new GameObject[NumTilesX, NumTilesY]; for (int i = 0; i < NumTilesX; ++i) { for (int j = 0; j < NumTilesY; ++j) { Tile tile = new Tile(baseTexture); tile.xIndex = i; tile.yIndex = j; // Left side tiles if (i == 0) { tile.SetPosNegType(Tile.Direction.LEFT, Tile.PosNegType.NONE); } else { // We have to create a tile that has LEFT direction opposite operation // of the tile on the left's RIGHT direction operation. Tile leftTile = mTiles[i - 1, j]; Tile.PosNegType rightOp = leftTile.GetPosNegType(Tile.Direction.RIGHT); tile.SetPosNegType(Tile.Direction.LEFT, rightOp == Tile.PosNegType.NEG ? Tile.PosNegType.POS : Tile.PosNegType.NEG); } // Bottom side tiles if (j == 0) { tile.SetPosNegType(Tile.Direction.DOWN, Tile.PosNegType.NONE); } else { // We have to create a tile that has LEFT direction opposite operation // of the tile on the left's RIGHT direction operation. Tile downTile = mTiles[i, j - 1]; Tile.PosNegType rightOp = downTile.GetPosNegType(Tile.Direction.UP); tile.SetPosNegType(Tile.Direction.DOWN, rightOp == Tile.PosNegType.NEG ? Tile.PosNegType.POS : Tile.PosNegType.NEG); } // Right side tiles if (i == NumTilesX - 1) { tile.SetPosNegType(Tile.Direction.RIGHT, Tile.PosNegType.NONE); } else { float toss = Random.Range(0.0f, 1.0f); if (toss < 0.5f) { tile.SetPosNegType(Tile.Direction.RIGHT, Tile.PosNegType.POS); } else { tile.SetPosNegType(Tile.Direction.RIGHT, Tile.PosNegType.NEG); } } // Up side tiles if (j == NumTilesY - 1) { tile.SetPosNegType(Tile.Direction.UP, Tile.PosNegType.NONE); } else { float toss = Random.Range(0.0f, 1.0f); if (toss < 0.5f) { tile.SetPosNegType(Tile.Direction.UP, Tile.PosNegType.POS); } else { tile.SetPosNegType(Tile.Direction.UP, Tile.PosNegType.NEG); } } tile.Apply(); mTiles[i, j] = tile; // Create a game object for the tile. mTileGameObjects[i, j] = Tile.CreateGameObjectFromTile(tile); mTileGameObjects[i, j].transform.SetParent(transform); } } }
Code language: C# (cs)

Click Play. You should see the following.

You can also select and drag a tile.

Sorting of Tiles for Rendering

Till now, all our sprites are on the Default layer. We will need to provide a proper layer ordering to our Jigsaw sprites.

Click on the Layers button on the Unity Editor and select Edit Layers…

Select Sorting Layers as shown below.

Create the following new Sorting Layers

  • Opaque: We will use the Opaque layer for the opaque image of the Jigsaw. We will display this image when the user requests to see the image as a hint.
  • Transparent: We will use the Transparent layer for the ghost image of the Jigsaw.
  • DropShadow: We will use the DropShadow layer for our drop shadows of the tiles. We did not implement the drop shadows yet.
  • Tiles: We will use the Tiles layer for our Jigsaw tiles.

Let’s go ahead and set these layers to our Sprite Renderers.

First in the CreateGameObjectFromTile. The highlighted lines below (18-19)

public static GameObject CreateGameObjectFromTile(Tile tile) { // Create a game object for the tile. GameObject obj = new GameObject(); // Give a name that is recognizable for the GameObject. obj.name = "TileGameObj_" + tile.xIndex.ToString() + "_" + tile.yIndex.ToString(); // Set the position of this GameObject. // We will use the xIndex and yIndex to find the actual // position of the tile. We can get this position by multiplying // xIndex by TileSize and yIndex by TileSize. obj.transform.position = new Vector3( tile.xIndex * TileSize, tile.yIndex * TileSize, 0.0f); // Create a SpriteRenderer. SpriteRenderer spriteRenderer = obj.AddComponent<SpriteRenderer>(); // Set the sorting layer for the tile spriteRenderer.sortingLayerName = "Tiles"; // Set the sprite created with the FinalCut // texture of the tile to the SpriteRenderer spriteRenderer.sprite = SpriteUtils.CreateSpriteFromTexture2D( tile.FinalCut, 0, 0, Padding * 2 + TileSize, Padding * 2 + TileSize); // Add a box colliders so that we can handle // picking/selection of the Tiles. BoxCollider2D box = obj.AddComponent<BoxCollider2D>(); // add the TileMovement script component. TileMovement tm = obj.AddComponent<TileMovement>(); tm.tile = tile; return obj; }
Code language: C# (cs)

Then in BoardGen.cs

void Start() { mBaseSpriteOpaque = LoadBaseTexture(); mGameObjectOpaque = new GameObject(); mGameObjectOpaque.name = ImageFilename + "_Opaque"; mGameObjectOpaque.AddComponent<SpriteRenderer>().sprite = mBaseSpriteOpaque; mGameObjectOpaque.GetComponent<SpriteRenderer>().sortingLayerName = "Opaque"; mBaseSpriteTransparent = CreateTransparentView(); mGameObjectTransparent = new GameObject(); mGameObjectTransparent.name = ImageFilename + "_Transparent"; mGameObjectTransparent.AddComponent<SpriteRenderer>().sprite = mBaseSpriteTransparent; mGameObjectTransparent.GetComponent<SpriteRenderer>().sortingLayerName = "Transparent"; CreateJigsawTiles(); // Hide the mBaseSpriteOpaque game object. mGameObjectOpaque.gameObject.SetActive(false); }
Code language: C# (cs)

Runtime Sorting of Tiles

Besides setting the Sorting Layers, we will also need to sort the layers based on depth values. This is to facilitate the order of tiles when we click and select them. Sorting Layer and Render Order only allow the order of rendering. That is not sufficient for us as we also want to select the tile on top and not one hidden below.

To obtain a proper z sorting so that our Raycast works, we will need to set the z value of the tiles at runtime and change the value when we select a tile.

TilesSorting

Go ahead and add a new C# file and name it TilesSorting.

using System.Collections.Generic; using UnityEngine; // the facilitate sorting of tiles. public class TilesSorting { private List<SpriteRenderer> mSortIndices = new List<SpriteRenderer>(); public TilesSorting() { } }
Code language: C# (cs)

Add a new method to this class called Clear. This method will clear the List.

public void Clear() { mSortIndices.Clear(); }
Code language: C# (cs)

Add two more methods called Add and Remove. In the Add method, we take in a SpriteRenderer and add it to the list. After that, we call an internal private method called SetRenderOrder. We are going to implement this method after the following section of the code.

public void Add(SpriteRenderer ren) { mSortIndices.Add(ren); SetRenderOrder(ren, mSortIndices.Count); } public void Remove(SpriteRenderer ren) { mSortIndices.Remove(ren); for (int i = 0; i < mSortIndices.Count; ++i) { SetRenderOrder(mSortIndices[i], i + 1); } }
Code language: C# (cs)

In the Remove method, we take in a SpriteRenderer and remove it from the list. After that, we call the internal private method called SetRenderOrder to restore the render orders of other sprites in the list.

Now, we implement the internal private method SetRenderOrder.

private void SetRenderOrder(SpriteRenderer ren, int order) { // First we set the render order of sorting. ren.sortingOrder = order; // Then we set the z value so that selection/raycast // selects the top sprite. Vector3 p = ren.transform.position; p.z = -order / 10.0f; ren.transform.position = p; }
Code language: C# (cs)

In this method, we first set the sorting order of the sprite based on the integer input order. Then to facilitate proper picking and selection of tiles, we set the z value by dividing the order by a fixed constant value of 10.

Finally, we add another method named BringToTop. In this method, we bring the selected SpriteRenderer to the top.

public void BringToTop(SpriteRenderer ren) { Remove(ren); Add(ren); }
Code language: C# (cs)

Using TilesSorting

We have implemented the necessary functionality to sort tiles at run time using the TilesSorting class. We will now use it actually to apply runtime tiles sorting in our game. For this, open the Tiles.cs file and add in the following static variable.

public static TilesSorting TilesSorting { get; set; } = new TilesSorting();
Code language: C# (cs)

Then in the CreateGameObjectFromTile function, add the highlighted line.

public static GameObject CreateGameObjectFromTile(Tile tile) { // Create a game object for the tile. GameObject obj = new GameObject(); // Give a name that is recognizable for the GameObject. obj.name = "TileGameObj_" + tile.xIndex.ToString() + "_" + tile.yIndex.ToString(); // Set the position of this GameObject. // We will use the xIndex and yIndex to find the actual // position of the tile. We can get this position by multiplying // xIndex by TileSize and yIndex by TileSize. obj.transform.position = new Vector3( tile.xIndex * TileSize, tile.yIndex * TileSize, 0.0f); // Create a SpriteRenderer. SpriteRenderer spriteRenderer = obj.AddComponent<SpriteRenderer>(); // Set the sorting layer for the tile spriteRenderer.sortingLayerName = "Tiles"; // Set the sprite created with the FinalCut // texture of the tile to the SpriteRenderer spriteRenderer.sprite = SpriteUtils.CreateSpriteFromTexture2D( tile.FinalCut, 0, 0, Padding * 2 + TileSize, Padding * 2 + TileSize); // Add a box colliders so that we can handle // picking/selection of the Tiles. BoxCollider2D box = obj.AddComponent<BoxCollider2D>(); // add the TileMovement script component. TileMovement tm = obj.AddComponent<TileMovement>(); tm.tile = tile; Tile.TilesSorting.Add(spriteRenderer); return obj; }
Code language: C# (cs)

And then, finally, in TileMovement.cs file, amend the OnMouseDown function by adding the highlighted line.

void OnMouseDown() { if (EventSystem.current.IsPointerOverGameObject()) { return; } Tile.TilesSorting.BringToTop(mSpriteRenderer); mOffset = transform.position - Camera.main.ScreenToWorldPoint( new Vector3(Input.mousePosition.x, Input.mousePosition.y, 0.0f)); }
Code language: C# (cs)

And, we are done!.

Click Play. Select and move a tile; you should be able to reorder a tile based on its selection.

Jigsaw board using jigsaw created by Bezier curves

We have concluded our Part 3 – Create a Jigsaw Board from an Existing Image tutorial. In the next part of the tutorial, we will implement the game elements such as the UI, the win condition and the scoring.

Read My Other Tutorials

  1. Solving 8 puzzle problem using A* star search
  2. A Configurable Third-Person Camera in Unity
  3. Player Controls With Finite State Machine Using C# in Unity
  4. Finite State Machine Using C# Delegates in Unity
  5. Enemy Behaviour With Finite State Machine Using C# Delegates in Unity
  6. Augmented Reality – Fire Effect using Vuforia and Unity
  7. Implementing a Finite State Machine Using C# in Unity
  8. Solving 8 puzzle problem using A* star search in C++
  9. What Are C# Delegates And How To Use Them
  10. How to Generate Mazes Using Depth-First Algorithm

Leave a Reply

Your email address will not be published. Required fields are marked *