Tutorial – Zelda style screen transitions in Unity

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

A few people have asked me how I’m handling screen transitions in my latest project, so I thought I’d go through how those work.

fnrprkt

update: Fixed a bug in the player movement script

Conceptually, this is pretty simple but there’s a bunch of ways you could implement it. You could lock the camera to a grid and move it in increments when the player steps outside of it, or you could use trigger boxes to start the animation, etc. I’ve chosen to use trigger boxes to keep track of the screen region we want to lock the camera to.

Scene Setup

First thing I’ve done here is create four sample ‘rooms’. You can create these however you like, but I’ve built them out of tiles, each room is 14 tiles wide by 10 tiles high.

kenneyrooms

We also need a player that can move around the screen in order to test that our screen regions work.

Create a new gameobject, and add a SpriteRenderer, then drag on your player sprite. For this test I’ve used a simple brown square. Then create a new script called Player and attach it.

    public class Player : MonoBehaviour {

	BoxCollider2D _col2d;
	Rigidbody2D _rb2d;

	float moveSpeed = 10f;

	void Awake () {
		gameObject.tag = "Player";
		_col2d = gameObject.AddComponent<BoxCollider2D>();
		_rb2d = gameObject.AddComponent<Rigidbody2D>();
		_rb2d.constraints = RigidbodyConstraints2D.FreezeRotation;
	}

	void Update () {
		Vector3 moveVector = Vector3.zero;

		moveVector.x += Input.GetAxisRaw("Horizontal");
		moveVector.y += Input.GetAxisRaw("Vertical");

		_rb2d.MovePosition(transform.position + moveVector * Time.deltaTime * moveSpeed);
	}
    }

This is pretty much the most basic movement controller you could ask for. First thing we do is set the objects tag attribute to ‘Player’, this will be referenced later on so that screen regions can ensure they are tracking the player and not some other collider.

Next we add a collider and a rigibody, ensuring that rotation is constrained and gravity is turned off for this collider. Then every update we just read the X and Y axis input, then move by that input times deltaTime times speed. Simple.

brownman

Camera Movement

Next up, we need to be able to smoothly move our camera to a boundary. Create another script which we’ll be attaching to our camera, called CameraAlign.

This script does a couple of things. Firstly, it keeps track of a Bounds object which will allow us to get all the information we need to move our camera. Secondly, it exposes a public method to set a new target bounds, which we’ll be calling from our trigger volumes.

Lastly is the actual movement code. For this example we lerp smoothly toward the bounds center over a period of one second. Vec3Lerp is a helper function that makes lerping over a fixed duration a little simpler.

    public class CameraAlign : MonoBehaviour {

	private Bounds currentBounds;

	private float alignDuration = 1f; 

	IEnumerator AlignToNewBounds(){

	    Vector3 startVect = transform.position;
	    Vector3 trackingVect = transform.position;

	    float targX = currentBounds.center.x;
	    float targY = currentBounds.center.y;
	    Vector3 targetPosition = new Vector3(targX, targY, transform.position.z);

	    float lerpTime = 0;
	    while(lerpTime < alignDuration){

	        lerpTime += Time.deltaTime;
	        trackingVect = Vec3Lerp(lerpTime, alignDuration, startVect, targetPosition);
	        transform.position = trackingVect;
	        yield return 0;
	    }

	    transform.position = targetPosition;
	}

	public void SetNewBounds(Bounds newBounds){

	    currentBounds = newBounds;
	    StartCoroutine(AlignToNewBounds());
	}

	public static Vector3 Vec3Lerp(float currentTime, float duration, Vector3 v3_start, Vector3 v3_target){
	    float step = (currentTime/duration);
	    Vector3 v3_ret = Vector3.Lerp(v3_start, v3_target, step);
	    return v3_ret;
	}
    }

Screen Regions

Now we can set up our actual screen region objects. Create an empty game object called “Region” and add a BoxCollider2D component.

This collider needs to match your desired screen region size. In this example, our screens are 14 units wide by 10 high. So our collider will need it size set to 14 by 10. You’ll also need to set it to be a trigger volume.

Next up, make sure the regions layer is set to Ignore Raycast. This isn’t strictly necessary for this tutorial, but if you make use of any raycasts in your game, this will stop screen regions from interfering with them.

The script that controls these regions is also quite simple at it’s core, all we really do is pass the collider bounds up to the camera when the player enters the trigger volume.

We also add a kinematic rigidbody to the object, because this is required in order to make the trigger volume work.

    public class ScreenRegion : MonoBehaviour {

	Rigidbody2D _rb2d;
	BoxCollider2D _collider;

	void Awake () {
	    _collider = gameObject.GetComponent<BoxCollider2D>();
	    _rb2d = gameObject.AddComponent<Rigidbody2D>();
	    _rb2d.isKinematic = true;
	}

	void SetNewCameraBounds () {
	    CameraAlign cam = Camera.main.gameObject.GetComponent<CameraAlign>();
	    cam.SetNewBounds(_collider.bounds);
	}

	void OnTriggerEnter2D(Collider2D other){

	    if(other.gameObject.tag == "Player"){
		SetNewCameraBounds();
            }
	}
    }

