Memory Match Game in Unity3D – Part 4

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

In part 1 and part 2 we covered making the memory match mechanics, and in part 3 we started the user interface. Here we are going to continue with the UI by making the deck selection/preview window and the options menu that will allow users to control the size and difficulty of the game grid.

mm_deckbacking.gif

Config Data Structure

First thing we’re going to need is a data structure that will hold our configuration values so that the UI can pass them up to the game manager when we create our game board, and some default values to fall back to. This may seem like an unnecessary extra step, but it helps us to separate our UI logic from our actual game logic, and that’s going to make it a lot easier to make additions or changes in the future.

First, create a new class called GameStateModelConfig, this is a very simple class which essentially acts as a container for the values we need in order to set up our game. Desired grid width and height, match length, deck texture reference and whether or not to show the card ‘peek’ animation.

    public class GameStateModelConfig {
	// grid size
	public int grid_x;
	public int grid_y;

	// number of cards required for a match
	public int matchLength;

	// whether or not to show the 'peek' animation at start
	public bool showPeekAnimation;

	//the index of the selected card backing
	public int deckTextureIdx;

	public GameStateModelConfig(){
		grid_x = GameVars.GameDefaults_GridX;
		grid_y = GameVars.GameDefaults_GridY;
		matchLength = GameVars.GameDefaults_MatchLength;
		deckTextureIdx = GameVars.GameDefaults_DeckTextureIdx;
		showPeekAnimation = GameVars.GameDefaults_ShowPeekAnimation;
	}
    }

Next, add the following to your GameVars class. By making the configuration value a static variable, and returning a new (default) instance of it if it’s not found, we can access it from anywhere in our program and be safe in the knowledge that if one hasn’t been created we’ll just get the default values instead. We’ll also add our default variables here. You could put them in the config class directly, but I find it easier to put them here.

    // Current game state (if this is null, defaults will be used)
    private static GameStateModelConfig _globalConfig;
    public static GameStateModelConfig GameStateConfig {
        get{
	    // if our config hasn't been set up, create a default one
            return _globalConfig == null ? new GameStateModelConfig() : _globalConfig;
	}
	set {
	    _globalConfig = value;
	}
    }

    // Game defaults
    public const int GameDefaults_GridX = 2;
    public const int GameDefaults_GridY = 2;
    public const int GameDefaults_MatchLength = 2;
    public const int GameDefaults_DeckTextureIdx = 0;
    public const bool GameDefaults_ShowPeekAnimation = true;

    // Max grid sizes
    public const int MinGridX = 2;
    public const int MaxGridX = 7;
    public const int MinGridY = 2;
    public const int MaxGridY = 6;

Deck Preview Code

The code for this is fairly straightforward. We keep track of a minimum, maximum and current index, and the current texture as well as a RawImage UI element to display it with.

In Initialize() we fetch our initial deck texture, and apply it to the raw image element, and query our texture manager to see how many deck images we have access to.

‘FetchDeckImage()’ is also pretty simple, it’s just a helper function for getting the texture that corresponds to our ‘deckIndex’ and applying it with ‘UpdatePreviewImage()’.

OnNextClicked() and OnPrevClicked() will be hooked into buttons when we create the UI below.

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class MenuDeckPreview : MonoBehaviour
{
    int max, min, deckIndex = 0;
    public RawImage rawImage;
    private Texture2D t2dPreviewImage;

    void Awake()
    {
 	Initialize();
    }

    public int GetDeckIndex()
    {
	return deckIndex;
    }

    void Initialize()
    {
 	// find our upper and lower boundaries
	CardTexturesManager texMan = CardTexturesManager.GetInstance();
	int maxDeckTextures = texMan.GetNumDeckTextures();
	deckIndex = 0;
	min = 0;
	max = maxDeckTextures;

	// fetch the initial image
	FetchDeckImage();
    }

    void FetchDeckImage()
    {
	int idx = deckIndex;
	CardTexturesManager texMan = CardTexturesManager.GetInstance();
	Texture2D t2d = texMan.GetDeckTexture(idx);
	if (t2d != null)
	{
            t2dPreviewImage = t2d;
	    UpdatePreviewImage();
	}
    }

    void UpdatePreviewImage()
    {
	if (rawImage != null && t2dPreviewImage != null)
	{
   	    rawImage.texture = t2dPreviewImage;
	}
    }

    public void OnNextClicked()
    {
	deckIndex = Mathf.Clamp(deckIndex + 1, min, max);
	FetchDeckImage();
    }

    public void OnPrevClicked()
    {
	deckIndex = Mathf.Clamp(deckIndex - 1, min, max);
	FetchDeckImage();
    }
}

