Skip to content

Create a Jigsaw Puzzle Game in Unity

Jigsaw Puzzle Game

Section 2 – Create a Jigsaw Tile from an Existing 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 second section, Section 2 – Create a Jigsaw Tile from an Existing Image

In this second section, we will learn how to create a Jigsaw tile from an existing image using the Bézier curve. In the first section, we learnt how to create a Bézier curve in Unity. In this section, we will create a template curve, which we will apply to cut an image to create our Jigsaw tile.

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. Right-click on the Project window and create a new scene called Scene_JigsawTile. This scene will be our sample scene to test out the various features that we will implement in the following sections.

The Template Bézier  Curve

First, let’s create a set of control points that will define our Bézier curve to cut out jigsaw tiles. After some experimentation, we found the set of control points below fits our jigsaw curves.


  public static readonly List<Vector2> templateControlPoints = new List<Vector2>()
  {
      new Vector2(0, 0),
      new Vector2(35, 15),
      new Vector2(47, 13),
      new Vector2(45, 5),
      new Vector2(48, 0),
      new Vector2(25, -5),
      new Vector2(15, -18),
      new Vector2(36, -20),
      new Vector2(64, -20),
      new Vector2(85, -18),
      new Vector2(75, -5),
      new Vector2(52, 0),
      new Vector2(55, 5),
      new Vector2(53, 13),
      new Vector2(65, 15),
      new Vector2(100, 0)
  };Code language: C# (cs)

Of course, you can experiment and create your own sets of control points that define your desired Bézier curve. These control points are for x from 0 to 100 and y from -20 to 20. 

The picture here shows these control points and the Bézier curve formed by these points. For the rest of this tutorial, we are going to use the above template Bézier curve.

Let’s go ahead and visualise this template curve. 

Right-click and create an empty game object. Name it TemplateBézierCurve. Create a new script, name it TemplateBézierCurve and attach it to this game object as a component. Double-click and open the script in Visual Studio or your favourite editor.

Copy and paste the code we created in Bézier _Viz in Section 1 of this tutorial. Copy all the codes except OnGUI and InsertNewControlPoint. We won’t be needing that functionality here.

Replace the variable public List<Vector2> controlPoints with public static readonly List<Vector2> TemplateControlPoints and use the control points shown here. 

Amend the Start method accordingly.

  void Start()
  {
    // Here we will create the actual lines.
    mLineRenderers = new LineRenderer[2];
    mLineRenderers[0] = CreateLine();
    mLineRenderers[1] = CreateLine();

    // Set the name of these lines to distinguish.
    mLineRenderers[0].gameObject.name = "LineRenderer_obj_0";
    mLineRenderers[1].gameObject.name = "LineRenderer_obj_1";

    // Now create the instances of the control points.
    for (int i = 0; i < templateControlPoints.Count; i++)
    {
      GameObject obj = Instantiate(PointPrefab, templateControlPoints[i], Quaternion.identity);
      obj.name = "ControlPoint_" + i.ToString();
      mPointGameObjects.Add(obj);
    }
  }
Code language: C# (cs)

Making the TemplateControlPoints list static and readonly provides several advantages in our context.

By marking the TemplateControlPoints list as readonly, we ensure that its contents cannot be modified after initialisation. This helps maintain the integrity of the control points throughout the execution of the program. Since it’s static, it will be shared among all instances of the TemplateBézier Curve class, ensuring consistency across multiple instances.

Since the TemplateControlPoints are marked as static, they are shared among all instances of the TemplateBézier Curve class. This means that each instance of the class doesn’t need to allocate memory for its copy of the control points. Instead, they all reference the same list, saving memory.

Since the control points are static, they are initialised only once when the class is loaded. Subsequent instances of the class do not need to reinitialise the control points, reducing overhead and improving performance.

Making the control points readonly ensures that they cannot be modified by multiple threads simultaneously, thus avoiding potential concurrency issues in multithreaded scenarios.

Being static, the TemplateControlPoints list can be accessed from other classes without needing an instance of TemplateBézier Curve. This will be convenient if other classes need to access the control points.

Go to Unity editor and associate the Point prefab. Then, set the LineWidth and LineWidthBézier to 0.5 and 1, respectively. 

Select the MainCamera game object and change the transform position x to 50 and Size to 50. This way, we can position the view to see our result better.

Click Play and run. You should now be able to see the template Bézier curve.

Once we have verified our template control points for the Bézier curve, we can now hide the TemplateBézier Curve game object as we do not need to show it now.

The Sprite Utility Functions

We will now create some utility functions for sprites. Create a new C# file and name it SpriteUtils. In this file, we will implement these utility functions for sprites. Double-click and open the file in Visual Studio.

We will create two static methods for working with sprites and textures. The first one is called the CreateSpriteFromTexture2D. This method will take a Texture2D object along with parameters specifying a rectangular region within that texture. It will then create a Sprite object based on that region using the Sprite.Create method. 

public static Sprite CreateSpriteFromTexture2D(
  Texture2D spriteTexture,
  int x,
  int y,
  int w,
  int h,
  float pixelsPerUnit = 1.0f,
  SpriteMeshType spriteType = SpriteMeshType.Tight)
{
  Sprite newSprite = Sprite.Create(
    spriteTexture,
    new Rect(x, y, w, h),
    new Vector2(0, 0),
    pixelsPerUnit,
    0,
    spriteType);
  return newSprite;
}Code language: PHP (php)

