Unity 2D Minesweeper Tutorial

Foreword

Welcome to our Unity 2D Minesweeper Tutorial. Minesweeper is a single-player puzzle game, originally released back in the 1960s. The goal of the game is to uncover ("sweep") a minefield while trying to not trigger any of the mines. After uncovering an element without a mine, the game will always show a number that indicates the amount of surrounding mines. When only the mines remain on the board, the game is won. Set off any mines and it's Game Over. This adds a nice strategic aspect to the game.

What sounds simple is actually so much fun that different versions of Minesweeper are frequently included in some of the major operating systems. In fact, the Windows 3.x family of operating system all the way up to Windows 7 included Minesweeper as part of the base product, along with other favorites such as Solitare and FreeCell. Here's what it looked like:

At the end of this tutorial, our Minesweeper clone will be functionally on par with the original in only 85 lines of code and some pixel art. We will learn quite a few things about Unity programming and implement the popular Flood Fill algorithm.

As usual, everything will be explained as easy as possible so everyone can understand it. So, grab a cup of your favorite drink, a snack and let's get started!

Requirements

Knowledge

Our Tutorial does not require any special Unity skills besides some knowledge about the basics like GameObjects and Transforms. Understanding recursion (a function calling itself) will definitely come in handy for the Flood Fill algorithm.

Feel free to read our easier Unity Tutorials like Unity 2D Pong Game if you want to get used to this powerful (yet simple) game engine first.

Which version of Unity do I use?

Our Minesweeper Tutorial will use Unity 2018.3.14f1. Newer versions should work fine as well, older versions may or may not work. Unless you have a very specific reason not to use the version noted, it's recommended to keep to the tutorial's written version.

Project Setup

Let's get to it.

Firstly, we will start up the Unity Hub which should be installed when you installed Unity. At the time of this tutorial, the version of the Hub pictured is 2.0.2. If your version is not 2.0.2 or newer, then you may need to adapt some of these steps to your configuration. Once it's started up, we will click the "New" button as pictured below:

Now we select "2D" for the template, give it a name and pick somewhere to put it. Depending on your Operating System, if you're on a Mac you might have something different.

On Windows-based systems, it's recommended to put your project in a dedicated folder outside your User folder. It is not recommended to put any Unity Project inside your Dcouments or Desktop folder due to potential issues that can occur from doing so. For our case, we can put it in C:\GameDev or alternatively on a dedicated hard drive as shown below:

Allow some time for Unity to create the new project and initialize. Upon doing so, you will be greeted with the Unity editor. If you have followed our previous tutorials, this will start to look very familiar.

The first thing we want to do is to modify the Camera to make sure that the game will be in the middle of the screen later. At first we will select the Main Camera in the Hierarchy and then set the Background Color to black. We will also modify the Size and the Position like shown in the following image:

The Default Element

Let's add the default elements to our game. The default elements are those that we see if we didn't click on one yet. Their purpose is to hide whatever is below them.

At first we will need some kind of image that we can use. We will keep it simple and draw a 16 x 16 pixel image in a drawing tool like Paint.NET:



Note: right click on the image, select Save As... and save it in the project's Assets folder.

After saving it in our Assets folder, we can select the image in the Project Area:

And then we can modify the Import Settings in the Inspector:



Note: the Import Settings specify how big the image is in the final game, and if some kind of compression should be used or not.

Alright, now we can drag the image from the Project Area into the Scene:



Note: everything in the Project Area is just a file, something we may or may not use for our game. Once we drag our default element into the Scene it becomes part of the game world. Depending on your current zoom levels, you may find it appears as a huge block. Don't panic! Use your mouse scroll wheel in the Scene area to zoom in and out of the scene.

Let's select the default element in the Scene or in the Hierarchy and then take a look over to the Inspector. Here we will position it at X = 0, Y = 0:



Note: X is the horizontal position and Y is the vertical position. We will set Z to 0 because we want to make a 2D game and don't really need the third dimension here.

We want to get notified when the user clicks on an element. Unity already provides a function for this as we will see later on, this function only works for elements with Colliders though.

A Collider makes our object part of the physics world. Right now our default element is just an image in the game world. Once we add a Collider to it, it becomes part of the physics world, just like a wall.

We can add a Collider to it by selecting Add Component -> Physics 2D and selecting Box Collider 2D in the Inspector:

And that's all! That block is now part of the physics world.

If we press Play then we can now see the first element in our game:

