Nickolas Rau

Game Developer | Programmer | Designer

High Low

Overview

I built this game as part of a coding exam to show how I break down a problem and implement a working solution. This was also an assessment of how well I could estimate complexity of a task by setting and meeting deadlines. I submitted an estimate of 3-5 days to complete this game and finished it on the 4th day. I have included the exam requirements in this summary as well as my implementations.

Game Statistics

  • Engine: Unity
  • Platform: PC, Mobile, WebGL
  • Genre: Card Game
  • Team Size: 1
  • Time: 4 Days (35 hours)
Media 1

Exam Doc

Table of Contents

Programming

Initial Planning

 I started by first laying out some quick ideas for the basic construction of the game. Initially, my focus was on two things.
  1.  A way to compare cards
  2.  Selecting a card with a probability
 I decided a single card class with an integer to hold the value and a string to hold the suit would be enough to distinguish between each card. Then I sketched out a small range of numbers to model the probabilities of each card being drawn. My plan was to generate a number and check if it’s within a certain range. Each range would be sized differently according to the desired probability. So in this case, with the Ace of Spades needing to be 3 times as likely to appear, the range would be 3 times as large.

Project Image
Project Image

 I also briefly sketched out the flow of the game and the loop. Anticipating simple button down input that will call a function that compares the two cards and gives a score according to what button was pushed. Then, handle removing the cards and check if there are still cards remaining to either deal again or display a game over.

Card Class


public class Card : MonoBehaviour
{
    public Sprite face;
    public Sprite back;
                    
    public int value;
    public string suit;
                    
    private SpriteRenderer sRenderer;
                    
    private void Awake()
    {
        sRenderer = GetComponent();
        sRenderer.sprite = back;
    }
                    
}
                    

 I created a card class with variables for value and suit, plus two sprites for each card's back and face. I then made a prefab for each card in the editor. My intention was to provide extensibility by allowing custom decks to be created. No matter the game, a deck could be created out of a selection of card prefabs.

Game Manager

 I created a game manager script and object to fully contain all aspects of the game's flow. Within this script I wrote the functionality for initializing a new game, dealing cards based on probability, and comparing the cards when a guess is submitted. In the editor I had public variables:

  • An array to act as the deck, holding all 52 card prefabs
  • Position of the deck (off the screen to not be seen)
  • Positions for the left and right cards in the game view
Project Image

NewGame()

 On the initial load of the game manager script, NewGame() is called. This function iterates through all of the card prefabs that were added within the editor, instantiates them, and adds them to a list named currentDeck. It is designed to be extensible so that when the hand is over, NewGame() can be called and the player can continue with another hand.

I chose the List< > data structure here as it:

  • Would allow for easy additions and fast removal
  • Is dynamically sized and can adjust as cards are removed
  • Can use LINQ to search based on attributes like suit or value

public class GameManager : MonoBehaviour
{
    public GameObject[] deckPrefab;
    public Vector3 deckLocation;
    public GameObject CardPosLeft;
    public GameObject CardPosRight;

    private List<GameObject> currentDeck;
    private GameObject card1;
    private GameObject card2;

    private void Awake()
    {
        NewGame();
    }

    private void NewGame()
    {
        if(currentDeck != null)
        {
            currentDeck.Clear();
            foreach (GameObject card in deckPrefab)
            {
                GameObject tempCard = Instantiate(card, deckLocation, Quaternion.identity);
                currentDeck.Add(tempCard);
            }
        }
        else
        {
            currentDeck = new List<GameObject>();

            foreach (GameObject card in deckPrefab)
            {
                GameObject tempCard = Instantiate(card, deckLocation, Quaternion.identity);
                currentDeck.Add(tempCard);
            }
        }
    }
}

