Skip to content

Create a Jigsaw Puzzle Game in Unity

Jigsaw Puzzle Game

Section 3 – Create a Jigsaw Board from an Image

Download the assets needed for this tutorial from https://faramira.com/downloads/jigsaw/assets_to_dowmload.zip.

You can find the entire source code of this project in the GitHub repo. https://github.com/shamim-akhtar/jigsaw-puzzle/tree/main

View the Tutorial on YouTube

This tutorial is divided into four broad sections, with each containing one or more subsections. 

You are now reading the third section, Section 3 – Create a Jigsaw Board from an Image

In this third section, we will learn how to create an entire Jigsaw board from an image. In the previous section, we learned how to make a jigsaw tile from an existing image using the template Bézier curve. 

Jigsaw tiles generated by pressing the “F” key. These tiles are generated by applying flood fill in the region enclosed by the randomly chosen type (POS, NEG) Bézier curves on each direction (UP, RIGHT, DOWN and LEFT).

Here, in this section, we will apply that knowledge to create an entire Jigsaw board from an image.

The Jigsaw tiles from an image.

The Jigsaw Image

Right-click on the project window and create a new scene.

Name it Scene_JigsawBoard. 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 and name it BoardGen

Create a new script file named BoardGen and add it as a component to the BoardGen game object. 

Double-click and open BoardGen in Visual Studio or your favourite editor.

Before we create the Jigsaw tiles, we first want to display the image we will use to make our Jigsaw pieces. Our objective is to supply the name of the image as an input in Unity Editor. We will then load the image and add a 20-pixel padding to the image, typically to show it as a border or a frame.

We will create two game objects with SpriteRenderers. One of these two game objects will display an opaque version of the input picture, and the second game object will display a duplicate of this same image with transparency. We will call this the ghost image. We can decide the value of the transparency later; for now, we can put the transparency value at 10 per cent. The ghost version of the image will help provide a hint to the player while solving the puzzle. Later on, we will allow turning this ghost image on or off through a settings menu.

The Member Variables

Let’s add the necessary member variables.

We add a variable public string imageFilename. This variable will hold the filename of the image that will be used to generate the Jigsaw board.

We then add mBaseSpriteOpaque and mBaseSpriteTransparent of type Sprite. The first variable stores the base opaque sprite loaded from the image file. It will be used to create the jigsaw pieces of the board. The second variable stores the transparent version of the base sprite, which is used to make the ghosted view of the game board.

We also create two game objects, mGameObjectOpaque and mGameObjectTransparent, to hold the GameObject that will display the opaque sprite and the GameObject that will display the transparent sprite, respectively.

Finally, we add a variable called ghostTransparency to hold the transparency level of the ghosted pixels in the game board. The default value is set to 0.1, which represents 10% opacity.

public class BoardGen : MonoBehaviour
{
  private string imageFilename;
  Sprite mBaseSpriteOpaque;
  Sprite mBaseSpriteTransparent;

  GameObject mGameObjectOpaque;
  GameObject mGameObjectTransparent;

  public float ghostTransparency = 0.1f;
  // *** continued below **//
Code language: C# (cs)

The Load Base Texture Method

We now create a member function called the LoadBaseTexture. This function is responsible for loading the base texture from the provided image filename, adding padding to it, and creating a sprite from the modified texture. 

  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)

It first loads the texture from the image file using the SpriteUtils.LoadTexture with the given imageFilename. If the texture is not readable, it logs an error message and returns null.

It checks if the width and height of the loaded texture are multiples of Tile.tileSize. If not, it logs an error message and returns null.

It then creates a new texture with dimensions increased by twice the padding. The default colour of all pixels in the new texture is set to white.

After that, It iterates through each pixel of the original texture and copies its colour to the corresponding position in the new texture. The alpha value of each pixel is set to 1.0f.After copying the colours, it applies the changes to the new texture using the Apply method. Finally, it creates a sprite from the modified texture using SpriteUtils.CreateSpriteFromTexture2D and returns the created sprite.

