Memory Match Game in Unity3D – Part 1

UPDATE: ALL THESE AND MORE CAN BE FOUND ON MY NEW SITE http://www.returnofthebrain.co.uk/

This tutorial will take you through the process of creating a complete game from scratch using Unity3D. I’ve been working through some of the games on this list for practice, and didn’t want that work to go to waste. So I figured if it could be of any help to anybody as a tutorial or reference, that would be great!

What you should know upfront:

This isn’t necessarily a ‘first-time’ tutorial. I’m going to do my best to guide you through how the game was implemented and explain why and what I’m doing, and it should be accessible to beginners. But I won’t be explaining basic concepts. So you should probably have a little prior knowledge of C# and Unity3D before going in. That said, it shouldn’t be too difficult to follow what’s going on.

I’m also not making any promises about being a great programmer! I still have a ridiculous amount to learn, and if anybody has any suggestions for my own improvement or critique, I’d be happy to hear them.

What we’re going to be making:

  • A simple memory match game, the goal is for the player to overturn a number of cards and memorize their locations in order to find all the matching sets.
  • A simple but functional menu system to allow the player to configure and play the game.
  • You can play the completed game here.
  • If you’d like to view the completed source code, you can find it here.

What assets you will need:

All the code for this project was written in C# using Unity 5.4, but it should be adaptable to previous versions easily.

  • Card Back textures
  • Card Front textures
  • Card ‘highlight’ texture
  • A background image
  • Anything else you want to use to jazz up the menu screens!

I’ve chosen to use Kenney’s Boardgame Pack to provide the card textures, and this background image by Darkwood67, the highlight sprite I created myself using one of Kenney’s as a base for the shape. I’ve provided all the assets in a downloadable zip file.

I’ve modified Kenney’s images slightly, turned the Joker crown upside down and changed some colours just to make them a bit more easily identifiable for a matching game.

Getting Started: Rough Design

Untitled-1

A memory match game is quite simple, but we still need to make a few decisions before we get started.

We’re going to use a deck of cards that are dealt face-down to play our game. Each of these cards will have an image on their face which will be hidden until they are turned over.

The player must select a number of cards, each one being flipped over as it is selected. When the player has chosen a number of cards equal to the amount they are trying to match, the game will check to see if they have chosen matching cards.

If they have, the cards will remain face-up until the end of the game. Otherwise, they will be flipped back down after a short delay to allow the user a chance to memorize them.

Once all the cards have been matched, the player has won and the game ends.

A few additional features we want:

  • Our cards will be arranged in a grid layout.
  • The user can choose between different grid sizes.
  • And different numbers of cards to match.
  • We will have the ability to show a ‘peek preview’ of the board when the game begins. To give the player a chance to memorize some of the cards.
  • To make it a bit more interesting, we’ll give the player a choice of card backs.

Project Setup

Project setup is pretty simple, we just need a few folders to house our materials, textures and scripts. Note: Capital R in Resources is important and is required in order to be able to load files from the resources system. We will be loading and using the files in those folders at run-time.

  • Resources
    • materials
    • prefabs
    • textures
  • scenes
  • scripts

You’ll want to put all of the textures for the game in the textures folder.

Writing Some Code: Card and GameState Models

First thing we’re going to need is something to represent the state of the two core game components. A card object, and the state of the game itself or the ‘board’. As I’m going to be very roughly adhering to the Model View Controller architecture, we will not be dealing with Unity components at this stage. The code here is only going to be dealing with the data that determines the ‘state’ of the game.

We need to know 2 things about the state of a card, which image is on the face, and whether or not it’s been matched with another card or cards. In addition, we need the ability to compare it with another card to see if we have a successful match.

public class CardModel
{
    // the index of the image on the face of the card
    public int FaceValue { get; set; }

    // true if card has been successfully matched
    public bool MatchedState { get; set; }

    public CardModel () {
        FaceValue = -1;
	MatchedState = false;
    }

    public bool FaceDoesMatch (CardModel otherCard) {
        return FaceDoesMatch (otherCard.FaceValue);
    }

    public bool FaceDoesMatch (int otherFaceValue) {
        return FaceValue == otherFaceValue;
    }
}

The game state itself is a little more complex. There’s a lot more we need to keep track of; the list of cards for a start, we also need to know many cards high and wide the game board is, how many instances of each card exist on the board (and thus how many the player must turn to make a match attempt) as well as some general data we might want to know such as how many match attempts the player has made, and how many of those attempts were successful.

We will also need methods to determine whether or not a card match was successful taking into account how many cards the user needs to match, as well as whether or not the game is ‘complete’, IE all the cards have been matched successfully.