Attach this code to your screen region object, then place a duplicate of this object at each of your screen locations. You should be able to hit play and see them in action now! We’re basically done.

regionsworking.gif

There’s a couple of issues with this system, however. First, if you enter two trigger volumes at the same time (IE because there’s no walls stopping you and you move off a corner) it’ll glitch out.

How you deal with that will depend on the kind of game you’re making. You could change the cameras alignment code so that it’s a little more intelligent about where it moves, or you could place a second trigger at the edges of the screen which will disable the players input and then move him into position on the next screen, and then re-enable input once the camera has finished moving.

Secondly, setting these up manually is a bit of a pain. So if you use this in your game, you’ll most likely want to write a script which will create and place a grid of screen regions that matches your map and room size.

Advertisements
Posted in Tutorials, Unity3D | 1 Comment

Pixel-Perfect Virtual Camera – Unity3D

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

Note – This tutorial was made using Unity 5. But it’ll probably work on 4 as well. It won’t work for 3 unless you have pro as RenderTexture is pro-only in version 3.

Pixel-Perfect games can be a bit of a pain in the arse in Unity sometimes, particularly when your target resolution is quite low. It’s very easy to get badly stretched images or ‘pixel-walking’ where some pixels don’t seem to be the same size as others.

pixelwalking

Might be kinda hard to tell from the GIF above, but you should be able to see how the pixels distort when the camera moves across them.

Luckily, there’s a few things we can do about this.

We’re going to build a small but pretty handy virtual camera rig which will handle rendering our game at the resolution our assets were built for, and then scaling the resulting image properly to fit within a multiple of that target resolution. Ensuring that we get a crisp pixel-perfect view of our game!

Scene Setup

We need to set up a couple of things before we write our scaling script. First, our scene’s main camera needs to be adjusted to fit the target resolution of our assets.

It should be set to ‘Orthographic’ projection if it’s not already, and you’ll probably want to set it to clear to a solid color, and ensure that colors alpha value is set to 255, sometimes Unity likes to make it transparent.

The most important thing we need to set here is ‘Size’ setting, which controls our cameras orthographic size. It’s a little unintuitive at first, but this size represents half of the desired vertical resolution, in world units.

In this example, I’m aiming for a target resolution of 160×144, same resolution as the Gameboy. So, the orthographic size of our camera should be half of 144, or 72.

Now. Bare in mind that this is in world units, and so assumes that you’ve imported your assets with a unit-to-pixel ratio of 1. Which makes them pretty huge, and may break your game if you intend to rely on any physics calculations. The default is set to 100. If you’ve imported with any other size than 1, you’ll need divide the orthographic size by that.

orthographicSize = (verticalResolution/2) / pixelsPerUnit

For our example, half of 144 is 72. 72 divided by our pixelsPerUnit ratio (32 in this example) gives us an orthographic size of 2.25.

Last thing we need to do here is hop over to our project tab and create two things, a RenderTexture called rt_VirtualScreen and a material called m_VirtualScreen. The RenderTexture should be the dimensions of your target resolution (so here, it’s 160×144 to match my Gameboy resolution) Drag the RenderTexture into the TargetTexture slot of our camera.

gamepixels.gif

You should be greeted by a warning telling you that there are no cameras rendering, this means our camera is now rendering directly to that RenderTexture, and we can move onto the next step.

Update: You’ll want to make sure the RenderTexture rt_VirtualScreen has some settings applied. Ensure that Anti-Aliasing is set to ‘None’ and Filter Mode is set to ‘Point’ otherwise the scaled image will still be filtered later on!

point filtering.gif

Creating a UI to house our Virtual Screen

Create a UICanvas object inside of our scene,and a new Camera object called UICamera, attach this to the UICanvas root object. Leave the settings as default.

Next, create a RawImage element, and drag our rt_VirtualScreen onto its Texture slot. Drag the material into the Material slot and make sure Pixel Snap is enabled on the material.

This will already look a lot better, but we want to write some script that will scale the image properly.

Create a script called PixelPerfectScaleUI and attach it to the RawImage element.

Best-Fit Script

We’re going to allow the user to choose between three different scale modes.

  • Best Fit: This is the most accurate mode, and our aim here is to scale the image so that each pixel is scaled by a multiple of two.
  • Maintain Aspect Ratio: This mode will stretch the image to fit either the height or width of the screen, maintaining the correct aspect ratio.
  • Stretch to Fit: I don’t know why some people still want this mode, but they do! So I’ve included it anyway. It will still look better this way than it will through a regular camera setup.

The [ExecuteInEditMode] tag will allow us to preview our script without having to enter play mode.

The update function stores the current screen height, and will only re-size the screen if that changes, so that we aren’t doing it every frame. If it detects a change we will use a switch statement to determine which of our scale modes to apply.

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
public class PixelPerfectScaleUI : MonoBehaviour
{
	public enum FitModes { BEST_FIT, MAINTAIN_ASPECT, SCALE_TO_FIT }
	public FitModes fitMode = FitModes.BEST_FIT;

    public float gameHorizontalPixels;
    public float gameVerticalPixels;
    float minimumMultiplier = 1;

    private float screenPixelsY = 0;