The Start Method

We now implement The Start Method. The method begins by calling the LoadBaseTexture function to load the base opaque sprite from the image file specified by the imageFilename.

We then create a new empty GameObject and assign it to mGameObjectOpaque to hold the opaque sprite. Next, we add a SpriteRenderer component to this game object and set its sprite to the loaded opaque sprite.

We set the sorting layer for the opaque sprite to ensure it renders correctly with other objects in the scene.

After that, we create a transparent view of the base sprite by calling the CreateTransparentView function, which we will implement next. Similar to the opaque sprite, the function creates a new empty GameObject mGameObjectTransparent to hold the transparent sprite.

We then add a SpriteRenderer component to mGameObjectTransparent and set its sprite to the transparent sprite. We also set the sorting layer for the transparent sprite and hide the opaque GameObject by default.

Finally, we set the position of the main camera based on the dimensions of the opaque sprite to ensure a proper view of the game board using a function called SetCameraPosition, which we will implement later.

  void Start()
  {
    imageFilename = GameApp.Instance.GetJigsawImageName();

    mBaseSpriteOpaque = LoadBaseTexture();
    mGameObjectOpaque = new GameObject();
    mGameObjectOpaque.name = imageFilename + "_Opaque";
    mGameObjectOpaque.AddComponent<SpriteRenderer>().sprite = 
      mBaseSpriteOpaque;
    mGameObjectOpaque.GetComponent<SpriteRenderer>().sortingLayerName = 
      "Opaque";

    // we have not yet implemented CreateTransparentView.
    mBaseSpriteTransparent = CreateTransparentView(mBaseSpriteOpaque.texture);
    mGameObjectTransparent = new GameObject();
    mGameObjectTransparent.name = imageFilename + "_Transparent";
    mGameObjectTransparent.AddComponent<SpriteRenderer>().sprite = 
      mBaseSpriteTransparent;
    mGameObjectTransparent.GetComponent<SpriteRenderer>().sortingLayerName = 
      "Transparent";

    mGameObjectOpaque.gameObject.SetActive(false);

    SetCameraPosition(); // we have not yet implemented.
  }
Code language: C# (cs)

Create Transparent View Method

We then implement the CreateTransparentView method that we used in the Start method. This method in the script is responsible for creating a transparent view of the provided texture, effectively adding a ghosted view of the same image.

  Sprite CreateTransparentView(Texture2D tex)
  {
    Texture2D newTex = new Texture2D(
      tex.width,
      tex.height, 
      TextureFormat.ARGB32, 
      false);

    for(int x = 0; x < newTex.width; x++)
    {
      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 = ghostTransparency;
        }
        newTex.SetPixel(x, y, c);
      }
    }
    newTex.Apply();

    Sprite sprite = SpriteUtils.CreateSpriteFromTexture2D(
      newTex,
      0,
      0,
      newTex.width,
      newTex.height);
    return sprite;
  }
Code language: C# (cs)

First, we create a new texture with the same dimensions as the provided texture.

We then iterate through each pixel of the new texture. For each pixel, we retrieve the colour from the corresponding position in the original texture using the GetPixel method. We check if the current pixel is within the padding area by comparing its coordinates with the padding value. If the pixel is within the padding area, we adjust its alpha value to the specified ghostTransparency level, effectively making it transparent.

After changing the transparency, we set the colour of the current pixel in the new texture using the SetPixel method.

Once all pixel modifications are done, we apply the changes to the new texture using the Apply method.
Finally, we create a sprite from the modified texture using the SpriteUtils.CreateSpriteFromTexture2D and return the created sprite.

The Set Camera Position Method

