[ Game / Mini Tutorial ] King or Slave (Memory Game)

Hello everyone!

After finishing the Number Wizard UI section of the course, I decided to challenge myself and create a simple memory game since it only involves buttons, images and texts just like in Number Wizard UI. I thought it was a piece of cake so I decided to give it a go. Damn, there were lots of things needed that I didn’t knew! Fortunately, Google was very helpful and gave me the code snippets I needed to complete the game. I’m glad I did this project because I learned a ton. Let me first show the game and then later, I will discuss how to I created it. Hopefully, this can benefit someone.

Note: Images are not mine. They were not used for anything except as placeholders to finish this mini game project exercise. They are taken from:
http://www.dota2.com/heroes/
https://dota2.gamepedia.com/Dota_2_Wiki

I hope I didn’t violate any copyright. Fingers crossed.

Title Scene:

Intro Scene:

Game Scene:

Victory Scene:

Defeat Scene:

Here’s the entire gameplay code:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class Gameplay : MonoBehaviour
{
    // value corresponds to index in sprites array
    private const int ALCHEMIST = 0;
    private const int LIFESTEALER = 1;
    private const int PANGOLIER = 2;
    private const int TERRORBLADE = 3;
    private const int DEATH_PROPHET = 4;
    private const int CLOCKWERK = 5;
    private const int EARTH_SPIRIT = 6;
    private const int GRIMSTROKE = 7;

    [SerializeField] private int maxTries = 10;
    [SerializeField] private Image healthBar = null;
    [SerializeField] private Sprite[] sprites = null;
    [SerializeField] private Button[] tiles = null;

    private int[] tileArrangement = { 
        ALCHEMIST, ALCHEMIST, 
        LIFESTEALER, LIFESTEALER, 
        PANGOLIER, PANGOLIER, 
        TERRORBLADE, TERRORBLADE,
        DEATH_PROPHET, DEATH_PROPHET,
        CLOCKWERK, CLOCKWERK,
        EARTH_SPIRIT, EARTH_SPIRIT,
        GRIMSTROKE, GRIMSTROKE
    };

    private int indexOfCurr1stClick = -1;
    private int indexOfCurr2ndClick = -1;

    // logically this should be false as when the game starts, there are no matching tiles but 
    // i have to make this true so in the OnTileClicked(), the code inside "if(!match)" won't be 
    // executed until at a later time
    private bool match = true;

    // Needed since indexOfCurr1stClick and indexOfCurr2ndClick will be reset to -1 after every 2 tiles 
    // are opened/clicked. If the 2 tiles are not a match, they need to be closed (hide the images) and 
    // their indexes have to be remembered and thus, the 2 variables below
    private int indexOfPrev1stClick = -1;
    private int indexOfPrev2ndClick = -1;

    private float hpBarOrigHeight = 0;
    private int triesLeft = 0;
    private int correctTiles = 0;

    // Start is called before the first frame update
    void Start()
    {
        // get height of health bar Image
        hpBarOrigHeight = healthBar.GetComponent<RectTransform>().rect.height;

        triesLeft = maxTries;

        ShuffleTileArrangement();
        SetupTiles();
    }

    // Fisher–Yates modern shuffle
    // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm
    private void ShuffleTileArrangement()
    {
        for(int i = 0; i <= tileArrangement.Length - 2; ++i)
        {
            int randomIndex = Random.Range(i, tileArrangement.Length);

            // swap
            int temp = tileArrangement[i];
            tileArrangement[i] = tileArrangement[randomIndex];
            tileArrangement[randomIndex] = temp;
        }
    }

    private void SetupTiles()
    {
        for(int i = 0; i < tiles.Length; ++i)
        {
            SetImagesOnTiles(i);
            ShowHideImagesOnTiles(i, false);
            AddOnClickListeners(i);
        }
    }

    private void SetImagesOnTiles(int i)
    {
        Sprite sprite = null;

        switch (tileArrangement[i])
        {
            case ALCHEMIST:
                sprite = sprites[ALCHEMIST];
                break;

            case LIFESTEALER:
                sprite = sprites[LIFESTEALER];
                break;

            case PANGOLIER:
                sprite = sprites[PANGOLIER];
                break;

            case TERRORBLADE:
                sprite = sprites[TERRORBLADE];
                break;

            case DEATH_PROPHET:
                sprite = sprites[DEATH_PROPHET];
                break;

            case CLOCKWERK:
                sprite = sprites[CLOCKWERK];
                break;

            case EARTH_SPIRIT:
                sprite = sprites[EARTH_SPIRIT];
                break;

            case GRIMSTROKE:
                sprite = sprites[GRIMSTROKE];
                break;
        }

        // https://answers.unity.com/questions/464616/access-child-of-a-gameobject.html
        // get the child Image of a Button
        GameObject childImg = tiles[i].transform.GetChild(0).gameObject;

        // https://forum.unity.com/threads/solved-changing-ui-image-with-script.440347/
        // from the child Image, get the Image script component and assign sprite as the Source Image
        childImg.GetComponent<Image>().sprite = sprite;
    }

    private void ShowHideImagesOnTiles(int i, bool show)
    {
        // https://answers.unity.com/questions/464616/access-child-of-a-gameobject.html
        // get the child Image of a Button
        GameObject childImg = tiles[i].transform.GetChild(0).gameObject;

        // disable or enable the child Image object to make it disappear or show on screen
        childImg.SetActive(show);
    }

    private void AddOnClickListeners(int i)
    {
        // https://forum.unity.com/threads/addlistener-and-delegates-i-think-im-doing-it-wrong.413093/
        int iCopy = i;

        // https://answers.unity.com/questions/1288510/buttononclickaddlistener-how-to-pass-parameter-or.html
        // added onClick listeners so that each button can listen to click events and it is done in code 
        // so I can pass a parameter (index of the button clicked) to the click handler function: 
        // OnTileClicked()
        tiles[i].onClick.AddListener(delegate { OnTileClicked(iCopy); });
    }

    private void OnTileClicked(int i)
    {
        // "open" a tile by showing its image
        ShowHideImagesOnTiles(i, true);

        // a) at the start of the game, no tile has been clicked yet so indexOfCurr1stClick is -1
        // b) after every 2 tiles opened, indexOfCurr1stClick is reset to -1
        if (indexOfCurr1stClick == -1)
        {
            // at the start of the game, match is true so skip, else errors (array index out of bounds) will 
            // occur since indexOfPrev1stClick and indexOfPrev2ndClick are -1
            if (!match)
            {
                // make the buttons clickable again since the last 2 clicked tiles were not a 
                // match
                tiles[indexOfPrev1stClick].GetComponent<Button>().interactable = true; // get Button script component of this Button object and set Interactable to false
                tiles[indexOfPrev2ndClick].GetComponent<Button>().interactable = true;

                // "close" the last 2 clicked tiles by hiding their images
                ShowHideImagesOnTiles(indexOfPrev1stClick, false);
                ShowHideImagesOnTiles(indexOfPrev2ndClick, false);
            } // else if it is a match, leave them opened and don't do anything

            // save the index of the 1st clicked tile
            indexOfPrev1stClick = indexOfCurr1stClick = i;

            // make this button "unclickable" so even if the player clicks an already opened tile (image is showing), 
            // it will not mess up the game
            // get Button script component of this Button object and set Interactable to false
            tiles[indexOfCurr1stClick].GetComponent<Button>().interactable = false;
        } 
        
        else
        {
            // at this point, indexOfCurr1stClick is not -1 which means a 1st tile has been clicked 
            // and the current click is for the 2nd tile
            if (indexOfCurr2ndClick == -1)
            {
                // save the index of the 2nd clicked tile
                indexOfPrev2ndClick = indexOfCurr2ndClick = i;

                // make this button "unclickable" so even if the player clicks an already opened tile (image is showing), 
                // it will not mess up the game
                // get Button script component of this Button object and set Interactable to false
                tiles[indexOfCurr2ndClick].GetComponent<Button>().interactable = false;

                // compare the 2 tiles
                if (tileArrangement[indexOfCurr1stClick] == tileArrangement[indexOfCurr2ndClick])
                {
                    Debug.Log("match");
                    match = true;

                    correctTiles += 2;

                    // you won the game if you opened all tiles before consuming all tries
                    if(correctTiles >= tiles.Length)
                    {
                        SceneManager.LoadScene("VictoryScene");
                    }
                }

                else
                {
                    Debug.Log("didn't match");
                    match = false;

                    // https://answers.unity.com/questions/988686/recttransform-how-to-change-height.html
                    // get Rect Transform component of health bar Image
                    RectTransform rt = healthBar.GetComponent<RectTransform>();

                    // don't forget to set pivot point (anchors option) to "bottom center" so height 
                    // will decrease only from 1 direction (from the top)

                    // decrease height of health bar
                    rt.sizeDelta = new Vector2(rt.sizeDelta.x, 
                        rt.sizeDelta.y - (hpBarOrigHeight / maxTries));


                    --triesLeft;

                    // you lost if all tries have been consumed before opening all tiles
                    if(triesLeft <= 0)
                    {
                        SceneManager.LoadScene("DefeatScene");
                    }
                }

                // reset to match a new pair of tiles
                indexOfCurr1stClick = indexOfCurr2ndClick = -1;
            }
        }
    }
}