Deck Preview Panel Setup

Create a new UI Panel inside of our ConfigPanel called DeckPreviewPanel, with dimensions of Left:305, Top:20, Right:15, Bottom:125

Create a new UI->RawImage element inside that, set its dimensions to an appropriate size for the deck images. In this case the element will be anchored to center, and offset by 30 in Y with a Width and Height of 121 by 153.

Create two buttons underneath the raw image for navigation, one for previous and one for next. I’ve set their text to ”, size 22 bold, using the default Arial font, but you could make a fancier button! Their positions are set as follows:

  • Left/Previous: Anchored Center, offset by -40 X and -80 Y, Width and Height set to 42 by 48.
  • Right/Next: Anchored Center, offset by 440 X and -80 Y, Width and Height set to 42 by 48.

Once they are set up, wire up the two buttons to their corresponding functions in the MenuDeckPreview script. OnNextClicked() and OnPrevClicked(). Don’t forget to attach our RawImage element to the script, either and save your prefab changes by selecting your root menu object and hitting ‘apply’.

mm_4_previewpanel.png Options Panel Code

The code for our config menu is a little more complex, but not much.

First we have values to track the selected width, height, match length and whether or not to show the peek animation. These are handled locally so that we can apply them to the GameVars config variable after they’ve been changed. Then we have a reference to our deck preview script, and some UI elements which we’ll need to make the script aware of: two sliders, a dropdown and a toggle. Lastly we have two text labels that we’ll use to display the grid width and height.

SetMenuConstraintsAndDefaults() is straightforward, it steps through each of these and initialises them to the defaults we declared at the start of this tutorial, then applies those values to the associated UI elements.

RecalculateMatchLengthOptions() re-constructs the dropdown box so that it only contains valid options for the currently selected grid size. For example, if we have a grid size of 3×3, we don’t want to allow a match length of 2 or 4, because 9 isn’t divisible by either.

ValueWasUpdated() is called every time the user changes a UI element, and updates the stored values as well as our GameVars configuration, it also fetches the selected deck texture index from our MenuDeckPreview script.

UpdateMatchLengthSelection() is just a helper function which parses the string into an integer and updates it. UpdateLabels() just updates our UI label elements to reflect the changed settings.