        void Update () {
	    if(screenPixelsY != (float)Screen.height)
            {
		switch(fitMode){
		    case FitModes.BEST_FIT:
			BestFit();
			break;
		    case FitModes.MAINTAIN_ASPECT:
			MaintainAspectFit();
			break;
		    case FitModes.SCALE_TO_FIT:
			ScaleToFit();
			break;
		}
            }
	}

Remember to set ‘screenVerticalPixels’ and ‘screenHorizontalPixels’ in the inspector, they should be the resolution you want to render your game at and should match the size of your RenderTexture.

BestFit is the meat of our script, and the most complex part.

Update: 02/10/2016 – Updated this script to be a bit more flexible and remove the while loop which is a bit dangerous in an update function! On the advice of my good buddy @salamander3

targetHeight represents our games render height (so again for our Gameboy example, this would be 144 pixels), multiplier represents how much we’re going to scale up by, and screenPixelsY is the current height of the viewport.

Multiplier starts at 2 because our game is so small that it’s going to be very rare that we would need to display it at original size (after-all, scaling up properly is the whole reason we’re writing this script!)

Next we work out the current multiplier based on the size of the screen, and step it down to a multiple of 2. If the result is less than 2, we force it to be minimumMultiplier (which should either be 1 or 2 depending on how small your original resolution is)

Next thing we do is work out the aspect ratio of our game by dividing the game width by the game height. As we know our height, we can get our UI component’s RectTransform, and set that height. Then we can set the width to our new height multiplied by the aspect ratio.

best_fit

    private void BestFit() {
	float targetHeight = gameVerticalPixels;
	float multiplier = minimumMultiplier;

	multiplier = screenPixelsY / targetHeight;
        multiplier -= multiplier % 2;
        if(multiplier < 2){
            multiplier = minimumMultiplier;
        }

        float aspect = gameHorizontalPixels / gameVerticalPixels;
        float height = gameVerticalPixels * multiplier;
        float width = height * aspect;

</code><code>	RectTransform rt = gameObject.GetComponent();
	rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height);
	rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, width);
    }

Next up is a much easier script, but it results in a slightly less accurate result that scales as best it can to the screen size without distorting the aspect ratio.

First thing we do is find out our games aspect by dividing width by height. Then we create two float variables which will store our width and height.

If our screen width is less than our screen height, we should fit our image to the screen width. Which means setting our width to match the screen, and then dividing width by our aspect ratio to find the correct height.

Otherwise, we scale it to fit the height, by setting our height to match the screen and our width to the height of the screen multiplied by the aspect. This will ensure our entire image stays on-screen at all times.

Then we simply apply these values to our RectTransform as previously.

maintain

    private void MaintainAspectFit() {
        float aspect = gameHorizontalPixels / gameVerticalPixels;

        float targetWidth;
        float targetHeight;

        if(Screen.width < Screen.height){
	    // fit to width
	    targetWidth = Screen.width;
	    targetHeight = Screen.width / aspect;
        } else {
	    / fit to height
	    targetHeight = Screen.height;
	    targetWidth = Screen.height * aspect;
        }

        RectTransform rt = gameObject.GetComponent();
        rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, targetHeight);
        rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, targetWidth);
    }

Lastly is our simple scale-to-fit option. This one’s really easy! Just apply the screen height and width to our RectTransform. Done!

scaletofit

    private void ScaleToFit() {
	RectTransform rt = gameObject.GetComponent();
	rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, Screen.height);
	rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, Screen.width);
    }
}

This gives us a much better way of rendering pixel-perfect games and, if necessary, stretching them to fit in a much cleaner way than simply using a default camera.

There are a couple of drawbacks here however. First, you now need to manage this system alongside your UI if you intend to use the Unity UI system. Secondly, this technique works best with texture sheets that are power-of-two (square) sizes. Using odd sizes you can’t guarantee you won’t still see some artifacting.

I’m using this exact setup to make Gameboy style games at the moment. Give it a try for yourself!

Posted in Tutorials, Unity3D | Tagged | 10 Comments

Chess Post-Mortem, also Content Production is Scary!

Couple things I want to cover here, first up is how my recent 48 hour project, a Gameboy style Chess Game went, then I’m gonna ramble for a bit about my next project and the horrors of creating bespoke content.

Mini Post-Mortem – 48 Hour Chess

What went well:

  • Singular focus
  • Artwork complete from the start kept motivation up
  • Defined ruleset cut down on design time
  • Pixel-perfect virtual camera

The entire game is based around one set of sprites I found on opengameart (credit to Clint Bellanger!) that I was so enamored with I felt they had to be put to use. This helped my motivation tremendously.

The advantage this gave me was that I had a complete set of sprites for the game, no finding out halfway through that I needed to quickly fudge together some corner sprites or doorways or whatever else was missing. I just needed a title screen and a cursor, and could then focus on making the game.

Also, importantly, I had a pre-defined ruleset going in (it’s just Chess after-all), so any design work I had to do was minimized. I only had to worry about how the player would interact with the board. Which meant I could start implementing the game immediately.

randompieces

While it took more time to figure out than perhaps it was worth in the space of a 48 hour project, I’m really happy with how the virtual camera setup turned out. It renders the game view at actual Gameboy resolution (160×144 pixels) and then scales it by either 1, 2 or 4 based on the viewport size and fills the rest of the space with a ‘Super Gameboy’ style border. Not only that, but it’s nicely packaged up allowing me to use it in future projects with very little hassle.