The PixelsPerUnit parameter will determine how many pixels in the texture correspond to one unit in world space. The spriteType parameter will specify the type of mesh generated for the sprite (defaulting to Tight). Finally, it returns the newly created sprite.

public static Texture2D LoadTexture(string resourcePath)
{
  Texture2D tex = Resources.Load<Texture2D>(resourcePath);
  return tex;
}Code language: PHP (php)

The second method is called the LoadTexture. This method loads a Texture2D object from a specified resource path using Unity’s Resources.Load method. It takes a string resourcePath as an input, which represents the path to the texture resource within the project’s Resources folder. It then returns the loaded texture.

These two methods will provide convenient utilities for loading textures and creating sprites from regions within those textures. These can be helpful in dynamically generating or manipulating sprites within our application.

Download the assets needed for this tutorial from https://faramira.com/downloads/jigsaw/assets_to_dowmload.zip.The content of this downloaded folder is as shown below.

The Image Tile

Next, we will load the image, where we will apply our template Bézier curve to create the jigsaw tiles. 

Open the Unity editor, right-click on the Projects window and create a new folder called Images in Resources. Use the sunflower_140 image that is available in the downloaded assets folder.

We will use this image to create our jigsaw tile. Go to Unity editor, select the image sunflower_140, go to the inspector, select Advanced and enable the Read/Write checkbox.

Right-click on the Hierarchy window and add an empty GameObject to the scene. Call the game object as TilesGen. Select TilesGen from the Hierarchy window, go to Inspector and add a New Script component. Name this script as TilesGen. Double-click and open TilesGen in Visual Studio or your favourite editor.

Add a public string variable named imageFilename. This variable will store the name of the image to which we want to apply our Bézier curve to create the jigsaw tile. We will also add another private variable of type Texture2D of name mTextureOriginal. This private Texture2D variable stores the original texture loaded from the image file.

public string imageFilename;
private Texture2D mTextureOriginal;
Code language: C# (cs)

Next, we create a method called CreateBaseTexture. This method is responsible for creating the base texture and displaying it as a sprite within the Unity scene. It loads the main image texture using the SpriteUtils.LoadTexture method, passing in the imageFilename. It checks if the loaded texture is readable. If not, it logs a message and returns, indicating that the texture cannot be used further. It adds a SpriteRenderer component to the current GameObject to display the sprite.

void CreateBaseTexture()
{
  mTextureOriginal = SpriteUtils.LoadTexture(imageFilename);
  if(!mTextureOriginal.isReadable)
  {
    Debug.Log("Texture is nor readable");
    return;
  }

  SpriteRenderer spriteRenderer = gameObject.AddComponent<SpriteRenderer>();
  mSprite = SpriteUtils.CreateSpriteFromTexture2D(
    mTextureOriginal,
    0,
    0,
    mTextureOriginal.width,
    mTextureOriginal.height);
  spriteRenderer.sprite = mSprite;
}
Code language: C# (cs)

It sets the sprite of the SpriteRenderer component using the SpriteUtils.CreateSpriteFromTexture2D method, passing in the loaded texture and defining the region to be used as the entire texture, from (0,0) to (mTextureOriginal.width and mTextureOriginal.height).Go to the Unity editor, select the TilesGen game object and set the image filename in the inspector to Images/sunflower_140.

Select the MainCamera game object and change the transform position x to 70, y to 70  and Size to 100. This way, we can position the view to see our result better.

Click Play and run. You should now be able to see the sunflower image as shown here.

We now have the image that we want to make into a jigsaw tile. We also have our template Bézier  control points that we will use to cut our image. 

We will standardise our Jigsaw tile to be of a regular square size of 140 by 140 pixels. The picture above shows one typical block of an image representing one Jigsaw tile area, with ABCD representing the square of 100 by 100 pixels. Next, we will apply our Bézier  curves to the four sides AB, BC, CD and DA. Also, remember that, for each side,  we will have to use our curves in two ways. One will be the usual way, and the other will be the reflected control points along the line. We thus end up with eight variations of the curve. 

Note that we do not have to recreate the Bézier  curves again and again to achieve the above. Instead, we only have to calculate the Bézier  curve once.

Then, we need to transform these points to get all the other points required for the 8 Bézier curves. For curves along the vertical line, we will swap the x and y coordinates.

To understand this, let’s visualise our Bézier  curves on the image.

For this, let’s create a new C# class named Tile. Right-click on the scripts folder and create a new script file named Tile. Double-click and open it in Visual Studio or your favourite editor. Remove the MonoBehavior, the Start and the Update methods. We are not using mono behaviour for this class. It is going to be a plain C# class.

We have 4 sides of the image. They are the UP, RIGHT, DOWN and LEFT. Let’s put these into an enumeration type called direction. For each side, we can either apply the normal curve, the inverse of it or not apply it at all. We won’t apply the cutting of the image with the Bézier  curve in cases where the image is on the sides. For each side, we thus get three options. We will represent these three options into another enumeration type called Pos Neg Type. 