Finally, we have some methods that our UI elements will call when the user interacts with them: SetGrid_X(), SetGrid_Y(), SetMatchLength() and SetShowPeekAnimation() which simply update the associated value and then calls ValueWasUpdated() so our config is updated as well.

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

    public class MenuGameConfigurator : MonoBehaviour {

        // internal vars
        private int gridX;
        private int gridY;
        private int matchLength;
        private bool showPeek;

        // separate script for deck preview
        public MenuDeckPreview deckPreview;

        // ui elements
        public Slider grid_x_slider;
        public Slider grid_y_slider;
        public Dropdown match_length_dropdown;
        public Toggle peek_toggle;

        // match length dropdown optiondata
        private List match_length_options;

        // labels
        public Text lbl_grid_x;
        public Text lbl_grid_y;

        // Use this for initialization
        void Awake () {
            SetMenuConstraintsAndDefaults();
        }

        void SetMenuConstraintsAndDefaults() {
        	// gather min/max values for these options and set them
	    int min_x = GameVars.MinGridX;
	    int max_x = GameVars.MaxGridX;
	    int default_x = GameVars.GameDefaults_GridX;

	    int min_y = GameVars.MinGridY;
	    int max_y = GameVars.MaxGridY;
	    int default_y = GameVars.GameDefaults_GridY;

	    // min/max/default for grid width
	    grid_x_slider.minValue = min_x;
	    grid_x_slider.maxValue = max_x;
	    grid_x_slider.value = default_x;
	    gridX = default_x;

	    // min/max/default for grid height
	    grid_y_slider.minValue = min_y;
	    grid_y_slider.maxValue = max_y;
	    grid_y_slider.value = default_y;
	    gridY = default_y;

	    // update labels to match the sliders
	    lbl_grid_x.text = grid_x_slider.value.ToString();
	    lbl_grid_y.text = grid_y_slider.value.ToString();

	    // peek toggle
	    showPeek = GameVars.GameDefaults_ShowPeekAnimation;
	    peek_toggle.isOn = showPeek;

	    // set initial values for our match length dropdown
	    RecalculateMatchLengthOptions();

	    // initialise default config
	    UpdateConfig();
        }

        void RecalculateMatchLengthOptions ()
        {
	    match_length_options = new List();

	    int totalCards = gridX * gridY;

	    if (totalCards % 2 == 0)
	    {
                match_length_options.Add(new Dropdown.OptionData("2"));
	    }

	    if (totalCards % 3 == 0)
	    {
	        match_length_options.Add(new Dropdown.OptionData("3"));
	    }

	    if (totalCards % 4 == 0 && totalCards > 4)
	    {
	        match_length_options.Add(new Dropdown.OptionData("4"));
	    }

	    if (totalCards % 5 == 0 && totalCards > 5)
	    {
	        match_length_options.Add(new Dropdown.OptionData("5"));
	    }

	    match_length_dropdown.options = match_length_options;
	    UpdateMatchLengthSelection(0); // set to first options

	    UpdateConfig();
        }

        // helper method for calling everything we need on a value update
        void ValueWasUpdated()
        {
            RecalculateMatchLengthOptions();
	    UpdateLabels();
	    UpdateConfig();
        }

        // update config from out internal variables
        public void UpdateConfig()
        {
            // Configure
	    GameStateModelConfig cfg = new GameStateModelConfig();
	    if (gridX > 0)
	        cfg.grid_x = gridX;
	    if (gridY > 0)
	        cfg.grid_y = gridY;
	    if (matchLength > 1)
	        cfg.matchLength = matchLength;

	    if (deckPreview != null)
	        cfg.deckTextureIdx = deckPreview ? deckPreview.GetDeckIndex() : 0;

            //  card peek animation
	    cfg.showPeekAnimation = showPeek;

	    // Initialize global vars
	    GameVars.GameStateConfig = cfg;
        }     

        void UpdateMatchLengthSelection(int dropdownValue)
        {
	    // update the selection itself
	    string selected = match_length_dropdown.options[match_length_dropdown.value].text;
	    int selectedMatchLen = 2;
	    if (!int.TryParse(selected, out selectedMatchLen))
	    {
	        Debug.LogError("Could not parse match length string...");
	    }

	    matchLength = selectedMatchLen;
        }

        // should be called AFTER a variable is updated
        void UpdateLabels()
        {
            if (lbl_grid_x != null)
	    {
                lbl_grid_x.text = gridX.ToString();
	    }

	    if (lbl_grid_y != null)
	    {
	        lbl_grid_y.text = gridY.ToString();
	    }
        }

        // UI interaction points		

        public void SetGrid_X(float value){
	    gridX = (int)value;
	    ValueWasUpdated();
        }

        public void SetGrid_Y(float value){
	    gridY = (int)value;
	    ValueWasUpdated();
        }

        public void SetMatchLength(int value){
	    UpdateMatchLengthSelection(value);
	    ValueWasUpdated();
        }

        public void SetShowPeekAnimation()
        {
	    showPeek = peek_toggle.isOn;
	    ValueWasUpdated();
        }
    }

Attach this script to our ConfigPanel ui element, now we can move on to setting up the menu itself.

Creating and wiring up the Options Panel

In our ConfigPanel underneath our DeckPreviewPanel, create a UI > Slider element. Leave it anchored to the center and set the position to -80 x, and 140 y. You’ll also need to add two text elements to it, call these NameLabel and ValueLabel, and set their positions to X:0, Y:10 and X:140, Y:10. These name labels will be used to display the name of our option elements. Duplicate this setup, and move it beneath the previous one.

Set their name labels to ‘Grid Width’, ‘Grid Height’ and their ‘value label’s to ‘2’ as this will be their initial value. We also need to adjust the settings on our Slider scripts, make sure that the slider is using whole numbers by clicking that toggle.

In the same way that buttons can initiate a callback function when clicked, sliders can initiate one when their value is changed. We’ll want to do this for both sliders, by calling the corresponding functions on the ConfigPanel’s MenuGameConfig script: SetGridX, SetGridY.

Next, we need a UI > Dropdown element to house our Match Length options. The reason we’re not going to use a slider for these is because they’ll need to be re-calculated each time the user changes the grid size, as the total number of cards in play must be divisible by the match length, or the game will break.