Hierarchy setup:

Scene setup:

scene

Gameplay script in Inspector:

BASICS

  1. get component of an object:
// get height of health bar Image
hpBarOrigHeight = healthBar.GetComponent<RectTransform>().rect.height;

get component

// https://forum.unity.com/threads/solved-changing-ui-image-with-script.440347/
// from the child Image, get the Image script component and assign sprite as the Source Image
childImg.GetComponent<Image>().sprite = sprite;

get component 2

  1. get child object:
// https://answers.unity.com/questions/464616/access-child-of-a-gameobject.html
// get the child Image of a Button
GameObject childImg = tiles[i].transform.GetChild(0).gameObject;

get child

  1. disable / enable a Game Object:
// disable or enable the child Image object to make it disappear or show on screen
childImg.SetActive(show);

enable

  1. add onClick listener via code to pass parameters:
// https://forum.unity.com/threads/addlistener-and-delegates-i-think-im-doing-it-wrong.413093/
// fix a "bug" in delegates
int iCopy = i;

// https://answers.unity.com/questions/1288510/buttononclickaddlistener-how-to-pass-parameter-or.html
// added onClick listeners so that each button can listen to click events and it is done in code
// so I can pass a parameter (index of the button clicked) to the click handler function: 
// OnTileClicked()
tiles[i].onClick.AddListener(delegate { OnTileClicked(iCopy); });

I don’t know how to pass a parameter via:

onClick

  1. make a button clickable or not:
// make this button "unclickable" so even if the player clicks an already opened tile (image is showing), 
// it will not mess up the game
// get Button script component of this Button object and set Interactable to false
tiles[indexOfCurr1stClick].GetComponent<Button>().interactable = false;

interactable

  1. create a simple health bar:
// https://answers.unity.com/questions/988686/recttransform-how-to-change-height.html
// get Rect Transform component of health bar Image
RectTransform rt = healthBar.GetComponent<RectTransform>();

// don't forget to set pivot point (anchors option) to "bottom center" so height 
// will decrease only from 1 direction (from the top)

// decrease height of health bar
rt.sizeDelta = new Vector2(rt.sizeDelta.x, 
                           rt.sizeDelta.y - (hpBarOrigHeight / maxTries));

set the pivot point via (hold shift):

pivot

Privacy & Terms