public enum Direction
{
  UP, DOWN, LEFT, RIGHT,
}
Code language: C# (cs)

Go ahead and add this enumeration type to the Tile class. The three choices for this enumeration are POS, which is the standard curve; NEG, which is the negation of the curve; and NONE, which means not applying any curve at all.

public enum PosNegType
{
  POS,
  NEG,
  NONE,
}
Code language: C# (cs)

We will add a variable called padding. This variable represents an offset used for positioning the curves. It is an integer value, which determines how much the curves will be shifted from their default positions. For an image size of 140 by 140 pixels, the padding will be 20.

// The offset at which the curve will start.
// For an image of size 140 by 140 it will start at 20, 20.
//public Vector2Int mOffset = new Vector2Int(20, 20);
public static int padding = 20;Code language: C# (cs)

Then, we add the variable called the tileSize of type integer. This variable defines the size of the tile. It determines the dimensions of the tile used for calculations related to the positioning and scaling of curves.

// The size of our jigsaw tile.
public static int tileSize = 100;Code language: C# (cs)

After that, we add the mLineRenderers. This variable is a dictionary that stores LineRenderer objects based on direction and type. It associates each combination of direction and type with a LineRenderer, allowing for easy retrieval and management of LineRenderers for drawing curves. Finally, we add the variable BezCurve. This variable holds a list of Vector2 points that define a Bézier  curve. It is initialised with the points generated from a Bézier curve calculation using the BézierCurve.PointList2 method, which takes the template control points as input. BezCurve represents a pre-calculated Bézier  curve that will be utilised as a template for generating actual curves based on different parameters, such as direction and type, within the Tile class.

// The line renderers for all directions and types.
private Dictionary<(Direction, PosNegType), LineRenderer> mLineRenderers
  = new Dictionary<(Direction, PosNegType), LineRenderer>();Code language: C# (cs)

Then, we add the CreateLineRenderer function. It is a static method designed to streamline the process of generating LineRenderer components in Unity. Upon invocation, it should instantiate a new GameObject to serve as the container for the LineRenderer. Subsequently, it will add a LineRenderer component to this GameObject, configuring its properties such as start colour, end colour, start width, and end width based on the provided arguments. Additionally, it will also assign a material to the LineRenderer, which determines how the line will be rendered; in this case, it utilises a default sprite shader. Once all configurations are complete, the function will return the created LineRenderer, allowing for further customisation or immediate use within the Unity scene.

public static LineRenderer CreateLineRenderer(UnityEngine.Color color, float lineWidth = 1.0f)
{
  GameObject obj = new GameObject();
  LineRenderer lr = obj.AddComponent<LineRenderer>();

  lr.startColor = color;
  lr.endColor = color;
  lr.startWidth = lineWidth;
  lr.endWidth = lineWidth;
  lr.material = new Material(Shader.Find("Sprites/Default"));
  return lr;
}
Code language: C# (cs)

Now, we will create the constructor. We will also add a variable called mOriginalTexure, a private variable that stores the input texture. We won’t modify this texture.

// The original texture used to create the jigsaw tile.
private Texture2D mOriginalTexture;
public Tile(Texture2D texture)
{
  mOriginalTexture = texture;
}Code language: PHP (php)

Next, we implement the TranslatePoints method. We declare this method as public static thus allowing it to be accessed without an instance of the class. It takes two parameters: iList, a list of Vector2 points to be translated, and offset, a Vector2 representing the amount by which the points should be translated. Inside the method, we use a for loop that iterates over each point in the input list iList. For each point, the method adds the offset vector to it. The translation is performed directly on the points in the input list iList. This means that the original list is modified in place, and there’s no need to return a new list with translated points.

public static void TranslatePoints(List<Vector2> iList, Vector2 offset)
{
  for (int i = 0; i < iList.Count; i++)
  {
    iList[i] += offset;
  }
}
Code language: C# (cs)

Next, we implement the InvertY method. We declare this method as public static. The purpose of this function is to invert the y-coordinate of each point in a given list of 2D vectors. By iterating through the list, the method replaces each vector’s y-coordinate with its negation, effectively reflecting the points across the x-axis. This operation is performed in-place, directly modifying the original list.

public static void InvertY(List<Vector2> iList)
{
  for (int i = 0; i < iList.Count; i++)
  {
    iList[i] = new Vector2(iList[i].x, -iList[i].y);
  }
}Code language: PHP (php)

Next, we implement the SwapXY method. Similar to our previous two utility methods, we declare this method as public static too. This method will provide a simple means to interchange the x and y coordinates of each point within a given list of 2D vectors. By iterating through the list, the method replaces each vector’s x-coordinate with its y-coordinate and vice versa. 

public static void SwapXY(List<Vector2> iList)
{
  for (int i = 0; i < iList.Count; ++i)
  {
    iList[i] = new Vector2(iList[i].y, iList[i].x);
  }
}
Code language: C# (cs)

These three utility methods, namely, the TranslatePoints, the InvertY and the SwapXY methods will be used when we want to apply transformations to our template Bézier curve.

