You can catch, train, and manage a solid team of Pokemon, yet asside from the joy of the journey itself we are still lacking any real goal for the game. Much like the cartoon, the goal here is to earn gym badges. The first player to defeat all four gyms is the winner! In this lesson we will lay the ground work for this by creating and updating the models, factories, and systems necessary to support it.
Gym
Let’s start with the model for a gym. It’s pretty simple really, all we need is a “type” for the badge and a list that holds the actual pokemon you will have to defeat in a battle to earn the badge.
public class Gym { public string type; public List<Pokemon> pokemon = new List<Pokemon>(); }
Gym Factory
You might have guessed that we will use a factory to handle the creation of our gym objects. Our game will use four gyms in total, so the main factory method, “CreateGyms” will return a list of gyms. The count and strength of the gyms will both be determined by passing in an array of ints – the number of gyms will be the same as the length of the list, and the strength of the gym will be determined by the values within the array, each representing the level of the Pokemon to populate it with.
We begin by grabbing a list of all the potential “types” in the database. For each type that we use as a candidate, we will expect there to be a minimum “poolSize” of types of Pokemon that we can look at. The general idea is that the more Pokemon we grab in a pool, the likelier it is we will be able to get something strong, and as we go up through the gyms, we want stronger and stronger opponents. I set the initial poolSize to ‘3’ which is only one higher than the minimum number of Pokemon that a gym will hold.
Next we enter a “while” loop that will continue until one of two conditions is met: either we will have created the requested number of gyms, or we will have run out of candidate gym types – while play balancing (tweaking the number of gyms etc) you should make sure to check for this potential outcome and add error handling if necessary.
Within the loop, we pick a type at random, then remove the type from the list of types so we wont need to consider it again. We will then fetch all of the SpeciesStats that match that type. If there aren’t enough to pass our poolSize requirement then we will continue to the next loop. Otherwise, we will create a new gym with what we’ve found and add it to the return value list. We will also increment the poolSize requirement so that subsequent gyms are hopefully a little stronger.
Once we have completed the “while” loop I will sort the gyms based on which gym’s Pokemon have the highest max CP stat.
Finally, I will loop over each gym with an inner loop over each Pokemon, assigning the Pokemon the level that was passed along in the levels array parameter.
public static List<Gym> CreateGyms (int[] levels) { int count = levels.Length; var connection = DataController.instance.pokemonDatabase.connection; var types = new List<ECS.Type>(connection.Table<ECS.Type>()); int poolSize = 3; var retValue = new List<Gym>(count); while (retValue.Count < count && types.Count > 0) { int random = UnityEngine.Random.Range(0, types.Count); var type = types[random]; types.RemoveAt(random); var stats = type.StatsWithType(); if (stats.Count < poolSize) continue; var gym = CreateGym(type, stats, poolSize); retValue.Add(gym); poolSize++; } retValue.Sort(SortAscendingByMaxCP); for (int i = 0; i < retValue.Count; ++i) { foreach (Pokemon pokemon in retValue[i].pokemon) { pokemon.SetLevel(levels[i]); } } return retValue; }
To create just a single gym, I created the “CreateGym” method which expects the Type of the gym, a list of SpeciesStats, and a number which indicates the count of candidates to pull from that list.
We begin by creating a list to hold candidates from the list of stats. It is created with a capacity already at the poolSize requirement. Then I use a “for” loop to grab one of the SpeciesStats at random, and insert it into my candidate list. I also remove it from the stat list so that I won’t risk having any duplicates.
When the loop is complete I sort the candidates by their MaxCP stat – this time the sort is in descending order so that the first two will be the strongest. I create they gym, set the type, then add its team. There is a lot going on with the statement where I populate the gyms list of pokemon. Using a candidate (SpeciesStat) I am able to get an Entity from the database. The Entity is passed to the PokemonFactory’s create method which returns a fully configured Pokemon instance, and the instance is added to the gyms Pokemon list. This could have been done in a lot of lines for better readability, but it wasn’t so long that I felt it mattered.
static Gym CreateGym (Type type, List<SpeciesStats> stats, int poolSize) { List<SpeciesStats> candidates = new List<SpeciesStats>(poolSize); for (int i = 0; i < poolSize; ++i) { int random = UnityEngine.Random.Range(0, stats.Count); candidates.Add(stats[random]); stats.RemoveAt(random); } candidates.Sort(SortDescendingByMaxCP); Gym gym = new Gym (); gym.type = type.name; gym.pokemon.Add( PokemonFactory.Create(candidates[1].GetEntity()) ); gym.pokemon.Add( PokemonFactory.Create(candidates[0].GetEntity()) ); return gym; }
We used two methods to sort lists, one for sorting a list of species stats and one for sorting a list of gyms:
static int SortDescendingByMaxCP (SpeciesStats x, SpeciesStats y) { return y.maxCP.CompareTo(x.maxCP); } static int SortAscendingByMaxCP (Gym x, Gym y) { int xMax = Mathf.Max(x.pokemon[0].Stats.maxCP, x.pokemon[1].Stats.maxCP); int yMax = Mathf.Max(y.pokemon[0].Stats.maxCP, y.pokemon[1].Stats.maxCP); return xMax.CompareTo(yMax); }
Gym System
We will need a system to work with Gyms. First, we need a way to “GetCurrentGym” which will return a gym based on the player’s location, or null if there is not one available. It works by getting the tile that the player is currently located on, and then attempting a “GetComponent” for the “GymSite” component type. If that component isn’t found then we return null, otherwise we can use the component’s “index” field to grab a gym from the game’s list of gyms.
public static Gym GetCurrentGym (Game game, Board board) { int tileIndex = game.CurrentPlayer.tileIndex; GymSite site = board.tiles[tileIndex].GetComponent<GymSite>(); if (site == null) return null; Gym gym = game.gyms[site.index]; return gym; }
Every time I battle with a gym, I want the same challenging experience. If I almost beat it, then heal my team and battle again, but the gym’s Pokemon weren’t also healed, then the second battle will be no challenge. Even worse would be if one player whittled it down, and the second player to come along was able to finish it off without any real effort. To solve for this, I created a “Reset” method to fully heal all of the Pokemon for a specified gym, and to reset the Pokemon’s energy levels – wouldn’t want to start a match against a really powerful charge attack!
public static void Reset (Gym gym) { foreach (Pokemon pokemon in gym.pokemon) { HealSystem.FullHeal(pokemon); pokemon.energy = 0; } }
I want to be able to obtain a Sprite to represent the badge and Pokemon types of a gym. I overloaded a “GetBadge” method for this so that I could get it from a gym instance directly, or from a string representing the name of a type.
public static Sprite GetBadge (this Gym gym) { return GetBadge(gym.type); } public static Sprite GetBadge (string type) { string fileName = string.Format("Types/Badge{0}", type); Sprite sprite = Resources.Load<Sprite>(fileName); return sprite; }
Assuming you actually win a gym battle, I will need a method to award the gym’s badge to a player. That looks like this:
public static void AwardBadge (Player player, Gym gym) { player.badges.Add (gym.type); }
Finally, I want some rules around whether or not a player should be able to challenge a gym or not. Merely standing on a gym site isn’t enough. The rules are that you can’t challenge a gym if you already have its badge, and you must meet a minimum team size requirement, where only non KO’d pokemon count.
public static bool CanChallenge (Player player, Gym gym) { if (player.badges.Contains (gym.type)) { return false; } return HasTeamSize (player, gym.pokemon.Count); } static bool HasTeamSize (Player player, int requiredSize) { int teamSize = 0; foreach (Pokemon p in player.pokemon) { if (p.hitPoints > 0) teamSize++; } return teamSize >= requiredSize; }
Game
The “Game” model will hold the list of gyms that are created by our factory. Add this field:
public List<Gym> gyms;
Game Factory
Now we can also update the Game Factory class to use our new Gym Factory and add gyms to the game. In the “Create” with “playerCount” method, add the following two lines just before the “return” statement:
var levels = new int[4] { 5, 10, 15, 20 }; game.gyms = GymFactory.CreateGyms (levels);
Next, for the “Create” with “json” method, add the following before the “return” statement:
foreach (Gym gym in game.gyms) { foreach (Pokemon pokemon in gym.pokemon) { pokemon.Restore (); } }
Game System
Because the game now has a reference to the gyms, we can also add a method to the Game System that indicates whether or not a game has been won – if the current player has as many badges as there are gyms, then they are the winner!
public static bool IsGameOver (Game game) { return game.CurrentPlayer.badges.Count == game.gyms.Count; }
Gym Site
I have already created the game board in this project. If you examine its prefab you will find that some of the tiles have a component called “GymSite” attached. The presence of this component will allow the various systems to know where Gyms are located. We did something similar for the “Pokestop” and “PokeCenter”, although in those cases an empty script was sufficient since all we needed was a location indicator. In this case, each gym is different – you will face a different pair of stronger Pokemon at each site. Also each gym will have a different badge indicating the kind of Pokemon you will face.
In order to help our systems differentiate one gym site from another, I added an int field called “index” which relates to the index of a gym in a list of gyms which we will make later. To help a player visually determine the difference I also added a method that lets you “Refresh” with an actual “Gym” object, from which we can get the sprite that represents its type.
public class GymSite : MonoBehaviour { public int index; [SerializeField] Transform display; public void Refresh (Gym gym) { SpriteRenderer sr = display.GetComponentInChildren<SpriteRenderer>(); sr.sprite = gym.GetBadge(); } }
Game View Controller
We will need some way to trigger the “Refresh” of the “GymSite” component. We can handle this at the same time as we call “LoadGame” in the “GameViewController”. I left a comment in that method which you can replace with:
LoadGyms ();
…and then add the method itself like this:
void LoadGyms () { var gymSites = board.GetComponentsInChildren<GymSite> (); for (int i = 0; i < gymSites.Length; ++i) { var gymSite = gymSites [i]; var gym = game.gyms[gymSite.index]; gymSite.Refresh(gym); } }
We are able to easily grab all the GymSite components from the game board even though they are on different game objects thanks to a hierarichal search with “GetComponentsInChildren”. Then it is trivial to loop over them all, grab its relevant gym based on index, and call “Refresh” with the gym that was referenced.
Demo
For the most part, all we have done is set up a foundation that we will use soon when we implement the gym battles. However, there are a few new things you can see already. If you start a new game, you will be able to see the list of Gyms in the inspector for the Flow Controller. Remember that it will be found under the Data Controller then Game.
Another update is that until now the gym tiles always displayed the “Bug” badge as a default placeholder. Now, you should see each one update to reflect the type of the gym that is actually located there.
Summary
In this lesson we laid the foundation we will need to support gym battles. We have the models, factories, and systems necessary to support gym related actions. We also updated a component and view controller so that our gyms appear on the board itself.
Don’t forget that there is a repository for this project located here. Also, please remember that this repository is using placeholder (empty) assets so attempting to run the game from here is pretty pointless – you will need to follow along with all of the previous lessons first.
If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!