Adding more Elements

Our 2D Minesweeper game would be boring with just one element. We can add more elements by either repeating the previous work flow or right clicking the default GameObject in the Hierarchy and selecting Duplicate. Alternatively, you can also select it and press Control + D (Command + D for Mac users) to duplicate it - this keyboard shortcut saves some time. Either way, you'll end up with a duplicate like so:

We will position the duplicated element at X = 1, Y = 0. Again, we'll keep Z set to 0:

Now we can duplicate the elements over and over again until we have 10 horizontal and 13 vertical elements:



Note: the bottom left element is at X = 0, Y = 0. The top right element is at X = 9, Y = 12. It's important that the elements in between are always at rounded positions like X = 2, Y = 3 instead of X = 2.04, Y = 3.002.

Our game already looks a bit like Minesweeper now! We're getting there.

About Adjacency

Let's take a minute to understand the adjacent mine property that will be a big part of our Minesweeper game.

Note: adjacent is a fancy word for surrounding, or direct neighbor.

After clicking an element that was not a mine, the user should see a number that indicates the amount of adjacent mines. We will use what's called 8 neighbor adjacency here. Or in other words, instead of just looking at the top/bottom/left/right we will also look at the top-left/top-right/bottom-left/bottom-right elements.

Here are the 9 different cases that we can encounter:

So all we have to do is count the amount of adjacent mines for each field and then draw the number, or draw nothing if there are no adjacent mines.

Adding more Images

Alright so in order to draw those numbers we can either use Unity's GUI system or we just don't worry much about it and quickly draw one texture for each number:



Note: right click each image, select Save As... and save them all in the project's Assets folder.

We will also need an image for the mines:



Note: right click on the image, select Save As... and save it in the project's Assets folder.

Once we saved all those images in the Project Area, we will select them (click on empty0, holding shift on your keyboard down and then clicking on mine to select them all in the Project view) and then use the following Import Settings in the Inspector, clicking Apply when you're done changing the settings:

The Code

Let's write some code! We will begin by right clicking in the Project Area, selecting Create -> C# Script and naming it Element:

Our Script doesn't do anything yet, but let's select all default elements in the Hierarchy and then add the Script to them by selecting Add Component -> Scripts -> Element in the Inspector. This way we won't forget it later on:

Let's double click the Script in the Project Area so it opens up with our code editor (usually Visual Studio, or another code editor if you aren't using Visual Studio):



using UnityEngine ;

using System.Collections ;



public class Element : MonoBehaviour {



// Use this for initialization

void Start ( ) {



}



// Update is called once per frame

void Update ( ) {



}

}

We can remove the Update function because we won't need it. Let's also add a variable that indicates whether or not this element is a mine:



using UnityEngine ;

using System.Collections ;



public class Element : MonoBehaviour {



// Is this a mine?

public bool mine ;



// Use this for initialization

void Start ( ) {



}

}

Now we can randomly decide if this element is supposed to be a mine or not by using Random.value in the Start function:



using UnityEngine ;

using System.Collections ;



public class Element : MonoBehaviour {



// Is this a mine?

public bool mine ;



// Use this for initialization

void Start ( ) {

// Randomly decide if it's a mine or not

mine = Random . value < 0.15 ;

}

}

Let's create a little helper function. We want to be able to switch from the default texture to the empty texture, a number texture or the mine texture any time. At first we will define a few texture variables:



using UnityEngine ;

using System.Collections ;



public class Element : MonoBehaviour {



// Is this a mine?

public bool mine ;



// Different Textures

public Sprite [ ] emptyTextures ;

public Sprite mineTexture ;



// Use this for initialization

void Start ( ) {

// Randomly decide if it's a mine or not

mine = Random . value < 0.15 ;

}

}

Now we can see some new slots in the Inspector:

This is where we can drag our textures into. So let's select one after another in the Project Area and then drag them right into the slots:

Now we can make use of our Sprite variables by creating a loadTexture function:



using UnityEngine ;

using System.Collections ;



public class Element : MonoBehaviour {



// Is this a mine?

public bool mine ;



// Different Textures

public Sprite [ ] emptyTextures ;

public Sprite mineTexture ;



// Use this for initialization

void Start ( ) {

// Randomly decide if it's a mine or not

mine = Random . value < 0.15 ;

}



// Load another texture

public void loadTexture ( int adjacentCount ) {

if ( mine )

GetComponent < SpriteRenderer > ( ) . sprite = mineTexture ;

else

GetComponent < SpriteRenderer > ( ) . sprite = emptyTextures [ adjacentCount ] ;

}

}