We now implement the SetCameraPosition method, which adjusts the position and orthographic size of the main camera based on the dimensions of the opaque sprite. It positions the camera at the centre of the sprite’s texture, ensuring it’s appropriately distanced from the scene while setting the orthographic size to cover half of the sprite’s width, maintaining the aspect ratio. This method guarantees that the camera provides a suitable view of the game board, encompassing the entire opaque sprite within its viewport for player visibility and interaction.

  void SetCameraPosition()
  {
    Camera.main.transform.position = new Vector3(
      mBaseSpriteOpaque.texture.width / 2,
      mBaseSpriteOpaque.texture.height / 2, 
      -10.0f);
    Camera.main.orthographicSize = 
      mBaseSpriteOpaque.texture.width / 2;
  }Code language: JavaScript (javascript)

We will further modify this function later in the next section.

Go to the Unity editor, find the flower_12_8.jpg in the assets_to_dowmload and put it in a new folder called jigsaws within the Images folder. Ensure that you have checked read/write enable for this image.

Select the BoardGen game object from the hierarchy window and set the Image Filename field to Images/jigsaws/flower_12_8

Click Play. You should now be able to see the ghosted picture of the flower image as shown here.

The Transparent Image of the Jigsaw Board.

Create The Jigsaw Tiles

We shall now proceed with the creation of the jigsaw tiles. For this, we will add some more member variables. 

Open the file BoardGen again and continue adding the member variables integer numTilesX and numTilesY. These are public properties representing the number of tiles in the horizontal and vertical axes of the jigsaw puzzle, respectively. 

We then add a two-dimensional array of Tile objects representing the individual pieces of the jigsaw puzzle.

Similarly, we also add a two-dimensional array of GameObjects that represents the visual representations of the jigsaw puzzle tiles in the Unity scene.

Finally, we add a transform variable called parentForTiles. This public variable represents the parent transform under which the jigsaw puzzle tiles will be instantiated as child objects. This allows for flexibility in organising the scene hierarchy.

public class BoardGen : MonoBehaviour
{
  public string imageFilename;
  Sprite mBaseSpriteOpaque;
  Sprite mBaseSpriteTransparent;

  GameObject mGameObjectOpaque;
  GameObject mGameObjectTransparent;

  public float ghostTransparency = 0.1f;

  // Jigsaw tiles creation.
  public int numTileX { get; private set; }
  public int numTileY { get; private set; }

  Tile[,] mTiles = null;
  GameObject[,] mTileGameObjects= null;

  public Transform parentForTiles = null;

// *** continued below **//
Code language: C# (cs)

Create Game Object From Tile Method

We now move on to implement the methods. First of all, we will add the method CreateGameObjectFromTile. This function is designed to generate a GameObject representing a tile in a jigsaw puzzle based on the provided Tile object. 

  public static GameObject CreateGameObjectFromTile(Tile tile)
  {
    GameObject obj = new GameObject();

    obj.name = "TileGameObe_" + 
      tile.xIndex.ToString() + 
      "_" + 
      tile.yIndex.ToString();

    obj.transform.position = new Vector3(
      tile.xIndex * Tile.tileSize, 
      tile.yIndex * Tile.tileSize, 
      0.0f);

    SpriteRenderer spriteRenderer = 
      obj.AddComponent<SpriteRenderer>();

    spriteRenderer.sprite = 
      SpriteUtils.CreateSpriteFromTexture2D(
        tile.finalCut,
        0,
        0,
        Tile.padding * 2 + Tile.tileSize,
        Tile.padding * 2 + Tile.tileSize);

    BoxCollider2D box = obj.AddComponent<BoxCollider2D>();
    return obj;
  }
Code language: C# (cs)

We begin by creating a new GameObject instance and assign it a name indicating its position within the grid. The position of the GameObject is then set using the xIndex and yIndex properties of the Tile, taking into account the tileSize. A SpriteRenderer component is added to the GameObject to render the visual representation of the tile, utilising a sprite created from the finalCut texture of the tile.

Additionally, we add a BoxCollider2D component to facilitate the picking and selection of the tiles. Finally, the function returns the generated GameObject. This function encapsulates the process of creating visual representations of tiles, making it easier for us to manage and manipulate them within the Unity environment.

The Create Jigsaw Tiles Method

We then create the CreateJigsawTiles function. This function is responsible for generating the jigsaw puzzle tiles based on the opaque base texture.