We then implement the CreateCurve method. This method will be responsible for creating the various types of curves from the template Bézier  curve by transforming the points. The transformation will be done by the above three utility functions we just implemented. In the method, we initialise some local variables by caching the offset x and y and the tile size.

We then copy the template Bézier into a  new list. We will then apply our transformation to these points based on the direction and the type of the curve. The method then enters a switch case statement based on the direction parameter.

public List<Vector2> CreateCurve(Direction dir, PosNegType type)
{
  int padding_x = padding;// mOffset.x;
  int padding_y = padding;// mOffset.y;
  int sw = tileSize;
  int sh = tileSize;

  List<Vector2> pts = new List<Vector2>(BezCurve);
  switch (dir)
  {
    case Direction.UP:
      break;
    case Direction.RIGHT:
      break;
    case Direction.DOWN:
      break;
    case Direction.LEFT:
      break;
  }
  return pts;
}
Code language: C# (cs)

If direction is UP, it then checks the type. If type is POS, it translates the points by adding padding_x to x-coordinates and padding_y + tilesize to y-coordinates.

If type is NEG, it inverts the points along the y-axis, then translates it as before.

If type is NONE, it clears pts and creates a straight line by adding points from the offset to offset x plus 99 in x-axis and offset y plus tileSize in y-axis.

case Direction.UP:
  if (type == PosNegType.POS)
  {
    TranslatePoints(pts, new Vector2(padding_x, padding_y + sh));
  }
  else if (type == PosNegType.NEG)
  {
    InvertY(pts);
    TranslatePoints(pts, new Vector2(padding_x, padding_y + sh));
  }
  else
  {
    pts.Clear();
    for (int i = 0; i < 100; ++i)
    {
      pts.Add(new Vector2(i + padding_x, padding_y + sh));
    }
  }
  break;
Code language: C# (cs)

Let’s implement the logic for the RIGHT, DOWN and LEFT directions. We will use the three transformation utility functions based on our needs. Remember, the Bézier  curve is the same. We are just applying transformations to these points to either translate, invert or rotate by swapping the x and y values. Be careful when you code this section as it could be very error prone. Check your implementation and debug if necessary.

case Direction.RIGHT:
  if (type == PosNegType.POS)
  {
    SwapXY(pts);
    TranslatePoints(pts, new Vector2(padding_x + sw, padding_y));
  }
  else if (type == PosNegType.NEG)
  {
    InvertY(pts);
    SwapXY(pts);
    TranslatePoints(pts, new Vector2(padding_x + sw, padding_y));
  }
  else
  {
    pts.Clear();
    for (int i = 0; i < 100; ++i)
    {
      pts.Add(new Vector2(padding_x + sw, i + padding_y));
    }
  }
  break;
case Direction.DOWN:
  if (type == PosNegType.POS)
  {
    InvertY(pts);
    TranslatePoints(pts, new Vector2(padding_x, padding_y));
  }
  else if (type == PosNegType.NEG)
  {
    TranslatePoints(pts, new Vector2(padding_x, padding_y));
  }
  else
  {
    pts.Clear();
    for (int i = 0; i < 100; ++i)
    {
      pts.Add(new Vector2(i + padding_x, padding_y));
    }
  }
  break;
case Direction.LEFT:
  if (type == PosNegType.POS)
  {
    InvertY(pts);
    SwapXY(pts);
    TranslatePoints(pts, new Vector2(padding_x, padding_y));
  }
  else if (type == PosNegType.NEG)
  {
    SwapXY(pts);
    TranslatePoints(pts, new Vector2(padding_x, padding_y));
  }
  else
  {
    pts.Clear();
    for (int i = 0; i < 100; ++i)
    {
      pts.Add(new Vector2(padding_x, i + padding_y));
    }
  }
  break;
Code language: C# (cs)

Finally, we implement the DrawCurve function. The DrawCurve function takes in the parameters for direction, type, and colour. It checks whether a LineRenderer associated with the given direction and type exists in the mLineRenderers dictionary; if not, it creates a new LineRenderer with the specified colour using the CreateLineRenderer function we implemented earlier and adds it to the dictionary.

Then, it retrieves the LineRenderer from the dictionary and sets its start and end colour to the provided colour. The function also assigns a descriptive name to the LineRenderer’s game object for clarity in the Unity editor.

Next, it calls the CreateCurve function to generate the list of points representing the curve based on the provided direction and type. It sets the position count of the LineRenderer to match the number of points in the curve. It iterates over each point, setting the corresponding position in the LineRenderer to draw the curve.

public void DrawCurve(Direction dir, PosNegType type, UnityEngine.Color color)
{
  if (!mLineRenderers.ContainsKey((dir, type)))
  {
    mLineRenderers.Add((dir, type), CreateLineRenderer(color));
  }

  LineRenderer lr = mLineRenderers[(dir, type)];
  lr.gameObject.SetActive(true);
  lr.startColor = color;
  lr.endColor = color;
  lr.gameObject.name = "LineRenderer_" + dir.ToString() + "_" + type.ToString();
  List<Vector2> pts = CreateCurve(dir, type);

  lr.positionCount = pts.Count;
  for (int i = 0; i < pts.Count; ++i)
  {
    lr.SetPosition(i, pts[i]);
  }
}
Code language: C# (cs)