Let's explain what our new function does before we go too much further. The loadTexture function first checks if the element is a mine or not. If the element is a mine then it loads the mine texture, otherwise it loads one of the emptyTextures (the numbers), depending on the adjacentCount. The GetComponent<SpriteRenderer>().sprite thing is just how we change the current texture.

We can test our function by changing our Start function for a second:



// Use this for initialization

void Start ( ) {

// Randomly decide if it's a mine or not

//mine = Random.value < 0.15;



// TEST

loadTexture ( 1 ) ;

}

If we press Play then we can see how every single element loads the number one texture:

We can change it back to our original Start function now:



// Use this for initialization

void Start ( ) {

// Randomly decide if it's a mine or not

mine = Random . value < 0.15 ;

}

Later on we will need to know if an element is still covered (as in: not clicked on yet) or not, so let's add a little function that simply compares the current texture's name to the default name:



// Is it still covered?

public bool isCovered ( ) {

return GetComponent < SpriteRenderer > ( ) . sprite . texture . name == "default" ;

}

We will add one more function to our Element Script so we can detect mouse clicks. Each of our elements already has a Collider2D attached to it, which means that whenever we click on an element, the function OnMouseUpAsButton will be called by Unity. Of course, this only happens if we actually have a function with that name in our Script, so let's add one:



void OnMouseUpAsButton ( ) {

// TODO: do stuff..

}

There are two things that can happen after clicking on an element. Either it's a mine or it's not:



void OnMouseUpAsButton ( ) {

// It's a mine

if ( mine ) {

// TODO: do stuff..

}

// It's not a mine

else {

// TODO: do stuff..

}

}

If it was a mine then all other mines should be revealed (we will implement that soon) and the game is over:



void OnMouseUpAsButton ( ) {

// It's a mine

if ( mine ) {

// TODO: uncover all mines

// ...



// game over

print ( "you lose" ) ;

}

// It's not a mine

else {

// TODO: do stuff..

}

}

We should also find out if all elements except those with mines were uncovered, in which case the game was won. Here is the first version with a few things still uncommented:



void OnMouseUpAsButton ( ) {

// It's a mine

if ( mine ) {

// TODO: uncover all mines

// ...



// game over

print ( "you lose" ) ;

}

// It's not a mine

else {

// TODO: show adjacent mine number

//loadTexture(...);



// TODO: uncover area without mines

// ...



// TODO: find out if the game was won now

// ...

}

}

All our TODO features have one thing in common: they require information not just about the element itself, but about other elements as well. So let's create one more Script that takes care of all elements.

Note: using "TODO: Something" comments is a great way of marking parts of your game code that you need to come back in the future and revise, fix, or improve on.

The Grid

Creating the Class

The Grid will be our helper class that knows all the elements and can take care of more complicated game logic like counting the adjacent mines for a certain element, or uncovering a whole area of mineless elements. Unfortunately, due to a naming conflict in Unity 2018.3, we'll have to call our Grid script the Playfield.

We will begin by creating a new C# Script and naming it Playfield:



using UnityEngine ;

using System.Collections ;



public class Playfield : MonoBehaviour {



// Use this for initialization

void Start ( ) {



}



// Update is called once per frame

void Update ( ) {



}

}

This Script doesn't have to be the type of Script that can be attached to a GameObject, so let's remove the MonoBehaviour definition and the Start and Update functions:



using UnityEngine ;

using System.Collections ;



public class Playfield {



}

The Elements 2D-Array

Our Grid should keep track of all the elements in our game. We can use a 2 Dimensional array (also known as a Matrix or Table) to do this:



using UnityEngine ;

using System.Collections ;



public class Playfield {

// The Grid itself

public static int w = 10 ; // this is the width

public static int h = 13 ; // this is the height

public static Element [ , ] elements = new Element [ w, h ] ;

}

Note: this creates a new 2 Dimensional array with the width of 10 and the height of 13, or in other words: 10 by 13 Elements. If we wanted to access the element at X = 0, Y = 1 we would write elements[0, 1].

Registering in the Grid

Let's switch back to our Element Script really quick and modify the Start function so each element registers itself in the Playfield automatically:



// Use this for initialization

