This article is still under construction. You may prefer to download the Microsoft Word document instead. Because of its large file size, it has been split into two parts. You can download them here:
What makes this game computational?. 2
What similar games already exist?. 3
My vision is to make a digital card game, with new, unique elements to create a competitive and fun experience. There are already several card games, but many are either too simple, lack creativity, or are unappealing to newcomers. I would like to create a game that ticks all these boxes, while maintaining a high standard of quality and introducing my own unique ideas.
People are always looking for new games to try, play and master, and I would like to create a game that's fun for everyone to get into and one that they can have fun mastering.
What makes this game computational?
Card games, by nature, are either partially or fully based on luck. And humans, by nature, are not random. Human biases will always affect your card game. Moving the game to a digital platform attempts to fix this as computers are much more random than humans. This ensures a fair and fun game.
Physical card sets also always have the possibility of being damaged. Making the move to a digital platform mitigates this and allows the game to be accessed from any device with the program installed, or any device that is connected to the internet.
I also want to expand upon the accessibility that I previously mentioned. I want to make my game accessible to as many people as possible to ensure that no one is left out; video games should be available to everyone. Making the game computational makes the game much more accessible, since things like the colour of the UI and the size of the buttons can be changed easily according to the needs of the stakeholder.
The last problem that I aim to fix is cheating. In real life cheating is a widespread problem in card games, it can simply be done by sneaking a peak at your opponents' cards. However, this is not possible digitally and thus cheating is much more difficult, requiring actual manipulation of networks and programs, which is much more difficult to achieve.
In conclusion, I believe that there are many small aspects that come together to make this game an obvious candidate for virtualisation.
What similar games already exist?
There are already many programs that are either completely new games, or digital recreations of physical card games that have existed for many, many years. For example, Microsoft's window solitaire is an extremely famous example, built into many windows computers, and accessible to many. However, most of these games are quite old and thus have been tried and tested. Most people do not play them anymore since they are old games. My game directly addresses this by trying to create a new, novel, exciting experience.
Microsoft Solitaire:
Klondike solitaire is a game where you are given seven stacks of random cards, and a pile of hidden cards. The goal is to sort all the cards into their respective houses, in order, in the empty spaces in the top right. Cards can be moved around in the piles at the bottom of the screen, but they must be placed in descending order, in alternating colours.
Source: https://en.wikipedia.org/wiki/Microsoft_Solitaire
Solitaire has luck involved in it, which is an element that I want to incorporate into my game as well. The randomness ensures that the game does not become stale, and makes the player react to new situations every time.
Solitaire is also only a single player game. In my game, I want to try and include a multiplayer mode, where you play against other people online. However, this will be difficult to implement, so I cannot guarantee that it will be implemented. The multiplayer game mode should keep players engaged for longer.
Finally, Solitaire is a very repetitive game. After you have mastered the techniques, every game blends into the next, causing the game to get monotonous. In my game I am going to introduce some random elements that can help to keep the game interesting for longer. These elements should be relevant to players of all skill levels, since they are luck based and not skill based.
However, I do like the simplicity of solitaire, the only control is to drag the cards and yet it provides engaging gameplay. I want to incorporate this simplicity into my game as it increases the level of accessibility and will make It less overwhelming to newcomers.
UNO:
Another popular card game is UNO, a simple game where you and other players compete to get rid of all the cards in your hand first. The aim is to place all your cards into the centre pile, one by one. You cannot see other's cards, and you must shout 'UNO' when you have one card remaining.
In UNO, instead of using the normal house/number system for cards, they use their own colour/number system. There are four colours, and ten numbers, 0 to 9. This makes the game much easier to understand, makes it much more unique, and opens it up to younger and older people to play. I want to include something like this in my game too, as I want my game to be targeted at a large age group, and I also want to make the cards look as unique as possible.
An image advertisement for UNO on Steam
Source: http://store.steampowered.com/app/470220/UNO/
UNO is also a digital recreation of a physical card game. Compared so solitaire, the game has much more depth to it. It is multiplayer, has special cards, such as cards that switch the direction of the game and cards that can stop a player's turn, and even cards that can give your opponent's more cards, sabotaging them. This is the sort of creativity that I aspire to include in my project, as it makes the game fun, unique, and differentiates it from other card games.
The UI in this game is also simple, easy to understand and attractive. It features big buttons and special effects throughout the game. I do think that all the special effects are distracting, and I probably won't include them, however I want to try and make an easy-to-use UI like the one in UNO.
One last thing that I like is the ability for uno to track your previous games, and the total amount of games that you have won and lost. This is something that I would like to implement into my game as well.
Uno's Statistics page
Source: My own screenshot
Top Trumps:
Top trumps is a physical card game that is highly popular among young children due to its simplicity. In this game, you purchase a pack of cards where each card has a unique set of statistics. The aim is to win all your opponents' cards. You shuffle the cards and pick one card. You read out the statistic that you think will be higher than your opponents. If it is higher, you take their card.
An image advertisement for Top Trumps
Website: https://toptrumps.com/
There are many variations of the game that users can collect. They can collect all the different packs, or just stick to one. The game offers freedom and flexibility, as you can do either without any limitations. This does help to keep the game fun, but since the packs themselves are paid, it can be expensive. I want to incorporate this statistics system into my own game, since the stats can be so granular it allows for a vast variety of cards. However, I do intend to make my game free of cost and give it a bit more depth, and of course, make it digital and computational.
In the image below, you can see Top trump's statistics system. The panda has 5 statistics, and it has a different value for each one. You can see how much customisation is possible with just 5 numbers.
Another thing I like about top trumps is how easy the game is to understand. You simply have to match up 2 numbers, and the player that has the greater number wins. This makes the game accessible to all ages, old and young people alike. This inclusivity is something that I want to include in my game. I want to incorporate this into my game too.
Additionally, since there are so many different card packs, users are unlikely to get bored. I won't be able to add as many cards as top trumps, but I will try my best to include as many cards as possible to keep every round interesting and keep users engaged.
However, the main issue with top trumps is obviously that they are not a digital game, though digital recreations do exist. My game will be digital and will fix a lot of the issues present in the physical versions of top trumps, like being damaged, lost or stolen.
Finally, top trumps might be perceived as too simple compared to some other games. As I mentioned before this is also actually one of its strengths; making it easier to understand. However, I understand that my stakeholders might want some extra challenge and so I will add some other elements to address this.
The stakeholders that I have chosen are all looking for something to do in their free time. I'm hoping that my game can be something that they can do in that free time. Additionally, they all use desktop computers, which is what I plan to develop my game for.
My first stakeholder is Brett, a 17-year-old A level student. He plays card games regularly, though he plays physical sets more than digital card games. He is very experienced and knows what makes a card game good. I will be asking him about the gameplay and how fun it is compared to other card games that he has played previously. He has just finished a game and he is looking for a fun game to play in his free time. I want to try and make my game as unique and fun as possible, so my game suits him well. [1DJ1] He will represent my 'Experienced' user.
My second stakeholder is Joshua, a 24-year-old young adult who typically plays action computer games but is now looking for something strategy based. Joshua does not typically play card games, so he will not be able to help with the strategy. However, he can help me with the UI and UX of the program, and the computational side. Since my game is going to be a card game, which is strategy based, this suits Joshua well. [1DJ2] He will represent my 'Average' user.
My third stakeholder is Richard, a 74-year-old who has played many card games in his life, though casually. He also knows what makes a card game fun, but he has almost no experience with technology. I have chosen him as a stakeholder as he can help me decide what to do to make the software more accessible. with the recent lockdowns from COVID-19 he has struggled to move online and play games with his friends, and most software is unintuitive for elderly and inexperienced people to use. Furthermore, Richard finds that most games he finds lack proper explanation, so my goal of accessibility suits him well. [1DJ3] He will represent my 'Inexperienced' user.
This questionnaire will tell me exactly what my stakeholders want so that I can implement it into the game. I plan to take input from my stakeholder at all key points to make the game the best that I can be. The questionnaire is open ended since I want full thoughts from my stakeholders for their input and potentially some creative ideas.
Brett's response:
Q: What type of theme would you like in a card game?
A: I would like to see a medieval theme, involving things like knights and dragons. Fantasy is great in video games.
Q: What level of complexity would you like in a card game?
A: I like my card games simple but unique.
Q:' What are some factors that make a card game fun?
A: As I previously mentioned, it must be unique, and it should not have any bugs. I can't have an enjoyable experience if the game doesn't run well.
Q: How many unique cards do you think that there should be in a card game?
A: To me the number of cards does not matter too much, but I think at least 20 is good.
Q: Would you be interested in seeing a feature to record statistics like games played?
A: That would be a very interesting feature, being able to see everything that I've done in the past would be great.
Q: Is a multiplayer function important for you?
A: Personally, I don't really care.
Q: Are there any unique ideas that you would like to see in this game?
A: I don't have any suggestions.
Brett seems to want a simple, unique, and well-polished game. Personally, I like his idea of a fantasy themed game, so I think that I will implement this. Brett doesn't seem to care much for a multiplayer function, though he does like my idea of a statistics screen.
Joshua's response:
Q: What type of theme would you like in a card game?
A: I think that the theme could be anything from futuristic to fantasy, it doesn't matter to me as long as it's interesting.
Q: What level of complexity would you like in a card game?
A: I would like it to be a bit simple since card games with lots of rules like poker can be overwhelming.
Q:' What are some factors that make a card game fun?
A: I like action in my video games. I know that card games don't usually have much action, but I think maybe a timer of some sort could be put in to increase the stakes.
Q: How many unique cards do you think that there should be in a card game?
A: I want to see lots of cards, at least 40 or 50 though this could be hard to do.
Q: Would you be interested in seeing a feature to record statistics like games played?
A: I do not care for it at all, though I also don't care if it is implemented.
Q: Is a multiplayer function important for you?
A: To me multiplayer is really important and can make or break a game, but I don't know if multiplayer can be implemented into all card games. So I'll be really happy to see it.
Q: Are there any unique ideas that you would like to see in this game?
A: Something that I've seen before in shooter games is different terrain that causes your character to behave differently. Maybe this can be implemented into the card game by boosting certain cards with a randomly picked terrain.
Joshua seems to want his game fast paced, coming from shooter games. He wants the multiplayer function to be implemented. I really like his idea of a terrain system. I think it would be a great idea to have different environments that boost certain characteristics or statistics of cards.
Richard's response:
Q: What type of theme would you like in a card game?
A: I do not really care but I think fantasy would be nice.
Q: What level of complexity would you like in a card game?
A: I like simple games.
Q:' What are some factors that make a card game fun?
A: I need to understand a game to have fun with it, so I want it to be easy to understand. I want it to be easy to use since I am not particularly good with technology.
Q: How many unique cards do you think that there should be in a card game?
A: It doesn't matter to me; the gameplay and concept is more important.
Q: Would you be interested in seeing a feature to record statistics like games played?
A: That would be good.
Q: Is a multiplayer function important for you?
A: No. I like to play alone from time to time.
Q: Are there any unique ideas that you would like to see in this game?
A: I think that maybe each card could have its own unique art.
Richard's response wasn't too detailed, but he wants a simple card game to play alone. He wants to see each card have unique art, which is something that I plan to implement. He is also interested in seeing the statistics feature. Since he's not very good with electronics he wants the game to be easy to use, which is something that I want to try and achieve.
In conclusion, most stakeholders voted for the statistics page feature, so I will be implementing that. Most stakeholders said that a multiplayer mode is not important, so that will be excluded. The game will be fantasy/medieval themed.
I'll also be implementing Joshua's suggestion, which is the terrain feature. I'll also try and make the game as accessible as possible as per Richard's request.
A 'Gallery' section to view player stats:
When a player performs any action in the game, this will be logged into a statistics sheet. The user can view these statistics at any time. These statistics will include the number of games played, the number of cards played, wins, losses, and the win to loss ratio. This should be a useful tool for the user to track how they are performing over time. This isn't a necessary feature, as the game can function without it.
The enemy AI:
The player will need to be able to play against an AI. This should also have varying difficulties to match the skill level of the player. Without difficulty levels, the players may get bored or frustrated, reducing their engagement. This is a necessary part of the solution. The game cannot function without it.
An 'Environments' system:
This is the unique part of my game. The environment system serves as a way to differentiate the game, add a layer of strategy, and provide a better experience for the user. Depending on the environment, certain statistics of a card will be boosted. I will be asking the stakeholders for a lot of feedback on this as I have little experience.
Accessibility:
I'm going to be developing his game around the premise of accessibility. Making it easy to play for elderly people, and young people, by making the game as easy to navigate and play as possible, without sacrificing depth and strategy in the game.
Tutorial:
To make the game easy to learn for all people, there will be an optional tutorial that teaches the basics of the game as the user goes along.
One limitation is the lack of a multiplayer mode. Players may eventually become bored of the game despite my attempts to make it as fun as possible. A lack of a multiplayer mode means that people may potentially not want to play the game at all (since multiplayer can be an essential part of many modern games) or become bored quickly. The reason I haven't implemented this since most of the stakeholders did not consider it a point of importance. The time that I would have spent on designing a multiplayer mode will be spent on designing cards instead.
Another limitation is the limited selection of cards. Users will eventually learn about all the cards in the game, and at that point the card system will lose its novelty. However, this is inevitable. Without an infinite number of cards, the user will eventually discover all of them. Of course, I will try my best to make as many unique cards as possible.
Finally, the lack of music may be unappealing to some users, since in many games music can make or break the entire game and set the mood. I do not plan to make any music for the game since it is difficult, and the time could be better spent on improving other parts of the game instead.
Hardware:
The game will have light hardware requirements to make it as accessible as possible. The game is 2D and can be run easily, so it should run on all computers. However, the computer must have some form of graphics processing capabilities. You will need hardware like a keyboard, mouse, and monitor. You must have an internet connection initially to download the game, but it is not needed to pay the game.
Software:
The game will come as an executable file (.exe) and thus will only run on the Windows operating system. Linux, MacOS and other operating systems like FreeBSD will not be supported. I aim to support Windows 11 and Windows 10.
Gameplay:
1. A system where players can select their cards. This provides a strategic element to the game.
2. An environment system, this is the unique twist on the game that I want to add. This is to differentiate the game, and to increase engagement. There should be at least 5 environments, one for each statistic.
3. Different and unique statistics for each card, as each card needs to be unique to keep every battle interesting. No two cards must be the same.
4. A variety of different cards (20+ cards), so that users have lots of cards to explore, and to keep every battle unique.
5. Fair gameplay. There should be no situation where it is impossible for the user to win.
6. No cheating. It should be impossible for the user to manipulate the game to their advantage.
7. Round based system. There should be a set number of rounds, and the user with the most points will win.
UI:
1. Clear, large, legible buttons that provide visual feedback, such as a colour or size change.
2. Every UI element (100%) should be clearly labelled to avoid confusion and uncertainty.
3. Show the environment in the background, an essential part of the game.
4. A victory or a loss should be displayed clearly. The user needs to know exactly when the game ends, and the ending should not be vague as to whether they lost or won.
5. There must be a clear tutorial for the user. The user needs to know how to play the game, or they will lose engagement.
6. The game should display the current round. The user must be able to keep track of what stage of the game they are in.
User experience:
1. The game needs to be unique. Ideally, there should be no mechanics that are shared with other games. Similarities to other games can bore the user, which in turn can lead to losing user engagement.
2. Difficulty settings, this makes the game accessible to users of all skill. This is part of my goal of making the game as accessible as possible, as per the request of my third stakeholder, Richard.
3. The game needs to be easily accessible. It must come bundled as an application that the user can simply double click to run. For example, a .exe file.
4. There should no or very few bugs. This will be determined in the post development section, where I will ask the user if they have encountered any bugs. If they encounter no bugs or only a small number, then this will be achieved.
To measure the success of all these criteria, I will be asking my stakeholders what they think. For example, for the difficulty criteria, I will ask my users what they think of the difficulties, and if they complain that it is too much, or too little, I will amend it.
I'll also be asking specific stakeholders on their opinions. For example, for the accessibility criteria I will be specifically asking my third stakeholder, Richard since he was the one that requested this feature.
First, I need to break down the problem into some smaller steps. I will be using some diagrams, such as flow charts to demonstrate this. This will be my process of abstraction, allowing me to view the entire project by parts at a time, allowing me to understand it easier.
UI Flowchart:
Main menu: The main menu of the game containing a Play, gallery and quit button.
Select cards: This will be where the user can select their cards for the upcoming game. They can exit back to the main menu if they wish.
Select difficulty: This is where the player will select the difficulty of the AI they will face. They can still go back to the card selection menu if they wish.
Play game: This is where all the main gameplay will happen. The user cannot return to the main menu when the game is being played.
Game end: This will display who won the game, and the final score. It will allow you to return to the main menu.
Gameplay Flowchart:
Compare player and enemy values: This function will find the statistics that the player and the enemy has chosen. It will then apply the multiplier and compare them. It will then award either the player or enemy score.
Game over: This will check the current round. If it is 13, the game will end.
Reset round: Increments the round counter by 1 and gives both the enemy and the player their next card in the queue. Clears the battlefield.
Development Flowchart:
This diagram shows how I will be developing the game. For each scene, I will be using a cycle, where I first create a prototype, based on what I think the stakeholders would want. I will then ask them what they think of it. Based on their responses, I will implement these changes and create a new prototype.
For the development of the game, I will be using the Unity IDE, which uses VS Code and C#. The sprites of the game will be taken from free sources online or created in image manipulation software like Photoshop or Krita. I'll be planning the layout of UI elements in Figma.
In the end there will be a final analysis where I will do a report on the project as a whole.
This will be a rough layout of how all the elements are laid out in separate scenes.
Card selection screen:
Main playing scene:
Statistics/Gallery screen:
'''''''''''''''
I want the game to be accessible to as many people as possible, and therefore the GUI will be clear, with large buttons and a clean, readable font. A game made in the command line may feel uninviting to users such as my third stakeholder, Richard. He is quite elderly and will not have the same amount of digital literacy as the younger generation, making it difficult for him to figure out how to play the game.
Additionally, the GUI's layout should be as neat as possible to make it easy to navigate and make it visually appealing. I want it to have a modern appearance to make it easy on the eyes. All of this should make the game more attractive to all types of users.
Tutorial
There will also be a small tutorial included in the game to make sure that everyone knows how to play it, even if they have no experience. This is part of my aim to make the game as inclusive as possible.
Device accessibility
The game will be quite simple in terms of graphics so that anyone with any sort of computer, even a very weak one, will be able to play the game.
These are data structures that I plan to use throughout my programs code, along with their names. The names may be changed to something more convenient during the actual development of the program.
Structure name |
Structure type |
Reason / Justification |
Cards |
Array |
This array will store every card in the game, along with their statistics. This will be imported from a text file at launch. |
Deck |
Array |
This array will hold all the cards that the user has brought into the game, and their status, if it is useable or not. |
Statistics |
External CSV file |
This will save the statistics of the user when they close the game, for it to be loaded upon the next launch. |
PlayerScore and EnemyScore |
Integer |
This will keep track of both players scores. |
CurrentEnviroment |
String |
This variable will hold what the current environment is. |
RoundNumber |
Integer |
Holds the current round number, to display to the user and to end the game when the max round has been reached. |
SelectedStatistic and EnemySelectedStatistic |
String |
Holds the statistics that the user and enemy have chosen to battle with. |
Screen |
Integer |
Keeps track of which screen the user is on. Each number will correspond to a certain screen. |
BoostedStat |
String |
This variable holds the statistic that is currently boosted. |
Multiplier |
Float |
Holds the value to boost the statistic by. |
Times card chosen, times card played, games won, games lost etc' |
Integer / Float |
These will be various statistics that will be present in the gallery's statistics menu. |
There will be other small variables that are used throughout the program; however, they are not relevant here since they are generic and used in almost all programs, and therefore not specific to this solution. These include things like i and j that are used in loops, or Boolean statements for stopping conditions.
I am familiar with most of these data types, except for the external csv file. This will be something that I have to learn. I want to use a csv file since it is not volatile. This is important for saving high scores, the selection of the user's deck, their volume settings (if applicable) and other things that should not be volatile.
During the testing phase of the program, these are testing methods that I plan to implement to make sure that the game is functioning correctly.
Test name |
Test type |
Test justification/reason |
Expected outcome |
Button function (Valid) |
Valid |
Buttons need to work when they are clicked on. |
The button will execute the command that it is tied to when clicked on. |
Button function (Invalid) |
Erroneous |
Buttons should not work if the user does not click on them. |
When the user does not click on a button, but nearby, the button should not activate. |
User input (Valid) |
Valid |
The game must function as expected if the user provides a valid input. |
The game should proceed normally. |
User input (Invalid) |
Erroneous |
The game needs to act appropriately in case of unexpected inputs. |
The game should provide the user with an error message. It must not crash. |
Saving data (Empty file) |
Valid |
The program must correctly save the users data. |
When the file is empty, the game will save the appropriate data to the file along with the appropriate structure. |
Saving data (File previously written to) |
Valid |
The program needs to know what to do if scores have already been recorded previously. |
The program should overwrite the previous scores with the new one if the best scores have changed. |
Saving data (Unknown file) |
Erroneous |
The program should not overwrite files that do not belong to it. |
If the program detects that the file does not belong to it, it will instead return an error to the user and it will not save the file. |
Cards selected test (Invalid) |
Erroneous |
The game must not proceed if too many or too few cards are selected. |
The game shows and error and nothing else happens. |
Cards selected test (Valid) |
Valid |
The game must proceed normally if the correct number of cards is selected. |
The game will proceed to the next scene as normal. |
I will need a script that lets the user to select cards.
Algorithm to compare cards:
Algorithm to randomly change the background and return the multiplier.
Class for player:
These functions are not particularly specific as it is impossible for me to know exactly what functions, libraries and such Unity will use. Furthermore, due to the scene-based nature of Unity, the code that I currently have may change significantly. I may run into errors that I cannot fix, or I may not be able to execute certain actions beyond my programming skill. Due to the nature of this project, everything is highly subject to change.
Functions for showing statistics
In the gallery, I will need to iterate through every card in the game, then call values for each one.
Here I will be planning for the cards, sprites, background, and art that I will be using in the game. This is for me for future reference.
Here are the names of the cards, and their respective statistics. These will be made into cards in Paint 3D to be used in the game. Each card has an icon, and the statistics. The icons are sourced from https://flaticon.co. The name of the card is not actually present on the card, as it is unnecessary. Here is an example of a card, the Wolf card:
The wolf card, as it appears in game.
Here is the name of each card I am planning to use, along with its statistics.
Card Name |
Statistics |
Dragon |
Speed: 13 Strength: 94 Durability: 88 Intelligence: 3 Mana: 27 |
Wolf |
Speed: 37 Strength: 37 Durability: 37 Intelligence: 37 Mana: 37 |
Troll |
Speed: 8 Strength: 100 Durability: 68 Intelligence: 1 Mana: 14 |
Goblin |
Speed: 79 Strength: 18 Durability: 6 Intelligence: 76 Mana: 2 |
Knight |
Speed: 11 Strength: 52 Durability: 34 Intelligence: 18 Mana: 3 |
Fairy |
Speed: 81 Strength: 1 Durability: 1 Intelligence: 81 Mana: 91 |
Mage |
Speed: 19 Strength: 9 Durability: 9 Intelligence: 69 Mana: 100 |
Paladin |
Speed: 1 Strength: 65 Durability: 100 Intelligence: 25 Mana: 5 |
Rouge |
Speed: 98 Strength: 27 Durability: 6 Intelligence: 45 Mana: 4 |
Librarian |
Speed: 27 Strength: 17 Durability: 17 Intelligence: 100 Mana: 67 |
Rat |
Speed: 100 Strength: 1 Durability: 1 Intelligence: 85 Mana: 1 |
Bat |
Speed: 65 Strength: 15 Durability: 65 Intelligence: 15 Mana: 65 |
Cannoneer |
Speed: 0 Strength: 85 Durability: 75 Intelligence: 45 Mana: 0 |
This has achieved gameplay success criteria #3 and partially gameplay success criteria #4, which are different statistics for each card, and a variety of card, 13 of the 20 I want to add have been created.
Next, I need to plan the environments. To represent an environment change I will change the background of the game and display some text showing what the current environment is with the statistic multiplier value. I want to have at least 5 environments, one for each background. Ideally, the size of every image should be the same to avoid scaling issues. I will aim for 1920x1080.
Environment name |
Environment style and modifier |
Fire (1) |
This will be a fiery hellscape that boosts attack by 1.25x. |
Jungle (2) |
This will be a magical jungle that boosts mana by 1.25x. |
Water (3) |
This will be an ocean that boosts intelligence by 1.25x. |
Earth (4) |
This will be a mountainous landscape that boosts durability by 1.25x |
Ice (5) |
This will be a frozen landscape that hinders speed by 0.75x. |
I'm going to be asking my stakeholders some questions to know how they feel about the program after it's complete. I'm leaving these questions open ended because I want clear communication from my stakeholders. I will improve on any areas that they highlight.
Post development questions:
Q: How satisfied are you with the clarity, ease of use, and fluidity of the user interface?
A:
Q: How satisfied are you with the gameplay?
A:
Q: How satisfied are you with the unique mechanics of the game?
A:
Q: How satisfied are you with the variety of cards?
A:
Q: Is there anything else that you would like to say?
A:
Question 1: The fluidity of the program is important in making sure that it feels complete.
Question 2: The gameplay is the core part of my project so feedback on this is vital.
Question 3: This is the differentiating part of my game, so it is crucial that I get feedback on this.
Question 4: I am unsure about how many cards I should be adding, so feedback on this is important.
Question 5: Anything that the stakeholders want to say that I might not have thought about. This is an open-ended question.
Play screen:
Here I have a couple classes that I plan to use. I want the enemy classes to inherit from the player class. I'll be using override functions to change the AI for the harder difficulties.
First, I need to set up a Unity project using the '2D core' template which is useful for the 2D style of game that I am creating.
This provided me with a blank canvas that I could create my game on.
Now I need to create my first scene, which will be the main menu.
Prototype 1:
First, I will develop the first iteration of the home screen. I used an included library called Text Mesh Pro included in Unity to create UI elements. I'll add a play button, and a gallery button.
I've tried to go for a modern design. Currently, the buttons are non-functional. In unity, to add functionality to buttons, you need to create a script which has a function with the code you want to execute, then attach it to the button. You then need to add a listener for the function in the unity inspector.
First, I need to create a script that allows the buttons to switch scenes.
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneLoader : MonoBehaviour
{
public void LoadScene(string SceneName)
{
SceneManager.LoadScene(SceneName);
// This simply loads the scene by the name of SceneName.
}
}
Now I need to attach the scene switching script to the buttons and assign the values. This is done inside of the Unity IDE.
The scene loader script attached.
The button has been set to move to the 'select cards' scene on click.
Test |
Expected result |
Actual result (Pass/Fail) |
Comments |
Press 'Play' button |
User is taken to the next scene |
Pass |
|
Press 'Gallery' button |
User is taken to the gallery |
Fail |
This functionality will be added in a future prototype. |
Richard's feedback:
I appreciate the modern style, but I think it is far too bright and hard on the eyes. I think the game should use more cool colours and maybe have a fantasy style. The position of the buttons also does not make sense, I think it should be more linear.
Joshua's feedback:
The UI is ok, but I think it should follow a medieval style, not a modern one.
Prototype 2:
I need to change the home screen to accommodate my stakeholders' feedback.
First, I added a new background.
Then I added the buttons with a new font.
I've also added an animation to these buttons so that they change in size slightly when hovered over. This provides visual feedback and achieves UI success criteria #1, for a reactive UI experience.
I've also added a quit button. The quit button needs its own script. It is small and simple.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class QuitGame : MonoBehaviour
{
public void Clicked(){
Application.Quit();
}
}
Upon testing, the quit button did not do anything. After researching on the internet, I found that it does work, but not when you're testing the game in the viewport. I tried fully compiling and building the game, then using the quit button.
The Unity editor's project compile/build interface.
The quit button did work without any issues.
Test |
Expected result |
Actual result (Pass/Fail) |
Comments |
Press 'Play' button |
User is taken to the next scene |
Pass |
|
Press 'Gallery' button |
User is taken to the gallery |
Pass |
|
Press 'Quit' button |
The game closes |
Pass |
|
Hover over all buttons |
The buttons increase in size slightly |
Pass |
|
Brett's feedback:
It looks good though I think the buttons need a black background to make them easier to distinguish.
Joshua's feedback:
I like this a lot more than the previous one. I think the game still needs a name other than 'Placeholder'.
Richard's feedback:
This looks better but the buttons are a bit harder to read.
Prototype 3:
This is a simple revision of the last screen that makes the buttons a bit smaller. This was due to a change in the target aspect ratio, from 800x600 to 1920x1080. The old target aspect ratio caused some scaling issues, like buttons clipping outside of the visible area, or buttons clipping inside each other. It also adds a black background to the buttons to make them easier to read.
Screenshot of Unity editor showing the ability to change the target resolution for scaling issues.
Prototype 1:
First, I added the top and bottom scroll view. This is an element in unity that allows you to scroll through content. I also added the confirmation button, a button to go back and a title at the top of the screen telling the user what to do:
Next, I manually populated the scroll view with my cards. The cards are buttons, with the sprite of the button set to the cards that I designed. Making them buttons allows me to add animations and code.
Here I've added all the cards, some to the top and some to the bottom. I've also added the same animation to them as the other buttons, so they change in size when hovered over. Now I need to create a script that allows them to move from one panel to the other. First, I need to reference the scroll views. Unity calls any object in a scene a 'GameObject'.
This is just a normal definition for a variable in C#:
Then I need to assign them using the GameObject.Find() method.
public void Start()
{
CardLibrary = GameObject.Find("Card Library Content"); // Find the top panel by name.
SelectedCards = GameObject.Find("Selected Cards Content"); // Find the bottom panel by name.
}
These lines were written in a special function called Start(). Unity executes any code in this function before the scene starts. This means that all these calculations should be done before the user can see them.
The strings that I've provided to the function are the names of the GameObjects in the viewport.
I won't be showing this in the future since it needs to be done for all game objects in all scripts and is redundant. This script that I am creating is attached to the cards themselves. My idea is for the card to find what scroll view it is attached to, find its name as a string, and then move itself accordingly.
To find the scroll view a card is attached to, it needs to find its great grandparent in the hierarchy:
The Dragon's great grandparent would be 'Card Library'
To find the great grandparent, I can find the parent 3 times:
GreatGrandParent = transform.parent.parent.parent.name; // Gets the name of the great grandparent.
At first I tried to change the parent object by getting the coordinates for other scroll view, and then teleporting the card to the new coordinates. However, I quickly realised that this was not a robust solution and could easily fail, and was also difficult to code. I looked for other methods, and found the transform.parent property.
Then I can change the card's parent accordingly:
if (GreatGrandParent == "Card Library")
{
gameObject.transform.parent = SelectedCards.transform; // ReParent the child to the new panel.
}
else
{
gameObject.transform.parent = CardLibrary.transform; // Does the inverse if clicked again.
}
'
GameObject refers to the type that unity uses to refer to objects in the scene, while gameObject refers to the GameObject that the script is attached to. The difference is in the capitalisation.
After testing this, it worked flawlessly.
Now I need to add code for the 'Next' button. It needs to find the number of children in the bottom scroll view, and only proceed to the next scene if exactly 6 cards are present. First, I created an error message:
Now I need to create the script. First, I need to create an array called 'Deck'. This will contain the user's cards.
private List<string> Deck = new List<string>();
I need to disable the error message by default. To do this, I used the Start() function.
void Start()
{
ErrorMessage.SetActive(false); // This hides the error message by default.
}
If it's 6, I need to add all the cards to Deck, and move to the next scene. Validating the amount of children can be done with transform.childCount, however getting the names of all these children is more difficult.
To do this I can try and find the names of all the children of SelectedCards. After doing some research online, I came across this script.
'foreach (Transform child in transform)
'''''''''''' Debug.Log("Foreach loop: " + child);
I was having some difficulty understanding this script, but I eventually figured it out.
1. 'Transform' is the type of Object. In Unity, most GameObjects have a Transform attached to them, that determines their position and scale amongst other things.
2. 'child' is a variable with local scope, used to refer to the current Transform being targeted in the loop. This variable could be anything, but I will keep it as 'child'.
3. 'transform' is the parent GameObject that you want to find the children of.
After I figured this out, I adapted the script for my game.
if (SelectedCards.transform.childCount == 6) // Validate the number of cards.
{
foreach (Transform child in SelectedCards.transform)
{
Deck.Add(child.name);
// Adds all the selected cards to deck.
}
SceneManager.LoadScene("play screen"); //Load the next scene.
}
If there aren't 6 cards, I need to show an error.
else
{
ErrorMessage.SetActive(true); // This shows the error message
// when the amount of cards selected does not equal six.
}
}
I don't want the error message to be permanently displayed, so every time a card is clicked it will attempt to hide the error message. I added this line of code into the script that is attached to the cards.
ErrorMessage.SetActive(false); // Hide the error message when a card is clicked.
This resulted in an error:
This is because the next button disables the error message before the cards can find it. To solve this, the cards need to reference the error message in a different function called Awake(), which is called before Start().
void Awake()
{
ErrorMessage = GameObject.Find("Error Message"); // Find the error message by name.
}
Testing once again, no errors were produced. Cards can be moved from the top to the bottom. An error is produced if the number of cards does not equal 6. This error disappears once a card is moved again. If the number of cards was equal to 6, it takes you to the next scene.
Test |
Expected result |
Actual result (Pass/Fail) |
Comments |
Card pressed when in the top scroll view |
Card moves to the bottom scroll view |
Pass |
|
Card pressed when in the bottom scroll view |
Card moves to the top scroll view |
Pass |
|
No cards are selected |
The game prompts the user to select 6 cards |
Pass |
|
More than 6 cards are selected |
The game prompts the user to select 6 cards |
Pass |
|
Exactly 6 cards are selected |
The game moves to the next scene |
Pass |
|
Cards are hovered over |
The card slightly increases in size |
Pass |
|
Brett's feedback:
The system for selecting cards works well and I like that it tells me when I have not selected 6 cards. However I think it would be better if it told you how many cards you have selected, so you can see if you are over or under the limit.
Joshua's feedback:
This is good and the cards can be selected easily. The spacing of the card and UI elements is still a bit weird.
Richard's feedback:
The system for selecting cards is good and I like that I can customise my game using any of these cards.
The purpose of this prototype is to achieve gameplay success criteria #1, which is to allow users to select their own cards and deck.
Prototype 2:
I want to change the card creation system to something more procedural. Currently, all the cards have been created manually in the viewport. I began by creating a Prefab. This is a special type of GameObject in unity that contains some values by default, allowing you to instantiate this anywhere. Here is my prefab for a card:
The prefab as it appears inside of Unity.
By default, it is a dragon. Now I need to create script to make a prefab for every card and change the values of these prefabs to be different for every card.
First, I created a list with the name of every card in the game:
private List<string> CardsToGenerate = new List<string>{"dragon", "wolf", "bat", "cannoneer", "fairy", "goblin", "knight", "librarian", "mage", "paladin", "rat", "rouge", "troll"};
Then I created a loop that iterates through every card, instantiates the prefab, moves it to the top scroll view, assigns a new sprite, assigns the script to move the cards that I created earlier, and changes the name of the card:
foreach (string i in CardsToGenerate)
{
GenerateCardsInst = Instantiate(CardPrefab, new Vector3(0, 0, 0), Quaternion.identity); // Generate an instance of the card.
GenerateCardsInst.transform.parent = CardLibraryContent.transform; // Move it to the card library.
GenerateCardsInst.GetComponent<Image>().sprite = Resources.Load<Sprite>(i); // Assign the image.
GenerateCardsInst.AddComponent<ReParent>(); // Add the script.
GenerateCardsInst.name = i; // Set the name of the gameobject. Otherwise it defaults to "clone".
}
After testing, I got an error.
'The type or namespace name 'image' could not be found (are you using a directive or an assembly reference?)
This is because I had to import a library:
using UnityEngine.UI;
The script was also unable to find the card sprites. This is because they had to be moved to a folder called 'Resources' for the Resources.Load() function to work. I also modified the spacing of the cards.
This is the final result:
The object tree inside of the Unity editor.
This is much more flexible and now I can add cards much easier in the future if I wish. I will not be asking stakeholders for further feedback as this is functionally identical to the last prototype.
With this, I have achieved success criteria number #1, which is to make a fully customisable deck.
Prototype 1:
First, I need to lay out the basic UI elements.
In the bottom left corner are the cards that the user currently has on hand. Below the ready button are cards that are next in the queue. To the right of the ready button is the card that the user has chosen to battle with. The spaces at the top right are for the enemy AI. The 'ready' button will be used when the player is ready to play.
I explained to my stakeholders what each section was going to be for, and I asked them for some feedback.
Brett's feedback:
The general layout is good, and I can see what's going to happen, I can see where my stuff will be and where the enemy's stuff is. But it really should be labelled because people that do not have it explained to them will struggle.
Joshua's feedback:
The overall layout looks good but It must be clearer what each panel does. There need to be instructions or labels.
Richard's feedback:
I can't tell what each section does, so it would be better if each section were labelled. But it makes sense, my stuff is in the bottom left and the enemy's stuff is in the top right.
I added some labels as my stakeholders requested.
I'll continue doing this throughout the rest of the game. With this, UI success criteria #2 is complete.
Now I need to move the cards that the user selected in the previous scene to the new one. First, I was thinking of creating a getter method, but to do this I would have to get an instance of the CheckCount.cs script in the previous scene, which has the 'Deck' variable in it. This was not feasible in this situation.
I decided to use static variables. First, I went back to CheckCount.cs, and made 'Deck' a public static variable.
public static List<string> Deck = new List<string> {"dragon", "cannoneer", "rouge", "troll", "paladin", "fairy"};
Now in CopyCards.cs, which is in my new scene, I can reference the Deck variable like this:
CheckCount.Deck
Now in CopyCards.cs, I need to move 3 of the selected cards to the hand, and 3 of the selected card to the queue. Also, the cards in the queue must not be selectable.
void Start()
{
for (int i=0; i<3; i++) // Only apply this to the first 3 cards.
{
InstCard = Instantiate(MyPrefab, new Vector3(0, 0, 0), Quaternion.identity); // Create a new object based on the card prefab.
InstCard.transform.parent = DeckPanel.transform; // Move this new prefab to the Deck Panel.
InstCard.GetComponent<Image>().sprite = Resources.Load<Sprite>(CheckCount.Deck[i]); // Change the image of the new instance to match the selected cards.
// This is because it is the dragon card by default.
Destroy(InstCard.GetComponent<ReParent>()); // Removes the old script.
InstCard.AddComponent<SelectBattleCard>(); // Adds the new script.
InstCard.name = CheckCount.Deck[i]; // Renames the new card from "clone"
DeckRef.Add("Card" + i, InstCard); // Add this to a dictonary to keep track of it in the future.
}
for (int i=3; i<6; i++) // Only apply this to the last 3 cards.
{
InstCard = Instantiate(MyPrefab, new Vector3(0, 0, 0), Quaternion.identity); // This loop is mostly the same.
InstCard.transform.parent = DeckQueue.transform; // Move this new prefab to the Queue panel.
// The queue panel shows the cards that are not in the hand.
InstCard.GetComponent<Image>().sprite = Resources.Load<Sprite>(CheckCount.Deck[i]);
InstCard.GetComponent<Button>().interactable = false; // This makes these cards unusable.
Destroy(InstCard.GetComponent<ReParent>());
InstCard.AddComponent<SelectBattleCard>();
InstCard.name = CheckCount.Deck[i];
DeckRef.Add("Card" + i, InstCard);
}
}
This is mostly the same script that I've used before to generate cards. There are a couple extra lines. For the last 3 cards, I set their interactable state to false. Next, I had to remove the 'ReParent' script attached to the cards. This is the script responsible for moving cards between scroll views in the card selection scene.
I also needed to add a new script, SelectBattleCard.cs. This is similar to the ReParent script.
void OnClick()
{
if (transform.parent.name == "Deck Content" & CardToBattle.transform.childCount == 0)
{
gameObject.transform.parent = CardToBattle.transform; // Move to the selection area if there's nothing there.
}
else if (transform.parent.name == "Selected Card")
{
gameObject.transform.parent = Deck.transform;
}
}
}
It will move cards to the 'Selected' panel, if there are none there.
Now I need to test. To make testing easier, I've made Deck contain some cards by default. This means I don't have to select from the card selection screen every time.
public static List<string> Deck = new List<string> {"dragon", "cannoneer", "rouge", "troll", "paladin", "fairy"};
So that this does not affect normal gameplay, I cleared the deck when the user is playing normally.
public void Clicked()
{
Deck.Clear();
Now I can test.
You can see the cards in the queue are too large and are not fully visible. To fix this, I made the queue scroll view use 'control child size', which is included functionality.
This had the opposite effect and made the cards even larger, hence I reversed this change. I can also try to change the size of things using the transform.sizeDelta function.
InstCard.image.rectTransform.sizeDelta = new Vector2(160f, 100f);
Upon testing, this had no effect on the cards. I could not get this function to work.
I tried experimenting myself and found that if I changed the size of the 'Deck Queue Content' component, I could solve my issue. A scroll view has 3 components in the hierarchy:
I changed the size of 'Deck Queue Content', and this was the result.
The queue now works successfully. I can also move cards to and from the selected pane. Furthermore, cards in the queue cannot be selected.
I noticed during my testing that I was getting my card in the same order every single time. One of my goals was to give the user a random selection of cards to add an element of luck to the game. I can solve this by shuffling the Deck list.
CheckCount.Deck = CheckCount.Deck.OrderBy(x => Random.value).ToList(); // Randomly shuffle the list.
This is in CopyCards.cs.
Test |
Expected result |
Actual result (Pass/Fail) |
Comments |
Cards in hand selection |
When a card is selected, it is moved to the selected pane, and it can be moved back |
Pass |
|
Clicking cards in the queue |
Cards in the queue should do nothing when clicked on |
Pass |
|
Random selection of cards |
The user should be given a random selection of cards |
Pass |
|
In the top right corner, I've added cards with a question mark to show that the user cannot see the enemy's cards. I've also added a label below that to show what the enemy is currently doing.
Now I need to start implementing the Enemy AI. I'm going to be creating this in a script called Enemy.cs First, I want to add a delay to show that the enemy AI is thinking. I did some research online and found that I have to change the method to an IEnumerator method.
public IEnumerator Wait()
Then I can add the delay. I want the enemy to wait for between 5 and 15 seconds. This will be lowered on harder difficulties. For now, I have set the timer to be between 1 and 2 seconds to make the game easier to test.
yield return new WaitForSeconds(Random.Range(1,2)); // The enemy waits between 5 and 15 seconds.
Now I can re-use the code from the card selection screen to create a card with the unknown sprite.
CardInst = Instantiate(CardPrefab, new Vector3(0, 0, 0), Quaternion.identity); // This adds the unknown card sprite to the enemy selected card pane.
CardInst.name = "Enemy Selected Card Unknown Sprite"; // Sets the name.
CardInst.transform.parent = EnemySelectedCardContent.transform; // Changes the parent object.
CardInst.GetComponent<Image>().sprite = Resources.Load<Sprite>("unknown card"); // Changes the sprite.
Now I also need to update the text to show that the enemy is ready.
EnemyStatus.GetComponent<TextMeshPro>().text = "Enemy is ready."; // Updates the text.
Next, to run this function I need to create a round controller script. I created one called InitiateRound.cs, and attached it to the 'Ready' button. In this, I created an instance of my Enemy class.
Enemy MyEnemy = new Enemy();
Then I called the Wait() method.
void OnClick()
{
MyEnemy.Wait();
}
After testing, I got an error.
ArgumentException: The Object you want to instantiate is null.
UnityEngine.Object.Instantiate (UnityEngine.Object original, UnityEngine.Vector3 position, UnityEngine.Quaternion rotation) (at <24d45e813e524a99bfb7a145158a7980>:0)
UnityEngine.Object.Instantiate[T] (T original, UnityEngine.Vector3 position, UnityEngine.Quaternion rotation) (at <24d45e813e524a99bfb7a145158a7980>:0)
Enemy+<Wait>d__18.MoveNext () (at Assets/03Scripts/Enemy.cs:56)
UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) (at <24d45e813e524a99bfb7a145158a7980>:0)
This is because the Start() function in Enemy.cs has not been called. Start() is supposed to call at the start of the game, but this doesn't happen if it's not attached to an object. My Enemy.cs exists only as a class. So, I need to call the start function for my Enemy in the start function InitiateRound.cs.
void Start()
{
MyEnemy.Start();
}
After testing again, I got another error.
NullReferenceException: Object reference not set to an instance of an object
This is because I did not import a library. This is the TextMeshPro library, and it allows for the manipulation of TextMeshPro objects in code.
using TMPro;
After testing again, I got the same error again.
After reading the documentation, and doing some research, I discovered that there are two types of TextMeshPro text:
1. TextMeshPro
2. TextMeshProUGUI
TextMeshPro, the one that I am currently using, is used for older versions of unity and currently exists as a legacy option. The one I should be using is TextMeshProUGUI. I changed the line that alters the label:
EnemyStatus.GetComponent<TextMeshProUGUI>().text = "Enemy is ready."; // Updates the text.
Testing once more, I got this result. The enemy thinks for 1 or 2 seconds, then it changes.
Now I need to start developing the statistic selection.
I added score counters, buttons to select a statistic, and an area in the top left to show the cards that are currently battling. Now I will begin with adding a script to the ready button. First, it must only activate when 3 conditions are met. These are:
1. The enemy is ready
2. A statistic has been selected
3. A card has been selected
I can check this using an if statement:
if (EnemyStatus.GetComponent<TextMeshProUGUI>().text == "Enemy is ready." & CardToBattleContainer.transform.childCount == 1 & SelStatTextGameObject.GetComponent<TextMeshProUGUI>().text != "Statistic: none")
{
}
Now I need to make a script that allows the user to select a statistic. I created this script and attached it to every button:
void OnClick()
{
// Get the name of the GameObject this script is attached to, get the last word and edit the text to reflect this.
Word = gameObject.name.Split(" ");
SelStat.GetComponent<TextMeshProUGUI>().text = "Statistic: " + Word[0].ToLower();
}
When the button is clicked, it gets the name of itself, splits the name and adds it to the list, gets the first word (since the buttons are named like 'Strength Button'), changes it to lowercase and changes the labels text to show this. By doing it this way I can have the same script for every button. I do not need to create 5 different scripts.
Now I can begin creating the Player class. First, in my player class, I created a method called PlaySelectedCard.
public void PlaySelectedCard()
First, this moves the card that the player has selected to the top left.
PlayerCard = GameObject.Find(PlayerCardContainer.transform.GetChild(0).name);
PlayerCard.transform.parent = CardDisplay.transform;
Then it gets the selected statistic from the label I created earlier, and assigns it to a variable. This will be called later. Now I need to write a function to return the satistic that the user has selected as a value (for comparison in the round controller to find out who won):
public int GetChosenStatistic()
{
// Returns the players chosen statistic.
SelectedStatisticString = SelStatTextGameObject.GetComponent<TextMeshProUGUI>().text;
Word = SelectedStatisticString.Split(" ");
SelectedStatisticString = Word[1];
SelectedStatisticIndex = Array.FindIndex(Statistics, row => row.Contains(SelectedStatisticString));
SelectedStatistic = Int32.Parse(CardArray[Array.FindIndex(CardArray2D, row => row.Contains(PlayerCard.name)), SelectedStatisticIndex + 1]);
return SelectedStatistic;
}
This uses a similar logic to the script that changes the selected statistic. First, it references the selected statistic label. It pulls the text from it, splits it, and adds it to a list, then takes the second word, which will be the statistic the user has selected. Then it performs a search in this list that I've created:
private static string[,] CardArray = new string[13, 6] {{"bat", "65", "15", "65", "15", "65"}, {"cannoneer", "0", "85", "75", "45", "0"}, {"dragon", "13", "94", "88", "3", "27"}, {"fairy", "81", "1", "1", "81", "91"}, {"goblin", "79", "18", "6", "76", "2"}, {"knight", "11", "52", "34", "18", "3"}, {"librarian", "27", "17", "17", "100", "67"}, {"mage", "19", "9", "9", "69", "100"}, {"paladin", "1", "65", "100", "25", "5"}, {"rat", "100", "1", "1", "85", "1"}, {"rouge", "98", "27", "6", "45", "4"}, {"troll", "8", "100", "68", "1", "14"}, {"wolf", "37", "37", "37", "37", "37"}};
This is a lengthy 2D array that contains the name and respective statistics of all cards in the game. Using the string from earlier. First, it gets the name of the card that the user has selected, then finds the row. Then it gets the statistic the user has selected, turns it to an index using a separate list:
private string[] Statistics = {"speed", "strength", "durability", "intelligence", "mana"};
Then finally gets the statistic the user has selected and returns it, as the exact value.
Next, I need to write a function to increment the score for the player.
public void IncrementScore()
{
// Increments the players score and updates the label.
PlayerScore++;
Victor.GetComponent<TextMeshProUGUI>().text = "You won!";
PlayerScoreText.GetComponent<TextMeshProUGUI>().text = "Score: " + PlayerScore;
}
This is simple. It just increments PlayerScore by 1 and changes the label to show the score. Now that most of the Player class's functionality is finished, I need to start adding the same functionality to the enemy class. I wanted to have the enemy class Inherit from the player class, but Player and Enemy are too different, so it would take more effort than to just create a new class.
First, the enemy must generate their deck.
It adds 3 random cards to the enemy's hand, and 3 to their queue. All the cards are stored in this string:
private string[] CardArray2D = {"bat", "cannoneer", "dragon", "fairy", "goblin", "knight", "librarian", "mage", "paladin", "rat", "rouge", "troll", "wolf"};
After testing this I realised that the enemy was able to pick the same card multiple times in a deck. This is unfair for the user playing the game, as the user cannot do the same.
To solve this, I wanted to remove the card that the enemy selects from the list of cards. However, this is very difficult with an Array. Additionally, CardArray2D needs to be used later in the script. If it were modified, the game would break later.
To work around this, I created a duplicate of CardArray2D that is a list.
List<string> CardLibrary = new List<string>{"bat", "cannoneer", "dragon", "fairy", "goblin", "knight", "librarian", "mage", "paladin", "rat", "rouge", "troll", "wolf"};
'Then I can use this new list, and use the RemoveAt() method.
public void GenerateDeck()
{
for (int i=0; i<3; i++)
{
// Add cards to the deck.
int j = UnityEngine.Random.Range(0,12);
CardLibrary.RemoveAt(j);
Queue.Add(CardLibrary[j]);
}
for (int i=0; i<3; i++)
{
// Add cards to the hand.
int j = UnityEngine.Random.Range(0,12);
CardLibrary.RemoveAt(j);
Hand.Add(CardLibrary[j]);
}
}
After testing, the enemy can no longer select the same card twice.
Now I need to write a function to make the enemy display their card. To do this I can just copy the code that I used earlier to generate the 'Unknown card'. But first, the enemy must select a random card from its hand and choose a random statistic. To do this, I made the enemy select a random number from 1 to 6 inclusive. Normally I would do 0 to 5, but in CardArray, the first value in each row is the name of the card, so I have to add one.
EnemySelectedStatisticIndex = UnityEngine.Random.Range(1,6);
I added this to the Wait() function. Now, I can write the code for the enemy to play their card.
public void PlaySelectedCard()
{
// Make the enemy choose a random card.
ChosenCard = Hand[UnityEngine.Random.Range(0,2)];
// Select the stat.
SelectedStatistic = Int32.Parse(CardArray[Array.FindIndex(CardArray2D, row => row.Contains(ChosenCard)), EnemySelectedStatisticIndex]);
// This block generates the card that the enemy AI has chosen.
CardInst = Instantiate(CardPrefab, new Vector3(0, 0, 0), Quaternion.identity); // Generate an instance of the card.
CardInst.GetComponent<Image>().sprite = Resources.Load<Sprite>(ChosenCard); // Assign the image.
CardInst.transform.parent = EnemyCardDisplay.transform; // Move it to the enemy card display.
CardInst.name = EnemyAI.EnemySelCard; // Set the name of the gameobject. Otherwise it defaults to "clone".
// Move the chosen card back to the deck.
Queue.Add(ChosenCard);
Hand.Remove(ChosenCard);
Hand.Add(Queue[0]);
Queue.Remove(Queue[0]);
}
While this looks complex this is similar to what happens in the Player Class. There is also some code at the bottom to move the card that the enemy selected to the back of the queue, and to move the card at the front of the queue to the hand.
There is also some code that makes the enemy select a statistic.
SelectedStatistic = Int32.Parse(CardArray[Array.FindIndex(CardArray2D, row => row.Contains(ChosenCard)), EnemySelectedStatisticIndex]);
First, it references the enemies ChosenCard. Then, it searches in CardArray2D for the row containing that card. Then, it uses EnemySelectedStatisticIndex. It uses this to reference CardArray, parses it to an Integer, and assigns it to SelectedStatistic. This makes the enemy select a random statistic.
Now to write a function that increments the score:
public void IncrementScore()
{
// This means that the enemy has won.
Score++; // Increment the enemies score.
Victor.GetComponent<TextMeshProUGUI>().text = "You lost.";
EnemyScoreText.GetComponent<TextMeshProUGUI>().text = "Score: " + Score;
}
And function to return the score.
public int GetScore()
{
return Score;
}
Now I can use my Player and Enemy classes inside of my main round script, InitiateRound.cs. To do this, I create an instance of Player alongside Enemy.
Player MyPlayer = new Player();
Enemy MyEnemy = new Enemy();
Then in the start function, I have to put the start function for player like I did for the enemy.
void Start()
{
// Initialise the variables in player and enemy.
MyPlayer.Start();
MyEnemy.Start();
}
In my if statement that I created earlier, I can begin coding the main round logic for the game. This is what I want to happen when the ready button is pressed:
1. The button disables itself, so the user doesn't press it while the round is happening and break the game.
2. The player plays their card.
3. The game waits for 1 second.
4. The enemy plays their card.
5. The game waits for 1 second.
6. The values are compared, and the scores are incremented based on who wins.
Here's the code for the first 4 steps.
MyselfAsButton.enabled = false; // Disable the button so that the user does not break the game.
MyPlayer.PlaySelectedCard(); // Have the player move their selected card to the top left.
yield return new WaitForSeconds(1);
MyEnemy.PlaySelectedCard(); // Have the enemy move their selected card to the top left.
When I tested this I got an error.
Assets/03Scripts/InitiateRound.cs(81,10): error CS1624: The body of 'InitiateRound.OnClick()' cannot be an iterator block because 'void' is not an iterator interface type
This is because I did not make OnClick an IEnumerator function.
IEnumerator OnClick()
But OnClick cannot be an IEnumerator, as it provides this error.
Assets/03Scripts/InitiateRound.cs(67,44): error CS0407: 'IEnumerator InitiateRound.OnClick()' has the wrong return type
Instead, I have to use the StartCoroutine() funciton. I moved the logic to a new funciton, called MyCoroutine(). Then in OnClick(), I put this.
void OnClick()
{
// This button only activates when the enemy is ready, the player has selected a card and a statistic.
if (EnemyStatus.GetComponent<TextMeshProUGUI>().text == "Enemy is ready." & CardToBattleContainer.transform.childCount == 1 & SelStatTextGameObject.GetComponent<TextMeshProUGUI>().text != "Statistic: none")
{
StartCoroutine(MyCoroutine()); // This links to the main method, "MyCoroutine".
}
}
And here is the logic in the new function.
IEnumerator MyCoroutine()
{
MyselfAsButton.enabled = false; // Disable the button so that the user does not break the game.
MyPlayer.PlaySelectedCard(); // Have the player move their selected card to the top left.
yield return new WaitForSeconds(1);
MyEnemy.PlaySelectedCard(); // Have the enemy move their selected card to the top left.
}
When it tested I got this error.
ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
System.Collections.Generic.List`1[T].RemoveAt (System.Int32 index) (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
Enemy.GenerateDeck () (at Assets/03Scripts/Enemy.cs:95)
InitiateRound.Start () (at Assets/03Scripts/InitiateRound.cs:74)
This is because of my code earlier in Enemy.cs. When the Enemy selects a card and removes it, the size of the list decreases by one. But I haven't accounted for this. So instead, I it use the length of the list.
int j = UnityEngine.Random.Range(0, CardLibrary.Count-1);
After testing once more, the game works as expected.
The card can be selected, played, and will be moved to the top left. The enemy will also play their card.
Joshua's feedback:
It looks good but there is still no implementation of the gameplay. I also think that there should be a round counter.
Test |
Expected result |
Actual result (Pass/Fail) |
Comments |
Press ready button (successful) |
Ready button only activates if all the conditions are met |
Pass |
|
Card move to top left |
The selected card moves to the top left when the ready button is clicked |
Pass |
|
Enemy plays card after player |
Enemy card is played after the player's card |
Pass |
|
Press ready button (erroneous) |
The game does not continue |
Pass |
|
Press ready button (erroneous) |
The game does not continue when a statistic is not selected |
Pass |
|
Press ready button (erroneous) |
The game does not continue when the enemy is not ready |
Pass |
|
Delays before action |
There should be delays between each action to make the game easier to track |
Pass |
|
Prototype 2:
Now I need to create the comparison between the players statistics. I already created a return class for the player's statistic, now I need to create one for the enemy.
public int GetChosenStatistic()
{
return SelectedStatistic;
}
Then I can very simply compare the two statistics in InitiateRound.cs.
// Compare the two selected statistics.
if (MyPlayer.GetChosenStatistic() > MyEnemy.GetChosenStatistic()) // Player wins.
{
MyPlayer.IncrementScore();
}
else if (MyPlayer.GetChosenStatistic() < MyEnemy.GetChosenStatistic()) // Enemy wins.
{
MyEnemy.IncrementScore();
}
else if (MyPlayer.GetChosenStatistic() == MyEnemy.GetChosenStatistic()) // Draw.
{
Victor.GetComponent<TextMeshProUGUI>().text = "It was a draw!";
}
I've also added a draw condition, where neither player's score will increase. Now I need to make the game loop. I want to make it loop for 13 rounds. To do this, I'll need to first check if the game has finished. If it has, move to the game end menu. If not, the player and enemy cards should be removed from the top left, and the next card should be given.
First, I need to create the round system. I'll display the round at the top of the screen. This also accommodates Joshua's request for a round counter. I've also created a variable called 'RoundNo' that starts at 1 in InitateRound.cs
The round counter, displayed at the top of the User's screen.
Now, I can create a function to check if round 13 has been reached.
void CheckEnd()
{
if (RoundNo >= 13)
{
// Move to the next scene if the game has ended.
SceneManager.LoadScene("game end");
}
}
'Now I will create a function in Player.cs to get the next card from the queue.
void NextCard()
{
if (DeckQueueContent.transform.childCount > 0)
{
// Move the next card in the queue into the hand, if there is one.
DeckQueueContent.transform.GetChild(0).GetComponent<Button>().interactable = true;
DeckQueueContent.transform.GetChild(0).transform.parent = DeckContent.transform;
// Move the played card back into the queue.
CardDisplay.transform.GetChild(0).GetComponent<Button>().interactable = false;
CardDisplay.transform.GetChild(0).transform.parent = DeckQueueContent.transform;
}
}
Now I need to create a function to reset the round.
void ClearRound()
{
// Delete the enemy card display and the enemy selected card.
Destroy(EnemySelectedCard.transform.GetChild(0).GetChild(0).gameObject);
Destroy(EnemyCardDisplay.transform.GetChild(0).gameObject);
// Reset labels.
EnemyChose.GetComponent<TextMeshProUGUI>().text = "";
Victor.GetComponent<TextMeshProUGUI>().text = "";
EnemyStatus.GetComponent<TextMeshProUGUI>().text = "The enemy is thinking...";
SelStatTextGameObject.GetComponent<TextMeshProUGUI>().text = "Statistic: none";
// Ready the next round and increase round number.
RoundNo++;
RoundNumber.GetComponent<TextMeshProUGUI>().text = "Round: " + RoundNo + "/13";
MyselfAsButton.enabled = true;
StartCoroutine(MyEnemy.Wait());
}
This uses the Destroy() function to remove the cards in the top left. It also resets every label to their default state. It also Increments the round number and changes the label.
After testing, the game loops through the rounds, though the player card is not visible as it happens too fast. I added a delay of 1 second before the round ends.
I fixed this and the game worked perfectly. It switches to the next scene at the end of round 13.
With this gameplay criteria #7 is complete, the game is now in a best of 13 format.
Brett's feedback:
I think this is a good addition. Rounds can get quite long so being able to keep track of how far in you are is good.
Joshua's feedback:
The round counter is useful and I was able to keep track of my place in the game better.
Richard's feedback:
This is useful. But I still struggle when the round ends as everything happens so suddenly, maybe there should be another delay or it should tell me who won.
Test |
Expected result |
Actual result (Pass/Fail) |
Comments |
Enemy has higher score |
The enemy's score is incremented |
Pass |
|
Player has higher score |
The player's score is incremented |
Pass |
|
Game selects correct statistic |
In a series of 10 tests, the correct value should be chosen 10/10 times. |
Pass |
This is a check for robustness, and it seems this works every time. |
Round 13 game end |
The game should end on round 13 and move to the next scene |
Pass |
It is triggered at the end of round 13, or the start of round 14, which are the same. |
Prototype 3:
First, I want to create a prompt to show which player won. I want this prompt to count down for about 5 seconds. This should let the user register what happened. First, I created an empty text object in the middle of the screen. It is invisible.
I want the message to look something like this:
You won!
You chose <statistic>(value)'
The enemy chose <statistic>(value)' (counter)
Getting the value can be done with Player.GetChosenStatistic(). For this prompt to work I also need to create a function to return the statistic as a string, for both the player and the enemy.
In Player.cs (GetChosenStatistic() needs to be run at least once.)
public string GetChosenStatisticAsString()
{
GetChosenStatistic();
return SelectedStatisticString;
}
In Enemy.cs
public string GetChosenStatisticAsString()
{
return Statistics[EnemySelectedStatisticIndex-1];
}
Now I can create the message.
for (int i=5; i>=0; i--) // Reverse for loop used to display a message that counts down..
{
EnemyChose.GetComponent<TextMeshProUGUI>().text = "The enemy chose " + MyEnemy.GetChosenStatisticAsString() + " (" + MyEnemy.GetChosenStatistic() + ")/nYou chose " + MyPlayer.GetChosenStatisticAsString() + " (" + MyPlayer.GetChosenStatistic() + ")... " + i;
yield return new WaitForSeconds(1);
}
This is a reverse loop that counts from 5 to 0. This is what it looks like after testing.
It counts down from 5 to 0 perfectly, as expected.
Richard's feedback:
It looks good but the prompt it hard to read, I would appreciate if there were a background.
'To implement this normally I would just give it a background tint like the other buttons inside the editor, but then that tint would be visible even when the message isn't displayed. Thus, I must tint the background in code only when the message is being displayed.
I did some research and I discovered that there is no background element for text. Instead, I can create a panel and put it behind the text.
Now I need to modify the transparency of this panel. I can do it like this:
InfoPromptPanel.GetComponent<Image>().color = new Color32(255, 0, 0, 64);
This uses RGB values with the last one being for alpha, or transparency. Since the red channel is maxed out, and the last channel is ' full, I expect the panel to be red with a quarter transparency when I start, as I've put this in the Start() function. Here is the result:
This is what I expected. Now I need to set it to be invisible by default.
InfoPromptPanel.GetComponent<Image>().color = new Color32(0, 0, 0, 0);
Then I need to make it visible when the loop starts, and invisible again after.
InfoPromptPanel.GetComponent<Image>().color = new Color32(0, 0, 0, 64);
for (int i=5; i>=0; i--) // Reverse for loop used to display a message that counts down..
{
InfoPrompt.GetComponent<TextMeshProUGUI>().text = "The enemy chose " + MyEnemy.GetChosenStatisticAsString() + " (" + MyEnemy.GetChosenStatistic() + ")/nYou chose " + MyPlayer.GetChosenStatisticAsString() + " (" + MyPlayer.GetChosenStatistic() + ")... " + i;
yield return new WaitForSeconds(1);
}
InfoPromptPanel.GetComponent<Image>().color = new Color32(0, 0, 0, 0);
This satisfied Richard's request. With this, UI criteria #4 is complete.
Test |
Expected result |
Actual result (Pass/Fail) |
Comments |
Round end |
The message should display at the end of every round |
Pass |
|
Countdown |
The message should count down for 5 seconds |
Pass |
|
Legibility |
The message should be readable |
Pass |
Asked stakeholders, 3/3 said yes. |
Now I need to create the environments system. First, I took 5 1920x1080 size images from online, and put them inside of a folder. I named them with numbers so it's easier in code later.
Next, I need to create a script that handles the background and the statistics modification. I will call this BackgroundController.cs. First, I will generate a random number.
public void GenerateBackroundNumber()
{
BackgroundNumber = UnityEngine.Random.Range(1,6);
}
This generates a random number between 1 and 5. Next, I need to create a function to change the background.
public void ChangeEviroment()
{
Background.GetComponent<Image>().sprite = Resources.Load<Sprite>("enviroments/" + BackgroundNumber);
}
This script also needs to change some text in the game to show the user what they've selected. I will be using this label that I created earlier.
As you can see, I need to display the environment's name, the statistic it boosts and the multiplier. First, I tackled the problem of displaying the environment name. To do this, I stored all the environment names in an array, in order.
private List<string> EnviromentNames = new List<string>{"fire", "jungle", "water", "earth", "ice"};
Order is important here, because now I can call the name of the environment simply by doing EnviromentNames[BackgroundNumber-1]. Next, I need to show the statistic that is being boosted. I will create a function for this.
public string GetStatisticToBeMultiplied()
{
// Returns the statistic that is being multiplied.
// It is formatted like this because it's being used in a label.
switch(BackgroundNumber)
{
case 1:
return "Strength: ";
break;
case 2:
return "Mana: ";
break;
case 3:
return "Intelligence: ";
break;
case 4:
return "Durability: ";
break;
case 5:
return "Speed: ";
break;
default:
return "invalid input";
break;
}
}
I am using case switch here because it is faster than using multiple if statements and provides greater readability. Now I need to code the function to return the multiplier.
public float GetMultiplier()
{
switch(BackgroundNumber)
{
case 1 or 2 or 3 or 4:
return 1.25f;
break;
case 5:
return 0.75f;
break;
default:
return -1;
break;
}
}
Now to make this execute in game, I'll add a statement in ChangeEnviroment() to change this label.
public void ChangeEviroment()
{
EnviromentDisplay.GetComponent<TextMeshProUGUI>().text = "Enviroment: " + EnviromentNames[BackgroundNumber-1] + "/n" + GetCapitalisedStatisticToBeMultiplied() + GetMultiplier() + "x";
Background.GetComponent<Image>().sprite = Resources.Load<Sprite>("enviroments/" + BackgroundNumber);
}
Then, in InitiateRound.cs I need to create an instance of this class, and call some functions.
BackgroundController MyBackgroundController = new BackgroundController();
In InitateRound.Start():
MyPlayer.Start();
MyEnemy.Start();
MyBackgroundController.Start();
MyBackgroundController.GenerateBackroundNumber();
MyBackgroundController.ChangeEviroment();
After testing, this is the result.
As you can see, the background has been changed successfully, and the label has been updated too.
With this, UI criteria #3 is complete.
Now I need to apply this modifier. The modifier applies to both the player and the enemy. It must only apply when they have selected the correct statistic. I can code this logic in InitiateRound.cs.
To do this, I can create return functions in Enemy, Player, and BackgroundController.cs. There is already a return function in Player and Enemy to return the statistic as a string. I need to create one in BackgroundController.cs. I cannot use the one that already exists, as it has capitalisation, and a comma. To work around this I created another return function.
public string GetStatisticToBeMultiplied()
{
switch(BackgroundNumber)
{
case 1:
return "strength";
break;
case 2:
return "mana";
break;
case 3:
return "intelligence";
break;
case 4:
return "durability";
break;
case 5:
return "speed";
break;
default:
return "invalid input";
break;
}
}
The other function was renamed to GetCapitalisedStatisticToBeMultiplied(). Now I can compare the values in InitiateRound.cs.
if (MyPlayer.GetChosenStatisticAsString() == MyBackgroundController.GetStatisticToBeMultiplied())
{
}
Next, I need to actually multiply the values. To do this, I will create a function in Player and Enemy. In Player.cs:
public void MultiplyStatistic(float Multiplier)
{
SelectedStatistic = (int)Math.Round(SelectedStatistic*Multiplier, 0);
}
'In Enemy.cs:
public void MultiplyStatistic(float Multiplier)
{
SelectedStatistic = (int)Math.Round(SelectedStatistic*Multiplier, 0);
}
As you can see, I've used rounding because I don't want to use floats. They are difficult to work with, as they can produce some results like recurring decimals, and they look messy and can be confusing for the user.
Now, in InitiateRound.cs I need to multiply the values.
if (MyPlayer.GetChosenStatisticAsString() == MyBackgroundController.GetStatisticToBeMultiplied())
{
MyPlayer.MultiplyStatistic(MyBackgroundController.GetMultiplier()); // Boost the player's statistic.
}
if (MyEnemy.GetChosenStatisticAsString() == MyBackgroundController.GetStatisticToBeMultiplied())
{
MyEnemy.MultiplyStatistic(MyBackgroundController.GetMultiplier()); // Boost the enemy's statistic.
}
I compiled the code and tested it:
As you can see, I selected the dragon, and selected mana. It is displaying the value before being multiplied. I can debug this using Debug.Log(), which prints to the console.
if (MyPlayer.GetChosenStatisticAsString() == MyBackgroundController.GetStatisticToBeMultiplied())
{
Debug.Log("Player's chosen statistic: " + MyPlayer.GetChosenStatisticAsString());
Debug.Log("BackgroundController statistic to be multiplied: " + MyBackgroundController.GetStatisticToBeMultiplied());
MyPlayer.MultiplyStatistic(MyBackgroundController.GetMultiplier()); // Boost the player's statistic.
}
This was the output:
Both are the same, and the if statement is executing. That means there's a problem with the multiplication. I added debug statements to display the value of the statistic.
if (MyPlayer.GetChosenStatisticAsString() == MyBackgroundController.GetStatisticToBeMultiplied())
{
Debug.Log("Stat before multiplying: " + MyPlayer.GetChosenStatistic());
MyPlayer.MultiplyStatistic(MyBackgroundController.GetMultiplier()); // Boost the player's statistic.
Debug.Log("Stat after multiplying: " + MyPlayer.GetChosenStatistic());
}
Here's the result:
Once again, the debug statements are being executed so the if statement is working. We can see it is not a problem with the message displaying something different, as the actual statistic has not changed. Now I'll try debugging Player.MultiplyStatistic().
public void MultiplyStatistic(float Multiplier)
{
Debug.Log("MultiplyStat before: " + SelectedStatistic);
Debug.Log("Multiplier: " + Multiplier);
SelectedStatistic = (int)Math.Round(SelectedStatistic*Multiplier, 0);
Debug.Log("MultiplyStat after: " + SelectedStatistic);
}
This is the output:
The statistic is multiplying correctly, but it is somehow resetting. This leads me to believe that the value is being changed somewhere else. I realised that the problem lies in GetChosenStatistic().
public int GetChosenStatistic()
{
// Returns the players chosen statistic.
SelectedStatisticString = SelStatTextGameObject.GetComponent<TextMeshProUGUI>().text;
Word = SelectedStatisticString.Split(" ");
SelectedStatisticString = Word[1];
SelectedStatisticIndex = Array.FindIndex(Statistics, row => row.Contains(SelectedStatisticString));
SelectedStatistic = Int32.Parse(CardArray[Array.FindIndex(CardArray2D, row => row.Contains(PlayerCard.name)), SelectedStatisticIndex + 1]);
return SelectedStatistic;
}
This function does not just return the selected statistic, it also is responsible for getting the value from the 2D array. Thus, every time I called it to get the value of SelectedStatistic, I was unknowingly resetting the value. To solve this, I can simply move this code to a function that is called earlier. I'll move it to PlaySelectedCard().
public void PlaySelectedCard()
{
// Moves the selected card from the bottom of the screen to the top left.
PlayerCard = GameObject.Find(PlayerCardContainer.transform.GetChild(0).name);
PlayerCard.transform.parent = CardDisplay.transform;
// Sets the players chosen statistic.
SelectedStatisticString = SelStatTextGameObject.GetComponent<TextMeshProUGUI>().text;
Word = SelectedStatisticString.Split(" ");
SelectedStatisticString = Word[1];
SelectedStatisticIndex = Array.FindIndex(Statistics, row => row.Contains(SelectedStatisticString));
SelectedStatistic = Int32.Parse(CardArray[Array.FindIndex(CardArray2D, row => row.Contains(PlayerCard.name)), SelectedStatisticIndex + 1]);
}
Now GetChosenStatistic() is just a return function.
public int GetChosenStatistic()
{
return SelectedStatistic;
}
I tested once again.
As you can see in the image, the enemy does not have this problem, so no changes need to be made. Now I can finish the multiplication code by removing all the Debug.Log statements.
if (MyPlayer.GetChosenStatisticAsString() == MyBackgroundController.GetStatisticToBeMultiplied())
{
MyPlayer.MultiplyStatistic(MyBackgroundController.GetMultiplier()); // Boost the player's statistic.
}
if (MyEnemy.GetChosenStatisticAsString() == MyBackgroundController.GetStatisticToBeMultiplied())
{
MyEnemy.MultiplyStatistic(MyBackgroundController.GetMultiplier()); // Boost the enemy's statistic.
}
public void MultiplyStatistic(float Multiplier)
{
SelectedStatistic = (int)Math.Round(SelectedStatistic*Multiplier, 0);
}
After testing one final time it continued to work. Now the core gameplay is complete.
Brett's feedback:
The core gameplay is good and satisfying. However, I think the user experience could be better. The score counter is confusing, I think both the player score and the enemy score should be shown next to each other.
Richard's feedback:
I think the game is good, it is fluid, and the game does not crash. It is enjoyable and I like that I can select my own cards. However, I would like to be able to select different difficulties.
Joshua's feedback:
The game is fun, and functional. However, I think the label that displays the current can be confusing. It would be better if it showed percentages instead of multiplication.
Test |
Expected result |
Actual result (Pass/Fail) |
Comments |
Correct values are displayed in the end message |
In a test of 10 times, the correct values should be displayed 10/10 times |
Pass |
|
With this I have achieved gameplay success criteria #2, which is the environment system.
Prototype 5:
First, I want to implement a better score counter. First, I'll move some UI elements around.
Then I shortened the labels and put them in coloured boxes, red for the enemy and blue for the player.
Now I need to go into the code for the player and enemy increase score functions and update them.
public void IncrementScore()
{
// Increments the players score and updates the label.
PlayerScore++;
Victor.GetComponent<TextMeshProUGUI>().text = "You won!";
PlayerScoreText.GetComponent<TextMeshProUGUI>().text = PlayerScore;
}
public void IncrementScore()
{
// This means that the enemy has won.
Score++; // Increment the enemies score.
Victor.GetComponent<TextMeshProUGUI>().text = "You lost.";
EnemyScoreText.GetComponent<TextMeshProUGUI>().text = Score;
}
When trying to run this I got an error.
Assets/03Scripts/Enemy.cs(123,63): error CS0029: Cannot implicitly convert type 'int' to 'string'
To fix this I can use the ToString() method.
PlayerScoreText.GetComponent<TextMeshProUGUI>().text = PlayerScore.ToString();
After testing, this is the result.
Joshua said that percentages would be easier to read than decimal multiplication, and I agree. Currently, the message reads like:
Strength: 1.25x
I would like to change it to:
+25% Strength
First, I'll modify the GetCapitalisedStatisticToBeMultiplied() function to remove the colon.
public string GetCapitalisedStatisticToBeMultiplied()
{
// Returns the statistic that is being multiplied.
// It is formatted like this because it's being used in a label.
switch(BackgroundNumber)
{
case 1:
return "Strength";
break;
case 2:
return "Mana";
break;
case 3:
return "Intelligence";
break;
case 4:
return "Durability";
break;
case 5:
return "Speed";
break;
default:
return "invalid input";
break;
}
}
And I'll write another function to return the multiplier as a percentage.
public string GetPercentage()
{
// Returns the multiplier based on the background.
switch(BackgroundNumber)
{
case 1 or 2 or 3 or 4:
return "+25%";
break;
case 5:
return "-25%";
break;
default:
return "invalid input";
break;
}
}
Now I can modify the message.
EnviromentDisplay.GetComponent<TextMeshProUGUI>().text = "Enviroment: " + EnviromentNames[BackgroundNumber-1] + "/n" + GetPercentage() + " " + GetCapitalisedStatisticToBeMultiplied();
This is the result.
No testing or feedback is required as this is a simple cosmetic change.
Prototype 1:
First I'll lay out the basic UI elements.
It displays 'Game over!', a button to return to the main menu, and there is a label that I will use to display the final score. To do this, I'm going to use static variables. I can get player score and enemy score and then define them as public static variables, like I did with Deck.
public static int PlayerScore = 0;
public static int EnemyScore = 0;
void CheckEnd()
{
if (RoundNo >= 13)
{
// Move to the next scene if the game has ended.
PlayerScore = MyPlayer.GetScore();
EnemyScore = MyEnemy.GetScore();
SceneManager.LoadScene("game end");
}
}
Then I can create a new script in 'game end' called EndMessage.cs.
void Start()
{
if (InitiateRound.PlayerScore > InitiateRound.EnemyScore)
{
// In the case that the player won.
gameObject.GetComponent<TextMeshProUGUI>().text = "You won!/nThe final score was " + InitiateRound.PlayerScore + " - " + InitiateRound.EnemyScore;
}
else if (InitiateRound.PlayerScore < InitiateRound.EnemyScore)
{
// In the case that the enemy won.
gameObject.GetComponent<TextMeshProUGUI>().text = "You lost./nThe final score was " + InitiateRound.PlayerScore + " - " + InitiateRound.EnemyScore;
}
else if (InitiateRound.PlayerScore == InitiateRound.EnemyScore)
{
// In the case that the enemy won.
gameObject.GetComponent<TextMeshProUGUI>().text = "It was a draw!/nThe final score was " + InitiateRound.PlayerScore + " - " + InitiateRound.EnemyScore;
}
}
'
This is the result.
Brett's feedback:
Looks a bit plain but good.
Joshua's feedback:
This is a good addition to the game, seeing the final score is vital.
Richard's feedback:
I like this addition.
Test |
Expected result |
Actual result (Pass/Fail) |
Comments |
Correct final score displayed |
The correct final score from the game should be displayed |
Pass |
|
Click back to main menu |
The user should be taken back to the main menu |
Pass |
|
Prototype 1:
First, I have to create a new scene called 'select difficulty'. Then I will set the 'Next' button in the card selection screen to redirect to this scene instead.
if (SelectedCards.transform.childCount == 6) // Validate the number of cards.
{
foreach (Transform child in SelectedCards.transform)
{
Deck.Add(child.name);
// Adds all the selected cards to deck.
}
SceneManager.LoadScene("select difficulty"); //Load the next scene.
}
Then I'll lay out the UI elements.
Now, I have to create a script for these buttons. My idea is for the script to get the name of itself, then assign that to a public static variable. Then can assign the appropriate difficulty in InitiateRound.cs.
{
public Button MyselfAsButton;
public static string Difficulty;
void Start()
{
MyselfAsButton = gameObject.GetComponent<Button>();
MyselfAsButton.onClick.AddListener(OnClick);
}
void OnClick()
{
Difficulty = gameObject.name;
SceneManager.LoadScene("play screen");
}
}
I will assign this to all 3 buttons. Next, in InitiateRound.cs I need to use if statements to assign the enemy difficulty.
// Create enemy object based on difficulty.
if (SelectDifficulty.Difficulty == "Easy Difficulty")
{
Enemy MyEnemy = new Enemy();
}
else if (SelectDifficulty.Difficulty == "Medium Difficulty")
{
MediumEnemy MyEnemy = new MediumEnemy();
}
else if (SelectDifficulty.Difficulty == "Hard Difficulty")
{
HardEnemy MyEnemy = new HardEnemy();
}
The classes MediumEnemy and HardEnemy do not exist yet. I will create them using inheritance. First, I'll create MediumEnemy, which inherits from Enemy.
public class MediumEnemy : Enemy
The only function I will need to override is PlaySelectedCard(). First I need to define Enemy.PlaySelectedCard() as virtual.
public virtual void PlaySelectedCard()
Now in MediumEnemy I can create the override function.
public override void PlaySelectedCard()
{
// This method overrides Enemy.PlaySelectedCard().
}
I will copy most of the code.
public override void PlaySelectedCard()
{
// This method overrides Enemy.PlaySelectedCard().
// Make the enemy choose a random card.
ChosenCard = Hand[UnityEngine.Random.Range(0,2)];
// Select the stat.
SelectedStatistic = Int32.Parse(CardArray[Array.FindIndex(CardArray2D, row => row.Contains(ChosenCard)), EnemySelectedStatisticIndex]);
// This block generates the card that the enemy AI has chosen.
CardInst = Instantiate(CardPrefab, new Vector3(0, 0, 0), Quaternion.identity); // Generate an instance of the card.
CardInst.GetComponent<Image>().sprite = Resources.Load<Sprite>(ChosenCard); // Assign the image.
CardInst.transform.parent = EnemyCardDisplay.transform; // Move it to the enemy card display.
CardInst.name = EnemyAI.EnemySelCard; // Set the name of the gameobject. Otherwise it defaults to "clone".
// Move the chosen card back to the deck.
Queue.Add(ChosenCard);
Hand.Remove(ChosenCard);
Hand.Add(Queue[0]);
Queue.Remove(Queue[0]);
}
After compiling the code, I got many errors.
At first I thought this was because the other methods were not declared as virtual. After declaring all methods as virtual, the errors remained. I also tried changing the inheritance.
public class MediumEnemy : Enemy.Enemy
This also did not work. I tried declaring both Enemy and MediumEnemy in the same file.
public class Enemy : MonoBehaviour
{
// Code.
}
public class MediumEnemy : Enemy
{
// Code.
}
Simplified example view. Both were declared in Enemy.cs.
This also did not work. I then realised it may be because of the way I test; no difficulty has been assigned and so no Enemy class is created.
else
{
// Default to medium enemy if no difficulty is selected.
MediumEnemy MyEnemy = new MediumEnemy();
}
I added this code to fall back to medium difficulty if no difficulty is manually selected. This solved part of the issue. I also had to change all the variables from private to protected to allow them to be inherited. After testing again, the error remained. I did some extensive research online and found that this operation is not actually possible in C#. C# is what is known as a 'statically typed language'. I will have to look for a different way to implement the difficulty system.
My next idea is to have the multiple difficulties under the same function, and switch difficulties using parameters. I'll supply the parameter and Enemy.PlaySelectedCard() can execute based on that.
public virtual void PlaySelectedCard(string UserDifficulty)
{
if (UserDifficulty == "Easy Difficulty") // This is when the player has chosen the easy difficulty.
{
// Make the enemy choose a random card.
ChosenCard = Hand[UnityEngine.Random.Range(0,2)];
// Select the stat.
SelectedStatistic = Int32.Parse(CardArray[Array.FindIndex(CardArray2D, row => row.Contains(ChosenCard)), EnemySelectedStatisticIndex]);
}
else if (UserDifficulty == "Medium Difficulty") // When the player has chosen medium difficulty.
{
}
else if (UserDifficulty == "Hard Difficulty") // When the player has chosen hard difficulty.
{
}
This is what I want the difficulties to do:
Easy ' Select a random card, and a random statistic. Makes the decision at a slower speed (6 ' 12 seconds).
Medium ' Select a random card, and the greatest statistic of that card. Makes the decision at a normal speed (4 ' 8 seconds).
Hard ' Select the greatest statistic of any card that it has, taking into account the environment. Makes the decision faster (2 ' 4 seconds).
First, I'll change the Start() function to take into account the faster thinking time on the higher difficulties.
if (UserDifficulty == "Easy Difficulty") // This is when the player has chosen the easy difficulty.
{
yield return new WaitForSeconds(UnityEngine.Random.Range(1,2)); // The enemy waits between 6 and 12 seconds.
}
else if (UserDifficulty == "Medium Difficulty") // When the player has chosen medium difficulty.
{
yield return new WaitForSeconds(UnityEngine.Random.Range(1,2)); // The enemy waits between 4 and 8 seconds.
}
else if (UserDifficulty == "Hard Difficulty") // When the player has chosen hard difficulty.
{
yield return new WaitForSeconds(UnityEngine.Random.Range(1,2)); // The enemy waits between 2 and 4 seconds.
}
I've left the actual timing as 1-2 seconds for easier testing. Now I need to implement the Medium and Hard difficulties. I'll begin with the medium difficulty.
// Make the enemy choose a random card.
ChosenCard = Hand[UnityEngine.Random.Range(0,2)];
MediumCompare.Clear();
for (int i =1; i<7; i++)
{
// Find every value in the card and add it to "MediumCompare".
MediumCompare.Add(Int32.Parse(CardArray[Array.FindIndex(CardArray2D, row => row.Contains(ChosenCard)), i]));
}
SelectedStatistic = MediumCompare.Max(); // Find the greatest value in MediumCompare and set that to the SelectedStatistic.
}
It selects a random card like the easy difficulty. It adds every card statistic to the 'MediumCompare' list and then finds the highest value. It also clears the deck beforehand for future repetitions.
Now I need to provide the parameters in InitiateRound.cs.
StartCoroutine(MyEnemy.Wait(SelectDifficulty.Difficulty));
In Start().
StartCoroutine(MyEnemy.Wait(SelectDifficulty.Difficulty));
In ClearRound().
MyEnemy.PlaySelectedCard(SelectDifficulty.Difficulty);
In MyCoroutine().
I tested the game and got this error.
IndexOutOfRangeException: Index was outside the bounds of the array.
Enemy.PlaySelectedCard (System.String UserDifficulty) (at Assets/03Scripts/Enemy.cs:133)
InitiateRound+<MyCoroutine>d__26.MoveNext () (at Assets/03Scripts/InitiateRound.cs:136)
UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) (at <24d45e813e524a99bfb7a145158a7980>:0)
An out of bound error probably means that the value of I in the loop is invalid at some point. I'll use Debug.Log() on for more information.
Debug.Log("i: " + i);
Debug.Log("Value to be added: " + CardArray[Array.FindIndex(CardArray2D, row => row.Contains(ChosenCard)), i]);
It's trying to use the wolf card. It crashes when trying to add the 6th value. I forgot arrays start at 0, so I need to decrease the loop by 1.
for (int i=0; i<6; i++)
After testing I got this error.
FormatException: Input string was not in a correct format.
System.Number.ThrowOverflowOrFormatException (System.Boolean overflow, System.String overflowResourceKey) (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
System.Number.ParseInt32 (System.ReadOnlySpan`1[T] value, System.Globalization.NumberStyles styles, System.Globalization.NumberFormatInfo info) (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
System.Int32.Parse (System.String s) (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
Enemy.PlaySelectedCard (System.String UserDifficulty) (at Assets/03Scripts/Enemy.cs:133)
InitiateRound+<MyCoroutine>d__26.MoveNext () (at Assets/03Scripts/InitiateRound.cs:136)
UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) (at <24d45e813e524a99bfb7a145158a7980>:0)
This means that it's trying to convert an invalid string to an integer. I forgot that the first value is the name of the card. I modified the loop to start from 1.
for (int i=1; i<6; i++)
This is what happens after testing.
You can see the enemy does pick the greatest statistic, but the name of it is incorrect. This is because I forgot to modify GetChosenStatisticAsString(). First, in PlaySelectedCard() I added this.
SelectedStatistic = MediumCompare.Max(); // Find the greatest value in MediumCompare and set that to the SelectedStatistic.
MediumSelectedStatisticAsString = MediumCompare.IndexOF(SelectedStatistic);
Then in GetChosenStatisticAsString() I added some logic.
public string GetChosenStatisticAsString(string UserDifficulty)
{
if (UserDifficulty == "Easy Difficulty") // This is when the player has chosen the easy difficulty.
{
return Statistics[EnemySelectedStatisticIndex-1];
}
else if (UserDifficulty == "Medium Difficulty") // When the player has chosen medium difficulty.
{
return MediumSelectedStatisticAsString;
}
else if (UserDifficulty == "Hard Difficulty") // When the player has chosen hard difficulty.
{
return "none";
}
}
I also provided the parameter for all instances of Enemy.GetChosenStatisticAsSring() in InitiateRound.cs. When testing, I got this error.
Assets/03Scripts/Enemy.cs(86,19): error CS0161: 'Enemy.GetChosenStatisticAsString(string)': not all code paths return a value
I need to add one more line.
else if (UserDifficulty == "Hard Difficulty") // When the player has chosen hard difficulty.
{
return "none";
}
return "none";
After fixing, this is the result.
I can fix it by changing this line.
MediumSelectedStatisticAsString = Statistics[MediumCompare.IndexOf(SelectedStatistic)]
Here is the result.
The AI successfully picks the greatest statistic every time and is certainly much harder to play against. Now I need to code the Hard Difficulty. My idea is for it to add all the statistics of the 3 cards to a single list. Since I know that each card has 5 statistics, and you always have 3 cards, there should be 15 statistics in the list. Then I know the first 5 belong to the first card, the middle 5 belong to the second card, and the last 5 belong to the last card.
else ( == "Hard Difficulty") // When the player has chosen hard difficulty.
{
( )
{
( =; <; ++)
{
.(.([.(, => .()), ]));
}
}
( )
{
.();
}
( )
{
.();
}
}
Now I'll change the default value of UserDifficulty to Hard Difficulty to make it easier to test. This is the output:
It has correctly added the value of all the cards. Next, it needs to multiply the value of the boosted stats. I can do this by finding the statistic to be boosted, turning it into a number n, then boosting every 5nth value. For example, if I wanted to boost strength, I would multiply the 2nd, 7th, and 12th values in the list.
To do this, I can create a function in BackgroundController.cs.
public int GetStatisticIndexToBeMultiplied()
{
// Returns the statistic that the background is boosting as an integer.
// Used for checking if the user or the enemy has selected the boosted statistic.
switch(BackgroundNumber)
{
case 1:
return 2;
break;
case 2:
return 5;
break;
case 3:
return 4;
break;
case 4:
return 3;
break;
case 5:
return 1;
break;
default:
return -1;
break;
}
}
Now I will need to create a loop that begins at this number (minus one as the list starts at 0), then increments in steps of 5 until 15, and multiplies these statistics. To do this, I will need to make MyBackgroundController static.
public static BackgroundController MyBackgroundController = new BackgroundController();
Then in Enemy.cs I can call this object and the function I created inside a loop.
for (int i = InitiateRound.MyBackgroundController.GetStatisticIndexToBeMultiplied() - 1; i < 15; i += 5)
{
Debug.Log(HardCompare[i]);
}
Here is the result.
The environment selected was the Ice environment, and all the statistics related to speed were returned correctly. Now I can take the next step to multiply them.
Debug.Log("==========================");
for (int i = InitiateRound.MyBackgroundController.GetStatisticIndexToBeMultiplied() - 1; i < 15; i += 5)
{
Debug.Log(HardCompare[i]);
HardCompare[i] = (int)Math.Round(HardCompare[i]*InitiateRound.MyBackgroundController.GetMultiplier());
Debug.Log(HardCompare[i]);
}
Debug.Log("==========================");
foreach (int test in HardCompare)
{
Debug.Log(test);
}
Here is the result.
The environment selected was the Jungle, which boosts mana. Every statistic relating to mana was successfully multiplied. We now have a list of all the cards statistics with the environment taken into account.
The next step is to find the greatest statistic, find which card it belongs to and play that. First I will create the if statements.
if (HardCompare.IndexOf(SelectedStatistic) >= 0 && HardCompare.IndexOf(SelectedStatistic) < 5)
{
ChosenCard = Hand[0];
}
else if (HardCompare.IndexOf(SelectedStatistic) >= 5 && HardCompare.IndexOf(SelectedStatistic) < 10)
{
ChosenCard = Hand[1];
}
else if (HardCompare.IndexOf(SelectedStatistic) >= 10 && HardCompare.IndexOf(SelectedStatistic) < 15)
{
ChosenCard = Hand[2];
}
I've assigned the greatest value to SelectedStatistic beforehand, otherwise the List.Max() function would be called twice per if statement, which could affect performance. Now I can test.
As you can see, the enemy chose a statistic that is not possible, and the name is 'none'. I think this may be because I have not cleared the 'Hard Compare' list.
HardCompare.Clear();
The value of the selected statistic is now accurate. However, it still states 'none'. This is because I have not filled in the 'Hard Difficulty' section for GetChosenStatisticAsString(). First, I changed the name of 'MediumSelectedStatisticAsString' to 'SelectedStatisticAsString' so I can use it for both the medium and hard difficulties. Then I added this code in PlaySelectedCard.
switch (HardCompare.IndexOf(SelectedStatistic))
{
case 0 or 5 or 10:
SelectedStatisticAsString = "speed";
break;
case 1 or 6 or 11:
SelectedStatisticAsString = "strength";
break;
case 2 or 7 or 12:
SelectedStatisticAsString = "durability";
break;
case 3 or 8 or 13:
SelectedStatisticAsString = "intelligence";
break;
case 4 or 9 or 14:
SelectedStatisticAsString = "mana";
break;
}
Then in GetChosenStatisticAsString:
else if (UserDifficulty == "Medium Difficulty" || UserDifficulty == "Hard Difficulty") // When the player has chosen medium difficulty.
{
return SelectedStatisticAsString;
}
After testing, this is the result.
'
You can see that the highest statistic on the rat card is 100. However, the environment is ice, and the actual value is reduced to 75. So, the enemy realises this and chooses intelligence instead. The hard difficulty is working as expected.
Upon further testing, I found this. The dragon's base durability is 88 and it should be boosted to 110 but instead it is boosted to 138, which is beyond the limit of what should be allowed. I want to test this further, but to try to get the earth environment, I have to keep pressing play until I randomly get it. To solve this I'm going to create a custom console where I can input commands to make the game easier to test. First, I create a small text box in the top right corner.
Next, I add a function to detect when the user presses enter.
public void OnTextChange(string text)
{
if (text.EndsWith("/n"))
{
// Command.
}
}
I'm going to take a similar approach to the statistics selection and split each word into a list.
private string[] Word;
public void OnTextChange(string text)
{
if (text.EndsWith("/n"))
{
Word = text.Split(" ");
}
}
Now, I can check for my custom commands. I want to implement a way to change the environment while the game is running. My command will look like this:
changeenvironment 3
This will change the environment to the Earth environment, as earth is indexed by the number 3.
I need to check the first word.
if (Word[0] == "changeenvironment")
then based on the second word, I can call a function inside of BackgroundController.cs to change the background. Currently, the function BackgroundController.ChangeEnvironment() will only change the background to a random number that it has selected. So, I'm going to create a new function called ChangeEnvironmentCustom().
public void ChangeEnvironmentCustom(int custom)
{
BackgroundNumber = custom;
ChangeEviroment();
}
I'm going to implement error handling. I will need to use try/except. Also, I will need to get the BackgroundController object from the InitiateRound.cs script.
private BackgroundController MyBackgroundController = GameObject.Find("Ready Button").GetComponent<BackgroundController>();
Now I can implement the try/except block.
try
{
{
MyBackgroundController.ChangeEnvironmentCustom(Int32.Parse(Word[1]));
}
}
catch (System.Exception)
{
throw;
}
Then finally I want to clear the console when a command is executed.
gameObject.GetComponent<TMP_InputField>().text = "";
When compiling the script, I got this error.
UnityException: Find is not allowed to be called from a MonoBehaviour constructor (or instance field initializer), call it in Awake or Start instead. Called from MonoBehaviour 'CustomConsole' on game object 'Console'.
See "Script Serialization" page in the Unity Manual for further details.
CustomConsole..ctor () (at Assets/03Scripts/CustomConsole.cs:10)
I need to assign the BackgroundController in a Start() function.
public void Start()
{
MyBackgroundController = GameObject.Find("Ready Button").GetComponent<BackgroundController>();
}
When testing, I was unable to input any code. After commenting out my code so that it doesn't execute, I was able to type in it. This means my code is doing some thing that does not allow the user to enter text.
I realised that I had put the line to clear user input in the wrong place, so it was clearing input every time the user typed something, instead of every time the user input presses enter. After fixing that, this is the result.
After pressing enter, nothing happens. The field does not clear. This means that there is an error with the detection.
public void OnTextChange()
{
if (gameObject.GetComponent<TMP_InputField>().text.EndsWith("/n"))
{
Word = gameObject.GetComponent<TMP_InputField>().text.Split(" ");
When testing again, I realised that I had forgotten to clear the input field when an error was detected.
catch (System.Exception)
{
gameObject.GetComponent<TMP_InputField>().text = "";
throw;
}
Testing again:
As you can see, with an invalid input, after pressing enter the input is cleared. When typing in 'changeenvironment 2', the following error is shown.
NullReferenceException: Object reference not set to an instance of an object
CustomConsole.OnTextChange () (at Assets/03Scripts/CustomConsole.cs:34)
UnityEngine.Events.InvokableCall.Invoke () (at <24d45e813e524a99bfb7a145158a7980>:0)
UnityEngine.Events.UnityEvent`1[T0].Invoke (T0 arg0) (at <24d45e813e524a99bfb7a145158a7980>:0)
TMPro.TMP_InputField.SendOnValueChanged () (at Library/PackageCache/com.unity.textmeshpro@3.0.6/Scripts/Runtime/TMP_InputField.cs:3139)
TMPro.TMP_InputField.Insert (System.Char c) (at Library/PackageCache/com.unity.textmeshpro@3.0.6/Scripts/Runtime/TMP_InputField.cs:3113)
TMPro.TMP_InputField.Append (System.Char input) (at Library/PackageCache/com.unity.textmeshpro@3.0.6/Scripts/Runtime/TMP_InputField.cs:3086)
TMPro.TMP_InputField.KeyPressed (UnityEngine.Event evt) (at Library/PackageCache/com.unity.textmeshpro@3.0.6/Scripts/Runtime/TMP_InputField.cs:2065)
TMPro.TMP_InputField.OnUpdateSelected (UnityEngine.EventSystems.BaseEventData eventData) (at Library/PackageCache/com.unity.textmeshpro@3.0.6/Scripts/Runtime/TMP_InputField.cs:2155)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IUpdateSelectedHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:106)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:272)
UnityEngine.EventSystems.EventSystem:Update() (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:501)
This is caused by the throw; line. I removed this as it is not needed. After fixing this, the console did not seem to accept the changenvironment input, it cleared it and did nothing like it would with everything else. To debug, I added a Debug.Log() line. To see if the object is being referenced correctly.
Debug.Log(MyBackgroundController.GetStatisticToBeMultiplied());
I got this error.
NullReferenceException: Object reference not set to an instance of an object
CustomConsole.OnTextChange () (at Assets/03Scripts/CustomConsole.cs:19)
UnityEngine.Events.InvokableCall.Invoke () (at <24d45e813e524a99bfb7a145158a7980>:0)
UnityEngine.Events.UnityEvent`1[T0].Invoke (T0 arg0) (at <24d45e813e524a99bfb7a145158a7980>:0)
TMPro.TMP_InputField.SendOnValueChanged () (at Library/PackageCache/com.unity.textmeshpro@3.0.6/Scripts/Runtime/TMP_InputField.cs:3139)
TMPro.TMP_InputField.Insert (System.Char c) (at Library/PackageCache/com.unity.textmeshpro@3.0.6/Scripts/Runtime/TMP_InputField.cs:3113)
TMPro.TMP_InputField.Append (System.Char input) (at Library/PackageCache/com.unity.textmeshpro@3.0.6/Scripts/Runtime/TMP_InputField.cs:3086)
TMPro.TMP_InputField.KeyPressed (UnityEngine.Event evt) (at Library/PackageCache/com.unity.textmeshpro@3.0.6/Scripts/Runtime/TMP_InputField.cs:2065)
TMPro.TMP_InputField.OnUpdateSelected (UnityEngine.EventSystems.BaseEventData eventData) (at Library/PackageCache/com.unity.textmeshpro@3.0.6/Scripts/Runtime/TMP_InputField.cs:2155)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IUpdateSelectedHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:106)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:272)
UnityEngine.EventSystems.EventSystem:Update() (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:501)
As I thought, the object is not being referenced properly. I completely forgot that the ready button does not have a BackgroundController.cs attached to it. It has the InitiateRound.cs script attached to it, which contains an instance of BackgroundController.cs. InitiateRound.cs's BackgroundController object is already public and static.
MyBackgroundController = GameObject.Find("Ready Button").GetComponent<InitiateRound>().MyBackgroundController;
I got this error:
Assets/03Scripts/CustomConsole.cs(14,34): error CS0176: Member 'InitiateRound.MyBackgroundController' cannot be accessed with an instance reference; qualify it with a type name instead
Instead I decided to create a return function. In InitiateRound.cs:
BackgroundController ReturnBackgroundController()
{
return MyBackgroundController;
}
And in CustomConsole.cs:
MyBackgroundController = GameObject.Find("Ready Button").GetComponent<InitiateRound>().ReturnBackgroundController();
After testing, it now works as expected.
Furthermore, the console commands are working as expected.
You can see that the environment was successfully changed to earth. I decided to change the command from changeenvironment to envchange to make it easier to write.
if (Word[0] == "envchange")
The environment multiplier label also changes in real time, and the actual multiplier changes.
Here, we can see the dragon's issue once again. I thought this might be because the durability value for the dragon is written in the code incorrectly.
You can see this issue does not apply to the player's dragon.
In Enemy.cs, the values for the dragon are coded correctly, so the issue is with the multiplier, in the enemy file.
public void MultiplyStatistic(float Multiplier)
{
SelectedStatistic = (int)Math.Round(SelectedStatistic*Multiplier, 0);
}
It seems fine but I will put in some debug statements.
public void MultiplyStatistic(float Multiplier)
{
Debug.Log("Multiplier: " + Multiplier);
Debug.Log("Value before: " + SelectedStatistic);
SelectedStatistic = (int)Math.Round(SelectedStatistic*Multiplier, 0);
Debug.Log("Value after: " + SelectedStatistic);
}
The value before being multiplied is already greater than the maximum value which any card can have, which is 100. The enemy chose strength on the dragon, which should calculate as:
94 x 1.25 = 117.5, which rounds to 118.
This is the 'value before' that we see. This means that the multiplier has already been applied before, so it's being applied twice.
if (MyEnemy.GetChosenStatisticAsString(SelectDifficulty.Difficulty) == MyBackgroundController.GetStatisticToBeMultiplied())
{
// MyEnemy.MultiplyStatistic(MyBackgroundController.GetMultiplier()); // Boost the enemy's statistic.
}
I've commented out the following code to see where the other multiplication is taking place.
With the code commented out, it's working as it should, which is odd.
Debug.Log("Enemy value before selection statement: " + MyEnemy.GetChosenStatistic());
yield return new WaitForSeconds(1);
if (MyPlayer.GetChosenStatisticAsString() == MyBackgroundController.GetStatisticToBeMultiplied())
{
Debug.Log("Enemy value before player multiplication: " + MyEnemy.GetChosenStatistic());
MyPlayer.MultiplyStatistic(MyBackgroundController.GetMultiplier()); // Boost the player's statistic.
Debug.Log("Enemy value after player multiplication: " + MyEnemy.GetChosenStatistic());
}
if (MyEnemy.GetChosenStatisticAsString(SelectDifficulty.Difficulty) == MyBackgroundController.GetStatisticToBeMultiplied())
{
Debug.Log("Enemy value before enemy multiplication: " + MyEnemy.GetChosenStatistic());
// MyEnemy.MultiplyStatistic(MyBackgroundController.GetMultiplier()); // Boost the enemy's statistic.
Debug.Log("Enemy value after enemy multiplication: " + MyEnemy.GetChosenStatistic());
}
I added various debug statements throughout for further testing.
The value is being multiplied even before the selection statement begins. After some searching,' I found that the issue lies here:
for (int i = InitiateRound.MyBackgroundController.GetStatisticIndexToBeMultiplied() - 1; i < 15; i += 5)
{
HardCompare[i] = (int)Math.Round(HardCompare[i]*InitiateRound.MyBackgroundController.GetMultiplier());
}
The list created for the hard difficulty (which is am testing right now) already multiplies the values in the list to compare them. So when I return the value, I need to return the original value, and not the equivalent in the list. Here is the current code:
SelectedStatistic = HardCompare.Max();
I can replace it with this:
SelectedStatistic = Int32.Parse(CardArray[Array.FindIndex(CardArray2D, row => row.Contains()), ])
But I need to fill in two values. First, I need to find the row, which will be the name of the card selected. For this I can use the variable ChosenCard that I've used before. The next value is the column. For this I can put SelectedStatistic into a the Statistics list, return the index value, and add one, since the first value is the card name.
SelectedStatistic = Int32.Parse(CardArray[Array.FindIndex(CardArray2D, row => row.Contains(ChosenCard)), Statistics.IndexOf(SelectedStatisticAsString)]);
When testing, I got the following error:
ArgumentNullException: Value cannot be null.
Parameter name: value
System.String.IndexOf (System.String value, System.Int32 startIndex, System.Int32 count, System.StringComparison comparisonType) (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
System.String.IndexOf (System.String value, System.StringComparison comparisonType) (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
System.String.Contains (System.String value) (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
Enemy.<PlaySelectedCard>b__27_1 (System.String row) (at Assets/03Scripts/Enemy.cs:204)
System.Array.FindIndex[T] (T[] array, System.Int32 startIndex, System.Int32 count, System.Predicate`1[T] match) (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
System.Array.FindIndex[T] (T[] array, System.Predicate`1[T] match) (at <9aad1b3a47484d63ba2b3985692d80e9>:0)
Enemy.PlaySelectedCard (System.String UserDifficulty) (at Assets/03Scripts/Enemy.cs:204)
InitiateRound+<MyCoroutine>d__27.MoveNext () (at Assets/03Scripts/InitiateRound.cs:141)
UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) (at <24d45e813e524a99bfb7a145158a7980>:0)
Array.IndexOf was not returning a value. I cannot use. IndexOf on a list.
SelectedStatistic = Int32.Parse(CardArray[Array.FindIndex(CardArray2D, row => row.Contains(ChosenCard)), Statistics.FindIndex(row => row.Contains(SelectedStatisticAsString))]);
However, after fixing that, I still got an error. I added a Debug.Log statement and realised that ChosenCard had a value of null, even though I had put this code after the code which assigns the value of ChosenCard. This is because the value of ChosenCard is dependant on the value of SelectedStatistic. I have to temporarily assign the maximum value of HardCompare to SelectedStatistic like I did before.
SelectedStatistic = HardCompare.Max();
After uncommenting the previous code, the system now works perfectly.
With this, Gameplay requirement #5 is complete, the user can select whichever difficulty they want and now they always have a chance to win.
Brett's feedback:
After testing some of the difficulties I can definitely feel that the higher difficulties are much more challenging. The greatest difficulty might even be too much, but to be honest I quite like that. There should be a super-challenging mode for those who want it.
Richard's feedback:
The higher difficulties are a bit too much for me, but I am fine with that, after all, that is what they are for. Maybe I can improve to beat the higher difficulties.
Joshua's feedback:
The difficulties are quite varied. Personally medium is good for me. I think this is a good addition to the game.
Test |
Expected result |
Actual result (Pass/Fail) |
Comments |
Select easy difficulty |
Easy difficulty is applied (random card + random statistic) |
Pass |
|
Select medium difficulty |
Medium difficulty is applied (random card + greatest statistic) |
Pass |
|
Select hard difficulty |
Hard difficulty is applied (greatest statistic of all cards) |
Pass |
|
This achieves UI criteria #2, for selectable user difficulties.
First when entering the gallery, the user will need to be presented with two options, view cards, and view statistics. I'll add two buttons for this.
First, I'll create the view statistics scene. First, I'll create the basic layout with a panel in the middle containing a list of statistics.
I originally planned on using a CSV, but Unity has a built-in method that is more convenient, called PlayerPrefs. Before I create a script for the view statistics scene, I will need to go back through all the other scripts to add code which will save data to PlayerPrefs.
First, I will increment 'Games played' by one every time the user successfully enters a game. I can do this in the Start() function of InitiateRound, as it will only be called once.
PlayerPrefs.SetInt("GamesPlayed", PlayerPrefs.GetInt("GamesPlayed") + 1);
Since there is no function to increment the value, I have to call the value, add one, then save it again. For the games won and games lost, I can add some code to the game end scene.
if (InitiateRound.PlayerScore > InitiateRound.EnemyScore)
{
// In the case that the player won.
gameObject.GetComponent<TextMeshProUGUI>().text = "You won!/nThe final score was " + InitiateRound.PlayerScore + " - " + InitiateRound.EnemyScore;
PlayerPrefs.SetInt("GamesWon", PlayerPrefs.GetInt("GamesWon") + 1);
}
else if (InitiateRound.PlayerScore < InitiateRound.EnemyScore)
{
// In the case that the enemy won.
gameObject.GetComponent<TextMeshProUGUI>().text = "You lost./nThe final score was " + InitiateRound.PlayerScore + " - " + InitiateRound.EnemyScore;
PlayerPrefs.SetInt("GamesLost", PlayerPrefs.GetInt("GamesLost") + 1);
}
else if (InitiateRound.PlayerScore == InitiateRound.EnemyScore)
{
// In the case of a draw.
gameObject.GetComponent<TextMeshProUGUI>().text = "It was a draw!/nThe final score was " + InitiateRound.PlayerScore + " - " + InitiateRound.EnemyScore;
PlayerPrefs.SetInt("GamesDrawn", PlayerPrefs.GetInt("GamesDrawn") + 1);
}
'
I've also added an extra variable for games drawn. The win to loss ratio will be calculated on the loading of the scene. The total cards played can be incremented every time the OnClick() function is called in InitiateRound.cs.
void OnClick()
{
// This button only activates when the enemy is ready, the player has selected a card and a statistic.
if (EnemyStatus.GetComponent<TextMeshProUGUI>().text == "Enemy is ready." & CardToBattleContainer.transform.childCount == 1 & SelStatTextGameObject.GetComponent<TextMeshProUGUI>().text != "Statistic: none")
{
StartCoroutine(MyCoroutine()); // This links to the main method, "MyCoroutine".
PlayerPrefs.SetInt("CardsPlayed", PlayerPrefs.GetInt("CardsPlayed") + 1);
}
}
Now I can create a script to edit the label in the view statistics scene. I will call it ViewStatistics.cs.
gameObject.GetComponent<TextMeshProUGUI>().text = "Games played: " + PlayerPrefs.GetInt("GamesPlayed") + "/n" + "Games won: " + PlayerPrefs.GetInt("GamesWon") + "/n" + "Games lost: " + PlayerPrefs.GetInt("GamesLost") + "/n" + "Win to loss ratio: " + PlayerPrefs.GetInt("GamesWon")/PlayerPrefs.GetInt("GamesLost") + "/n" + "Total cards played: " + PlayerPrefs.GetInt("CardsPlayed") + "/n";
All this code is just on one line. It should be called every time the scene is loaded. I use gameObject here because I will be attaching it directly to the text object. I forgot to put PlayerPrefs.Save() after every line I've added, so I have to go back and do that. I played part of a game, then tried to view the statistics screen, and got this error:
DivideByZeroException: Attempted to divide by zero.
ViewStatistics.Start () (at Assets/03Scripts/ViewStatistics.cs:12)
Of course, since the player has lost no games there is a divide by 0 error. To counteract this, I'll create a small function including a try catch statement.
float CalcWLRatio()
{
try
{
return PlayerPrefs.GetInt("GamesWon")/PlayerPrefs.GetInt("GamesLost");
}
catch (System.Exception)
{
return 0;
throw;
}
}
Now I can use this function. After implementing and testing, I got this result:
The screen seems to be fully functional. I played one more game to see if the statistics would update.
As you can see, it works perfectly. Finally, I want to add a reset statistics button. First, I'll create the button.
Next, I need to create a script. I do not want the player to accidentally clear their statistics, so I will need the button to be pressed twice. First, if the button's has not been pressed yet, I update the text:
if (gameObject.GetChild(0).GetComponent<TextMeshProUGUI>().text == "Reset Statistics")
{
// If the button has not been pressed yet, move it to the next state where it has to be pressed again.
gameObject.GetChild(0).GetComponent<TextMeshProUGUI>().text = "Are you sure?";
}
If it has already been pressed, I will clear all the statistics, and update the label.
else if (gameObject.GetChild(0).GetComponent<TextMeshProUGUI>().text == "Are you sure?")
{
gameObject.GetChild(0).GetComponent<TextMeshProUGUI>().text = "Done."; // Change the button to show "Done.".
// Clear all the statstics.
PlayerPrefs.SetInt("GamesPlayed", 0);
PlayerPrefs.SetInt("GamesWon", 0);
PlayerPrefs.SetInt("GamesLost", 0);
PlayerPrefs.SetInt("CardsPlayed", 0);
PlayerPrefs.Save();
// Update the label.
GameObject.Find("Panel Text").GetComponent<ViewStatistics>().Start();
}
When executing, I got some errors:
I have to use transform.GetChild, and set Start() to be public.
gameObject.transform.GetChild(0)
public void Start()
As you can see, this now works perfectly. Now I can move onto the next section, which is the card view.
In the card view, I want the user to be able to horizontally scroll through every card in the game. Next to each card, there should be statistics showing how many times they've used each card. This data needs to be saved to some sort of file so that the data stays when the game is restarted. First, I'll create a scrolling element for the view cards scene.
I'm going to create a script to populate this scroll view with the cards and text. To make this easier, I'll make a prefab.
This is the prefab and what I want each section to look like. Now I need to create a script with a loop to add these sections.
void Start()
{
SectionPrefab = Resources.Load("gallery_stats_section_prefab")as GameObject;
foreach (string Card in CardLibrary)
{
}
}
Now I need to instantiate the object, put it in the scroll view, and change the image to be of the correct card.
foreach (string Card in CardLibrary)
{
CurrentPrefab = Instantiate(SectionPrefab, new Vector3(0, 0, 0), Quaternion.identity); // Instantiate object.
CurrentPrefab.name = Card + " Section"; // Sets the name.
CurrentPrefab.transform.parent = MainScrollViewContent.transform; // Changes the parent object.
CurrentPrefab.transform.GetChild(0).GetComponent<Image>().sprite = Resources.Load<Sprite>(Card); // Set the image in the prefab to that of the current card in the loop.
}
When executing, nothing happened. I added some Debug.Log statements to try and figure out what was happening. I realised that I had not assigned the script to any object in game, so it was not executing at all. Before executing this new script, I decided to test first without procedurally generated objects, so I commented out all of the code, and added some sections.
The sections are too tightly packed and are clipping inside each other, I need to add spacing.
This looks much better. Now I can remove those objects and use the code in the script. When executing:
While it is successfully changing the image of the card, the layout is different from when I added the sections manually. I tried editing the prefab and changing its anchor to the top left, previously it did not have an anchor.
You can see that it now displays properly. The next step is to collect the statistics using PlayerPrefs. First, I'll do the 'Times put in deck'. This can be done in CheckCount.cs, where the card List is created.
foreach (Transform child in SelectedCards.transform)
{
Deck.Add(child.name); // Adds all the selected cards to deck.
PlayerPrefs.SetInt(child+"InDeck", PlayerPrefs.GetInt(child+"InDeck") + 1);
PlayerPrefs.Save();
}
In my foreach loop, I've made it so each card's variable is <card>InDeck, so dragon would be dragonInDeck. Times played in game can be done in Player.cs.
public void PlaySelectedCard()
{
PlayerPrefs.SetInt(PlayerCardContainer.transform.GetChild(0).name + "TimesPlayed",PlayerPrefs.GetInt(PlayerCardContainer.transform.GetChild(0).name + "TimesPlayed") + 1);
PlayerPrefs.Save();
This saves the variable in the form <card>TimesPlayed, so paladin would be paladinTimesPlayed. Next, I need to do rounds won with or rounds lost with card. This can be done in InitiateRound.cs.
PlayerPrefs.SetInt(CardToBattleContainer.transform.GetChild(0).name + "TimesWon", PlayerPrefs.GetInt(CardToBattleContainer.transform.GetChild(0) + "TimesWon") + 1);
PlayerPrefs.Save(); // Update the number of times won with this card.
Here the variable would be saved like batTimesWon. I did the same for losses:
PlayerPrefs.SetInt(CardToBattleContainer.transform.GetChild(0).name + "TimesWon", PlayerPrefs.GetInt(CardToBattleContainer.transform.GetChild(0) + "TimesWon") + 1);
PlayerPrefs.Save(); // Update the number of times won with this card.
This would be saved like librarianTimesLost. Now I need to actually write these values. In my ViewCardsController's foreach loop, I've put this.
CurrentPrefab.transform.GetChild(1).GetComponent<TextMeshProUGUI>().text = "Times put in deck: " + PlayerPrefs.GetInt(CurrentPrefab.transform.GetChild(0).name) + "InDeck" + "/nTimes played in game: " + PlayerPrefs.GetInt(CurrentPrefab.transform.GetChild(0).name) + "TimesPlayed" + "/nRounds won with card: " + PlayerPrefs.GetInt(CurrentPrefab.transform.GetChild(0).name) + "TimesWon" + "/nTimes lost with card: " + PlayerPrefs.GetInt(CurrentPrefab.transform.GetChild(0).name) + "TimesLost";
Now I can begin testing. I put some cards in my deck and tested. When the round tried to compare two cards, I got this error.
UnityException: Transform child out of bounds
InitiateRound+<MyCoroutine>d__27.MoveNext () (at Assets/03Scripts/InitiateRound.cs:171)
UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) (at <24d45e813e524a99bfb7a145158a7980>:0)
This is because the CardToBattleContainer no longer has the card in it. It has now been moved to Card Display. I changed the variable and tested again. I got this:
PlayerPrefs.GetInt(CurrentPrefab.transform.GetChild(0).name) + "InDeck"
The 'InDeck' needs to be inside of the brackets. After fixing the error, I got this:
The values are zero. This probably means that I am using the wrong player prefs name. I'll put some debug statements to find what player prefs variable this script is trying to reference.
The image does not actually have the name of the card and is just called image. So instead I can use the variable that the loop is using, Card.
CurrentPrefab.transform.GetChild(1).GetComponent<TextMeshProUGUI>().text = "Times put in deck: " + PlayerPrefs.GetInt(Card + "InDeck") + "/nTimes played in game: " + PlayerPrefs.GetInt(Card + "TimesPlayed") + "/nRounds won with card: " + PlayerPrefs.GetInt(Card + "TimesWon") + "/nTimes lost with card: " + PlayerPrefs.GetInt(Card + "TimesLost");
The statistics are now being recorded and saved. With this, my goal of a statistics page is complete. However, I still want to add a button to clear the card statistics. First, I'll create the button.
Now I can create the script which has the same basic structure.
// Execute when the button is clicked.
void OnClick()
{
if (gameObject.transform.GetChild(0).GetComponent<TextMeshProUGUI>().text == "Reset Card Statistics")
{
// If the button has not been pressed yet, move it to the next state where it has to be pressed again.
gameObject.transform.GetChild(0).GetComponent<TextMeshProUGUI>().text = "Are you sure?";
}
else if (gameObject.transform.GetChild(0).GetComponent<TextMeshProUGUI>().text == "Are you sure?")
{
}
}
Now I will have to iterate through every card in the game and clear the statistics for each. First, I'll copy and paste my list of cards.
private List<string> CardLibrary = new List<string>{"bat", "cannoneer", "dragon", "fairy", "goblin", "knight", "librarian", "mage", "paladin", "rat", "rouge", "troll", "wolf"};
Now I can use a foreach loop, and set each statistic to zero, then finally update the scroll view.
else if (gameObject.transform.GetChild(0).GetComponent<TextMeshProUGUI>().text == "Are you sure?")
{
foreach (string Card in CardLibrary)
{
PlayerPrefs.SetInt(Card + "InDeck", 0);
PlayerPrefs.SetInt(Card + "TimesPlayed", 0);
PlayerPrefs.SetInt(Card + "TimesWon", 0);
PlayerPrefs.SetInt(Card + "TimesLost", 0);
PlayerPrefs.Save();
}
foreach (Transform transform in GameObject.Find("Main Scroll View Content").transform)
{
Destroy(transform);
}
GameObject.Find("Main Scroll View").GetComponent<ViewCardsController>().Start();
}
I also have to loop through the scroll view and delete all the sections, otherwise there will be duplicates. Testing:
After clicking on 'Are you sure?' I got 13 duplicates of the same error (one for each section):
Can't remove RectTransform because Image (Script), Image (Script), HorizontalLayoutGroup (Script), HorizontalLayoutGroup (Script), HorizontalLayoutGroup (Script), ContentSizeFitter (Script) depends on it
This is because I have to call transform.gameObject:
Destroy(transform.gameObject);
It now works perfectly.
There is one more small issue. The knight's 'Times put in deck' never incremented, which should have happened since I have clearly played with the card. This is because in CheckCount.cs I was using just child, when I should have been using child.name.
PlayerPrefs.SetInt(child.name + "InDeck", PlayerPrefs.GetInt(child.name + "InDeck") + 1); // Increase the times selected statistic.
The statistics section is now fully functional, and my goal is complete. Now I can move onto final testing.
Test |
Expected result |
Actual result (Pass/Fail) |
Comments |
View gallery |
User is taken to the gallery, where they can choose to either view all card's statistics, or the player's statistics. |
Pass |
|
View card statistics |
User can scroll through and see the statistics of all the cards that they have played. |
Pass |
The user can scroll perfectly. |
Reset statistics |
User is able to reset the statistics of their cards, with a confirmation message. |
Pass |
|
View player statistics |
User can view all of their statistics. |
Pass |
|
Reset player statistics |
Use is able to reset all player statistics, with a confirmation. |
pass |
|
Brett's feedback:
This is really good. I love being able to keep track of everything I've done, and I can reset it too whenever I want, like if I am giving my computer to someone new. I do wish that there were more statistics though.
Joshua's feedback:
This statistics section works quite well. I honestly was not expecting to like it that much when it was suggested, but it is pretty interesting.
Richard's feedback:
I think it is good. I accidentally clicked on the reset statistic button and I was glad to see that there was a confirmation prompt. Otherwise that could have gone quite badly.
First, I will add a button on the main menu to act as the tutorial. This will simply bring up a prompt explaining the game.
I will create a new scene called 'tutorial', and create the background, a back button, and a scroll view.
The tutorial does not have to be procedurally generated, so I can just put in text and images. Here's the text I've input:
Hello, welcome to Card Fantasy. In this game, you will battle against an AI with a deck of your own choosing. You'll battle in multiple environments, and be able to choose the difficulty of you AI. You'll be able to record your adventure in the form of statisitcs to look back on fondly.
First, press the "Play" button on the main menu.
Now you can choose an AI difficulty. Please choose whichever difficulty you please.
Next, you will be greeted with the main playing screen:
This may seem overwhelming, but do not worry! I will explain it all. Let me explain the aim of the game. As you can see at the top, there are 13 rounds. Each round, you will pick a card, and then pick a number on that card. Your card will face off against the enemies, and whoever has chosen the greater number will win. Please note that the current environment can affect your chosen statistic, which can be seen to the right, just below the score counter. Now let's move onto the gameplay.
First of all, you can see your cards in the bottom left. The three bigger cards are the ones that you can select, and the 3 smaller cards are in your queue. When you are finished using a card, it will move to the back of the queue.
Once you have selected a card, you can use one of the five buttons to the right to select the number, or statistic that you want to use. After you have chosen both a card and a statistic, press the "Ready?" button and the round will play out.
The game is a best of 13 format. You can keep track of the score on the right. Your score is in blue, and the enemies score is in red.
Once you have played enough games, you can explore the gallery and view various statistics relating to yourself and all the cards you have played.
That is all. Have fun!
And this is what it looks like in the game:
I've chosen to use a more legible font for easier reading. There is no real coding so the tutorial worked flawlessly first time.
Brett's feedback:
A nice addition. Personally I'm not too fussed since I like to explore the game myself but I can see it being pretty effective for newer players. I think the tutorial has sufficient depth too.
Joshua's feedback:
This looks good and will be very useful for younger and older players. The tutorial is very detailed and the fact that it has images is great.
Richard's feedback:
This is really useful. I would have loved to have read this at the start of the game. But now it is not so useful since I know the ins and outs of the game from testing. I think it is quite detailed and would have gotten useful information.
Test |
Expected result |
Actual result (Pass/Fail) |
Comments |
Scroll test |
The user can scroll through the tutorial |
Pass |
|
With this, UI criteria #5 is complete.
Many remnants of development still exist in the game. I did the following to clean up:
1. Deleted all Debug.Log statements.
2. Removed access to the custom console.
a. This achieves gameplay criteria #6, it is impossible for the user to cheat.
3. Reset all the statistics.
4. Removed useless / non-functional code.
5. Changed the waiting time for all the difficulties.
I renamed the game on the title screen from 'Placeholder' to 'Card Fantasy'.
What has been done?
1. The main menu was created.
2. Different difficulties for the AI were created (Easy, medium, and hard)
3. The game allows the user to select their own custom deck.
4. The user can play the main game for 13 rounds, with the environment system implemented, being able to select their own card.
5. The user can view their own statistics, such as the number of times they've played a card, or how many wins and losses they have.
6. A tutorial has been added for new players to learn how the game works.
This testing was done for the final compiled version of the game for Windows 10.
Firstly, I will check for any typos.
Statistics is spelled incorrectly.
Next I'll go through the game and check that every button works. That is, it is clickable and
Button |
Working? |
Play |
Yes |
Gallery |
Yes |
Tutorial |
Yes |
Quit |
Yes |
Card Statistics |
Yes |
Player statistics |
Yes |
Reset player statistics |
Yes |
Reset card statistics |
Yes |
Select deck |
Yes |
Ready (in game) |
Yes |
Statistics (in game) |
Yes |
Back to main menu (end game) |
Yes |
All back buttons |
Yes |
Next, I'm going to be using the test data and methods that I described earlier (control + click) to test my game.
Test No. |
Evidence |
Explanation |
Pass / Fail |
1 |
Testing if all the buttons on the main menu have the expected result (move to the related scene or close the game)
Each button was pressed individually:
Pressing Play -> Card Selection Scene was loaded. Pressing Gallery -> Gallery Scene was loaded. Pressing Tutorial -> Tutorial was loaded. Pressing Quit -> Game is closed. |
All buttons pass |
|
2 |
Testing if the buttons react when they are not clicked on.
I clicked next to, in between, and far away from the buttons.
There was no reaction in all cases. |
Pass |
|
3 |
Testing if the statistics are kept when the game is restarted, and if they are displayed correctly. |
Pass |
|
4 |
Testing if the data can be deleted.
When pressing the delete button, the data is cleared, and it does not return. |
Pass |
|
5 |
Testing if cards can be moved to the deck. |
Pass |
|
6 |
Testing if cards can be moved back to the card library. |
Pass |
|
7 |
Normal test ' testing if the game continues if exactly 6 cards are selected.
The game should proceed to the next scene. |
Pass |
|
8 |
Erroneous test ' testing what happens if the user selects less than 6 cards. The game should display an error message and should not proceed to the next scene. |
Pass |
|
9 |
Erroneous test ' testing when the user selects more than 6 cards. The game should display an error and not proceed. |
Pass |
|
10 |
All buttons should proceed to the main game and set the appropriate difficulty. |
Pass |
|
11 |
User can select a card. |
Pass |
|
12 |
User can unselect a card |
Pass |
|
13 |
User cannot start the round if the enemy is not ready |
Pass |
|
14 |
User can only start the round when a statistic is selected |
Pass |
|
15 |
If the enemy's statistic is greater, the enemy's score is incremented by one |
Pass |
|
16 |
If the player's statistic is greater, the player's statistic is incremented by one. |
Pass |
|
17 |
If both players have the same statistic, then no points should be awarded. |
Pass |
|
18 |
Every time a round ends, the round counter should increment by one. |
Pass |
Everything works so no corrective action needs to be taken.
Usability testing
Next, I'm going to be comparing against the usability features that I identified in Design > Usability features.
'
Test no. |
Evidence |
Explanation |
Pass / Fail |
1 |
Large buttons: |
The buttons must be large with a clean, readable font. The game should not be made in the command line. |
Pass |
2 |
Dark background on buttons to separate from the image background. Grid layout to avoid confusion. Similarly sized buttons and spacing. |
The game should be as neat as possible. |
Pass |
3 |
There should be a tutorial available in the game so people know what they are doing. |
Pass |
|
4 |
The game was tested on a low-end intel i3-9100F CPU. The game ran successfully without any lag. |
The game should be able to run on low-end hardware. |
|
There are no unmet usability features, so no corrective action needs to be taken.
I also tested the data function; I played the game a bit to allow some statistics to build up and then restarted the game. The data stayed, meaning it is non-volatile as I wished. There exist no text or value inputs, apart from the custom console that I created earlier, but that is disabled and not available for use for the end user. Thus, there is no boundary testing or erroneous testing to do.
Then I tested the final flow of the game. I played the game with all difficulties, and I played a game the full way through. There were no errors, bugs, or crashes.
I also asked my stakeholders if they found any bugs, typos, or issues with robustness. I gave them a list of things to test:
1. Play a game in full.
2. Select your own deck, including too many, too little, and the correct number of cards (6).
3. Go through the UI at random, selecting buttons quickly with the intent of trying to stall the system.
4. Check if all the buttons you press have the desired result.
5. Validate that scores and statistics are what you expect.
I did not find any errors.
Joshua's response
I didn't see anything wrong, the game worked perfectly.
Richard's response
The game never crashed, and I did not see anything wrong.
Thus, I conclude that the final program is robust, with no errors or bugs, in my scope of testing.
With this, the final testing is complete, and I can compile the program to provide to my end users for the stakeholder feedback. I will send them the final compiled project, along with the post-development questionnaire that I created earlier.
Brett's response
Q: How satisfied are you with the clarity, ease of use, and fluidity of the user interface?
A: I think it's pretty good. All the buttons are reactive when you hover over them, and provide plenty of visual feedback. The buttons are all clear in what they do and legible. I found it easy to navigate wherever I wanted, like the gameplay or the tutorial.
Q: How satisfied are you with the gameplay?
A: I think it's fine. Personally I think the games are a bit too long, but I do like that I can choose my own deck. The difficulty settings are good too, I like a bit of a challenge.
Q: How satisfied are you with the unique mechanics of the game?
A: I think the environments system is good. I have seen something like this before, but not in a card game, so I would say it is unique. It also has a significant impact on the flow of the game.
Q: How satisfied are you with the variety of cards?
A: I would have liked to see more cards but the current selection is ok. I also think it would have been cool if there were some environments which affected multiple card statistics at once in the higher difficulties.
Q: Is there anything else that you would like to say?
A: I think a different font could have been chosen for the game. In the current font, all the letters are capitalised and it's sort of difficult to read.
Brett liked the overall gameplay and the fluidity of the UI, but he had an issue with the font as it was difficult to read.
Brett would have liked to see more cards which makes sense as I was unable to meet my goal of 13 cards. Furthermore, Brett had an idea of an environment modifying multiple statistics. This would have been interesting to implement but due to time constraints I cannot.
Joshua's response
Q: How satisfied are you with the clarity, ease of use, and fluidity of the user interface?
A: I think the interface is very clear and reactive. It looks great and I think even less experienced users will be able to navigate it easily.
Q: How satisfied are you with the gameplay?
A: The games are a bit too long for me, but I think the overall gameplay is good, and meets my expectations.
Q: How satisfied are you with the unique mechanics of the game?
A: The environments system is ok, I think it adds some depth but there could have been more environments, such as some that alter multiple statistics at the same time.
Q: How satisfied are you with the variety of cards?
A: I think the number of cards is on the low side, I would have liked to see more. I also think that the design for the cards was bland, they could have had a background or something to make them look more interesting.
Q: Is there anything else that you would like to say?
A: I would have liked to see a multiplayer system.
Brett also found the UI very clear, but had an issue with the length of the games. He was also unsatisfied with the appearance of the cards, and he thought that they could have been designed better. He also wishes for a multiplayer mode. Due to time constraints, and coding and designing that is potentially out of the scope of my skill, these cannot be added.
Richard's response
Q: How satisfied are you with the clarity, ease of use, and fluidity of the user interface?
A: I think it is good, I was able to navigate ok though the interface. But maybe there could have been sounds when clicking a button or the card.
Q: How satisfied are you with the gameplay?
A: I think it is good. The games are engaging and have strategic depth. But I would have liked to add music to the game.
Q: How satisfied are you with the unique mechanics of the game?
A: I really like the environments system. It adds a good layer of extra thinking to the game.
Q: How satisfied are you with the variety of cards?
A: I think it is ok, but I would have liked to see more, the game can get a bit boring with just a few cards.
Q: Is there anything else that you would like to say?
A: I think it would be nice if I could save my own deck, so that I can use it again in the future without having to select all my cards again.
Richard was unsatisfied with the UI and thought that there could have been audio every time a button was clicked. He also wanted to see music during the game. He also had an idea for a system where you can save a deck so you do not need to select it multiple times. Richards requests unfortunately cannot be added due to time constraints, and limits on my musical ability.
Analysis
Most of my stakeholders said that the game's UI was easy to navigate, which means I've successfully achieved my goal of an accessible game that is easy to navigate. My stakeholders said that they were mostly satisfied with the gameplay, but Joshua said he would have liked to see more environments, and Brett said the games are too long. For future development, maybe I could add other game modes, like one with shorter time or one where environments have a greater impact.
All my stakeholders said that the number of cards was lacking. In future development I could come up with more card ideas. Since my game is procedural, it should be easy to maintain it in the future.
Stakeholders also had some ideas for game features:
1. Richard: Deck saving feature.
2. Joshua: multiplayer mode.
3. Brett: More complex environments.
These could not be added due to time constraints.
Checking against success criteria
I will now check the game against all the success criteria that I developed at the start of the development. I have added hyperlinks in this section so you can press Control + Click to go back to the relevant area.
Yes = Criteria has been fully met.
Partially = Criteria has been partially met.
No = Criteria has not been met.
Gameplay #1 ' Yes, the user can make a fully custom deck. The user also can scroll through all the cards to see cards that do not fit on the screen. If the user no longer wants to use a card, they can move it back. There is an error that does not crash the game if the user selects too many or too few cards. This can be seen in Development > Card Selection Screen.
Gameplay #2 ' Yes, the environment system has fully been added and affects gameplay. Each environment has a chance to be selected, and each is unique. The environments affect the statistics by the correct amount. This can be seen in Development > Play Screen.
Gameplay #3 ' Yes, each card has its own unique statistics. There are no duplicate statistics, and each one has its own unique identity. This can be seen in Design > Artistic Planning.
Gameplay #4 ' No, only 13 cards are present in the final build. I wanted to add at least 20 cards. This can be seen in Design > Artistic Planning.
Gameplay #5 ' Yes, the user is always given a chance to win. There is no situation where it is impossible for the user to win. This can be seen when the stakeholders reported no errors in Evaluation > Final Testing.
Gameplay #6 ' Yes, it is impossible for the user to cheat. The console has been disabled in the final build. Without complex external tools, the game can only proceed as expected. The disabled console can be seen in Development > Final clean-up, and the game has been in such a way throughout that it is impossible to cheat, which can be seen throughout all of Development.
Gameplay #7 ' Yes, the game is in a best of 13 format. This can be seen in Development > Play Screen.
UI #1 ' Yes, all stakeholders said that the buttons are legible and easy to navigate with. The buttons increase in size when hovered over, providing visual feedback. The UI is mostly linear in structure, with back buttons at almost all points (deliberately omitted from the actual gameplay) to go back and amend previous choices. This can be seen in Evaluation > Questionnaire.
UI #2 ' Yes, all buttons and panels are clearly labelled. This can be seen in all of Development.
UI #3 ' Yes, the current environment is shown as the background. It provides a refreshing sense every round. This can be seen in Development > Play Screen.
UI #4 ' Yes, there is a prompt at the end of each round showing who won, and a screen at the end of the game showing who won. This can be seen in Development > Play Screen.
UI #5 ' Yes, there is a clear tutorial that Is easily accessible on the main menu. Stakeholders agreed that it had a sufficient amount and depth of information. This can be seen in Development > Tutorial.
UI #6 ' Yes, the current round is displayed at the top of the screen. This can be seen in Development > Play Screen.
User experience #1 ' Partially, the game has some similarities to 'Top Trumps'.
User experience #2 ' Yes, there are 3 difficulty settings: easy, medium, and hard. This can be seen in Development > Select Difficulty.
User experience #3 ' Yes, the game comes as a .exe file that is easy to run.
User experience #4 ' Yes, no bugs are present in the final build. This can be seen in Evaluation > Final Testing.
Extra development
Furthermore, I was able to fully implement the statistics menu, with users being able to view all statistics relating to their cards and themselves. These values are saved upon a game restart. Users were also able to delete their statistics.
In terms of further development, I did not add, I would have liked to add the stakeholder's requests:
One is a system to save decks. The user would be able to save a deck to a pre-set to quickly load whenever they start a game. The user would be able to save multiple decks, perhaps with a limit of 5. This would be stored in memory for a game restart.
I would also like to add music to my game, but this may be outside of my scope. I cannot create my own music, and I cannot take any music from online. The Copyright, Design and Patents Act 1988 means that I would have to find royalty free music and then credit that author. I would also like to add sound effects, whenever a round ends, or a button is clicked. This would need a similar procedure.
Furthermore, I would also like to add more complex environments. These could modify multiple statistics' at the same time, maybe a sky environment that boosts mana but decreases speed. However the system I have in place only allows for one statistic to be modified, so I would need to overhaul the system.
Finally, I would like to add better card designs. They are quite plain and do not really fit in with the game. I could add a background to the cards, with a better font and art.
Software support
The final code was compiled for Windows 10 / Windows 11. Due to the nature of Unity, I was also able to compile it for Android. This was not the target platform, but I compiled it for Android anyway to see how it would run. Hence, I tested on three systems:
1. A 64-bit Intel i3-9100F CPU, which is a low-end desktop CPU, running Windows 10. The game worked without any errors.
2. A 64-bit AMD Ryzen 7 5800H CPU, which is a mid-high-end laptop CPU, running Windows 10. The game worked without any errors.
3. A Xiaomi Pocophone F3, with a Qualcomm Snapdragon 870 running Android 12. It is a mid-range mobile CPU. The game lacked smoothness (ran at a low fps), and due to the unusual aspect ratio, there were many instances of buttons clipping outside of the screen or into each other. Furthermore, the 'Ready?' button in the main game lacked a proper hitbox. It had to be tapped on the bottom right edge for it to register a click. To run on Android, the game needs many optimisations.
Proof
Ability to select a custom deck. All the cards can be clicked and moved to the lower section. They can also be moved back to the top. The game only proceeds with 6 cards selected and will not crash otherwise. All the buttons provide visual feedback.
Tutorial, the user can scroll down to the bottom to see more information and screenshots.
Difficulty selection, each difficulty setting changes the difficulty of the AI significantly.
Gameplay with environments system. The statistic you choose can be influenced by the environment. There is a score counter, a round counter, and no crashes occur.
Screen to clearly display who won:
Gallery and statistics:
Limitations
Limitation 1
One of the biggest limitations is a lack of a multiplayer mode. Because of this, players may get bored quickly, as they do not have the enjoyment of playing with their friends or other humans.
The solution would have been to add a multiplayer mode, with either full internet access or LAN access.
Limitation 2
Another limitation is the lack of music. The game could have been better with sound effects, like when you click buttons or during the gameplay.
To solve this, I could have taken some royalty free music and implemented it to the game or made my own music and sound effects.
Limitation 3
Furthermore, I was not able to add all the cards that I wanted to. I wanted to add at least 20, but I was only able to add 13.
The solution would have been to simply add more cards.
Limitation 4
In my coded solution, I think a better approach would have been to define each card as its own object, instead of using a 2D array. This could have allowed for the use of encapsulation techniques.
The solution would have been to create a card class, then create an object for each card. This class would have attributes like the strength, speed and name, and functions to return multiplied statistics.
Maintenance
Commented Code
All functions, variables, and code are fully annotated. This should aid future development in the future in case I decide to re visit this project, or I give the source code to someone else. The file, variable and functions are all also appropriately named so they should be able to find their way around easily. Using this documentation, it should be easy for anyone to expand my game.
Descriptive variable names
I've chosen sensible variable names that are self-explanatory, for easier maintenance in the future. Most variables for GameObject match the name of the GameObject in the Unity editor hierarchy tree. This should reduce confusion and maybe allow people to collaborate on the project together.
Updates
As a distributed game, I could add updates in the future to address my limitations. To stop users getting bored, I could periodically add new cards and features. The code is mostly modular and procedural, so adding new code in the future as it is all annotated should not be an issue.
However, since the program has no built in update functionality, distributing updates will be difficult. I will have to make an announcement so that all users of the program can download the newer version.
Final code
Here, I am pasting the final code for my game. I've attached an OpenDocument text to make the document look cleaner. Please double click the code to view it in full.
BackgroundController.cs
CheckCount.cs
CopyCards.cs
CustomConsole.cs
EndMessage.cs
Enemy.cs
GenerateCards.cs
InitiateRound.cs
Player.cs
QuitGame.cs
ReParent.cs
ResetCardStatistics.cs
ResetStatistics.cs
SceneLoader.cs
SelectBattleCard.cs
SelectDifficulty.cs
SendStat.cs
ViewCardsController.cs
ViewStatistics.cs