We have now finished implementing the necessary functionalities for displaying our template Bézier curves on the different locations of the image tile. We have also created the various possibilities for the curves to be drawn. Now, we want to show these curves on the image tile.

To do so, go ahead and open the script file TileGen. Add a variable mTile of type Tile and initialise it by the default constructor.

Now, let’s show the POS-type curve for all directions. To do so, add the DrawCurve function 4 times once each for UP, RIGHT, DOWN and LEFT directions with type as POS. Make the colour of the curve to be blue.

void Start()
{
  CreateBaseTexture();
  mTile = new Tile(mTextureOriginal);
  mTile.DrawCurve(Tile.Direction.UP, Tile.PosNegType.POS, Color.blue);
  mTile.DrawCurve(Tile.Direction.RIGHT, Tile.PosNegType.POS, Color.blue);
  mTile.DrawCurve(Tile.Direction.DOWN, Tile.PosNegType.POS, Color.blue);
  mTile.DrawCurve(Tile.Direction.LEFT, Tile.PosNegType.POS, Color.blue);
}
Code language: C# (cs)

Go to Unity editor and click play. You should now be able to see the curves on all 4 sides.

Now, let’s show the NEG-type curve for all directions. To do so, comment on the previous 4 DrawCurves with POS type and add the DrawCurve function once more 4 times. Once each for UP, RIGHT, DOWN and LEFT directions with type as NEG. Make the colour of the curve to be red. Go to Unity editor and click play. You should now be able to see the inverse curves on all 4 sides with red colour.

void Start()
{
  CreateBaseTexture();
  mTile = new Tile(mTextureOriginal);
  //mTile.DrawCurve(Tile.Direction.UP, Tile.PosNegType.POS, Color.blue);
  //mTile.DrawCurve(Tile.Direction.RIGHT, Tile.PosNegType.POS, Color.blue);
  //mTile.DrawCurve(Tile.Direction.DOWN, Tile.PosNegType.POS, Color.blue);
  //mTile.DrawCurve(Tile.Direction.LEFT, Tile.PosNegType.POS, Color.blue);
  mTile.DrawCurve(Tile.Direction.UP, Tile.PosNegType.NEG, Color.red);
  mTile.DrawCurve(Tile.Direction.RIGHT, Tile.PosNegType.NEG, Color.red);
  mTile.DrawCurve(Tile.Direction.DOWN, Tile.PosNegType.NEG, Color.red);
  mTile.DrawCurve(Tile.Direction.LEFT, Tile.PosNegType.NEG, Color.red);
}
Code language: C# (cs)

Great! We have achieved visualising our Bézier curves on the image tile. Now let’s go a step further and try to mimic the actual Jigsaw tile that we can create out of these possible configurations of the curves.

For this, we will first create a function in the Tile script called HideAllCurves. This method iterates over all LineRenderers stored in the mLineRenderers dictionary and sets its gameObject.SetActive to false, effectively hiding the associated curve by deactivating its game object. This function provides a quick way to hide all curves stored within the class by toggling their visibility off simultaneously.

public void HideAllCurves()
{
  foreach (var item in mLineRenderers)
  {
    item.Value.gameObject.SetActive(false);
  }
}Code language: C# (cs)

We now move on to the TileGen script file and create another function called GetRandomType. This function will generate a random type and a corresponding color based on a randomly generated float value. 

We will Initially set the type to Tile.PosNegType.POS and the colour to blue. We will then generate a random float between 0 and 1 using a Random.Range with 0 and 1 as the input parameters. If this value is less than 0.5, it assigns the type POS and blue colour; otherwise, it assigns the type NEG and the colour red

Finally, it returns a tuple containing the randomly determined type and colour. This function serves as a convenient method to obtain randomised type-colour pairs, which can be used to generate the possible variations of the jigsaw tile in a randomised manner.

private (Tile.PosNegType, UnityEngine.Color) GetRendomType()
{
  Tile.PosNegType type = Tile.PosNegType.POS;
  UnityEngine.Color color = UnityEngine.Color.blue;
  float rand = UnityEngine.Random.Range(0f, 1f);

  if(rand < 0.5f)
  {
    type = Tile.PosNegType.POS;
    color = UnityEngine.Color.blue;
  }
  else
  {
    type = Tile.PosNegType.NEG;
    color = UnityEngine.Color.red;
  }
  return (type, color);
}
Code language: C# (cs)

Now, in the Update method, we associate the creation of a randomly created tile with the press of the Space key. The Update function, upon detecting the space bar down input, triggers a sequence of actions. Firstly, it calls the HideAllCurves method of the mTile object, effectively hiding all previously drawn curves.

Then, it generates a random type and colour pair using the GetRandomType function and draws a curve in the upward direction with the obtained type and colour. Subsequently, it repeats this process for the right, downward, and left directions, each time generating a new random type and colour pair and drawing a curve accordingly.

This function mimics the type of jigsaw tile that we could create out of all possible variations of the curves. We will ignore for now  the side image tiles where one of the sides will have no curve applied to it.

void Update()
{
  if(Input.GetKeyDown(KeyCode.Space))
  {
    TestRandomCurves();
  }
}
Code language: C# (cs)

