Memory Match Game in Unity3D – Part 3

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

What you’ve built with part 1 and part 2 comprises the bulk of the game, and you could wrap whatever UI you wanted around it and call it done. But I wanted to go through the process of creating the UI for customizing the size and difficulty of the game as well as transitions between screens here.

What screens do we need? how to transition between them?

First thing we need to do is design a simple menu system that we want to use, and break it up into the individual screens we’re going to need. A relatively simple task for such a simple game, but it’s useful to have it defined before you start laying down any code or creating Unity prefabs.

We only need a very basic setup, we need to be able to start a new game, exit a game in progress, choose whether to play another game or exit on completion of a game, and configure our game. With those things in mind, we will use the following screens to achieve this functionality.

  • Splash Screen
    • Not strictly necessary, but will serve as an entry point to our application
  • Main Menu
    • From the main menu we can access our options menu, or start a new game.
  • Options Screen
    • From our options screen we can configure our game, and return to the main menu.
  • Gameplay Screen
    • From this screen, we can quit and exit back to the main menu.
  • Game Completion Screen
    • This screen is triggered when a game is finished, from here we can choose to start another game or return to the main menu.

We also need to know how we’re going to be moving from one screen to the next. There are so many ways you can achieve this, we could slide our menus on/off the screen, we could fade the camera to a colour and then swap our screens before fading out again, we could produce some custom shader or animation (this is the one I was originally going to use for going in/out of game, but I wanted to simplify this section a little bit), anything you want!

We are just going to keep things simple, and use a sliding transition for our screens. We will pick a direction and the current screen will slide off-screen in that direction, with the screen we want to transition to following behind it, like this.

menu_transitions

Creating a menu prefab

Unity’s UI system requires objects to be ordered in a very specific way, but fortunately it provides tools for creating these objects within the editor. We could manually create all the gameobjects we need and add the associated scripts to them with code, but for UI it’s a heck of a slog, and unnecessary when we can use one simple prefab to house our UI canvas.

To create a UICanvas, selected GameObject->UI->Canvas, and Unity will create everything we need to get started. You will notice that it also creates an object called ‘EventSystem’, this is needed by Unity in order for the UI to function. For now, drag the ‘EventSystem’ object onto the new canvas object to parent it, and re-name the UI to something that makes sense. I’ve called it “menu”. Then, drag the menu object into your Resources->prefabs folder to create a base prefab for us to work from.

mmprefab_create

Last thing we need to do before creating our screens is configure the UICanvas. There’s quite a lot of settings here, and you may want to reference the documentation to see what each of the settings does. As these settings will work for our purposes, but may not be appropriate for a different project.

mm_canvas_settings

Creating the basic screens

For each of our screens, we will need to add a ‘panel’ object to our canvas. We can do this by right-clicking on our menu object, and selecting UI->Panel. We’re going to then create another panel parented to that first panel.

This might seem a little odd, but it will make sense when we come to do our screen transitions. The first panel is just an anchor to build our screen on, so that it will be easier to move around. The second panel will actually house all the visible elements on that screen. On the first panel, as we don’t need it to display anything, remove the ‘image’ script that comes pre-attached. Then, rename the panel something that makes sense, and repeat the process for each screen.

Be sure to update the prefab by clicking ‘apply’ in the inspector window.

mm_panelscreate

Now we’re going to create the basic layout of our screens by adding text, images and buttons. While working on each screen it can be helpful to disable the rest, but remember to enable all the screens again before you save the prefab.

Main Menu (Title Screen)

First, set our sub-panel’s anchor point to middle-center. This can be done by clicking the anchor-presets menu, then holding ‘alt’ and clicking the middle-center preset.

mm_set_anchorpoint

Then set its y-position to -125 to put it just below our center line.

Next, add two buttons to the panel by right clicking and selecting UI->Button. Name these ‘btn_Play’ and ‘btn_Options’ these will be used to trigger menu transitions to the game and options screens. Each button has a ‘text’ object attached to it, you’ll want to edit those to display “Play” and “Options” respectively.

Anchor both buttons to the middle-center of the panel, and then set their y-positions to 35 and -35. Set their width and height to 200 by 60.

Next thing we need to do it spice the screen up a little by adding a title image. You can get as fancy as you want here, but I’m just going to add two ‘Text’ objects and two ‘Raw Image’, these can be added the same way, by right clicking the sub-panel and selecting UI then selecting them from the menu.

Set the text objects to display the strings “MEMORY” and “MATCH”, then set font style to ‘Bold’, font size to 102 and color to white. Increase their width and height until the text is displayed properly and then position them accordingly.

