With our battle setup complete, now it is time to implement it with various states and display it on various screens. In this lesson we will finish implementing the first type of battle that our game will support – the capture battle. If you are successful, then you will be able to capture the wild Pokemon you encounter and add it to your team.
Random Encounter View Controller
Our first change is pretty minimal. After displaying a random encounter, the user has the ability to choose to attempt a “Capture” of the Pokemon, or to “Ignore” the Pokemon. Because we hadn’t implemented the capture feature yet, I left that handler as if the user was simply choosing “Ignore” regardless of what button they pressed. We need to correct this now by changing the “CaptureButtonPressed” method to the following:
public void CaptureButtonPressed () { if (didFinish != null) didFinish(Exits.Capture); }
Combat View Controller
I use the “Combat” view controller to handle the display of a battle. Unlike most of the other screens, this screen doesn’t respond to user input. It is more like the “GameViewController” in that sense, because it will just show the combatants, and animate attack moves, etc. If you’ve followed along so far your screen should look something like this:
The code shouldn’t look unfamiliar either. We have an action called “didCompleteMove” to let us know when we’ve completed animation as well as a few other fields to help connect the various views and components that help form its structure.
I have a public method “Display” which causes the screen to update its elements with current data about the battle and its combatants. Much of the work will be handled in a sub view component called Combatant View, and there is one of these views for each Pokemon that is fighting.
We also have a method called “ApplyMove” which will cause elements on the screen to animate to help illustrate the attack. This begins with the “ShowAttackProcess” where we list the name of the move that is being performed, then scale the attacker up and down again as if it were rearing back for extra impact strength. Its a simple way to illustrate who is doing the attacking but I think it gets the idea across well enough.
Once the “ShowAttackProcess” is completed, the next step in the sequence is to show the results with “ShowResultProcess”. Here we will show the damage done to the defender as well as the energy delta that gets applied to the attacker based on the type of move that was selected. The implementation for these are inside the combatant view, but we start the coroutine to animate it from here.
public class CombatViewController : BaseViewController { public Action didCompleteMove; public CombatantView playerCombatant; public CombatantView computerCombatant; public Text moveCallout; public AudioSource battleCrySource; void OnEnable () { Display(); } public void Display () { foreach (Combatant combatant in battle.combatants) { GetView(combatant).Display(combatant); } } public void ApplyMove () { StartCoroutine(ApplyMoveProcess()); } IEnumerator ApplyMoveProcess () { yield return StartCoroutine(ShowAttackProcess()); yield return StartCoroutine(ShowResultProcess()); if (didCompleteMove != null) didCompleteMove(); } IEnumerator ShowResultProcess() { var combatantView = GetView(battle.combatants[1]); StartCoroutine(combatantView.UpdateHitPointsProcess(battle.lastDamage)); combatantView = GetView(battle.combatants[0]); yield return StartCoroutine(combatantView.UpdateEnergyProcess()); } IEnumerator ShowAttackProcess () { var move = battle.move; var combatant = battle.combatants[0]; moveCallout.text = move.name; // TODO: play Battle cry sound fx CombatantView view = GetView(combatant); RectTransform avatar = view.avatarImage.rectTransform; Tweener tweener = avatar.ScaleTo( new Vector3(1.5f, 1.5f, 1.5f), 0.25f, EasingEquations.EaseOutCubic ); while (tweener != null) yield return null; tweener = avatar.ScaleTo(Vector3.one, 0.5f, EasingEquations.EaseOutBounce); while (tweener != null) yield return null; yield return new WaitForSeconds(0.5f); moveCallout.text = ""; } CombatantView GetView (Combatant combatant) { return combatant.mode == ControlModes.Player ? playerCombatant : computerCombatant; } }
Combatant View
The Combat View Controller screen is subdivided into a complex hierarchy of objects. Among them are two “CombatantView” components that operate on a particular collection of those objects. One view will be for the player, and the other for the computer opponent. I can reuse the same component for both because it simply needs to know how to display information for a Combatant, regardless of the control mode in place.
I provided a public method called “Display” with which we can pass a Combatant parameter and the method will then update its own collection of labels, hit point bar, avatar image, etc. Everything we would want to display all with a single method call.
There is a public method called “UpdateHitPointsProcess” to animate what happens when a combatant receives damage. The “lastDamage” parameter is passed along because the actual damage of an attack may not be able to be determined from other known stats – this is because the hit points of a Pokemon will be clamped so that they don’t go below zero. Since we know the actual damage, we can display it in a sort of “jumping” label that starts above the picture of the Pokemon and animates vertically to draw attention to itself. At the same time, I will animate the hit point bar to the new correct position to reflect the current hit points of the active Pokemon. The method has a return type of IEnumerator so that the ViewController that holds the view can perform sequence based logic around the animation and know when things have completed.
When the combatant is attacking we will also need a way to modify the energy bar. Some attacks will cause the bar to go up, and others to go down. Regardless, we will animate it to the new correct scale and will also return the IEnumerator as we did earlier.
public class CombatantView : MonoBehaviour { public Text nameLabel; public Text damageLabel; public RectTransform hitPoints; public RectTransform energy; public Image avatarImage; Combatant combatant; public void Display (Combatant combatant) { this.combatant = combatant; nameLabel.text = combatant.CurrentPokemon.Name; Poses pose = combatant.mode == ControlModes.Player ? Poses.Back : Poses.Front; avatarImage.sprite = combatant.CurrentPokemon.GetAvatar(pose); hitPoints.localScale = new Vector3( combatant.CurrentPokemon.HPRatio, 1, 1 ); energy.localScale = new Vector3( combatant.CurrentPokemon.EnergyRatio, 1, 1 ); } public IEnumerator UpdateHitPointsProcess (int lastDamage) { damageLabel.rectTransform.anchoredPosition = Vector2.zero; damageLabel.text = lastDamage.ToString(); damageLabel.rectTransform.AnchorTo(new Vector3(0, 25, 0), 0.5f, EasingEquations.EaseOutExpo); Tweener tweener = hitPoints.ScaleTo(new Vector3( combatant.CurrentPokemon.HPRatio, 1, 1 )); while (tweener != null) yield return null; damageLabel.text = ""; } public IEnumerator UpdateEnergyProcess () { Tweener tweener = energy.ScaleTo(new Vector3( combatant.CurrentPokemon.EnergyRatio, 1, 1 )); while (tweener != null) yield return null; } }
Command View Controller
When it is the player’s turn in battle and a command input needs to be provided, I will animate another menu screen on top of the combat screen. The user will use this to perform any of a variety of actions such as selecting a move to attack with, attempting a capture on a now weakened opponent, or retreating from a tough gym match before getting fully KO’d. If you’ve been following along, your screen should look something like this:
This screen can be modified based on the kind of battle that is taking place. For example, you can’t “catch” a Pokemon in a gym battle. Otherwise it largely just wraps each button press event in a call to “didFinish” with the relevant “Exits” enum value.
public class CommandViewController : BaseViewController { public enum Exits { FastMove, ChargeMove, Capture, Retreat } public Action<Exits> didFinish; public Button chargeButton; public Button captureButton; public Button retreatButton; void OnEnable () { chargeButton.interactable = CanSelectChargeMove(); } public void SetupGymBattle () { captureButton.gameObject.SetActive (false); retreatButton.gameObject.SetActive (true); } public void SetupEncounterBattle () { captureButton.gameObject.SetActive (true); retreatButton.gameObject.SetActive (false); } public void OnFastMovePressed () { if (didFinish != null) didFinish(Exits.FastMove); } public void OnChargeMovePressed () { if (didFinish != null) didFinish(Exits.ChargeMove); } public void OnCapturePressed () { if (didFinish != null) didFinish(Exits.Capture); } public void OnRetreatPressed () { if (didFinish != null) didFinish(Exits.Retreat); } bool CanSelectChargeMove () { var pokemon = battle.combatants[0].CurrentPokemon; var cost = Mathf.Abs(pokemon.ChargeMove.energy); return pokemon.energy >= cost; } }
As a quick note, I have made the effort to pre-configure all of the prefabs for you, but we need to make an update on the “Command Screen” prefab. In this lesson I added a new field for the “Retreat Button” that needs to be hooked up. I have made the change to the prefab in the repository in case you need help.
Capture View Controller
At some point in the battle, a capture will be attempted. This could occur manually from a command prompt, or as a result of a KO in battle. Regardless, as soon as the screen appears it simply begins its own animation where the Pokeball closes around the target and then we see three “struggles” for escape. When the struggling has concluded we show a label on the result, whether success or failure. On success, we also use a new system to actually handle the capture which adds the wild pokemon to the player’s team. If you’ve been following along, your screen should look something like this:
The code here follows the same pattern. A “didComplete” action is used to perform follow up code when the screen is done with its animation, there are fields for references like sprites and the image to show them in and the text label to print the success information within.
There is a property to grab the pokemon that we will attempt to capture. Currently it is grabbing the defender’s Pokemon. This is because I originally set it up so that you could only enter capture mode by manually selecting the option from the command menu or by winning by KO – in either case the defender will then be the wild Pokemon. However, I have just noticed a bug, because in the final flow, you will also attempt a capture even if your Pokemon is the one that is KO’d. What’s worse is that because your Pokemon will be seen as the defender and its hit points will be zero you will ALWAYS win the capture even if you did no damage at all. Unfortunately I already pushed up the faulty code so you will see that when checking out the relevant commit. I have a fix, which will be in my next commit, but if you want to fix it now, remove that property altogether and use the following two lines of code at the beginning of the “CaptureSequence” method instead:
var opponent = battle.combatants [0].mode == ControlModes.Player ? battle.combatants [1] : battle.combatants [0]; var pokemon = opponent.CurrentPokemon;
As soon as the view controller is enabled, it will call the “OnEnable” method thanks to inheriting from MonoBehaviour. From here I start the “CaptureSequence” coroutine that handles all of the animation and systems together.
Whether an attempt to escape will succeed or not is up to the “CaptureSystem” to determine, but its probability is based on damage done. If you KO the opponent then a capture is guaranteed. If you do barely any damage, then escape is nearly guaranteed.
public class CaptureViewController : BaseViewController { public Action didComplete; [SerializeField] Sprite open; [SerializeField] Sprite closed; [SerializeField] Image pokeball; [SerializeField] Text resultLabel; void OnEnable () { StartCoroutine(CaptureSequence()); } IEnumerator CaptureSequence () { var opponent = battle.combatants [0].mode == ControlModes.Player ? battle.combatants [1] : battle.combatants [0]; var pokemon = opponent.CurrentPokemon; resultLabel.text = ""; bool success = true; pokeball.sprite = open; yield return new WaitForSeconds(1); pokeball.sprite = closed; for (int i = 0; i < 3; ++i) { yield return StartCoroutine(Bounce()); if (CaptureSystem.TryEscape(pokemon)) { success = false; break; } } resultLabel.text = success ? "Success!" : "Escaped!"; yield return new WaitForSeconds(1); if (success) CaptureSystem.Capture (game.CurrentPlayer, pokemon); if (didComplete != null) didComplete(); } IEnumerator Bounce () { RectTransform rt = pokeball.rectTransform; Tweener tweener = rt.ScaleTo( new Vector3(1.5f, 1.5f, 1.5f), 0.25f, EasingEquations.EaseOutCubic ); while (tweener != null) yield return null; tweener = rt.ScaleTo(Vector3.one, 0.5f, EasingEquations.EaseOutBounce); while (tweener != null) yield return null; } }
Encounter Flow
There are a number of states we will need to implement to complete this lesson, so I thought it might be handy to take another look at the relevant flow chart:
The nodes are generally represented by a separate state. The mapping of the nodes to states is as follows:
- Fight? – “RandomEncounterState”
- Start Battle – “StartEncounterBattleState”
- Command – “EncounterCommandState”
- Attack – “EncounterAttackState”
- KO? – “EncounterAttackState”
- Capture – “CaptureState”
- Complete – “EncounterCompleteState”
Random Encounter State
In the “OnEnterRandomEncounterState”, there is a series of nested actions. In the innermost action which is passed to the view controller’s “Hide” method, we route to the subsequent state inside of a switch statement. The case for “Capture” needs to be changed from this:
case RandomEncounterViewController.Exits.Capture: // TODO: create a battle stateMachine.ChangeState (CheckDestinationState); // TODO: StartEncounterBattleState break;
… to this:
case RandomEncounterViewController.Exits.Capture: battle = BattleFactory.Create (game.CurrentPlayer, pokemon); stateMachine.ChangeState (StartEncounterBattleState); break;
This change makes use of the BattleFactory to create a new capture battle that is already configured based on the game’s current player and the wild encountered pokemon. Then, it goes to the next state where we can further initialize anything we might want to do using the new battle object.
Start Encounter Battle State
This is a pretty simple state that allows us to prepare the game assets for our new battle. In particular we need to customize the Command View Controller so that it shows the correct buttons based on what options we have available. We will also activate the Combat View Controller so we can see the battle as it takes place. Eventually we will use this state to play some battle themed music, but I will save that for later. Finally, we move on to the next state in the flow.
public partial class FlowController : MonoBehaviour { State StartEncounterBattleState { get { if (_startEncounterBattleState == null) _startEncounterBattleState = new State(OnEnterStartEncounterBattleState, null, "StartEncounterBattle"); return _startEncounterBattleState; } } State _startEncounterBattleState; void OnEnterStartEncounterBattleState () { // TODO: Play Battle Music commandViewController.SetupEncounterBattle (); combatViewController.gameObject.SetActive(true); stateMachine.ChangeState (EncounterCommandState); } }
Encounter Command State
Hopefully the code here will look familiar as well. The “OnEnter” method takes care of pretty much everything. It causes the CombatSystem to update so it knows who should be attacking. Then, the code branches based on the mode of the attacker. When the Computer has control (the else clause) then we allow the CombatSystem to pick a move. When the Player has control things are a bit more involved. We need to show the Command View Controller so they can pick what they want to do. Selecting any option in the list will cause the view controller to “finish” at which point we will remove the handler and “hide” the view controller because we wont need it anymore. In the completion of the “hide” we use a switch block to take an action depending on what kind of “exit” enum was passed along. Two of the options cause us to apply an attack, and they just choose which move to use while attacking. The other option attempts to perform a catch early – even before the battle has been fully resolved.
public partial class FlowController : MonoBehaviour { State EncounterCommandState { get { if (_encounterCommandState == null) _encounterCommandState = new State(OnEnterEncounterCommandState, null, "EncounterCommand"); return _encounterCommandState; } } State _encounterCommandState; void OnEnterEncounterCommandState () { CombatSystem.Next(battle); if (battle.attacker.mode == ControlModes.Player) { commandViewController.Show(delegate { commandViewController.didFinish = delegate(CommandViewController.Exits obj) { commandViewController.didFinish = null; commandViewController.Hide(delegate { switch (obj) { case CommandViewController.Exits.FastMove: SelectEncounterMove (battle.attacker.CurrentPokemon.FastMove); break; case CommandViewController.Exits.ChargeMove: SelectEncounterMove (battle.attacker.CurrentPokemon.ChargeMove); break; case CommandViewController.Exits.Capture: stateMachine.ChangeState (CaptureState); break; } }); }; }); } else { SelectEncounterMove (CombatSystem.PickMove (battle)); } } void SelectEncounterMove (Move move) { battle.move = move; stateMachine.ChangeState (EncounterAttackState); } }
Encounter Attack State
This state takes a move that was chosen previously (whether chosen by A.I. or player it doesn’t matter) and applies it, both to the model via the CombatSystem, and to the view via the Combat View Controller. Once the move has been applied to both and the animation has completed, we will use a completion handler to change state based on the hit points of the defender.
public partial class FlowController : MonoBehaviour { State EncounterAttackState { get { if (_encounterAttackState == null) _encounterAttackState = new State(OnEnterEncounterAttackState, null, "EncounterAttack"); return _encounterAttackState; } } State _encounterAttackState; void OnEnterEncounterAttackState () { CombatSystem.ApplyMove(battle); combatViewController.didCompleteMove = delegate { if (battle.defender.CurrentPokemon.hitPoints == 0) { stateMachine.ChangeState (CaptureState); } else { stateMachine.ChangeState (EncounterCommandState); } }; combatViewController.ApplyMove(); } }
Capture State
This simple state simply causes the Capture View Controller to become active. That screen automatically plays itself out, so we simply wait until it is done and then move on in the flow to the next state.
public partial class FlowController : MonoBehaviour { State CaptureState { get { if (_captureState == null) _captureState = new State(OnEnterCaptureState, OnExitCaptureState, "Capture"); return _captureState; } } State _captureState; void OnEnterCaptureState () { captureViewController.gameObject.SetActive(true); captureViewController.didComplete = delegate { stateMachine.ChangeState (EncounterCompleteState); }; } void OnExitCaptureState () { captureViewController.didComplete = null; captureViewController.gameObject.SetActive(false); } }
Encounter Complete State
Now that battle is over, we can dismiss the Combat View Controller and move on in the flow. When the project is fully completed, the next step in the flow would be the “GymState”, but for now we can keep the game playable by moving directly to the “CheckDestinationState” instead.
public partial class FlowController : MonoBehaviour { State EncounterCompleteState { get { if (_encounterCompleteState == null) _encounterCompleteState = new State(OnEnterEncounterCompleteState, null, "EncounterComplete"); return _encounterCompleteState; } } State _encounterCompleteState; void OnEnterEncounterCompleteState () { combatViewController.gameObject.SetActive(false); // TODO: Play Journey Music stateMachine.ChangeState (CheckDestinationState); // TODO: GymState } }
Demo
If you have been following along so far and have filled in all of the assets necessary to play, now would be a great time to give the project a test run. You should be able to have random encounters with wild Pokemon and then battle them! If you can weaken them enough you should also be able to capture them! Of course at the moment you still can’t “do” anything with a captured Pokemon, but at least you will see it added to your list of Pokemon if you look in the Inspector. This will be visible by selecting the MasterController game object then looking thru the Data Controller hierarchy such as: Game, Players, Player 1, Pokemon, Element 1.
Summary
In this lesson we finished the implementation of a pretty major feature – battle. This is the first of two kinds of battle in which you can potentially capture a wild encountered Pokemon when you succeed. There wasn’t much new in the way of concepts that you needed to learn, but it hopefully served as good practice in filling in the pieces of a large project one bit 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.