Now, in the Update method, we associate the creation of a randomly created tile with the press of the Space key. We implement a function called TestRandomCurves. We call this method from the Update whenever the user presses the Space key.

void TestRandomCurves()
{
  if(mTile != null)
  {
    mTile.DestroyAllCurves();
    mTile = null;
  }

  Tile tile = new Tile(mTextureOriginal);
  mTile = tile;

  var type_color = GetRendomType();
  mTile.DrawCurve(Tile.Direction.UP, type_color.Item1, type_color.Item2);
  type_color = GetRendomType();
  mTile.DrawCurve(Tile.Direction.RIGHT, type_color.Item1, type_color.Item2);
  type_color = GetRendomType();
  mTile.DrawCurve(Tile.Direction.DOWN, type_color.Item1, type_color.Item2);
  type_color = GetRendomType();
  mTile.DrawCurve(Tile.Direction.LEFT, type_color.Item1, type_color.Item2);

  SpriteRenderer spriteRenderer = GetComponent<SpriteRenderer>();
  spriteRenderer.sprite = mSprite;

}
Code language: C# (cs)

Open the Tile script once again and implement another method in the class called DestroyAllCurves. This method iterates through each entry in the mLineRenderers dictionary and destroys the game object associated with it. This ensures that the LineRenderer and its associated game object are removed from the scene, effectively erasing all curves drawn on the tile.

We will use this method when we test out our various jigsaw pieces created with a new tile instance. If we do not delete these line renderers game objects, then we will have dangling line renderers from previous tiles.

public void DestroyAllCurves()
{
  foreach (var item in mLineRenderers)
  {
    GameObject.Destroy(item.Value.gameObject);
  }
  mLineRenderers.Clear();
}Code language: PHP (php)

Go to the Unity editor, select the TileGen game object and change the position z value to 1. This is to ensure that the image is behind the curve in order or drawing.

Click Play and then press space bar to view possible configurations of the Jigsaw tiles. Keep pressing the space bar to view other possible configurations. 

Some other possible configurations are as follows.

We now know how to draw the Bézier curves on the image tile. We shall now proceed to cut the image based on the Bézier curves surrounding the image tile. Before we start going into the details of our implementation, let’s look at the high-level steps to achieve the solution. First, to cut our texture based on the lines and curves, we will take the following steps:

  • Step 1 – Set the original texture to the tile.
  • Step 2 – Create a new texture of the same width and height as the original texture and fill it up with complete transparency.
  • Step 3 – Set the newly created texture boundary based on the curves and straight lines depending on what PosNegType for each direction.
  • Step 4 – Apply Flood-fill to fill the region inside this boundary. The flood fill will set the colour of the newly created texture based on the colour from the original image.

We now proceed with our implementation into our Tile script. We already have a variable called mOriginalTexure, a private variable that stores the input texture. We won’t modify this texture. Instead, we will create a new texture called finalCut and alter this new texture. 

So, go ahead and add a property called the finalCut of Texture2D type. This finalCut property lets the user get access to the final texture. Note that we only provide access to this property. That means we cannot modify the finalCut texture outside of this class.

public Texture2D finalCut { get; private set; }
Code language: C# (cs)

We also add a new static readonly variable called TransparentColor and initiate it to zero for all components.

After that, we define an array mCurveTypes of size 4, where each element corresponds to a direction specified by the Direction enum. Initially, all elements are set to PosNegType.NONE. We then implement the SetCurveType method that allows modifying the curve type for a specific direction by taking in a Direction enum and a PosNegType enum and setting the corresponding element in the array to the provided type.

Conversely, we also write the GetCurveType method that retrieves the curve type for a given direction by indexing into the mCurveTypes array using the provided direction enum and returning the stored PosNegType

public static readonly Color TransparentColor = new Color(0.0f, 0.0f, 0.0f, 0.0f);

private PosNegType[] mCurveTypes = new PosNegType[4]
{
  PosNegType.NONE,
  PosNegType.NONE,
  PosNegType.NONE,
  PosNegType.NONE,
};
Code language: C# (cs)

We now create our constructor for the Tile class. This constructor initialises a Tile object with a given Texture2D as an input parameter. First, we compute tileSizeWithPadding as twice the padding plus the tileSize.

It initialises the mOriginalTexture field with the provided texture. Next, it initialises the Texture2D object named finalCut with dimensions tileSizeWithPadding by tileSizeWithPadding and format TextureFormat.ARGB32. Following this, it iterates over each pixel of finalCut and sets its colour to TransparentColor

public Tile(Texture2D texture)
{
  mOriginalTexture = texture;
  //int padding = mOffset.x;
  int tileSizeWithPadding = 2 * padding + tileSize;

  finalCut = new Texture2D(
    tileSizeWithPadding, 
    tileSizeWithPadding, 
    TextureFormat.ARGB32, 
    false);

  // We initialise this newly created texture with transparent color.
  for (int i = 0; i < tileSizeWithPadding; ++i)
  {
    for (int j = 0; j < tileSizeWithPadding; ++j)
    {
      finalCut.SetPixel(i, j, TransparentColor);
    }
  }
}
Code language: C# (cs)