void Start ( ) {

// Randomly decide if it's a mine or not

mine = Random . value < 0.15 ;



// Register in Grid

int x = ( int ) transform . position . x ;

int y = ( int ) transform . position . y ;

Playfield . elements [ x, y ] = this ;

}

Uncovering all Mines

Alright, let's go back to our Playfield class and implement the function that uncovers all the mines. This one will be really easy because all we have to do is go through every element, find out if it's a mine and then load the mine texture:



// Uncover all Mines

public static void uncoverMines ( )

{

foreach ( Element elem in elements )

if ( elem . mine ) elem . loadTexture ( 0 ) ;

}

Let's jump back into our Element Script and modify the OnMouseUpAsButton function so it uses our recently created uncoverMines function in case the user clicked on a mine:



void OnMouseUpAsButton ( ) {

// It's a mine

if ( mine ) {

// Uncover all mines

Playfield . uncoverMines ( ) ;



// game over

print ( "you lose" ) ;

}

// It's not a mine

else {

// TODO: show adjacent mine number

//loadTexture(...);



// TODO: uncover area without mines

// ...



// TODO: find out if the game was won now

// ...

}

}

If we press Play and click on a few elements until we hit a mine, then we can now see all of the other mines are now uncovered as well:

Counting Adjacent Mines

Next off we will add another function to our Playfield class. Given an element at position x, y, this function will count the amount of adjacent mines. It sounds slightly complicated, but in the end the function just looks at the 8 surrounding elements at the following positions:



top

top-right

right

bottom-right

bottom

bottom-left

left

top-left

And increases a counter by one whenever one of those elements is a mine.

So first of all we will add a little helper function to our Playfield class. This function will simply check if there is a mine at a certain position:



// Find out if a mine is at the coordinates

public static bool mineAt ( int x, int y ) {

// Coordinates in range? Then check for mine.

if ( x >= 0 && y >= 0 && x < w && y < h )

return elements [ x, y ] . mine ;

return false ;

}

Now we can create the actual adjacentMines function with the x and y coordinate as parameters and the counter that will be returned:



// Count adjacent mines for an element

public static int adjacentMines ( int x, int y ) {

int count = 0 ;



// TODO: count adjacent mines

// ...



return count ;

}

Afterwards we will just check all those adjacent elements:



// Count adjacent mines for an element

public static int adjacentMines ( int x, int y ) {

int count = 0 ;



if ( mineAt ( x, y + 1 ) ) ++ count ; // top

if ( mineAt ( x + 1 , y + 1 ) ) ++ count ; // top-right

if ( mineAt ( x + 1 , y ) ) ++ count ; // right

if ( mineAt ( x + 1 , y - 1 ) ) ++ count ; // bottom-right

if ( mineAt ( x, y - 1 ) ) ++ count ; // bottom

if ( mineAt ( x - 1 , y - 1 ) ) ++ count ; // bottom-left

if ( mineAt ( x - 1 , y ) ) ++ count ; // left

if ( mineAt ( x - 1 , y + 1 ) ) ++ count ; // top-left



return count ;

}

Let's jump back into our Element Script and modify the OnMouseUpAsButton function again:



void OnMouseUpAsButton ( ) {

// It's a mine

if ( mine ) {

// uncover all mines

Playfield . uncoverMines ( ) ;



// game over

print ( "you lose" ) ;

}

// It's not a mine

else {

// show adjacent mine number

int x = ( int ) transform . position . x ;

int y = ( int ) transform . position . y ;

loadTexture ( Playfield . adjacentMines ( x, y ) ) ;



// TODO: uncover area without mines

// ...



// TODO: find out if the game was won now

// ...

}

}

If we press Play then we can now see the adjacent mine number after uncovering an element:

Uncovering an Area

Alright, whenever the user uncovers an element without any adjacent mines then the whole area of elements without adjacent mines should be uncovered automatically like shown here:

There are many algorithms that can do this, but by far the easiest one is the Flood Fill algorithm (please click on the link for an awesome explanation with pictures and everything). Flood Fill is really simple if we understand recursion. In short, here is what Flood Fill does:



starting in some element

do whatever we want with that element

continue recursively for each neighbor element

We will begin by adding the the default Flood Fill algorithm to our Playfield class:

// Flood Fill empty elements