DrawCard()

 The DrawCard() function incorporates all the logic for selecting a card given the requirements of the exam document. This implementation, however, requires a lot of house keeping to ensure edge cases are accounted for and probabilities remain consistent while the conditions of the deck change, making it not very extensible. I spotted an error even when writing this summary, so if I were to do this again, I would consider a different approach.

  • Step 1: Go through and set the maximum value the range can be based on the conditions of the deck:
    • If there are normal cards remaining
    • If there are hearts remaining
    • If the ace of spades was already drawn
  • Step 2: Create a pool of cards that meet the conditions of the randomly selected value. For example, if the number fell within the probability range for a heart, then the selection pool would include all remaining hearts.
  • Step 3: Randomly select a card from the selection pool (mimics cards being shuffled), update flags according to what card was just removed from the deck, and return the card.

public GameObject DrawCard()
{
    if (currentDeck.Count == 0) return null;

    bool normalCardsAvailable = currentDeck.Any(c => c.GetComponent<Card>().suit != "Hearts" 
    && !(c.GetComponent<Card>().value == 1 
    && c.GetComponent<Card>().suit == "Spades"));
    int maxRange;
    
    //****************************************** STEP 1 ***************************************************
    if (aceOfSpadesDrawn)
    {
        maxRange = heartsAvailable ? (normalCardsAvailable ? 30 : 20) : (normalCardsAvailable ? 10 : 0);
    }
    else
    {
        maxRange = heartsAvailable ? (normalCardsAvailable ? 60 : 90) : (normalCardsAvailable ? 40 : 1);
    }

    int randomNumber = Random.Range(1, maxRange + 1);
    List<GameObject> selectionPool;

    //****************************************** STEP 2 ***************************************************
    if (randomNumber <= 10 && normalCardsAvailable)
    {
        selectionPool = currentDeck.Where(c => c.GetComponent<Card>().suit != "Hearts" 
        && !(c.GetComponent<Card>().value == 1 
        && c.GetComponent<Card>().suit == "Spades")).ToList();
    }
    else if (randomNumber <= 30 && heartsAvailable)
    {
        selectionPool = currentDeck.Where(c => c.GetComponent<Card>().suit == "Hearts").ToList();
    }
    else if (randomNumber <= maxRange && !aceOfSpadesDrawn)
    {
        selectionPool = currentDeck.Where(c => c.GetComponent<Card>().value == 1 
        && c.GetComponent<Card>().suit == "Spades").ToList();
    }
    else
    {
        selectionPool = new List<GameObject>(currentDeck);
    }

    //****************************************** STEP 3 ***************************************************
    if (selectionPool.Count > 0)
    {
        int index = Random.Range(0, selectionPool.Count);
        GameObject selectedCard = selectionPool[index];
        currentDeck.Remove(selectedCard);

        if (selectedCard.GetComponent<Card>().suit == "Spades" 
        && selectedCard.GetComponent<Card>().value == 1)
        {
            aceOfSpadesDrawn = true;
        }

        if (selectedCard.GetComponent<Card>().suit == "Hearts")
        {
            heartsAvailable = currentDeck.Exists(c => c.GetComponent<Card>().suit == "Hearts");
        }

        return selectedCard;
    }
    return null;
}
                

CompareCards()

 When the player presses one of the buttons in game, a CompareCards() function call is made, passing the 2 active cards along with a sting of the guess made. A check is then made to see if the values are the same. If they are, it calls a function to evaluate which suit is higher.
 To compare the suits, I used a switch statement to assign an integer value to each suit. (below)

private int GetSuitValue(string suit){
    switch (suit) {
        case "Spades": return 4;
        case "Hearts": return 3;
        case "Diamonds": return 2;
        case "Clubs": return 1;
        default: return 0;}
}          
private bool CompareCards(GameObject first, GameObject second, string guess) {
    if (first.GetComponent().value == second.GetComponent().value)
    {
        return CompareSuits(first, second, guess);
    }
    else
    {
        if (guess == "higher")
        {
            return second.GetComponent().value > first.GetComponent().value;
        }
        else
        {
            return second.GetComponent().value < first.GetComponent().value;
        }
    }
}

