Assets

In the Project tab, let’s look at the Asset hierarchy. This is a fairly typical style of organization where we have folders for each type of asset.
Assets

Card Sprites

Let’s find the cards.
There is no “Sprites” folder. Instead, look in the “Resources” folder. In there, you will find the “Resources/PlayingCards” subfolder.
Cards

Since there are over 50 images in this folder, it’s much easier to load these images using code.
If we want to load an asset, we should put it into the Resources folder.
We can only load files from a “Resources” or “StreamingAssets” folder.

Scripting

There are 4 cs files (classes) in the “Scripts” folder.
Scripts
Card is an individual card. This is the symbolic representation as well as the graphical sprite for the card.
Deck is the deck of unused cards. We will draw a card from the deck to place it into the player’s Hand.
Game is a manager to run the flow of the game.
Hand is an individual player’s hand (including the dealer). This is a collection of Cards.

Game.cs

Open up Game.cs and let’s take a look.

Variables

Let’s start with the variables. I’ve placed those at the top.

public class Game : MonoBehaviour
{
    public Hand m_playerHand;
    public TextMeshProUGUI m_playerScore;
    public Hand m_dealerHand;
    public TextMeshProUGUI m_dealerScore;
    public Deck m_deck;
    public GameObject m_hitButton;
    public GameObject m_stayButton;
    public GameObject m_win;
    public GameObject m_lose;
    public GameObject m_playAgain;

    int m_scorePlayer = 0;
    int m_scoreDealer = 0;

Those ‘public’ variables are automatically exposed in the Unity Editor.

We use ‘public’ variables to expose things in the Unity Editor.

I have used these public variables to hook up all the various parts of the game such as the “Hit” button, the Player’s Hand, and the Deck.

I use a convention where I put “m_” in the front of member variables.
The “m” stands for “member”, and it serves to remind me (and you) that the variable in question is a member of the class (as opposed to a local variable in the function).

Game Flow

The Start() function kicks things off with a coroutine. We’ll talk more about those later.
StartCoroutine(DelayedDeal());

    IEnumerator DelayedDeal()
    {
        yield return new WaitForSecondsRealtime(0.1f);
        Deal();
    }

    void Deal()
    {
        // deal 2 cards to the player
        m_playerHand.AddCard(m_deck.GetCard(), true);
        m_playerHand.AddCard(m_deck.GetCard(), true);
        UpdatePlayerScore();

        // deal 2 cards to the dealer
        m_dealerHand.AddCard(m_deck.GetCard(), true);
        m_dealerHand.AddCard(m_deck.GetCard(), false);
        UpdateDealerScore();
    }

Simply put, we wait for 0.1 seconds and then call Deal().
The Deal() function puts 2 cards into the player’s Hand and 2 cards into the dealer’s Hand.

Then, it just waits. Wait’s for what?

It is now the player’s turn to take an action. The player may either “Hit” or “Stay”. There are functions set up for those actions:

    public void PlayerHit()
    {
        m_playerHand.AddCard(m_deck.GetCard(), true);
        int score = UpdatePlayerScore();
        if (score > 21)
            PlayerStay();
    }

    public void PlayerStay()
    {
        m_hitButton.SetActive(false);   // disable the "Hit" button
        m_stayButton.SetActive(false);   // disable the "Stay" button too
        m_dealerHand.RevealAll();
        m_dealerScore.transform.parent.gameObject.SetActive(true);
        StartCoroutine(DealerTurn());
    }

This leaves us with the question, “How are these functions going to get called?”

Look at the Scene

Hit Button
Select the “Hit” button.
Scroll down a bit in the “Inspector” tab to find the “On Click” section of the “Button” component.
This is how we have hooked that up. The “Hit” button is set to call “PlayerHit” when clicked.

The “Stay” button is set up in the same way.

Check Out the Deck

Lets look at Deck.cs

    // Start is called before the first frame update
    void Start()
    {
        m_cards = new List<Card>();
        // fill the deck with cards
        for (int suit = 0; suit < 4; ++suit)
        {
            for (int rank = 1; rank <= 13; ++rank)
            {
                CreateCard((Card.Suit)suit, rank);
            }
        }
#if false
        {   // don't forget the jokers
            CreateCard(Card.Suit.none, 0);
            CreateCard(Card.Suit.none, 1);
        }
#endif
        Shuffle();
    }

    GameObject CreateCard(Card.Suit suit, int rank)
    {
        GameObject cardObject = Instantiate(m_cardPrefab, transform);
        Card card = cardObject.GetComponent<Card>();
        card.Initialize(suit, rank);
        m_cards.Add(card);
        cardObject.SetActive(false);
        return cardObject;
    }

We use a nested for loop to create all the cards… 4 suits with 13 cards per suit.
At the end of the function, we call ‘Shuffle()’. You’ll be implementing that function later… for now, it does nothing.

Notice the #if false. That disables the enclosed block of code meaning that we will not be including those 2 jokers.

The CreateCard() function instantiates a copy of the prefab m_cardPrefab.
It calls Initialize() on it,
adds the new card to the list m_cards,
sets the new GameObject to be inactive,
and finally returns the new GameObject.