For our RawImage objects, set one texture to a card back, I’ve chosen ‘cardBack_1’, and the other to a card face, I’ve chosen ‘card_1’. Set their width and height to 201 by 284. Then give each one a small z-axis rotation, I’ve set them to -12.5 and -33.5. Then move them into position next to the title.

mm_titlescreen.png

Splash Screen

The splash screen is very simple, just set the sub-panel’s image source to ‘None’, the colour to black (and alpha to 255), then add a text object, anchor it to the middle-center and set whatever size/font size and text you like.

Game Menu

The same menu is also very simple, we only need one button. Set the sub-panel’s anchor to middle-center, and position to (-382.5, 326.5, 0), width and height to (230, 85). Then add a button to it, using the same dimensions as the buttons on the main menu, and set the text to “Exit”.

mm_game_menu

Options Menu

The options menu is by far the most complex part of our UI system, so we will return to the bulk of it later. For now, set it up the same as the game menu we just made, and add an additional panel called ‘ConfigPanel’, anchored to the middle-center, set to position (0, -35, 0) and size (500, 400). This panel will house our options menu and associated script later on.

Completion Menu

Very similar again to our main menu. We need 2 buttons this time labeled “Play Again” and “Exit”. Then add a text object, with the same sizing as the two in the main menu, to display a message that says “You Win!”. Position it slightly up from the center of the screen.

mm_completion_menu.png

At this point, your scene heirarchy should look something like this.

mm_options_heirarch.png

Coding screen transitions and our MenuController script

Just before we dive into our menu script, there’s a couple of things we could do with housing elsewhere. Such as how long our splash screen will show for, and how long our screen transitions might take, or the locations of our menu prefab. So in our GameVars script, create another class called AppVars.

public class AppVars {

    // splash screen duration
    public const float SplashScreenDuration = 1.0f;

    // screen transition duration
    public const float ScreenTransitionDuration = 0.5f;

    // background prefab
    //public const string BackgroundPrefabLocation = "prefabs/background";

    // menu prefab
    public const string MainMenuPrefabLocation = "prefabs/menu";
    public const string MainMenuRootName = "menuRoot";
}

Create a new script called MenuController, and add it to our canvas object.

In order to create our transitions, we need a way of mapping a transition type to the screen you want to go from, and the screen you want to go to. So we need an enum of transition types and screens, and a mapping class that will combine these. I've also added an enum for slide directions just to make things a bit easier later.
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;
using System.Collections.Generic;

public enum TransitionTypes { SLIDE_UP, SLIDE_DOWN, SLIDE_LEFT, SLIDE_RIGHT };
public enum Screens { SPLASH, MAIN_MENU, OPTIONS_MENU, GAME_UI, COMPLETION_UI };
public enum SlideDirections { UP, DOWN, LEFT, RIGHT }

public class TransitionMapping {
    public TransitionTypes type;
    public Screens dest;
    public Screens orig;
    public TransitionMapping(TransitionTypes t, Screens o, Screens d){
	type = t;
	dest = d;
    	orig = o;
    }
}

Next, we'll create a list of these mapping objects to represent every screen transition. This can be a lot of work if you have many screens!

public class MenuController : MonoBehaviour {

    // ============== Transition Mappings =================

    // Transition Definitions
    // - Splash -> Main Menu	        SLIDE_UP
    // - Main Menu -> Options 	        SLIDE_LEFT
    // - Main Menu -> Game	        SLIDE_UP
    // - Options -> Main Menu	        SLIDE_RIGHT
    // - Game UI -> Main Menu	        SLIDE_DOWN
    // - Game UI -> Completion Screen	SLIDE_LEFT
    // - Completion Screen -> Main Menu	SLIDE_DOWN
    // - Completion Screen -> Game UI   SLIDE_RIGHT

