noobtuts

Unity 2D Minesweeper Tutorial

Unity Minesweeper

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 a mine field 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. 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.

Our Minesweeper clone will be really simple, with 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.

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.

Unity Version

Our Minesweeper Tutorial will use Unity 5.0.0f4. Newer versions should work fine as well, older versions may or may not work. The free version of Unity 5 now comes with all the engine features, which makes it the recommended version.

Project Setup

Let's get to it. We will start Unity and select New Project:
Unity New Project

We will name it minesweeper, select any location like C:\, select 2D and click Create Project:
Unity Create new 2D Project

Now we can 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:
Camera Properties

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:
Minesweeper Default Element
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:
Default Element in Project Area

And then we can modify the Import Settings in the Inspector:
Default Element Import Settings
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:
Drag Default Element into 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.

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:
Default Element Position in Inspector

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->Box Collider 2D in the Inspector:
Default Element with Collider

And that's all, now it's part of the physics world.

If we press Play then we can now see the first element in our game:
Default Element in 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:
Duplicate default Element

We will position the duplicated element at x=1, y=0:
Default Element Duplicate Position

Now we can duplicate the elements over and over again until we have 10 horizontal * 13 vertical elements:
Default Element all Duplicates

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!

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:

adjacency0 adjacency1 adjacency2

adjacency3 adjacency4 adjacency5

adjacency6 adjacency7 adjacency8

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:
Minesweeper Empty Element Minesweeper Empty Element Minesweeper Empty Element Minesweeper Empty Element Minesweeper Empty Element Minesweeper Empty Element Minesweeper Empty Element Minesweeper Empty Element Minesweeper Empty Element
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:
Minesweeper Mine
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 and then use the following Import Settings in the Inspector:
Empty Element and Mine Import 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:
Create Script

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->Script->Element in the Inspector. This way we won't forget it later on:
Element Script in Inspector

Let's double click the Script in the Project Area so it opens:

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:
Element Script Texture Slots

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:
Element Script Texture Slots filled

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];
    }
}

Note: the function first checks if the element is a mine or not. If it is a mine then it loads the mine texture. If it's not a mine then 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:
Element Script Load Texture Test

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 a mine:

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. At first 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:
Uncover mineless Elements

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.

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.

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

using UnityEngine;
using System.Collections;

public class Grid : 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 Grid {

}

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 matrix or table) to do this:

using UnityEngine;
using System.Collections;

public class Grid {
    // 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 * 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 Grid 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;
    Grid.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 Grid 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 Grid 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
        Grid.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 being uncovered as well:
UncoverMines function in game

Counting Adjacent Mines

Next off we will add another function to our Grid 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:

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 Grid 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
        Grid.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(Grid.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:
Adjacent Mine number in game

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:
Uncover mineless Elements

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:

We will begin by adding the the default Flood Fill algorithm to our Grid 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 Grid 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#.

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
        Grid.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(Grid.adjacentMines(x, y));

        // uncover area without mines
        Grid.FFuncover(x, y, new bool[Grid.w, Grid.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 Grid. 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 Flood Fill at work:
Uncover mineless Elements

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 Grid 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
        Grid.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(Grid.adjacentMines(x, y));

        // uncover area without mines
        Grid.FFuncover(x, y, new bool[Grid.w, Grid.h]);

        // find out if the game was won now
        if (Grid.isFinished())
            print("you win");
    }
}

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

Summary

And that was our Unity 2D Minesweeper Tutorial. We learned a lot about Unity and C# programming this time. Understanding Flood Fill and being able to implement it in any programming language is a very useful asset in every developer's 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, adding bigger levels, fancier graphics, a few decent sounds, win and lose screens or a restart button.


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!