What didn’t go well:

  • The rules of Chess… I didn’t actually know them.
  • Oblique Projection

I discovered halfway through the project that my knowledge of Chess was actually lacking a few key rules. I didn’t know how Check/Mate worked, and I didn’t know about en-passant or pawn promotion. This meant that the way I’d decided to implement potential moves (your list of legal moves is populated when you select a piece, for just that piece) made it a bit trickier to test for check/mate, and additional time spent researching how these rules even worked meant time not spent programming them into the game.

As a direct result, check/mate is not in the game (you have to take the king to win), and the first version had a bug with en-passant taking, and ‘castling’ wasn’t in the game either, it had to be sacrificed in the last-minute dash to get a title screen in and actually build the application. Pawn promotion however did make the game, and I’m pretty happy with how it works.

Oblique projection is only mentioned here because while it works perfectly fine in the game, I wouldn’t want to tackle a more feature-rich product with it. While math isn’t my strongest skill, working out the math for the perspective would have been a bit of a nightmare, and thinking about how I’d implement it took a sizable chunk of the first day. Thankfully, this game is only concerned about that 8×8 chess board, so I could just hardcode the math for moving up/down/left/right and not worry about things like converting a mouse-pointer or character position into/out of the oblique projection and how it interacts with screen space. Because I kept a separate model of the board, and the cursor is restrained to just that 8×8 grid, I didn’t need to work any of that stuff out.

What to take forward:

  • It’s ok to cut features to complete a project
  • Don’t assume you know everything, even if it seems really obvious!

This really go without saying, because no project ever finishes with 100% of the intended features. But I think it’s a lesson worth learning over and *over again* because it’s easy to beat yourself up and focus on the things you wanted that didn’t make it into the game, or the bugs you didn’t have time to fix.

But this distracts you from your actual accomplishment. You have a completed project! And in that regard, even though I had to cut castling, check/mate and en-passant didn’t really work properly, I still had a playable Chess game after just 48 hours. Considering I’d never implemented Chess before, I’m actually really proud of that.

Secondly, if I’d brushed up on my knowledge of the chess rules beforehand, I may have had enough time to squeeze in the rules that didn’t make the cut, however. I jumped straight into coding the game without stopping to ask myself, wait, do you actually know the rules for Chess? Rookie mistake!

Onwards and Upwards

I had initially planned to start researching HTML5 games next, using ImpactJS, but my motivation for that has waned significantly even though I’ve done a significant amount of work writing some code for resolution independency. I will return to that once I’ve gotten past the itch to make more Gameboy style stuff.

There’s been a couple of concepts sitting on my google docs for ages now that would be perfect for this style, so I found myself looking back to one of them now that I had a good setup for retro pixel games in Unity2D (I’ll probably make a tutorial on that relatively soon, too!)

pb_movement_1.gif

So I spent the weekend implementing very basic movement, and a workflow for maps using Tiled (I had planned to use my own map editor, but it’s very limited compared to what Tiled can do out of the box) and bringing them into Unity, and had some fun designing a couple of dungeons and a small overworld for a Zelda-clone. I tried to keep the scope as small as possible while still being able to tell the story I had in mind.

4-5 dungeons and a small overworld can’t be that much work can it? Oh boy I think I was wrong about that one. The concept of producing 4-5 dungeons and a small overworld to connect them didn’t sound like much, but when I started actually laying stuff out it dawned on me just how much content that actually is.

Each screen is going to be represented by a 15 high and 20 wide grid of 8×8 tiles. Knowing that it was going to be a good amount of work, I decided to restrict map sizes to just large enough to be able to provide some interesting puzzles. Dungeons are a maximum size of 6×6 screens, and the overworld is 12×10 screens.

That’s, potentially, 180 dungeon screens, and 120 overworld screens, plus a handful for the insides of buildings. Holy crap that got big fast… Even planning that the dungeons will only use around half of their available screens that’s still around 210 total screens. Which equates to about 63,000 individual tiles. Each one of which, because of how small that actually seemed once I started laying out the basic route through the game, will have to be carefully and purposefully placed.

I mean ok, that’s exaggerating slightly, even though the tiles are 8×8, I can paste a lot of structures and each game world tile represents a 16×16 space, the smaller tiles just give me more flexibility visually. But still, even at 16×16 and removing a few screens that aren’t going to be used, that’s still about 27,000 tiles! Spread across 5 environments, which means 5 different tilesets need to be produced in addition to the dungeon tileset for six total plus any unique tiles I need for things like decoration or dungeon entrances.

It’s no wonder so many devs choose to generate content procedurally rather than making bespoke worlds for small games like this. But I’ve always felt a nice hand-crafted world has a bit more soul than most procedural games. That and it’s nicer to speedrunners!

And that’s before I’ve even thought about the sprites I’ll need for actual game objects and all the triggers and events I’ll need…

… gulp

But I’ve started now, so I guess there’s no looking back! Here’s a sneak-peek at the overworld!

I’ve been wanting to channel my love for games like Link’s Awakening and Alundra into something tangible for years now. Hopefully this project can re-ignite my soul!