After that, we create a function called Apply. This is the main method that cuts the image tile based on the curves and modifies the texture finalCut. We do so by calling two functions that have not yet been implemented. The first is FloodFillInit, and the second is FloodFill. These two functions will make all necessary changes to the image finalCut. To finalise the changes on the finalCut image, we call the finalCut.Apply method.

public void Apply()
{
  FloodFillInit();
  FloodFill();
  finalCut.Apply();
}
Code language: C# (cs)

Flood Fill Algorithm

The flood fill algorithm is a technique used in computer graphics to determine and change the color of connected regions in a raster image or bitmap. The algorithm starts from a specified seed point and floods outwards, recursively visiting neighboring pixels or cells in the image. It checks each neighboring pixel to see if it meets certain criteria, typically if it has the same color as the seed pixel and has not been visited yet. If the criteria are met, the pixel’s color is changed, and the algorithm continues recursively with that pixel as the new seed.

This process continues until all connected pixels meeting the criteria have been visited, effectively filling the entire connected region with a new color. 

Flood-fill in progress

We will use the flood fill algorithm to fill the area that falls inside the curves in all four directions.

To implement flood fill, we will follow the following algorithm:

  • Step 1: Set up the boundary of the texture based on the curves and straight lines. Mark all pixels that fall in this set of points as visited.
  • Step 2: Take the centre pixel of the finalCut texture and set the colour value from the input texture’s centre pixel. Mark it as visited. Add this pixel to a stack.
  • Step 3: While the stack of pixels is not empty, go up, left, right and down to get the next pixel. If the next pixel is already marked as visited, then we do not process that pixel. If not, then we set that pixel as visited, set the colour from the original texture of the same pixel, and add it to the stack.

We will used the method named FloodFillInit to perform Step 1 and Step 2 and the FloodFill method to perform Step 3.

Before we start implementing these two methods, we will need to declare two variables. These are the 2D boolean array called the mVisited. The mVisited 2d array serves as a grid to store information about whether a particular pixel has been visited during a process like flood fill. 

// A 2d boolean array that stores whether a particular
// pixel is visited. We will need this array for the flood fill.
private bool[,] mVisited;

Code language: C# (cs)

The other variable is a Stack of Vector2Int called mStack. This stack is used to store coordinates of pixels that need to be processed or visited during a flood fill operation. The stack data structure follows the Last In, First Out principle, meaning that the last element added to the stack will be the first one to be removed. In the context of flood fill, the stack is used to keep track of pixels that need to be filled with color or otherwise processed. 

// A stack needed for the flood fill of the texture.
private Stack<Vector2Int> mStack = new Stack<Vector2Int>();
Code language: C# (cs)

We then add two indices variables needed to track the traversal of the 2d array.

// The indices.
public int xIndex = 0;
public int yIndex = 0;Code language: PHP (php)

Initialising Flood Fill

We can now start implementing the FloodFillInit method. This method initialises the flood fill process by first calculating the size of the tile with padding included. Subsequently, it initialises the 2D boolean array mVisited with dimensions corresponding to the tile’s size, ensuring that each pixel’s visitation status can be tracked. All elements of mVisited are initially set to false to indicate that no pixels have been visited yet.

Then, the method generates a list of points representing the closed curve of the tile by iterating over the mCurveTypes array and creating curves for each direction and type using the CreateCurve method. The pixels enclosed by the curve are marked as visited by setting the corresponding elements in the mVisited array to true, preventing the flood fill algorithm from attempting to fill them again. Finally, the flood fill process is initialized from the center of the tile, and marking this center pixel as visited. The coordinates of the center pixel are pushed onto the mStack, indicating that it is the starting point for the flood fill operation. 

void FloodFillInit()
{
  int tileSizeWithPadding = 2 * padding + tileSize;

  mVisited = new bool[tileSizeWithPadding, tileSizeWithPadding];
  for (int i = 0; i < tileSizeWithPadding; ++i)
  {
    for (int j = 0; j < tileSizeWithPadding; ++j)
    {
      mVisited[i, j] = false;
    }
  }


  List<Vector2> pts = new List<Vector2>();
  for (int i = 0; i < mCurveTypes.Length; ++i)
  {
    pts.AddRange(CreateCurve((Direction)i, mCurveTypes[i]));
  }

  // Now we should have a closed curve.
  for (int i = 0; i < pts.Count; ++i)
  {
    mVisited[(int)pts[i].x, (int)pts[i].y] = true;
  }
  // start from the center.
  Vector2Int start = new Vector2Int(tileSizeWithPadding / 2, tileSizeWithPadding / 2);

  mVisited[start.x, start.y] = true;
  mStack.Push(start);
}
Code language: C# (cs)

Essentially, the FloodFillInit method sets up the initial state for the flood fill algorithm by preparing the visitation array, marking the pixels enclosed by the curve, and defining the starting point for the flood fill operation.

We then implement a utility function called Fill. The Fill method retrieves the color of a pixel from the original texture at the specified position, adjusts its opacity to fully opaque, and then sets the corresponding pixel in the finalCut texture to this modified color.

void Fill(int x, int y)
{
  Color c = mOriginalTexture.GetPixel(
    x + xIndex * tileSize, 
    y + yIndex * tileSize);
  c.a = 1.0f;
  finalCut.SetPixel(x, y, c);
}
Code language: C# (cs)