    private static readonly List _TransitionMappings = new List()
    {
        // - Splash -> Main Menu
	new TransitionMapping (
	    TransitionTypes.SLIDE_UP,
	    Screens.SPLASH,
	    Screens.MAIN_MENU
	),
	// - Main Menu -> Options
	new TransitionMapping (
	    TransitionTypes.SLIDE_LEFT,
	    Screens.MAIN_MENU,
	    Screens.OPTIONS_MENU
	),
	// - Main Menu -> Game
	new TransitionMapping (
	    TransitionTypes.SLIDE_UP,
	    Screens.MAIN_MENU,
	    Screens.GAME_UI
	),
	// - Options -> Main Menu
	new TransitionMapping (
	    TransitionTypes.SLIDE_RIGHT,
	    Screens.OPTIONS_MENU,
	    Screens.MAIN_MENU
	),
	// - Game UI -> Main Menu
	new TransitionMapping (
	    TransitionTypes.SLIDE_DOWN,
	    Screens.GAME_UI,
	    Screens.MAIN_MENU
	),
	// - Game UI -> Completion Screen
	new TransitionMapping (
	    TransitionTypes.SLIDE_LEFT,
	    Screens.GAME_UI,
	    Screens.COMPLETION_UI
	),
	// - Completion Screen -> Main Menu
	new TransitionMapping (
	    TransitionTypes.SLIDE_DOWN,
	    Screens.COMPLETION_UI,
	    Screens.MAIN_MENU
	),
	// - Completion Screen -> Game UI (Play Again)
	new TransitionMapping (
	    TransitionTypes.SLIDE_RIGHT,
	    Screens.COMPLETION_UI,
	    Screens.GAME_UI
	)
    };

Next up, we need to keep track of the RectTransform components of each of our screen panels, as well as keeping a record of which screen we are currently on and whether or not a transition is in progress.

    private Screens _currentScreenType = Screens.SPLASH;

    private bool transitionInProgress = false;

    private RectTransform _splash;
    private RectTransform _mainMenu;
    private RectTransform _OptionsMenu;
    private RectTransform _GameUI;
    private RectTransform _CompletionScreenUI;

Then we need to populate those RectTransform values on awake, and ensure that only the splash screen is enabled at startup, and trigger a short delay before calling our first transition.

    void Awake(){
        // Ensure root has the correct name
        gameObject.name = AppVars.MainMenuRootName;

        // Find the UI panels
        // NOTE: These MUST match the names of your panels
        GameObject pnl_Splash = GameObject.Find("pnl_Splash");
        GameObject pnl_MainMenu = GameObject.Find("pnl_MainMenu");
	GameObject pnl_OptionsMenu = GameObject.Find("pnl_OptionsMenu");
	GameObject pnl_GameUI = GameObject.Find("pnl_GameMenu");
        GameObject pnl_CompletionUI = GameObject.Find("pnl_CompletionMenu");

        // Store their rect transforms
        _splash = pnl_Splash.GetComponent();
	_mainMenu = pnl_MainMenu.GetComponent();
	_OptionsMenu = pnl_OptionsMenu.GetComponent();
	_GameUI = pnl_GameUI.GetComponent();
	_CompletionScreenUI = pnl_CompletionUI.GetComponent();

        // disable screens until they are needed
        _splash.gameObject.SetActive(true);
        _mainMenu.gameObject.SetActive(false);
	_OptionsMenu.gameObject.SetActive(false);
	_GameUI.gameObject.SetActive(false);
	_CompletionScreenUI.gameObject.SetActive(false);

        // initiate transition from splash to main menu
        CoroutineSingleton.GetInstance().StartCoroutine(SplashDelay());
    }
    private IEnumerator SplashDelay()
    {
        yield return new WaitForSeconds(AppVars.SplashScreenDuration);
        StartTransition(Screens.MAIN_MENU, null);
    }

Transitions are kicked off by calling the static RequestTransition function, this function find our menu root object, finds the current instance of MenuController, and calls StartTransition which will start the coroutine.

The coroutine will only start if we're not already in the middle of a transition. First thing it does after checking is to set 'transitionInProgress' to true, which remains true until we're finished.

GetTransitionType tells us which direction to slide based on our destination screen and our current screen by looking it up from the dictionary of TransitionMappings we pre-defined. A switch statement enabled us to call SlideTransition with the correct direction, and we yield until the transition is complete.

    // ======== Transition Animations =======

    public static void RequestTransition(Screens destination, Action cb = null)
    {
        GameObject menuRoot = GameObject.Find(AppVars.MainMenuRootName);
        if(menuRoot != null)
        {
            MenuController mc = menuRoot.GetComponent();
            if(mc != null)
            {
                mc.StartTransition(destination, cb != null ? cb : null);
            }
        }
    }

    private void StartTransition(Screens destination, Action cb = null)
    {
        StartCoroutine(Co_StartTransition(destination, cb != null ? cb : null));
    }