  void CreateJigsawTiles()
  {
    Texture2D baseTexture = mBaseSpriteOpaque.texture;
    numTileX = baseTexture.width / Tile.tileSize;
    numTileY = baseTexture.height / Tile.tileSize;

    mTiles = new Tile[numTileX, numTileY];
    mTileGameObjects = new GameObject[numTileX, numTileY];

    for(int i = 0; i < numTileX; i++)
    {
      for(int j = 0; j < numTileY; j++)
      {
        mTiles[i, j] = CreateTile(i, j, baseTexture);
        mTileGameObjects[i, j] = 
          CreateGameObjectFromTile(mTiles[i, j]);
        if(parentForTiles != null)
        {
          mTileGameObjects[i, j].transform.SetParent(
            parentForTiles);
        }
      }
    }
  }
Code language: C# (cs)

We begin by obtaining the base texture from the opaque sprite. Then, we calculate the number of tiles in both the horizontal and vertical axes by dividing the width and height of the base texture by the tileSize.

Next, we initialise arrays mTiles and mTileGameObjects to store Tile objects and corresponding GameObject instances for each tile position. We iterate through each tile position, creating a Tile object using the CreateTile function, which we are going to create next.

After that, we create a game object using the CreateGameObjectFromTile function. If a parent transform for the tiles is specified, we set the parent transform of each GameObject.

The Create Tile Method

Before we start with the CreateTile method, let’s try to understand the basic requirements of creating the Jigsaw pieces. For the provided image, we will mark the image into rows and columns with each tile of size tileSize.

Now, for all tiles that are on the left edge, or for column 0, we will need to have the curve type in Direction.LEFT to be NONE. That means we will have a straight line for the left direction for all tiles on the left edge.

For the RIGHT, UP and DOWN directions, we will randomly choose either the POS or the NEG curve type and apply.

Similarly, all tiles on the last column will have the RIGHT curve type as NONE.

All tiles on the top row will have the UP curve type as NONE.

And all tiles on the bottom row will have the DOWN operation as NONE.

Since we will iterate the 2d array of the image tiles from left bottom to right top, we will follow the following rule.

For the bottom and leftmost tile, we will set the curve types for LEFT and DOWN directions as straight lines.

For RIGHT and TOP directions, we will randomise and choose either POS or NEG curve type.

For all other internal tiles of index i and j of column i and row j, we will check the tile on the left, represented by column i – 1, for its RIGHT curve type and the tile below, represented by row j – 1, for its UP curve type. We will then apply the opposite of this curve type for the tile i and j’s LEFT and BOTTOM directions, respectively. We will need to do this so that the tiles fit in once cut out based on the template Bézier curve.

With this knowledge, we now implement the CreateTile method. The CreateTile method is responsible for generating a Tile object at the specified position i, j in the jigsaw puzzle grid, based on the provided baseTexture.