Implementing Flood Fill

Finally, we will now implement the FloodFill method. This method initiates the flood fill operation for the tile. It first calculates the width and height of the tile with padding and tileSize values.

Then, it iteratively processes pixels stored in the mStack until there are no more pixels left to process. For each pixel popped from the stack, it fills the corresponding pixel in the finalCut texture using the Fill utility method we implemented earlier.

Next, it checks neighboring pixels in four directions: right, left, up, and down. For each neighbouring pixel, it verifies whether it falls within the bounds of the tile, if it has not been visited yet, and if it has a different color than the current pixel. If these conditions are met, the neighboring pixel is marked as visited, and its coordinates are pushed onto the stack for further processing.

This process continues until all connected pixels within the tile are filled, ensuring the entire region enclosed by the curve is colored.

void FloodFill()
{
  //int padding = mOffset.x;
  int width_height = padding * 2 + tileSize;

  while (mStack.Count > 0)
  {
    Vector2Int v = mStack.Pop();

    int xx = v.x;
    int yy = v.y;

    Fill(v.x, v.y);

    // Check right.
    int x = xx + 1;
    int y = yy;

    if (x < width_height)
    {
      Color c = finalCut.GetPixel(x, y);
      if (!mVisited[x, y])
      {
        mVisited[x, y] = true;
        mStack.Push(new Vector2Int(x, y));
      }
    }

    // check left.
    x = xx - 1;
    y = yy;
    if (x > 0)
    {
      Color c = finalCut.GetPixel(x, y);
      if (!mVisited[x, y])
      {
        mVisited[x, y] = true;
        mStack.Push(new Vector2Int(x, y));
      }
    }

    // Check up.
    x = xx;
    y = yy + 1;

    if (y < width_height)
    {
      Color c = finalCut.GetPixel(x, y);
      if (!mVisited[x, y])
      {
        mVisited[x, y] = true;
        mStack.Push(new Vector2Int(x, y));
      }
    }

    // Check down.
    x = xx;
    y = yy - 1;

    if (y >= 0)
    {
      Color c = finalCut.GetPixel(x, y);
      if (!mVisited[x, y])
      {
        mVisited[x, y] = true;
        mStack.Push(new Vector2Int(x, y));
      }
    }
  }
}
Code language: C# (cs)

This method is the heart of cutting and filling our images for a jigsaw tile. This method is also very error-prone. Please take extra attention while implementing this method. It is hard to debug if you make any mistake in coding this method.

We will now test our jigsaw tile creation based on the flood fill method. To do so, go to the TileGen script.In the existing Update method, add a new section that, when the ‘F’ key is pressed, we call a method called TestTileFloodFill.

void Update()
{
  if(Input.GetKeyDown(KeyCode.Space))
  {
    TestRandomCurves();
  }
  else if(Input.GetKeyDown(KeyCode.F))
  {
    TestTileFloodFill();
  }
}
Code language: C# (cs)

Testing the Jigsaw Tile Creation

After that, we implement this TestTileFloodFill method. The TestTileFloodFill method is designed to test the flood fill functionality of the tile component. Initially, it checks if the mTile reference is not null; if so, it clears all curves drawn on the tile and sets the reference to null.

Then, it creates a new instance of the Tile class using the original texture, assigning it to the tile. Random curve types and colours are generated using the GetRandomType method, and curves are drawn on each side of the tile accordingly. The type of each curve is set using the SetCurveType method. After drawing and configuring the curves, the Apply method is called on the tile to execute the flood fill operation. Finally, the resulting texture from the flood fill operation is applied to the sprite renderer component attached to the game object. 

void TestRandomCurves()
{
  if(mTile != null)
  {
    mTile.DestroyAllCurves();
    mTile = null;
  }

  Tile tile = new Tile(mTextureOriginal);
  mTile = tile;

  var type_color = GetRendomType();
  mTile.DrawCurve(Tile.Direction.UP, type_color.Item1, type_color.Item2);
  type_color = GetRendomType();
  mTile.DrawCurve(Tile.Direction.RIGHT, type_color.Item1, type_color.Item2);
  type_color = GetRendomType();
  mTile.DrawCurve(Tile.Direction.DOWN, type_color.Item1, type_color.Item2);
  type_color = GetRendomType();
  mTile.DrawCurve(Tile.Direction.LEFT, type_color.Item1, type_color.Item2);

  SpriteRenderer spriteRenderer = GetComponent<SpriteRenderer>();
  spriteRenderer.sprite = mSprite;

}
Code language: C# (cs)

Go to Unity editor and click Play. Now press the “F” button and see the cutout jigsaw tiles. Keep pressing “F” and see the various jigsaw pieces.

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).

This brings us to the end of Section 2: Create a Jigsaw Tile from an Existing Image. In the next section, we are going to learn how to create an entire Jigsaw board using from an image.

Pages: 1 2 3 4 5

7 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.

  2. I am extremely impressed with your writing skills
    as well as with the format in your blog. Is that this a paid subject
    or did you modify it your self? Anyway stay up the excellent quality writing, it is uncommon to peer a great weblog like this one these days..

Leave a Reply

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