Animations

Buttons

  I wanted to add some feedback for the user when interacting with the buttons. Particularly when hovering and pushing them. Therefore, I made button game objects with custom scripts that reside in the game world. To do this I added a sprite renderer, box collider and imported a tweening library called iTween.


//Hovering 
private void OnMouseEnter(){
sRenderer.sprite = hoverSprite;
iTween.ScaleTo(gameObject, iTween.Hash("scale", scaleTo, "time", scaleTime,
"looptype", iTween.LoopType.pingPong, "easetype", iTween.EaseType.linear));
AudioSource.PlayClipAtPoint(hoverSound, new Vector3(0, 0, 0));
}

//Pressing
private void OnMouseDown()
{
    AudioSource.PlayClipAtPoint(pressSound, new Vector3(0, 0, 0));
    iTween.PunchScale(gameObject, new Vector3(punchScale, punchScale, punchScale), punchScaleTime);
}

Cards

  Initially, I had it coded to change the position of the cards from the deck to the game view. This made it seem that cards just “appeared” out of nowhere. It didn’t impact the way the game was played, however I added an iTween script to interpolate the position of the cards offscreen to the game view. This was able to give the impression of cards being dealt.

 The first card 'flip' animation simply swapped the sprite displayed by the sprite renderer. Making the sprite smoothly give the 'flipping' effect was a bit more challenging. I couldn’t use the iTweening library because it lacked the flexibility I needed. Therefore, I created a coroutine with Mathf.Lerp() to perform half the rotation, swap the sprite, and then rotate back to the starting position. This method worked well and effectively created the impression of the card flipping. However, if you look closely, there is 'popping' that occurs with the left side of the card and the shadowing effect, due to how Unity rendering layers work

Before

After

Additional Features

Score Numbers

 I wanted a way to visually display score increasing for the player, to make it feel more like a game.

  • Imported and learned a new package from the Unity asset store called DamageNumbersPro.
  • Created number prefabs within the editor for each card and the streaking system
  • Coded them to be spawned when the player guessed correctly

Project Image

Static Score Manager

I implemented the Score Manager as a static instance to allow other scripts easy access to its functions. This script centralizes all the logic necessary for features such as:

  • Updating current streak graphic
  • Adding score
  • Counting the number correct
  • Calculating longest streak
  • Displaying game over statistics

Media 1

Static Class Creation

Media 1

Add Score & High Score

Media 1

Update Current Streak

Image 1

Retrospective

What went well

  • Choosing a list data structure for the deck proved invaluable as I queried the conditions of the deck often, with ease
  • Using a switch statement to return an integer value for each suit was successful for determining hierarchy
  • Initial concept and whiteboarding was detailed enough to guide programming and deliver a finished game within the timeframe
  • UI feedback and interactions felt meaningful and well telegraphed
  • Animations gave the impression of cards being “dealt”

What went wrong

  • My implementation of determining likelihood required a lot of housekeeping and was prone to errors. When writing this summary I found 3 places in the logic that didn’t properly account for edge cases and I needed to revise the code.
  • Making object buttons within the game world allowed for a customized feel, but introduced a bug with detecting mouse hover events after a mouse down event. This resulted in no hover animations.

What I learned

  • A stonger approach to determining edge cases and a thorough way of stepping through each outcome when working with probability
  • How to use a tweening library to animate game objects
  • Situations where a tweening library doesn't fit and how to code a custom behavior
  • How to create number prefabs in the editor and dynamically spawn them in the game world
  • PlayerPrefs in Unity and how to store / retrieve data like high score
  • Familiarity with importing custom fonts and creating assets for them
  • In this situation, my code worked fine, but moving forward, it would be more efficient to cache a reference to any object that I am using .GetComponent<>() on multiple times.