Posted in Blog, Game Design, Unity3D | Leave a comment

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.

Posted in Uncategorized | 2 Comments

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!

Posted in Tutorials, Unity3D | Leave a comment

Memory Match Game in Unity3D – Part 2

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

We ended the previous tutorial with a card and gamestate model that allows us to keep track of the state of our cards and our game progress, as well as a visual game object to represent our card.

flippy

This time we’re going to focus on the management objects which will control our card grid, card textures, and progress the game state.

CardTexturesManager

Before we can generate our game grid, we’re going to need to be able to load the textures we want to use on the front of our cards and apply them to the card views. If you remember from part 1, we gave the card views the ability to take a Texture2D and apply it. Here, we will write the script that will supply those textures to the code that generates the cards for our game board. This script also gives us the ability to load textures for our card backs in the same way.

We want to be able to access this code from anywhere, as we may also need it when designing the menu interface, additionally it doesn’t require a game object, so it seemed best to use the singleton pattern to access it. This allows us to load the textures just once, but access them from anywhere in the application.

For this to work, first add a few variables to our GameVars class.

    // Card texture vars
    public const string CardSpriteLocation = "textures/cards/";
    public const string DefaultCardBack = "cardBack_";
    public const string DefaultCardFace = "card_";
    public const int numDecks = 6;
    public const int numFaces = 15;

CardSpriteLocation represents the location of our card textures within the ‘Resources’ folder.

DefaultCardBack and DefaultCardFace represents the filename of our texture files, we will need to prepend a number in order to complete the filename.

Lastly, ‘numDecks’ and ‘numFaces’ should be the number of card back and card face textures you actually have sitting in that folder.

The card texture manager itself is relatively straight-forward. It maintains two lists of textures, which we can access by calling GetFaceTexture or GetDeckTexture and supplying an index representing which texture we require.

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

public class CardTexturesManager
{
    public static CardTexturesManager _instance;
    public static CardTexturesManager GetInstance()
    {

        if (_instance == null)
        {
            _instance = new CardTexturesManager();
            _instance.Initialize();
        }

        return _instance;
    }

    private List faceTextures;
    private List deckTextures;

    private int maxFaceTextures = GameVars.numFaces;

    public CardTexturesManager()
    {

    }

    private void Initialize()
    {

        LoadFaceTextures(maxFaceTextures);
        LoadDeckTextures();
    }

    public Texture2D GetFaceTexture(int faceIndex)
    {
        if (faceTextures == null || faceIndex < 0 || faceIndex > maxFaceTextures)
        {
            Debug.LogError("Error: Cannot get face texture for index: " + faceIndex);
            return null;
        }

        return faceTextures[faceIndex];
    }

    public int GetNumDeckTextures()
    {
        if (deckTextures != null)
            return deckTextures.Count - 1;

        return 0;
    }

    public Texture2D GetDeckTexture(int deckIndex)
    {
        if (deckTextures == null || deckIndex < 0 || deckIndex > deckTextures.Count - 1)
        {
            Debug.LogError("Error: Cannot get deck texture for index: " + deckIndex);
            return null;
        }

        Debug.Log("Returning Deck Texture: " + deckIndex);

        return deckTextures[deckIndex];
    }

    private void LoadDeckTextures()
    {
        deckTextures = new List();

        int count = GameVars.numDecks;
        string path = GameVars.CardSpriteLocation + GameVars.DefaultCardBack;

        for (int i = 0; i < count + 0; i++)
        {
            string spriteString = path + i.ToString();
            Texture2D t2d = Resources.Load(spriteString) as Texture2D;
            deckTextures.Add(t2d);
        }
    }

    private void LoadFaceTextures(int required)
    {

        if (required < 1 || required > maxFaceTextures)
        {
            Debug.LogError("Error: Cannot load face textures.");
            return;
        }

        faceTextures = new List();
        int count = GameVars.numFaces;
        string path = GameVars.CardSpriteLocation + GameVars.DefaultCardFace;

        for (int i = 1; i < count + 1; i++)
        {
            string spriteString = path + i.ToString();
            Texture2D t2d = Resources.Load(spriteString) as Texture2D;
            faceTextures.Add(t2d);
        }
    }
}
</code></pre>
<strong>CardGridManager</strong>

We again need to add some values to our GameVars class. We need a root object to parent our game board to, and it will be useful to know the name of this object so we'll define it here. Then, we need to be able to control the spacing between cards in both the X and Y axis.
<pre><code>
    // Gameobject Root
    public const string RootObjectName = "_GameRoot";

    // Card spacing 
    public const float CardXSpacing = 0.1f;
    public const float CardYSpacing = 0.1f;

  The CardGridManager is a little more involved. It’s responsible for setting up our game board by creating instances of our card view class. In order to do this, it will require a GameStateModel that we will create later in our GameManager class. First thing the grid manager does is pull all the values it needs from the game state, and calculate the total deck size, number of cards required to match, etc. Then it creates the card models that will represent our ‘deck’ of cards.

public class CardGridManager
{
    public static CardGridManager _instance;
    public static CardGridManager GetInstance()
    {

        if (_instance == null)
        {
            _instance = new CardGridManager();
            _instance.Initialize();
        }

        return _instance;
    }