  Tile CreateTile(int i, int j, Texture2D baseTexture)
  {
    Tile tile = new Tile(baseTexture);
    tile.xIndex = i;
    tile.yIndex = j;

    // Left side tiles.
    if (i == 0)
    {
      tile.SetCurveType(Tile.Direction.LEFT, 
        Tile.PosNegType.NONE);
    }
    else
    {
      // We have to create a tile that has LEFT direction opposite curve type.
      Tile leftTile = mTiles[i - 1, j];
      Tile.PosNegType rightOp = 
        leftTile.GetCurveType(Tile.Direction.RIGHT);

      tile.SetCurveType(Tile.Direction.LEFT, 
        rightOp == Tile.PosNegType.NEG ?
        Tile.PosNegType.POS : Tile.PosNegType.NEG);
    }

    // Bottom side tiles
    if (j == 0)
    {
      tile.SetCurveType(Tile.Direction.DOWN, 
        Tile.PosNegType.NONE);
    }
    else
    {
      Tile downTile = mTiles[i, j - 1];
      Tile.PosNegType upOp = 
        downTile.GetCurveType(Tile.Direction.UP);

      tile.SetCurveType(Tile.Direction.DOWN, 
        upOp == Tile.PosNegType.NEG ?
        Tile.PosNegType.POS : Tile.PosNegType.NEG);
    }

    // Right side tiles.
    if (i == numTileX - 1)
    {
      tile.SetCurveType(Tile.Direction.RIGHT, 
        Tile.PosNegType.NONE);
    }
    else
    {
      float toss = Random.Range(0f, 1f);
      if(toss < 0.5f)
      {
        tile.SetCurveType(Tile.Direction.RIGHT, 
          Tile.PosNegType.POS);
      }
      else
      {
        tile.SetCurveType(Tile.Direction.RIGHT, 
          Tile.PosNegType.NEG);
      }
    }

    // Up side tile.
    if(j == numTileY - 1)
    {
      tile.SetCurveType(Tile.Direction.UP,
        Tile.PosNegType.NONE);
    }
    else
    {
      float toss = Random.Range(0f, 1f);
      if (toss < 0.5f)
      {
        tile.SetCurveType(Tile.Direction.UP, 
          Tile.PosNegType.POS);
      }
      else
      {
        tile.SetCurveType(Tile.Direction.UP, 
          Tile.PosNegType.NEG);
      }
    }

    tile.Apply();
    return tile;
  }
Code language: C# (cs)

We create a new Tile object using the provided baseTexture as a parameter. We set the xIndex and yIndex properties of the tile to the values i and j, respectively, indicating its position within the grid. For left-side tiles, if the tile is at the leftmost edge (i equal to 0), we set its left curve type to NONE.

Otherwise, we determine the curve type of the left side based on the curve type of the tile on its left. If the left tile’s right curve type is NEG, we set the current tile’s left curve type to POS, and vice versa.

For bottom side tiles, if the tile is at the bottom most edge (j equal to 0), we set its bottom curve type to NONE. Otherwise, we determine the curve type of the bottom side based on the curve type of the tile below it.

Similarly, for right and top side tiles, if the tile is at the rightmost or topmost edge, respectively, we set its curve type to NONE. Otherwise, we randomly assign a positive or negative curve type for the right and top sides.

For the right and top sides, if the tile is not at the edge, we randomly decide whether the curve type should be POS or NEG using Random dot Range.

Finally, we apply the changes made to the tile’s curve types using the Apply method. Calling this method will finalise the finalCut image with flood fill to get the desired jigsaw tile.

We then return the generated Tile object.

Now go to the Start method and add the function call to CreateJigsawTiles