    private IEnumerator Co_StartTransition(Screens destination, Action cb = null)
    {
        if(transitionInProgress){
	    yield break;
	}

	transitionInProgress = true;

	// find the transition type
	TransitionTypes transitionType = GetTransitionType(destination);

	// choose which one to play
	switch(transitionType){

	    case TransitionTypes.SLIDE_UP:
	        yield return StartCoroutine(Transition_Slide(SlideDirections.UP, destination, cb != null ? cb : null));
		break;

            case TransitionTypes.SLIDE_DOWN:
                yield return StartCoroutine(Transition_Slide(SlideDirections.DOWN, destination, cb != null ? cb : null));
                break;

            case TransitionTypes.SLIDE_LEFT:
		yield return StartCoroutine(Transition_Slide(SlideDirections.LEFT, destination, cb != null ? cb : null));
		break;

	    case TransitionTypes.SLIDE_RIGHT:
		yield return StartCoroutine(Transition_Slide(SlideDirections.RIGHT, destination, cb != null ? cb : null));
	        break;
	}

	// switch to the current screen and re-enable transitions
	_currentScreenType = destination;
	transitionInProgress = false;
    }

The Transition_Slide function provides our slide animation, and isn't as complex as it looks. First we gather up all the data we need to perform the animation, the origin and destination RectTransforms and the animation duration.

Then we work out the start and end positions for both screens.

OriginInitialPosition is where our current screen starts, which is 0,0,0. It is also where our destination screen should end up.

OriginTargetPosition is the off-screen position our current screen will slide toward, and is calculate by passing the desired direction to a utility function we will define below called GetOffScreenTargetPosition.

DestinationInitialPosition is where our destination screen will begin before sliding toward 0,0,0. It is the negative of the value returned by GetOffScreenPosition, as we want it to slide in from the opposite direction.

Then we increment our counter variable by Time.deltaTime until it reaches the duration. And we lerp both our screens toward their target positions by using (counter/duration) as our timestep. Then we updated the RectTransforms anchoredPosition variable to their new positions. Then yield until the next frame.

Once this is complete, we de-activate the old screen, update the current screen and invoke the callback that was passed in to RequestTransition if it is not null. We use simple actionto pass a void function in as our callback, examples of using this will be shown a little further down.

    private IEnumerator Transition_Slide(SlideDirections direction, Screens destination, Action cb = null)
    {
	RectTransform rt_origin = GetScreenTransform(_currentScreenType);
	RectTransform rt_destination = GetScreenTransform(destination);

        // activate the destination
	rt_destination.gameObject.SetActive(true);

        // work out our position values
        float duration = AppVars.ScreenTransitionDuration;
        float counter = 0f;
        Vector2 originInitialPosition = Vector2.zero;
        Vector2 originTargetPosition = GetOffScreenTargetPosition(direction);
        Vector2 destinationInitialPosition = originInitialPosition - originTargetPosition;
        Vector2 destinationTargetPosition = originInitialPosition;

        while(duration - counter > float.Epsilon)
        {
            float t = counter / duration;
            Vector2 origin_newPosition = Vector2.Lerp(originInitialPosition, originTargetPosition, t);
            Vector2 destination_newPosition = Vector2.Lerp(destinationInitialPosition, destinationTargetPosition, t);

            rt_origin.anchoredPosition = origin_newPosition;
            rt_destination.anchoredPosition = destination_newPosition;

            counter += Time.deltaTime;
            yield return 0;
        }

        // ensure they are in the correct positions
        rt_origin.anchoredPosition = originTargetPosition;
        rt_destination.anchoredPosition = destinationTargetPosition;

        // de-activate the origin now that it's off-screen
        rt_origin.gameObject.SetActive(false);

        // invoke our callback
        if (cb != null)
        {
            cb.Invoke();
        }
        yield break;
    }

Here we define a few utility functions that help the above screen transition animation code be a little more readable.

GetTransitionType simple looks up the corresponding TransitionMapping and returns which direction should be used.

GetScreenTransform just returns the appropriate RectTransform for a requested screen, and GetOffScreenTargetPosition returns the destination position for a screen to slide to based on which direction we want to slide.

    // =============== Screen Transition Utils =================

    private TransitionTypes GetTransitionType(Screens destination)
    {
        // find out which transition type we need based on
	foreach(TransitionMapping tm in _TransitionMappings){
	    if(tm.dest == destination && tm.orig == _currentScreenType){
	        return tm.type;
	    }
	}
        // default
	return TransitionTypes.SLIDE_LEFT;
    }

    private RectTransform GetScreenTransform(Screens screen)
    {
        switch(screen){
	    case Screens.SPLASH:
                return _splash;

	    case Screens.MAIN_MENU:
	 	return _mainMenu;

	    case Screens.OPTIONS_MENU:
		return _OptionsMenu;

	    case Screens.GAME_UI:
		return _GameUI;

	    case Screens.COMPLETION_UI:
		return _CompletionScreenUI;

        }
	return null; // error, screen doesn't exist!
    }