public class GameStateModel {

    public CardModel[] cards;
    public int grid_x, grid_y;
    public int matchLength;
    public int FailedMatchAttempts { get; set; }
    public int TotalMatchAttempts { get; set; }

    public GameStateModel () {
	FailedMatchAttempts = 0;
    }

    public bool MatchWasSuccessful (int[] cardIndexes) {
        CardModel[] cards_to_match = new CardModel[cardIndexes.Length];
	for (int i = 0; i < cardIndexes.Length; i++) {
	    int index = cardIndexes [i];
	    CardModel card = cards [index];
	    cards_to_match [i] = card;
	}

	return MatchWasSuccessful (cards_to_match);
    }

    public bool MatchWasSuccessful (CardModel[] cards_to_match) {

        if (cards_to_match.Length <= 1) {
	    Debug.LogWarning ("GameStateModel: Cannot attempt to match a single card to itself...");
	    return false;
	}

	CardModel baseCard = cards_to_match [0];
	for (int i = 1; i < cards_to_match.Length; i++) {
	    CardModel otherCard = cards_to_match [i];
	    bool doesMatch = baseCard.FaceDoesMatch (otherCard);
	    if (!doesMatch) {
                return false;
	    }
	}

	// if a match was a success, mark them as matched
	for (int i = 0; i < cards_to_match.Length; i++) {
	    CardModel c = cards_to_match [i];
	    c.MatchedState = true;
	}

	return true;
    }

    public bool IsComplete () {
        for (int i = 0; i < cards.Length; i++) {
	    CardModel card = cards [i];
	    if (card.MatchedState == false) {
		return false;
	    }
	}

       return true;
    }
}

Something we can see: The CardView component

The next thing we need to create before writing code to link them together is a view for our Card model. This will control the visual representation of our card.

Based on our design, our card view needs to have a configurable front and back image, as well as the ability to be switch between being displayed face-up and face-down, it will also need to detect mouse clicks and we must be able to ‘lock’ the card in face-up position to represent being successfully matched. It will also be very helpful for us to be able to set the physical size of the card object.

We’re also going to create somewhere to store some global variables we will want to use as defaults when initializing our game. Let’s do that first by creating a GameVars class to house some configuration data we might need in more than one place later.

Here we are going to set the card width and height as well as the length of time it will take the card to turn over. We will be adding more to this class next time.

public class GameVars
{
    // CardView vars
    public const float CardFlipDuration = 0.35f;
    // CardView size
    public const float CardWidth = 1.0f;
    public const float CardAspect = 1.554f;
}

Before we write our CardView component, we’re going to need to set up a couple of materials for it to use.

In your Resources/materials folder, create 3 new materials.

  • card_back
  • card_front
  • selector

Assign a default back and front textures, and the selector texture to these materials.

materials

Our CardView will consist of a couple of different things. A front and back quad, and two components that will handle the turnover and mouseover behaviors. These will be detailed below.

We could build a Prefab object to represent our Card, but as it’s quite simple I’ve chosen to just create the object in code as we can easily ensure the object is created with our default values.

One of the advantages of separating our View logic from our State object and our game logic (which we’ll look at in part 2) is that it’s a lot easier to maintain and test. We can use is straight away by writing a simple test script and see if it’s all working as expected. In addition, it’s more easily copied to another project and re-used for a different game without having to spend much time de-coupling it from unrelated code.

public class CardView : MonoBehaviour
{
    GameObject face;
    GameObject back;
    Renderer faceRenderer;
    Renderer backRenderer;

    private BoxCollider2D c2d;

    CardFlip flipController;

    public int refIdx = 0;
    public bool isMatched = false;

    void Awake() {
        InitGameObject();
    }

    private void InitGameObject() {

        // create front (flipped 180)
	face = GameObject.CreatePrimitive(PrimitiveType.Quad);
	face.name = "face";
	face.transform.parent = transform;
	Quaternion faceRotation = Quaternion.Euler (new Vector3 (0, 180, 0));
	face.transform.localRotation = faceRotation;
	face.transform.localPosition = Vector3.zero;
	faceRenderer = face.GetComponent();

	// create back
	back = GameObject.CreatePrimitive(PrimitiveType.Quad);
	back.name = "back";
	back.transform.parent = transform;
	back.transform.localPosition = Vector3.zero;
	backRenderer = back.GetComponent();

	// create our hitbox for mouseOver detection
	c2d = gameObject.AddComponent();

	// add flip component
	flipController = gameObject.AddComponent();

        // add selector-highlight component
        gameObject.AddComponent();

        // set the default size and aspect of our card
	SetDefaultSize();
        // set default textures
	SetDefaultFaceTexture ();
	SetDefaultDeckTexture ();
    }

