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 () {
}
}
Note: The mine variable is public so that other elements can see it. The Start function is called once in the beginning of the game.
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;
}
}
Note: Random.value will always return a new random number between 0 and 1. We want a 15% probability that an element is a mine, so we use 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;
}
}
Note: Sprite is another word for Texture. Sprite[] is an Array, or in other words: more than one Sprite.
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";
}
Note: an element is covered as long as it has the default texture. It will not be covered anymore as soon as we load a different texture like the mine or one of the numbers.
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..
}
}
If it was not a mine then a couple of things should happen. Firstly, we should load the empty texture with the correct number depending on the amount of adjacent mines (we will implement that soon, too). If an element without any adjacent mines was clicked then we should uncover the whole area of elements without mines like shown in this image:
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;
}
Note: the transform.position's x and y coordinates are of type float, so we have to convert them to int before using them. The value this refers to the element itself.
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);
}
Note: We simply check each element's mine variable that we created before and then use our loadTexture function if it was a mine. The loadTexture function expects the adjacent mine number, which doesn't really matter if it was a mine itself, hence why we just use 0. The function is public and static because we want to be able to use it from everywhere and not just from within the Playfield class itself.
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;
}
Note: we have to check if the coordinates are in range of the elements array to prevent accessing things like elements[-1, -1] which would throw an error.
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);
}
Note: The visited variable is a 2D array that simply keeps track of whether or not the algorithm already visited a certain element. The rest is just the default Flood Fill 4-neighbor recursion. Or in other words: the algorithm starts in some element and then continues with the elements on the top, right, bottom and on the left of that element recursively until it visited every element. It doesn't do any real work yet, it just visits everything once.
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);
}
}
And that's how easy it is to implement and modify Flood Fill in C#. Pretty sweet, huh?
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
// ...
}
}
Note: we just called the algorithm at the current element's position with a new boolean array that has the size of our Playfield. The boolean array is for the Flood Fill algorithm to keep track of which elements it visited already.
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;
}
Note: the algorithm simply tries to find one element that is still covered and not a mine. If it finds such an element then it returns false because there is still work to do for the user. If it finds no such element then it returns true because the game was won (because all the covered elements contain mines).
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.
Download Source Code & Project Files
The Unity 2D Minesweeper Tutorial source code & project files can be downloaded by Premium members.All Tutorials. All Source Codes & Project Files. One time Payment.
Get Premium today!