    public Vector2 GetOffScreenTargetPosition(SlideDirections direction)
    {
        // Assuming the screen center is world center
        Vector2 targetPosition = Vector2.zero;

        switch (direction)
        {
            case SlideDirections.UP:
                targetPosition.y += GetWindowHeight();
                break;

            case SlideDirections.DOWN:
                targetPosition.y -= GetWindowHeight();
                break;

            case SlideDirections.LEFT:
                targetPosition.x -= GetWindowWidth();
                break;

            case SlideDirections.RIGHT:
                targetPosition.x += GetWindowWidth();
                break;
        }
         return targetPosition;
    }

    public float GetWindowWidth()
    {
        return Screen.width;
    }

    public float GetWindowHeight()
    {
        return Screen.height;
    }

Lastly we have our callback functions that will be invoked by our UI buttons. There is one for each button and they trigger a screen transition from the current screen to their target screen.

Play, PlayAgain and GameUI_Back perform some additional functions that pertain to starting or ending a game and reference a class called CardMatchGameBootstrapper. This class will be created in part 4.

We also pass an Action into some of these transitions that is used to invoke a callback function at the end of our transition.

In this instance, we're simple creating a new Action called 'cb', and setting that as equal to the function we want to call at the end of our transition. The function passed in this way must be void.

Note: Callbacks and delegate functions are a complex topic, and you can do an awful lot more than call a simple void function like we are doing here. They are explained in far greater detail than I'll ever be able to in the C# documentation.

// ============== UI Callbacks ===============

    // --- Main Menu UI ---

    // - btn_PLAY

    public void btnPress_MainMenu_Play()
    {
        Action cb = StartNewGame;
	StartTransition(Screens.GAME_UI, cb);
    }
    private void StartNewGame(){
	// initialise the game bootstrapper, this will handle setting the game up
        // We'll re-visit this method later on, for now comment this line out
	//CardMatchGameBootstrapper.InitiateBoostrapping();
    }

    // - btn_OPTIONS
    public void btnPress_MainMenu_Options(){
	StartTransition(Screens.OPTIONS_MENU);
    }

    // -- Options Menu UI ---
    // - btn_BACK
    public void btnPress_OptionsMenu_Back(){
	StartTransition(Screens.MAIN_MENU);
    }

    // --- Game UI ---
    // - btn_BACK/QUIT
    public void btnPress_GameUI_Back(){
        Action cb = CleanupGameInProgress;
        StartTransition(Screens.MAIN_MENU, cb);
    }
    private void CleanupGameInProgress()
    {
        Debug.Log("Exiting current game... Calling cleanup method...");
        //CardMatchGameBootstrapper.ExitGameInProgress();
    }

    // --- Completion Screen UI ---
    // - btn_BACK/QUIT
    public void btnPress_CompletionUI_Back(){
        Action cb = CleanupGameInProgress;
        StartTransition(Screens.MAIN_MENU, cb);
    }

    // - btn_PLAY AGAIN
    public void btnPress_CompletionUI_PlayAgain(){
        Action cb = StartNewGame;
        StartTransition(Screens.GAME_UI, cb);
    }

}

Add this script to our root menu object, now we can link the buttons we've created to the appropriate menu functions. This is done by selecting the button object, and on the button script you'll see a small box titled OnClick(), if you click the plus symbol on the bottom right, you can add a callback to a designated function on another object that will be called when the button is clicked by the user. This doesn't have to just be for menu functions, either, it could trigger anything you want it to.

mm_choose_button_callback.gif

We'll need to go through each of our buttons to match them to their callback functions. It should be evident from the names which functions to link them to, but I'll list them for completeness.

  • Main Menu
    • btn_Play ->                btnPress_MainMenu_Play()
    • brn_Options ->         btnPress_MainMenu_Options()
  • Options Menu
    • btn_Back ->               btnPress_OptionsMenu_Back()
  • Game Menu
    • btn_Exit ->                 btnPress_GameUI_Back()
  • Completion Menu
    • btn_PlayAgain ->     btnPress_CompletionUI_PlayAgain()
    • btn_Exit ->                 btnPress_CompletionUI_Back()

Don't forget to apply your changes to the menu prefab.

If you play the scene now, you should be able to navigate between the main, options and game screens. You won't be able to access the completion screen for the moment, and the options screen will be empty.

mm_transitions_wip.gif

Part 3 turned out a lot longer than I expected, so in Part 4 we will re-visit the options menu to implement our game configuration, deck-texture picker, and write a bootstrapper script which we can call to initialize an instance of our game for the player to actually play. By the end of Part 4, we'll have a completed game!

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

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