    private void SetDefaultSize(){
        // fetch our default size
	Vector2 size = new Vector2 (GameVars.CardWidth, GameVars.CardWidth * GameVars.CardAspect);
	SetSize (size);
    }

    private void SetDefaultFaceTexture () {
        Material mat = Resources.Load ("materials/card_front") as Material;

	if(mat)
	    faceRenderer.sharedMaterial = mat;
    }

    private void SetDefaultDeckTexture () {
        Material mat = Resources.Load ("materials/card_back") as Material;

	if(mat)
	    backRenderer.sharedMaterial = mat;
    }

    private void SetSize(Vector2 size) {
        if (!face || !back) {
	    Debug.LogError ("Error - Card: Card back or front not initialised! Cannot SetSize()");
        }

	// the scale we will use
	Vector3 cardScale = new Vector3 (size.x, size.y, 1f);

	// set local scale for our card quads
	face.transform.localScale = cardScale;
	back.transform.localScale = cardScale;

	// update our hitbox
	c2d.size = cardScale;
    }

    public void SetFaceTexture (Texture2D texture) {
	if (!faceRenderer) {
	    Debug.LogError ("Error - Card: Card back or front not initialised! Cannot SetFaceTexture()");
	}

	faceRenderer.material.SetTexture ("_MainTex", texture);
    }

    public void SetDeckTexture (Texture2D texture) {
    	if (!backRenderer) {
            Debug.LogError ("Error - Card: Card back or front not initialised! Cannot SetBackTexture()");
			}

	backRenderer.material.SetTexture ("_MainTex", texture);
    }

    private void ResetFlipController(){
	if(flipController.enabled){
	    flipController.StopAllCoroutines();
            flipController.enabled = false;
	}
    }

    public void FlipUp(){
        ResetFlipController();
	flipController.targetFacing = 0;
	flipController.enabled = true;
    }

    public void FlipDown(){
        ResetFlipController();
	flipController.targetFacing = 1;
	flipController.enabled = true;
    }

    public void SetMatched() {
        isMatched = true;
    }

    IEnumerator OnMouseDown () {

        if(isMatched)
	    yield break;

	// we will re-visit this method in Part 2

	yield return null;
    }
}

Note: One of the cool things you can do with OnMouseDown that I’ve used here, is use it as a Coroutine rather than just a regular method. This can be extremely useful. Although we won’t actually do anything with it until part 2 when we create the game manager, as this function is used to inform the game manager that the card has been selected.

This next script will handle flipping our card face-up and face-down. As we want it to function as a fire-and-forget, I’ve used a simple co-routine to animate the object that disabled the object on completion. We simply set whether we want the card to be facing up or down, and enable the script.

The actual logic for flipping the card is also quite simple. We pull the duration from our global variables class, determine whether we want to be face-up or face-down, then simply Lerp our Y axis rotation until it matches.

“timer/duration” gives us a value between 0.0 and 1.0 representing our progress through the animation. Which is then used as the Time input for our Lerp. Then we simply increment ‘timer’ using Time.deltaTime.

public class CardFlip : MonoBehaviour {

    private float duration = GameVars.CardFlipDuration;
    private int _targetFacing = 0; // 0 == from DOWN to UP
		                   // 1 == from UP to DOWN

    public int targetFacing
    {
        private get { return _targetFacing; }
	set
	{
	    if (value == 0 || value == 1)
	        _targetFacing = value;
	    else
	    {
	        Debug.LogWarning("WARNING: Card _targetFacing must be 1 or 0, defaulting to 0.");
	        _targetFacing = 0;
	    }
        }
    }

    // add disabled and enable manually
    void Awake () {
        this.enabled = false;
    }

    void OnEnable ()
    {
        StartCoroutine(TurnOverAnimation());
    }

    private IEnumerator TurnOverAnimation(){

        float targetYRotation = targetFacing == 0 ? 180f : 0f;
	float eulerY = transform.localEulerAngles.y;
	float timer = 0f;
	while (timer < duration) {
	    timer += Time.deltaTime;
	    float timeStep = (timer / duration);
	    eulerY = Mathf.Lerp (eulerY, targetYRotation, timeStep);

            transform.localEulerAngles = new Vector3 (transform.localEulerAngles.x, eulerY, transform.localEulerAngles.z);

	    yield return 0;
	}

	yield return 0;
	enabled = false;
    }
}