  void Start()
  {
    mBaseSpriteOpaque = LoadBaseTexture();
    mGameObjectOpaque = new GameObject();
    mGameObjectOpaque.name = imageFilename + "_Opaque";
    mGameObjectOpaque.AddComponent<SpriteRenderer>().sprite = mBaseSpriteOpaque;
    mGameObjectOpaque.GetComponent<SpriteRenderer>().sortingLayerName = "Opaque";

    mBaseSpriteTransparent = CreateTransparentView(mBaseSpriteOpaque.texture);
    mGameObjectTransparent = new GameObject();
    mGameObjectTransparent.name = imageFilename + "_Transparent";
    mGameObjectTransparent.AddComponent<SpriteRenderer>().sprite = mBaseSpriteTransparent;
    mGameObjectTransparent.GetComponent<SpriteRenderer>().sortingLayerName = "Transparent";

    mGameObjectOpaque.gameObject.SetActive(false);

    SetCameraPosition();

    // Create the Jigsaw tiles.
    CreateJigsawTiles();
  }
Code language: JavaScript (javascript)

Go to the Unity editor. Click Play. You should now be able to see all the jigsaw pieces created from the image.

As an enhancement, we will show the animated effect of creating the Jigsaw tiles. To do so, we will copy and paste the CreateJigsawTiles method and convert it to a coroutine. Change the name of this newly copied function to Coroutine_CreateJigsawTiles with a return type of IEnumerator. 

IEnumerator Coroutine_CreateJigsawTiles()
  {
    Texture2D baseTexture = mBaseSpriteOpaque.texture;
    numTileX = baseTexture.width / Tile.tileSize;
    numTileY = baseTexture.height / Tile.tileSize;

    mTiles = new Tile[numTileX, numTileY];
    mTileGameObjects = new GameObject[numTileX, numTileY];

    for (int i = 0; i < numTileX; i++)
    {
      for (int j = 0; j < numTileY; j++)
      {
        mTiles[i, j] = CreateTile(i, j, baseTexture);
        mTileGameObjects[i, j] = 
          CreateGameObjectFromTile(mTiles[i, j]);
        if (parentForTiles != null)
        {
          mTileGameObjects[i, j].transform.SetParent(
            parentForTiles);
        }

        yield return null;
      }
    }
  }
Code language: C# (cs)

We then add yield return null in the inner for loop. After that, go to the start method, comment off the CreateJigsaw call, and replace it with StartCoroutine with the input parameter as Coroutine_CreateJigsawTiles

  void Start()
  {
    mBaseSpriteOpaque = LoadBaseTexture();
    mGameObjectOpaque = new GameObject();
    mGameObjectOpaque.name = imageFilename + "_Opaque";
    mGameObjectOpaque.AddComponent<SpriteRenderer>().sprite = mBaseSpriteOpaque;
    mGameObjectOpaque.GetComponent<SpriteRenderer>().sortingLayerName = "Opaque";

    mBaseSpriteTransparent = CreateTransparentView(mBaseSpriteOpaque.texture);
    mGameObjectTransparent = new GameObject();
    mGameObjectTransparent.name = imageFilename + "_Transparent";
    mGameObjectTransparent.AddComponent<SpriteRenderer>().sprite = mBaseSpriteTransparent;
    mGameObjectTransparent.GetComponent<SpriteRenderer>().sortingLayerName = "Transparent";

    mGameObjectOpaque.gameObject.SetActive(false);

    SetCameraPosition();

    // Create the Jigsaw tiles.
    //CreateJigsawTiles();
    StartCoroutine(Coroutine_CreateJigsawTiles());
  }
Code language: JavaScript (javascript)

Go to Unity editor. Click Play. You should now be able to see the animated effect of all the jigsaw pieces created from the image.

Finally, we can now create a new empty game object named Tiles in the BoardGen game object. Then, we drag and drop this game object to the parent for tiles transform. This will help us to store all the jigsaw tiles under this transform, allowing us to organise our tiles in one transform.

With this, we conclude Section 3: Create a Jigsaw Board from an Image.

In the next section, we will apply tile movement using click and drag. We will also implement a custom depth sorting so that the clicked tiles are in focus. Finally, we will implement the User Interface and the game logic of a Jigsaw puzzle game.

Pages: 1 2 3 4 5

6 thoughts on “Create a Jigsaw Puzzle Game in Unity”

    1. Hello Akshit. It’s good to hear that you found the tutorials helpful. I plan to complete the remaining part of the tutorial but couldn’t find the time to complete it. The remaining portions are loading and saving the game, the state machine to control the game states. The codes for this is available on my GitHub https://github.com/shamim-akhtar/jigsaw-puzzle repo.
      I intend to complete the remaining part of the tutorial by November.

  1. Hi!! Thanks for the tutorial. I would like to ask how to make to work with Render Texture?
    I would like to take the image for the puzzle from Render Texture.

Leave a Reply

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