Go head-to-head with skilled mages in this matching card combat game! Select multiple of the same spell cards to add a multiplier to the effects of the card. Knockdown your opponent's health to zero to win the match.
Created using Unity game engine. I am responsible for game art, design, and development.
In-Game screen capture from Tarot Match Attack.
Pitch
Tarot Match Attack was a project I came up with for my rapid game design final. The task was to mix two different game genres to make one final game. I choose to combine a turn-based combat game with a card matching game for two main reasons. First off, I got good grades on my turn-based combat assignment, and the second was because I didn't do so well on the matching card game assignment. I wanted to challenge myself to improve where I had previously failed. To get my idea green-lit by my professor, I put together a short presentation.
Pre-Production
Once approved, I was able to move onto the planning phase. At this point, I wanted to review both my combat game and my matching card game to see where I needed to improve. I only had one month to complete this game from start to finish, so it was important to understand what shortcomings I needed to improve upon.
For the combat aspect of the game, I knew I needed to improve on the visual feedback, it was unclear at times which player's turn it was, and I could understand how a player may get confused when playing with a long back and forth. I also wanted to improve my scripting skills when it came to the character and player stats. The game wasn't as accurate or predictable as it needed to be when calculating and implementing damage taken or health healed.
The improvements I wanted to make with the card matching game were about my matching game's scripting needs. I needed to write a script that was more consistent at remembering what spots were empty. I also needed to improve turning over cards when cards are selected and turning to hide at the end of a turn. My matching card game wasn't bad, but there were some unpredictable bugs that I needed to remove for this new game.
After going over the areas in need of improvement, I wrote down a short game design document. The handwritten GDD allowed me a place to flesh out all of the things I wanted and needed the game to do. I always find it helpful to pseudo-code all of my main scripts before diving into them. I like to write down each decision in detail. I write each function down, so it's easier for me to translate what I want into code later. In this sense, it becomes the blueprint on which I base my code. Once my scripts become functional. I can then go in and make adjustments for things I may not have foreseen in my first concept. It also helps me keep sight of what my main objective is for each script.
Hand Written Tarot Match Attack GDD
Production
Level Layout
My original idea for this game was to make it a 2D tabletop-style game similar to how solitaire looks. I was creating the scene in unity with colors, representing different card types. I decided that it would be worth it to make this game 3 Dimensional.
I started with the 2D tabletop idea, so I wanted to keep the card pool face down in the center of the table between the two players. I started construction with cubes that changed colors when flipped over. I wanted to mimic the idea that the enemy is sitting across from you. The player's cards were standing up and in front of the camera in a row. That didn't quite allow the visibility of the playing field that I wanted. I decided to fan out the player's cards at an angle and mirror that card stance with the enemy's cards, similar to how a person might hold their cards when playing go-fish or poker.
Game Art
The art in the game was either created by me or a free public domain image that I photoshopped to suit my purposes. The table and the HUD would fall into the category of photoshopped images.
I decided to paint the tarot cards by hand in photoshop. I started by 3D modeling a playing card in Maya and UV unwrapping it. Once the mesh was complete. I took inspiration from bicycle playing cards and other tarot card decks I had seen online. You can take a look at my Pinterest board at the link below.
After researching the desired aesthetic, I went to work designing the cards. I decided only to paint some of the main cards of a tarot deck. I left out the number-based cards like the cups and swords, and I stuck to cards like wheel-of-fortune, the tower, death, etc. The art on these cards is by no means perfect, but having the chance to make original art for my projects is something I value.
I like to use unique fonts in my games whenever possible. For this game, I wanted to use some ornate display fonts to get that witchy calligraphy feeling. I used the following fonts in the game: Dark power, Dust Serif, Litos Script, and Season of the Witch Black.
I designed the VFX using Unity's visual effects graph. I wanted to incorporate visual feedback into the game because it was an aspect I wanted to improve on from my previous small projects. I decided to make two VFX, the first being a circle for the card selection. I wanted it to look like a ribbon of smoke circling the card. The second was a firework that would shoot out as spell cards have been cast. The firework would aim for the stats HUD. When the player got hit, the camera would shake.
Tarot Match Attack playing card art.
Gameplay Design
The most important task of this assignment would have to be scripting the gameplay. It took me two iterations on the scripts in the game to find a way to achieve the functionality I desired. I want to focus on going over the final version of the game's code for this blog post. Though I will briefly mention some of the problems I have faced before diving in.
The biggest issue I had to start off with was getting selected cards to move from the table into the player's hand. I also encountered a bug with the player selecting cards and all the cards moving into the same slot. This part of the game took many attempts to finish, and is the reason why I have many lines of code dedicated to checking whether or not a card slot is empty. Another problem I ran into was when the enemy had their turn, nothing would happen visually and it was hard to tell whose turn it was. To solve this, I added visual effects into the game but also timers between when the enemy had empty cards in their hand and when they had drawn cards to fill their hand. these are just a few of the major bugs I had to overcome while working on this project. There are still imperfections in the code I will be discussing, but most of the game breaking issues I was able to solve.
There are four scripts needed to make the core gameplay function properly. They are as follows: Player Controller, Enemy Controller, Battle Phase, and Table Controller. The game also needed five additional scripts: Battle HUD, Camera Shake, Card Slots, Cards, Scene Loader, and the Unit Script. To look at all the game's C# files in detail, I've added a link to my GitHub repository for Tarot Match Attack at the end of this section. This GitHub repository has two versions of the game's scripts. The first version was my first attempt at scripting the gameplay, and the second is the final version of the game's scripts.
I started the gameplay scripting process by creating the Cards Script. The card script is essential and determines the type of card at spawn. It holds on to all of the card's information until destroyed. The Cards script is called upon for card information by both the player controller script and the enemy controller script. The CardSlot script represents card spaces in the game. The card slot script handles who possesses the cards, whether the cards are selectable or not, and spawns all cards at the start of the game.
The BattlePhase script handles the start of the game and informs the other elements of the game's current state. There are seven possible phases of the game: START, PLAYERTURN, PLAYERDRAW, ENEMYTURN, ENEMYDRAW, WIN, LOSE. Each game state will help to determine what tasks are required for each script to accomplish. The battle phase script is also responsible for updating player stats, enemy stats, calling on player and enemy functions, and changing phases when the tasks are required. The battle phase script is one of the most important scripts in the game, as it is directly linking the player, enemy, and table together for all functions.
The player, enemy, and table controller scripts are very similar to each other. To start, the Table controller manages the five card slots on the table that both the player and the enemy draw from after they have taken their turns. At the START phase of the game, the table fills each space by calling on each slot's CardSlot script to spawn a random card. When the game is at the PLAYERDRAW phase, as long as the player's hand is not complete, the player can select any card on the table. Once a card on the table is selected, the Table script goes through a sequence of checks and movements. First, the table script checks if that card is selectable, then it will go through the player's list of card slots to find one that is empty. Once an available space is determined, it will move that card into the player's hand at the correct position and rotation, delete the card from its hand, and set the card to the corresponding empty player slot. After, the card is selected and placed in the player's hand. The table script will go through a CheckHand(PlayerController ThePlayer) function to see if the player has any other empty spots in their hand. If the player's hand has any gaps in their hand, they can select another card. Otherwise, the player will not be able to choose another card. After a timer runs out, the table spawns new cards to fill the gaps, and the battle phase will transition to ENEMYDRAW.
Table Controller script line's 114-139
// PLAYER DRAW
public void Selected1( CardSlots Slot1)
{
if (Slot1.Selectable)
{
//if player card 1 empty
if (Player.Card1 == null)
{
//Move card
Card1.transform.position =
Player.CardPose1.transform.position;
Card1.transform.rotation =
Player.CardPose1.transform.rotation;
Card1.transform.parent = Player.CardPose1.transform;
Slot1.Card = null;
Player.Card1 = Card1;
Card1 = null;
}
else
if (Player.Card2 == null)
{
Card1.transform.position =
Player.CardPose2.transform.position;
Card1.transform.rotation =
Player.CardPose2.transform.rotation;
Card1.transform.parent = Player.CardPose2.transform;
Slot1.Card = null;
Player.Card2 = Card1;
Card1 = null;
}
...
When it is the enemy's turn to draw a card, it's handled within the enemy controller script. The enemy controller and the Player controller look almost identical in writing. Similar to the table, the enemy has five cards in their hand. They reference a Unit script that manages and monitors stats, and the battle phase script. At the START phase of the game, the enemy will populate its empty card slots with the Hand() function.
When the battle phase is ENEMYTURN, the enemy script will start a coroutine called SelectCards(). The coroutine begins by choosing a random number from one to five. Each number corresponds to one of the cards. Once a card is determined, the script finds out whether they have enough magic to cast the card. If they do not, the coroutine will start over again from scratch. If they have enough magic, the card component of that slot is saved. Then, the coroutine searches the enemy's hand for matches to that card. Each match added to the total number of cards selected and saved. The coroutine will wait for two seconds before attacking the player with the selected cards.
Once the enemy attacks, the selected cards are erased from the enemy's hand. Instead of moving cards from the table to the enemy's hand, I decided to have the enemy's card slots spawn new random cards. There's a short waiting period, and then the hand re-populates. This illusion of selecting cards makes the back and forth easier to follow and allows the player a short time to strategize before continuing the battle.
EnemyController script lines 115-139 & lines 222-229
public IEnumerator SelectCards()
{
started = true;
int RandomNumber = thisBattlePHase.RandomNumber(6);
if(RandomNumber == 1)
{
if(Card1.CardCost < EnemyUnit.thisCurrentAP)
{
CardSlots slot1 = CardPose1.GetComponent<CardSlots>();
slot1.Select();
MainCard = Card1;
CardsSelected = 1;
Check2();
Check3();
Check4();
Check5();
}
else
{
StartCoroutine(SelectCards());
}
....
yield return new WaitForSeconds(2);
if(CardsSelected >=1)
{
StartCoroutine(thisBattlePhase.EnemyAttack(MainCard));
}
}
The PlayerController script is the longest in the game with 477 lines, closely followed by the BattlePhase script with 386 lines in total. The player controller handles interactions that the player has when selecting cards to use. The player control will use the same Hand() function to spawn new random cards into their hand at the start of the game. The player has the first move in the game. The player can select a card from their hand to attack use for that turn. Once a card is selected, it triggers a selection function within the player controller script to run.
The Select function that runs corresponds to the slot in the player's hand. The left of the player's hand is one, and the right of the player's hand is five. Once the function begins, it checks the battle phase and makes sure that it is the player's turn. Next, it determines if this card is already selected and if the player has less than three cards picked. The player and enemy can only match up to three cards of the same type. If the selection fails this check, the card will be re-set. Removing it from the list of selected cards, zooming out of the card, reducing the number of cards picked, and if the card was the main card, then the main card variable is set to null.
If the selection goes through, then the script will determine whether the player has enough magic to put the card into play. If the player does not, nothing happens, and they must choose a different card to use. When the player does have enough magic points, the number of cards selected increases. If this is the first card clicked, it will become the main card. All additional selections must match this card. Otherwise, if this is not the first selection, it will be compared to the main card already in place. If the card doesn't match the main card nothing will happen, and the player must choose another card. If the card does match the main card. The number of cards selected will increase, the card will zoom in, and the effect around the card will activate.
Once the player hits the spacebar on the keyboard, it will trigger a coroutine in the BattlePhase script attacking the enemy with the player's selected cards. Afterward, the player's used cards get erased.
PlayerController script lines 100-163
public void Select1(CardSlots Cardslot1)
{
// check if the its the players turn
if(thisBattlePHase.GameState == Battle.PLAYERTURN)
{
// check if this card has already been selected
if(!Cardslot1.Selected && CardsSelected < 3)
{
//check if the player can afford the card
if(Card1.CardCost < PlayerUnit.thisCurrentAP ||
CardsSelected < 0)
{
// find the players current number of selected
cards
if(CardsSelected == 0)
{
// select that card
Cardslot1.Select();
//make this the first selected card
MainCard = Card1;
//add to the number of cards selected
CardsSelected = 1;
// zoom in on the card
Cardslot1.ZoomIN();
//visual effect
VFX1.SetActive(true);
}
else
if(CardsSelected == 1 && Card1.tag == MainCard.tag)
{
Cardslot1.Select();
CardsSelected = 2;
VFX1.SetActive(true);
}
else
if(CardsSelected == 2 && Card1.tag == MainCard.tag)
{
Cardslot1.Select();
CardsSelected = 3;
VFX1.SetActive(true);
}
}
}
else
if (Cardslot1.Selected)
{
Cardslot1.Selected = false;
Cardslot1.ZoomOUT();
CardsSelected--;
VFX1.SetActive(false);
if (CardsSelected >= 0)
{
MainCard = null;
}
}
}
}
To get a closer look at my C# scripts for Tarot Match attack's gameplay, take a look at the linked GitHub repository below. There, you will find two iterations of my game scripts. The first version is my first iteration of the game's scripts, and the second version is what has been used in the examples above.
Post Mortem
I am proud of this project. It has challenged me to better myself as a game designer. I think that even though it may not be the prettiest game I have ever worked on, It is still a very well-rounded project that was fun to make.
Gameplay of Tarot Match Attack prototype.
If I were to start this project again from scratch. I would add more art into the scene or design the table more interestingly. As it is now, the scene looks plain, and I think bringing in more elements from my mood board would make the game more visually interesting. I would also like to make more variations of the visual effects when the player is healing or if an attack is critical. I would also like to add more visual effects to the enemy's turn of the game.
As far as changes I would make to the game's code, I would probably change almost everything about it. Since working on Tarot Match Attack, I've learned how to program in C++. My C++ object-oriented programming taught me a lot about how to organize and refactor my code. Looking at my code from before my C++ skills were developed, I cannot help but feel it's not as clean or as compact as I would like it to be. It is functional as it is, but I would not call it organized, and I would try to keep functions about each element within that element's script instead of jumping in and out of the BattlePhase.
Comments