And our last script will control whether or not our card is highlighted, this is used when the user’s mouse is over the card.

public class CardSelector : MonoBehaviour {

    private Transform selectorTransform;
    private Renderer selectorRenderer;
    private Collider2D c2d;

    void Awake () {
        GameObject selectorObject = GameObject.CreatePrimitive(PrimitiveType.Quad);
	selectorObject.name = "selector";
	selectorTransform = selectorObject.transform;
	selectorTransform.parent = gameObject.transform;
	selectorTransform.localPosition = Vector3.zero;

	// create our hitbox for mouseOver detection
	c2d = gameObject.AddComponent();

	// find our renderer
	selectorRenderer = selectorObject.GetComponent();

	// apply our material
	selectorRenderer.sharedMaterial = Resources.Load("materials/selector") as Material;

	// resize to match our card
	SetDefaultSize();

	// turned off by default
	HideSelector();
    }

    private void SetDefaultSize () {

	// fetch our default size
	Vector2 size = new Vector2 (GameVars.CardWidth, GameVars.CardWidth * GameVars.CardAspect);
	SetSize (size);
    }

    public void SetSize (Vector2 size) {
        if (!selectorRenderer) {
	    Debug.LogError ("Error - Selector: Selector not initialised! Cannot SetSize()");
	}

	// the scale we will use
	Vector3 cardScale = new Vector3 (size.x, size.y, 1f);

	// set local scale for our quad
	selectorTransform.localScale = cardScale;
    }

    public void ShowSelector(){
        selectorRenderer.enabled = true;
    }

    public void HideSelector(){
        selectorRenderer.enabled = false;
    }

    void OnMouseEnter(){
        ShowSelector ();
    }

    void OnMouseExit(){
        HideSelector ();
    }

    public void SetIgnoreMouseover(bool shouldReact){
        if (c2d != null) {
	    c2d.enabled = shouldReact;
	}
    }
}

I know this has been quite a long tutorial, but I want to end it with something visual and something that actually works. So we’re going to write a little test script so that we can show our card working.

public class TestCard : MonoBehaviour
{

    CardView cardViewScript;

    void Awake()
    {
        // create a test card
        GameObject cardView = new GameObject("card");
        cardViewScript = cardView.AddComponent();
    }

    void OnGUI()
    {
        GUI.BeginGroup(new Rect(10, 10, 150, 500));

        if(GUILayout.Button("Flip Up"))
        {
            cardViewScript.FlipUp();
        }

        if(GUILayout.Button("Flip Down"))
        {
            cardViewScript.FlipDown();
        }

        GUI.EndGroup();
    }
}

flippy

That’s all for this time. In part 2, we’ll look at creating the game board and creating a management class which will control the game state so we can actually play it.

Advertisements
This entry was posted in Tutorials. Bookmark the permalink.

8 Responses to Memory Match Game in Unity3D – Part 1

  1. Artist says:

    Cannot get this to work.
    Assets/scripts/CardView.cs(41,9): error CS0029: Cannot implicitly convert type `CardFlip’ to `UnityEngine.BoxCollider2D’

    Like

    • diablobasher says:

      Hi! Sorry for the late response, there’s an error:

      // create our hitbox for mouseOver detection
      c2d = gameObject.AddComponent();

      // add flip component
      flipController = gameObject.AddComponent();

      c2d should be AddComponent and flipController should be the CardFlip.

      Switch those and it should work. I’ve updated the post.

      Cheers.

      Like

  2. Can says:

    i liked that project, good work 🙂
    what if we want to add 8 cards by height, max option is 7 i think

    Like

    • diablobasher says:

      Thanks!

      You can absolutely have more cards than 7, I think I just set that because it fit comfortable on the screen.

      Just change the maximum number, this should support whatever you like (within reason)

      Like

      • Can says:

        i think i have wrong version of unity because when i hit play on unity, i have 2 UI which one is large and one is small.. maybe from canvas options..

        Like

      • diablobasher says:

        That might be something to do with your camera. Make sure your Canvas is set to Screen Spacer and not World Space. Then it shouldn’t be visible in the world anymore.

        Like

      • Can says:

        i dont know whats the problem i cloned your copy from github, maybe something wrong with unity version, heres the link; http://imgur.com/a/t8mKN

        Like

      • diablobasher says:

        Hmm that does look odd, looks almost like you’re seeing scene view objects in the game view.

        Try starting a new project and re-importing it, if that doesn’t fix it it could be something odd with your installation, I do occasionally see things that require a re-install of Unity to fix but not often.

        If that doesn’t fix it, it might be worth posting on Unity’s support forums.

        Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s