    // vars
    private CardView[] cardViews;
    private int grid_x, grid_y;

    // root transform
    Transform rootTransform;

    private void Initialize()
    {
    }

    // Initialization
    public void CreateCardGrid(GameStateModel state)
    {
        if (state.matchLength == 0 || state.grid_x == 0 || state.grid_y == 0)
        {
            Debug.LogError("Cannot create card grid, state not initialized.");
            return;
        }

        int matchLength = state.matchLength;
        grid_x = state.grid_x;
        grid_y = state.grid_y;
        int deckSize = grid_x * grid_y;
        int faceTypesCount = (deckSize % matchLength);

        if (faceTypesCount > 0)
        {
            Debug.LogError("Decksize is not divisible by MatchLength, cannot create deck.");
            return;
        }

        // instantiate the root object
        GameObject root = GameObject.Find(GameVars.RootObjectName);
        if (root == null)
        {
            Debug.LogError("Cannot find root object.");
            return;
        }
        rootTransform = root.transform;

        // position the root object
        CenterRootObject(state);

        // create the card models
        CardModel[] cardModels = new CardModel[deckSize];
        int faceType = 1;

        // for each face type
        for (int i = 0; i < deckSize; i += matchLength)
        {
            // create that many cards
            for (int j = 0; j < matchLength; j++)
            {
                int index = i + j;
                cardModels[index] = new CardModel();
                cardModels[index].FaceValue = faceType;
            }
            faceType++;
        }

        state.cards = cardModels;
    }

The next thing we do is shuffle the deck of cards. I’ve used a Fisher-Yates shuffle algorithm. This seemed like a simple shuffle algorithm to implement, and there are plenty of sources that can explain it better than I’d be able to.


    public void ShuffleCardGrid(GameStateModel state)
    {
        // use fisher-yates shuffle to re-arrange the array
        CardModel[] cards = state.cards;
        int n = cards.Length;
        System.Random rand = new System.Random();

        for (int i = 0; i < n; i++)
        {
            int r = i + (int)(rand.NextDouble() * (n - i));
            CardModel temp = cards[r];
            cards[r] = cards[i];
            cards[i] = temp;
        }

        state.cards = cards;
    }

The last part of our grid setup process is creating the card views. First, we create one to match each of the card models created previously, the next function positions those cards based on their size and the spacing values we placed into our GameVars class. Lastly we position our root object in order to move our game board into the center of the screen. This is done by using the same values to calculate the width and height of the game board, then offsetting the root object by half of that. Finally, we loop through all of our card views and assign the appropriate face texture.

    // Initialize the card view objects and face textures
    public void InitializeGridCardViews(GameStateModel state)
    {

        CardModel[] cardModels = state.cards;
        cardViews = new CardView[cardModels.Length];

        Vector3 origin = new Vector3(0f, 0f, 0f);

        for (int x = 0; x < grid_x; x++)
        {
            for (int y = 0; y < grid_y; y++)
            {
                GameObject cardObj = new GameObject("card");
                int idx = IdxForGridPos(x, y);
                CardView cardView = cardObj.AddComponent();
                cardViews[idx] = cardView;
                cardView.refIdx = idx;
                cardView.transform.SetParent(rootTransform);
                cardView.transform.localPosition = origin + new Vector3(x, y, 0);
            }
        }
    }

    public void SetCardSpacing(GameStateModel state)
    {
        float xSpacing = GameVars.CardXSpacing;
        float ySpacing = GameVars.CardYSpacing;
        float yMultiplier = GameVars.CardAspect;

        Vector3 origin = new Vector3(0f, 0f, 0f);

        for (int x = 0; x < grid_x; x++)
        {
            for (int y = 0; y < grid_y; y++)
            {
                int idx = IdxForGridPos(x, y);
                Transform t = cardViews[idx].transform;
                Vector3 position = new Vector3(
                    x + x * xSpacing,
                    y * yMultiplier + y * ySpacing,
                    0f);
                t.localPosition = origin + position;
            }
        }
    }

    public void CenterRootObject(GameStateModel state)
    {
        // work out the width of the grid and the height of the grid
        //  position the grid based on this offset

        float cardWidth = GameVars.CardWidth;
        float boardWidth = cardWidth * grid_x;
        float xSpacing = GameVars.CardXSpacing * (grid_x - 1);
        float xOffset = (boardWidth / 2) + (xSpacing / 2) - (cardWidth / 2);

        float cardHeight = GameVars.CardWidth * GameVars.CardAspect;
        float boardHeight = cardHeight * grid_y;
        float ySpacing = GameVars.CardYSpacing * (grid_y - 1);
        float yOffset = (boardHeight / 2) + (ySpacing / 2) - (cardHeight / 2);

        Vector3 position = rootTransform.localPosition;
        position.x = -xOffset;
        position.y = -yOffset;
        rootTransform.localPosition = position;
    }