Add a Text element to match our slider NameLabel elements, set the Y position to 20, and the text to “Match Length”. Add an entry for ‘OnValueChanged’ pointing to our ConfigPanel’s MenuGameConfig script SetMatchLength function.

Last thing we need is a checkbox to allow the user to turn the card ‘peek’ animation on or off, so add a UI > Toggle element, position it underneath the match length dropdown, and set its text to ‘Card Peek’. Same as previously, add an entry for OnValueChanged, set to our ConfigPanel’s MenuGameConfig script SetShowPeekAnimation function.

Finally we can make our MenuGameConfig script aware of these elements, by dragging the the corresponding elements into the inspector window as follows. You’ll want to set ‘Lbl_grid_x’ and ‘Lbl_grid_y’ to the value labels for those sliders.

mm_4_configpanel

Don’t forget to update the prefab by hitting ‘apply’ on the root menu object again.

Bootstrapper and finishing touches.

Our memory match game is almost complete now, we just need to write a small script that will start an instance of our game based on the configuration set by our options menu. The script won’t need to be attached to anything and will handle instantiation itself on a dummy object that it’ll remove once it has finished.

InitiateBootstrapping() starts everything off, it creates a dummy object and attaches the script to it. ExitGameInProgress() is called when we quit a game, and just clears up the game manager.

Startup() checks to see if a game is already in progress, and if not, begins the game.

using UnityEngine;
using System.Collections;

public class CardMatchGameBootstrapper : MonoBehaviour {

	GameManager _gameManager;

	// Use this for initialization
	void Awake () {
		_gameManager = new GameManager();
		StartCoroutine (Startup ());
	}

	public static void InitiateBoostrapping(){
		GameObject bootstrap = new GameObject("bootstrap");
		bootstrap.AddComponent();
	}

	public static void ExitGameInProgress(){
		GameObject root = GameObject.Find(GameVars.RootObjectName);
		if(root != null)
		{
			GameObject.Destroy(root);
		}
	}

	IEnumerator Startup () {

	    // check we don't already exist...
	    GameObject preExistingRoot = GameObject.Find(GameVars.RootObjectName);
	    if (preExistingRoot != null) {
		// There's already a game in progress... We shouldn't be here! Bail!!
		Debug.LogError("Error: Cannot start Card Match Game, another instance exists!");
            } else {
	        // Start our game!

		// create root object
		GameObject root = new GameObject();
		root.name = GameVars.RootObjectName;

		GameVars.GameManager = _gameManager;

		// Initialize
		yield return StartCoroutine(_gameManager.Initialize());

		// Start Game
		yield return  StartCoroutine(_gameManager.RevealGameFieldAndStartGame());
	    }

	// Cleanup this object
	Destroy(gameObject);
    }
}

Now that this is done, we can go back into our MenuController script and uncomment the two lines that reference our bootstrapper.

    private void StartNewGame(){
	CardMatchGameBootstrapper.InitiateBoostrapping();
    }

    private void CleanupGameInProgress(){
        CardMatchGameBootstrapper.ExitGameInProgress();
    }

Now that we’re actually passing a configuration object around, it’s time to make some changes to our GameManager script to account for those as well.

    // setup the default game model or fetch the configured one
    GameStateModelConfig cfg = GameVars.GameStateConfig != null ? GameVars.GameStateConfig : new GameStateModelConfig();
    state = new GameStateModel();
    state.grid_x = cfg.grid_x;
    state.grid_y = cfg.grid_y;
    state.matchLength = cfg.matchLength;

We also need to update our GameCompletionSequence to be aware of our menu screen transitions.

IEnumerator GameCompletionSequence()
    {
        yield return 0;

        // Cleanup! This is temporary so you can 'play' the game at least!
        GameObject.Destroy(GameObject.Find(GameVars.RootObjectName));

        // Trigger the completion screen
        MenuController.RequestTransition(Screens.COMPLETION_UI, null);

    }

This completes our Memory Match game!

Press play and have some fun!

menu_transitions

We now have a completed project, including a splash screen, title screen, options menu and the memory match game itself. All of these things can be quite easily polished and/or upgraded from here, too. So have a play around and see what you can add.

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

2 Responses to Memory Match Game in Unity3D – Part 4

  1. Let me be the first to say job well done. Only criticism, get better SEO so people don’t have to look at pages of crap on google before they find your tut! 😊👍 Keep it up.

    Liked by 1 person

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