Now that we can move around the board, we should start adding the activities related to that journey. The first thing I want to include is the random encounter. This is where a player encounters a wild Pokemon, which can be potentially captured and used as an ally. In this first part, we will simply handle the systems necessary for spawning them and displaying a new screen when they appear.
Spawn Pokemon
If you examine the tiles on the game board, you may notice that many of the tiles include a component called “SpawnPokemon”. There is nothing in the class at all, it is simply used as a sort of marker on the tile so that the relevent system can know which tiles are good for spawning and which ones shouldn’t allow a spawn. In this project, I structured it so that there is only one “special” type of component per tile because it felt better to me in playtesting. So for example, if you have a gym or a pokestop, you wont also see any Pokemon appear. However, it is totally possible to support multiple special events on the same tile if you wanted.
If you wanted a challenge to try on your own, it would be relatively simple to add a lot more complexity to the way spawning is handled. For example, in my demo project there is an equal chance of spawning Pokemon on any tile that includes this component. Instead of using a constant value, you could give each tile its own spawn chance, so some tiles such as a forest might have a higher probability of spawning something than a volcano tile would. You could choose to go even deeper, and add some sort of marker like a terrain type enum to each tile and have the system use that to determine what “kinds” of pokemon would appear. Perhaps a Bulbasaur would only appear on forest tiles, etc.
Random Encounter View Controller
Let’s begin by fleshing out the view controller code for the screen that displays the new wild Pokemon. If you followed along and provided the relevant assets it should look something like this now:
This screen provides another branch in our flow. When you encounter something, you can choose to try to capture it, or ignore it. This means we will provide another “Exits” enum that we will pass along with the “didFinish” action as we have done a couple of times now. I created a new version of the “Show” method which takes some new parameters before calling the base “Show” method. This allows me to pass it whichever Pokemon I have spawned so that it can be presented to the player.
public class RandomEncounterViewController : BaseViewController { public enum Exits { Capture, Ignore } public Action<Exits> didFinish; [SerializeField] Text titleLabel; [SerializeField] Image avatar; [SerializeField] Text cpLabel; public void Show (Pokemon pokemon, Action didShow = null) { avatar.sprite = pokemon.GetAvatar(); titleLabel.text = string.Format("A {0} has appeared!", pokemon.Entity.label); cpLabel.text = string.Format("CP: {0}", pokemon.CP); Show (didShow); } public void CaptureButtonPressed () { if (didFinish != null) didFinish(Exits.Ignore); // Exits.Capture } public void IgnoreButtonPressed () { if (didFinish != null) didFinish(Exits.Ignore); } }
Random Encounter System
The Pokemon in this game should not all have an equal chance of appearing. This is especially important in the early game, because as a beginner low-level player, you wouldn’t stand much of a chance against a top-tier spawn. You need to learn on low-tier fodder first. As a side bonus, this causes many of the Pokemon to be rare which makes them feel much more special when you actually see them.
The way that this challenge was approached was to give Pokemon the “Encounterable” component (assuming they could be encountered), where each one has its own “rate” for appearing. Note that this is a strength of the ECS approach to design, because I can load all of the relevant data for all of the Pokemon at once, but I only need to load the data that is relevant for the system to function. If I had designed this with a normal OOP oriented approach, I might have had all of the data of a Pokemon in a single large data structure, and that would have made memory requirements for working with all of them much more costly.
So, how to you pick something randomly using the weighted “rate” value? First all of the values are summed together to form a sort of large wheel. Perhaps you could imagine a pie chart with varying sizes of slices. If you had an arrow pointing to the side of the wheel and then spun the wheel, you can probably imagine that the sections of the wheel that had the largest slice would be most likely to be pointed to when the wheel stops spinning. So, the higher the rate the more likely the appearance. This is basically what happens in the code too. I do this by using a random number from 0 to the sum value, and then loop over each of the Pokemon until the value falls within the matching range.
Before I “spin the wheel” so to speak, there are a couple more checks that must happen. First, we need to verify that the tile where the player is located actually supports this feature. If we dont find the “SpawnPokemon” component at the current board tile location, then we can abort early. Next, even when we do see that component, I don’t want to always succeed in encountering a Pokemon, otherwise it could get a bit tiresome. Therefore I added a separate “spawnRate” to the system itself. I roll another random number and require the result to be lower than this value, or we will simply decide not to spawn anything at all.
When all of the checks pass so that we do spin the wheel and are ready to spawn something, I will then examine the level of the buddy Pokemon for the current player. The level of the spawned Pokemon will be created at a level at or below the level of the buddy so that the player has a greater chance of being able to defeat it in battle. This helps the game feel a lot more fun.
public static class RandomEncounterSystem { const float spawnRate = 0.2f; static List<Encounterable> Table { get { if (_table == null) { var all = DataController.instance.pokemonDatabase.connection.Table<Encounterable>(); _table = new List<Encounterable>(all); foreach (Encounterable item in all) wheel += item.rate; } return _table; } } static List<Encounterable> _table; static double wheel; public static Pokemon Apply (Player player, Board board) { var tileIndex = player.tileIndex; var tile = board.tiles[tileIndex]; SpawnPokemon component = tile.GetComponent<SpawnPokemon>(); if (component == null || Random.value > spawnRate) return null; Encounterable data = Table.First(); double randomValue = Random.value * wheel; double sum = 0; foreach (Encounterable item in Table) { sum += item.rate; if (sum >= randomValue) { data = item; break; } } Entity entity = data.GetEntity(); var level = UnityEngine.Random.Range(0, player.pokemon[0].level); var pokemon = PokemonFactory.Create(entity, level); return pokemon; } }
Random Encounter State
So we have a system to spawn Pokemon, and a screen that displays the one we spawned. Hopefully by now you also would have guessed that we will use a state in our flow controller to tie it all together. We will use the “RandomEncounterState” to handle this.
When this state is entered, it will request a Pokemon from the RandomEncounterSystem which we just implemented. If the system doesn’t return a Pokemon then we simply skip ahead to the next state in the sequence, or we will in the future, but for now we will just skip ahead to the “CheckDestinationState”.
If the system does return a Pokemon then we will show it to the player in the new screen we created. We will use a switch statement to handle the branching flow that can result from a users input, but for now we will just use the “CheckDestinationState” regardless of which button they press. Battle is a pretty involved topic that deserves its own lesson, but we will implement it soon.
public partial class FlowController : MonoBehaviour { State RandomEncounterState { get { if (_randomEncounterState == null) _randomEncounterState = new State(OnEnterRandomEncounterState, null, "RandomEncounter"); return _randomEncounterState; } } State _randomEncounterState; void OnEnterRandomEncounterState () { Pokemon pokemon = RandomEncounterSystem.Apply(game.CurrentPlayer, board); if (pokemon == null) { stateMachine.ChangeState (CheckDestinationState); // TODO: GymState return; } randomEncounterViewController.Show (pokemon, delegate { randomEncounterViewController.didFinish = delegate(RandomEncounterViewController.Exits obj) { randomEncounterViewController.didFinish = null; randomEncounterViewController.Hide (delegate { switch (obj) { case RandomEncounterViewController.Exits.Capture: // TODO: create a battle stateMachine.ChangeState (CheckDestinationState); // TODO: StartEncounterBattleState break; case RandomEncounterViewController.Exits.Ignore: stateMachine.ChangeState (CheckDestinationState); // TODO: GymState break; } }); }; }); } }
Move State
Now we need a way to trigger the encounter state. We will use the Move State for this, and instead of having it exit to the “CheckDestinationState”, we will now have it exit to the “RandomEncounterState” instead.
stateMachine.ChangeState (RandomEncounterState); // TODO: PokeCenterState
Flow Controller
One last change is needed to make sure the board can compile. Add the following property to the Flow Controller:
Board board { get { return dataController.board; } }
I did this so that I could easily pass the board to the systems that need it. Note that I could also just as easily have had the system grab the board itself since the board is easily obtained using the singleton data controller. The reason I chose to do it this way is because it is slightly more flexible if the systems dont acquire their own object references. For example if I wanted an A.I. system, I might evaluate possible outcomes by using a copy of the game and want to perform system actions on the copy instead of the original. I dont actually need anything like A.I in this project, so this may feel a bit unnecessary. Feel free to use whichever approach feels best to you.
Demo
Run the “Game” scene. After rolling and beginning a “journey” (especially if you roll a high number) you will probably see some random encounters. Most of the things you see will probably be what you would expect like a Pidgey or Rattata, but if you are lucky you will also find something rare and exciting. Enjoy!
Summary
With the foundation we have already put in place, we are now able to simply add the game features. We added a new system, screen and state for our flow. It is simple to insert or even remove features as we like in this way and can grow the complexity of our game one small step at a time.
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.