    public void SetCardTextures(GameStateModel state)
    {

        // Give cards the face textures associated with their face type
        CardModel[] cards = state.cards;
        int deckSize = cards.Length;

        for (int i = 0; i < deckSize; i++)
        {
            int faceValue = cards[i].FaceValue;
            Texture2D faceTex = CardTexturesManager.GetInstance().GetFaceTexture(faceValue);
            cardViews[i].SetFaceTexture(faceTex);
        }
    }

</code></pre>
I've also written a couple of utility functions that we can use. SetCardViewsMatched will lock a set of card views after a successful match, so that they remain face-up.

The next two functions help make it a bit easier to locate specific cards. IdxForGridPos gives us the card index for the requested grid position, and GridPositionForIdx does the reverse.
<pre><code>    // Utilities
    public void SetCardViewsMatched(int[] indexes)
    {
        for (int i = 0; i < indexes.Length; i++)
        {
            SetCardViewMatched(indexes[i]);
        }
    }

    public void SetCardViewMatched(int idx)
    {
        cardViews[idx].SetMatched();
    }

    public int IdxForGridPos(int x, int y)
    {
        return y + (x * grid_y);
    }

    public Vector2 GridPositionForIdx(int idx)
    {
        int x = (int)(idx / grid_x);
        int y = (int)(idx % grid_x);
        return new Vector2(x, y);
    }

    public CardView GetCardViewForGridPosition(int x, int y)
    {
        return cardViews[IdxForGridPos(x, y)];
    }
}

The last set of functions are used to produce the animation that will give players a quick peek at the cards before the game begins. The first two functions simply loop through all of our cards and flip them up, or flip them down.

‘FlipAllUnmatchedCardsDown’ allows us to flip cards back down in the event of an failed match attempt. And PeekAnimation makes use of FlipAllCardsUp and FlipAllCardsDown to create our animation.

PeekAtCards is blank for the moment, we will use it to begin the PeekAnimation coroutine. But first, we’re going to write a utility script that will allow us to start coroutines from scripts that aren’t monobehaviors.


    // Animations
    public void FlipAllCardsUp()
    {
        if (cardViews == null)
        {
            Debug.LogError("Error: Cannot flip cards, CardGridManager not initialized!");
        }

        for (int i = 0; i < cardViews.Length; i++)
        {
            CardView cardView = cardViews[i];
            cardView.FlipUp();
        }
    }

    public void FlipAllUnmatchedCardsDown()
    {
        if (cardViews == null)
        {
            Debug.LogError("Error: Cannot flip cards, CardGridManager not initialized!");
        }

        for (int i = 0; i < cardViews.Length; i++)
        {
            CardView cardView = cardViews[i];
            if (!cardView.isMatched)
                cardView.FlipDown();
        }
    }

    public void FlipAllCardsDown()
    {
        if (cardViews == null)
        {
            Debug.LogError("Error: Cannot flip cards, CardGridManager not initialized!");
        }

        for (int i = 0; i < cardViews.Length; i++)
        {
            CardView cardView = cardViews[i];
            cardView.FlipDown();
        }
    }

    public void PeekAtCards(float delay, float peekDuration)
    {

    }

    IEnumerator PeekAnimation(float delay, float peekDuration)
    {
        yield return new WaitForSeconds(delay);

        CardGridManager cgm = CardGridManager.GetInstance();
        cgm.FlipAllCardsUp();

        yield return new WaitForSeconds(peekDuration);

        cgm.FlipAllCardsDown();
    }

To implement our card peek animation, we could write a separate script, instantiate a game object, place the script on that object and then call on it. But this is just pointless extra steps frankly… so instead, we’re going to create a utility script that allows us to call coroutines from non-monobehavior scripts. Luckily, this is a very simple script. All it does is take away the minor annoyance of having to create an object every time. It should be noted, this should only be used for coroutines that you don’t really care about needing to be able to stop.

Using a simple singleton class that simply attaches itself to a dummy object and returns itself as a static instance, we can trigger coroutines from anywhere in our game.

public class CoroutineSingleton : MonoBehaviour {

	// A simple dummy object for running coroutines from non-monobehaviour classes

	public static CoroutineSingleton _instance;
	public static CoroutineSingleton GetInstance() {

		if (_instance == null) {
			Initialize ();
		}

		return _instance;
	}

	static void Initialize () {
		GameObject dummy = new GameObject ("coroutine_dummy");
		_instance = dummy.AddComponent ();
	}

}

Now that we have a nice simple way of starting our peek animation from within the card grid manager, we can implement ‘PeekAtCards’ properly.

    public void PeekAtCards(float delay, float peekDuration)
    {
        CoroutineSingleton.GetInstance().StartCoroutine(PeekAnimation(delay, peekDuration));
    }

GameManager

The game manager class will be responsible for beginning and ending our game, and updating the game state to reflect the results of the players match attempts. Again, we will first need to update our GameVars class.

This time, as well as some variables to control the length of the peek animation, we will store a static instance of the game manager script itself, so that other objects will be able to access it.

    // Current Game Manager instance
     public static GameManager GameManager { get; set; }

    // Animation Durations
    public const float PeekDuration = 1.0f;
    public const float FlipCheckDuration = 0.5f;

The first three functions in the game manager class form its startup routine.

‘Initialize’ temporarily disables input, creates a list that will hold references to cards the player selects, creates and initializes a fresh game state, and then does the same with our game grid. This should always be called first when the game manager is instantiated.

‘RevealGameFieldAndStartGame’ begins the game by briefly showing the cards to the player and then calling the last startup method ‘StartGame’ which turns input on so the player can start the game.

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