public static void FFuncover ( int x, int y, bool [ , ] visited ) {

// visited already?

if ( visited [ x, y ] )

return ;



// set visited flag

visited [ x, y ] = true ;



// recursion

FFuncover ( x - 1 , y, visited ) ;

FFuncover ( x + 1 , y, visited ) ;

FFuncover ( x, y - 1 , visited ) ;

FFuncover ( x, y + 1 , visited ) ;

}

We should also make sure that our algorithm never tries to visit any element outside of our Playfield by checking if the x and y coordinates are between 0 and the width or height:



// Flood Fill empty elements

public static void FFuncover ( int x, int y, bool [ , ] visited ) {

// Coordinates in Range?

if ( x >= 0 && y >= 0 && x < w && y < h ) {

// visited already?

if ( visited [ x, y ] )

return ;



// set visited flag

visited [ x, y ] = true ;



// recursion

FFuncover ( x - 1 , y, visited ) ;

FFuncover ( x + 1 , y, visited ) ;

FFuncover ( x, y - 1 , visited ) ;

FFuncover ( x, y + 1 , visited ) ;

}

}

Our algorithm should uncover each element that it visits. And it should not continue when an element is close to a mine:



// Flood Fill empty elements

public static void FFuncover ( int x, int y, bool [ , ] visited ) {

// Coordinates in Range?

if ( x >= 0 && y >= 0 && x < w && y < h ) {

// visited already?

if ( visited [ x, y ] )

return ;



// uncover element

elements [ x, y ] . loadTexture ( adjacentMines ( x, y ) ) ;



// close to a mine? then no more work needed here

if ( adjacentMines ( x, y ) > 0 )

return ;



// set visited flag

visited [ x, y ] = true ;



// recursion

FFuncover ( x - 1 , y, visited ) ;

FFuncover ( x + 1 , y, visited ) ;

FFuncover ( x, y - 1 , visited ) ;

FFuncover ( x, y + 1 , visited ) ;

}

}

Now we can go back to our Element Script and use the algorithm to uncover all empty elements whenever the user clicked on one:



void OnMouseUpAsButton ( ) {

// It's a mine

if ( mine ) {

// uncover all mines

Playfield . uncoverMines ( ) ;



// game over

print ( "you lose" ) ;

}

// It's not a mine

else {

// show adjacent mine number

int x = ( int ) transform . position . x ;

int y = ( int ) transform . position . y ;

loadTexture ( Playfield . adjacentMines ( x, y ) ) ;



// uncover area without mines

Playfield . FFuncover ( x, y, new bool [ Playfield . w , Playfield . h ] ) ;



// TODO: find out if the game was won now

// ...

}

}

If we press Play and uncover an empty element (that has no adjacent mines) then we can see the Flood Fill algorithm at work:

Checking if all Mines were found

There is one last thing to do, we still have to find out if the game was won after the user uncovered an element. This algorithm will be really easy again.

Let's go back to our Playfield class and write the code that finds out if every element that wasn't uncovered yet is a mine:



public static bool isFinished ( ) {

// Try to find a covered element that is no mine

foreach ( Element elem in elements )

if ( elem . isCovered ( ) && ! elem . mine )

return false ;

// There are none => all are mines => game won.

return true ;

}

Now we can use our isFinished function in the Element Script:



void OnMouseUpAsButton ( ) {

// It's a mine

if ( mine ) {

// uncover all mines

Playfield . uncoverMines ( ) ;



// game over

print ( "you lose" ) ;

}

// It's not a mine

else {

// show adjacent mine number

int x = ( int ) transform . position . x ;

int y = ( int ) transform . position . y ;

loadTexture ( Playfield . adjacentMines ( x, y ) ) ;



// uncover area without mines

Playfield . FFuncover ( x, y, new bool [ Playfield . w , Playfield . h ] ) ;



// find out if the game was won now

if ( Playfield . isFinished ( ) )

print ( "you win" ) ;

}

}

If we press Play then we can now enjoy the game:

Summary

Congratulations! That concludes our Unity 2D Minesweeper Tutorial with a fully functional Minesweeper clone.

We learned a lot about Unity and C# programming in this tutorial as well as a taking a brief look at understanding what Flood Fill is. Being able to implement Flood Fill in any programming language is a very useful asset in every developer toolbox.

As usual, now it's up to the reader to make the game more fun. There are lots of improvements that could be made, like tagging elements with a flag, making sure you can't continue uncovering the field once the game is over, adding bigger levels, fancier graphics, a few decent sounds, win and lose screens or a restart button. The possibilities are endless.