public class GameManager {

    // this handles one instance of a gamestate
    private GameStateModel state;

    // wether or not game input is being accepted right now
    public bool InputEnabled { get; set; }

    // Currently selected cards waiting to be matched
    private List<int> selectedCards;

    // Use this for initialization and prefetch
    public IEnumerator Initialize()
    {
        InputEnabled = false;
        selectedCards = new List<int>();

        // setup the default game model
        GameVars.GameStateConfig : new GameStateModelConfig();
        state = new GameStateModel();
        state.grid_x = 6;
        state.grid_y = 2;
        state.matchLength = 2;

        // initialize the grid
        CardGridManager cgm = CardGridManager.GetInstance();
        cgm.CreateCardGrid(state);
        cgm.ShuffleCardGrid(state);
        cgm.InitializeGridCardViews(state);
        cgm.SetCardSpacing(state);
        cgm.SetCardTextures(state);

        yield return new WaitForSeconds(0.2f);
    }

    // Reveal the game field
    public IEnumerator RevealGameFieldAndStartGame()
    {
        // give the player a peek at the cards
        CardGridManager cgm = CardGridManager.GetInstance();

        float peekDuration = GameVars.PeekDuration;
        float flipDuration = GameVars.CardFlipDuration;
        float delay = 0.5f;

        // Reveal the cards briefly
        cgm.PeekAtCards(delay, peekDuration);
        yield return new WaitForSeconds(delay + peekDuration + flipDuration);

        // start the game when we're done
        StartGame();
    }

    // Anything that needs to be done first frame after prefetch
    void StartGame()
    {
        // turn on relevant game components (such as input!)
        Debug.Log("Starting Game!");
        InputEnabled = true;
    }

The last 3 functions in GameManager control the actual game logic. ‘OnCardWasClicked’ responds to the user clicking on a card view object by flipping it over, adding it to the selected card list and then calling EvaluateMatch if the player has selected enough cards to attempt a match.

The last function, GameCompletionSequence will end the game if EvaluateMatch determines that all cards have been matched and the player has won the game.

    public void OnCardWasClicked(CardView cardView){

        if (!InputEnabled)
        {
            return;
        }

        // find our card index
        int cardIndex = cardView.refIdx;

        // if it's one we already selected, bail
        if (selectedCards.Contains(cardIndex))
        {
            return;
        }

        // flip!
        cardView.FlipUp();

        // store it
        selectedCards.Add(cardIndex);

        // evaluate match
        if (selectedCards.Count == state.matchLength)
        {
            // disable input
            InputEnabled = false;

            CoroutineSingleton.GetInstance().StartCoroutine(EvaluateMatch());
        }
    }

    IEnumerator EvaluateMatch()
    {
        // compare our selected cards to find a match
        int[] indexes = selectedCards.ToArray();
        bool matchFound = state.MatchWasSuccessful(indexes);
        CardGridManager cgm = CardGridManager.GetInstance();

        // reset all the cards after a delay
        yield return new WaitForSeconds(GameVars.FlipCheckDuration);

        if (matchFound)
        {
            // mark views as matched to lock them
            cgm.SetCardViewsMatched(indexes);
        }
        else
        {
            // return cards to their face-down positions
            cgm.FlipAllUnmatchedCardsDown();

            // account for the time it takes to flip cards back down, minus a small amount for feel
            yield return new WaitForSeconds(GameVars.CardFlipDuration * 0.5f);
        }

        // zero out the selected card list
        selectedCards = new List();

        if (state.IsComplete())
        {
            Debug.Log("Game is completed! Starting GameCompletion Sequence...");
            CoroutineSingleton.GetInstance().StartCoroutine(GameCompletionSequence());
            yield break;
        }

        // re-enable input!
        InputEnabled = true;
    }

    IEnumerator GameCompletionSequence()
    {
        yield return 0;

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

TestScript

One last thing before we can test our game, we need to re-visit the card view script from part 1 briefly. Now that our Game Manager class exists, we can inform it when a card is clicked by the user.

IEnumerator OnMouseDown () {

	if(isMatched)
	    yield break;

	// if input is enabled, flip the card and wait
	GameManager _gm = GameVars.GameManager;

        // inform our input manager a card has been clicked
	_gm.OnCardWasClicked (this);

	yield return null;
    }

Lastly, in order to test our game we’ll need to use a simple test script to set up our games root object and our game manager. Then we can initialize it and begin the game.

public class TestGame : MonoBehaviour {

    private GameManager gm;

    void Awake () {
        // create a root object for our game to anchor to
        GameObject root = new GameObject(GameVars.RootObjectName);

        // create our game manager object
        gm = new GameManager();
        GameVars.GameManager = gm;

        StartCoroutine(StartupRoutine());
    }

    IEnumerator StartupRoutine () {
        // Initialize the Game Manager
        yield return StartCoroutine(gm.Initialize());

        // Reveal the cards and begin the game
        yield return StartCoroutine(gm.RevealGameFieldAndStartGame());
    }
}

gameboard

Technically, we have a game now. But in part 3 we’re going to add the ability to customize the deck texture, and add a menu system.

Posted in Tutorials | Leave a comment

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.

Posted in Tutorials | 8 Comments