In this lesson we will pick up where we left off, and finish a working implementation of attacking. This will include creating a new game action, implementing it through a system, and providing a viewer so a user of our game can see it all happen. Along the way I will also do a bit of refactoring to make things more flexible.
Go ahead and open the project from where we left off, or download the completed project from last week here. I have also prepared an online repository here for those of you familiar with version control.
Attack Action
Let’s begin by providing the context object for an action to take place. I will define an attack as occuring between a single attacker and a single target – both will be cards that are active on the battlefield. The code looks like this:
[csharp]
public class AttackAction : GameAction {
public Card attacker;
public Card target;
public AttackAction (Card attacker, Card target) {
this.attacker = attacker;
this.target = target;
}
}
[/csharp]
Like our other actions, this is a subclass of the GameAction class. It provides public fields for the attacker and target, which we will be assigning through a constructor. I could have made them readonly, but then the game is constrained by potentially unnecessary rules. For example, there are Hearthstone cards with abilities which can modify an attack target – see “Ogre Brute” and “Mayor Noggenfogger” for examples.
Of course there are still other ways to implement the “redirect” sort of ability. I could have chosen to “cancel” the original attack and create a secondary attack with new targeting in place. This approach would allow the fields in this class to be readonly, but then you would have to determine when new attack action was the cause of a redirect so that your ability wouldn’t again try to redirect the redirected attack and end up in an endless loop.
Attack System
Now that we have an attack action, we will want the attack system to be able to handle its implementation. For starters, we will want to go ahead and make the class definition conform to the IObserve interface:
[csharp]public class AttackSystem : Aspect, IObserve[/csharp]
We can then add the “Awake” and “Destroy” methods to handle proper observation of the “validation” and “perform” notifications related to the action.
[csharp]
public void Awake () {
this.AddObserver (OnValidateAttackAction, Global.ValidateNotification
this.AddObserver (OnPerformAttackAction, Global.PerformNotification
}
public void Destroy () {
this.RemoveObserver (OnValidateAttackAction, Global.ValidateNotification
this.RemoveObserver (OnPerformAttackAction, Global.PerformNotification
}
[/csharp]
The validation handler will check whether or not the “attacker” and “target” of the action are valid. This will be true if they can be found in the system’s list of valid attackers and valid targets. If either is not found, then the action can be invalidated.
[csharp]
void OnValidateAttackAction (object sender, object args) {
var action = sender as AttackAction;
if (!validAttackers.Contains(action.attacker) ||
!validTargets.Contains(action.target)) {
var validator = args as Validator;
validator.Invalidate ();
}
}
[/csharp]
While implementing this, I discovered an unintentional logic bug. Everytime the “PlayerIdleState” exited, I had it call the “Clear” method of this system – this allowed me to un-highlight battlefield views when playing actions or switching turns etc. However, because the action system also exits the “PlayerIdleState” when it begins playing a new action sequence, then the “validation” step had only empty lists to check against. To solve this problem, I decided to change how the view highlighting was handled, and no longer needed to “clear” my lists. Remove the “DidUpdateNotification” and also remove the “Clear” method from this class.
Next we can provide the implementation for the “Perform” keyframe of the attack phase. We will use this as an opportunity to create a new damage action as a reaction to the attack action. It will be configured with the attack target and the attack stat of our attacker. We will also want to make sure to deduct one from the attacker’s “remainingAttacks” stat so that it can’t just attack forever. This step could have been handled by the CombatantSystem as well.
[csharp]
void OnPerformAttackAction (object sender, object args) {
var action = args as AttackAction;
var attacker = action.attacker as ICombatant;
attacker.remainingAttacks–;
var target = action.target as IDestructable;
var damageAction = new DamageAction (target, attacker.attack);
container.AddReaction (damageAction);
}
[/csharp]
Player Idle State
Because we removed the “DidUpdateNotification” notification from the Attack System, we are going to need some other sort of trigger to know when to highlight and unhighlight our views on screen. I decided to post notifications of this particular game state when it enters and exits. I considered doing something for all my game states like I do with the Global class and actions, but I guessed I probably wouldn’t need it. I can update it later if necessary. Add the following notification definitions:
[csharp]
public const string EnterNotification = “PlayerIdleState.EnterNotification”;
public const string ExitNotification = “PlayerIdleState.ExitNotification”;
[/csharp]
And post them at the end of the “Enter” and “Exit” methods. Also, dont forget to remove the call to “Clear” the Attack System from the “Exit” method since that will no longer exist.
[csharp]
public override void Enter () {
container.GetAspect
Temp_AutoChangeTurnForAI ();
this.PostNotification (EnterNotification);
}
public override void Exit () {
this.PostNotification (ExitNotification);
}
[/csharp]
Refactoring
You might have noticed that the “Minion” and “Hero” models both inherit from a shared base class, but “MinionView” and “HeroView” do not. Let’s take advantage of the shared features in both by refactoring them to share a common superclass.
Battlefield Card View
Create and add a new class called “BattlefieldCardView”. We will copy several of the fields and methods that existed in the two other “View” classes, and will also add a more generic “Card” property so that it will be easier to treat them the same in certain cases:
[csharp]
public abstract class BattlefieldCardView : MonoBehaviour {
public Image avatar;
public Text attack;
public Text health;
public Sprite inactive;
public Sprite active;
protected bool isActive;
public abstract Card card { get; }
void OnEnable () {
this.AddObserver (OnPlayerIdleEnter, PlayerIdleState.EnterNotification);
this.AddObserver (OnPlayerIdleExit, PlayerIdleState.ExitNotification);
this.AddObserver (OnPerformDamageAction, Global.PerformNotification
}
void OnDisable () {
this.RemoveObserver (OnPlayerIdleEnter, PlayerIdleState.EnterNotification);
this.RemoveObserver (OnPlayerIdleExit, PlayerIdleState.ExitNotification);
this.RemoveObserver (OnPerformDamageAction, Global.PerformNotification
}
void OnPlayerIdleEnter (object sender, object args) {
var container = (sender as PlayerIdleState).container;
isActive = container.GetAspect
Refresh ();
}
void OnPlayerIdleExit (object sender, object args) {
isActive = false;
}
protected abstract void Refresh ();
void OnPerformDamageAction (object sender, object args) {
var action = args as DamageAction;
if (action.targets.Contains (card as IDestructable)) {
Refresh ();
}
}
}
[/csharp]
Note that I also added the notifications from the “PlayerIdleState” as the opportunity to determine when a card view should be active or not. This class is “abstract” which means you can’t instantiate it, and that there are potentially elements of it which must be defined by a subclass. In this case we have the “Card” property as well as the “Refresh” method which needs to be called when something has changed that needs to be reflected visually.
Hero View
We will need to make a bunch of changes to this class including:
- Make it inherit from “BattlefieldCardView”
- Remove any fields which are now defined in the superclass
- Implement the “Card” Property (shown below)
- Remove the OnEnable and OnDisable methods – all of this is handled in the superclass
- Mark the “Refresh” method as “protected override”
- Remove the notification handlers “OnPerformDamageAction” and “OnAttackSystemUpdate” – all of this is handled in the superclass
[csharp]public override Card card { get { return hero; } }[/csharp]
Minion View
We will also need to make a bunch of changes to this class including:
- Make it inherit from “BattlefieldCardView”
- Remove any fields which are now defined in the superclass
- Implement the “Card” Property (shown below)
- Remove the OnEnable and OnDisable methods – all of this is handled in the superclass
- Mark the “Refresh” method as “protected override”
- Remove the notification handler “OnAttackSystemUpdate” – it is handled in the superclass
[csharp]public override Card card { get { return minion; } }[/csharp]
Drag To Attack Controller
Now that we have a new action, we need a way to trigger it. I decided to show an alternative to “tapping” like we did for playing cards. Also, I wanted an opportunity to show that you don’t have to handle a “drag” on the object that it occurs on. The event will also travel up the hierarchy so you can handle it at a higher level, and aren’t required to put special components on everything that you wish to drag.
In this case we will create a new input controller. It is similar in many ways to the “ClickToPlayCardController” such as how it has its own private state machine and state classes, and the way it modifies the game state to show that the player is in an interaction mode. Create the script, and add a component instance of it to the “Board” GameObject in the Scene Hierarchy.
[csharp]
public class DragToAttackController : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
// Add code here…
}
[/csharp]
In the “Tap” system we had made a tappable component and listened to notifications that were fired. In this case, we will observe the input events directly. Note that you need all three interfaces for everything to work correctly. Even though I dont technically need to use the “IDragHandler” I can not remove it or the class wont receive events properly.
[csharp]
IContainer gameContainer;
StateMachine gameStateMachine;
Container container;
StateMachine stateMachine;
Card attacker;
Card defender;
void Awake () {
gameContainer = GetComponentInParent
gameStateMachine = gameContainer.GetAspect
container = new Container ();
stateMachine = container.AddAspect
container.AddAspect (new WaitingForInputState ()).owner = this;
container.AddAspect (new FinishInputState ()).owner = this;
container.AddAspect (new CompleteState ()).owner = this;
stateMachine.ChangeState
}
[/csharp]
I defined several fields, many of which I assign in the “Awake” method. These are cached references for convenience. Note that there are two containers and two state machines. The one with the “game” prefix is the main container that all systems attach to. The other is for a local container and state machine that is private to itself. The “attacker” and “defender” fields will be dynamic and are based on drag events.
[csharp]
public void OnBeginDrag (PointerEventData eventData) {
var handler = stateMachine.currentState as IBeginDragHandler;
if (handler != null)
handler.OnBeginDrag (eventData);
}
public void OnDrag (PointerEventData eventData) {
var handler = stateMachine.currentState as IDragHandler;
if (handler != null)
handler.OnDrag (eventData);
}
public void OnEndDrag (PointerEventData eventData) {
var handler = stateMachine.currentState as IEndDragHandler;
if (handler != null)
handler.OnEndDrag (eventData);
}
[/csharp]
In the implementation of the drag interface methods, I merely pass the event handling on to the current state, assuming of course that the current state also implements the matching handler interface. I am not doing anything with the “OnDrag” at the moment, but I left it in place so it would be easy to add polish in the future. For example, in Hearthstone, they draw an arrow from where you started dragging to where your finger is currently pointing. To implement a similar feature we would need the “OnDrag” event to update the arrow per frame.
[csharp]
private abstract class BaseControllerState : BaseState {
public DragToAttackController owner;
}
[/csharp]
Finally we have the private states for our private state machine. I created three simple states that inherit from a base class state so I can easily assign the owner field to each.
[csharp]
private class WaitingForInputState : BaseControllerState, IBeginDragHandler {
public void OnBeginDrag (PointerEventData eventData) {
if (!(owner.gameStateMachine.currentState is PlayerIdleState))
return;
var press = eventData.rawPointerPress;
var view = (press != null) ? press.GetComponentInParent
if (view == null)
return;
owner.attacker = view.card;
owner.gameStateMachine.ChangeState
owner.stateMachine.ChangeState
}
}
[/csharp]
The “WaitingForInputState” class responds to the “Begin” portion of a drag event. It will be the state which can begin a new “flow” of input, but there are several requirements. For example, it will only be able to activate during a “PlayerIdleState” game state. In addition, we only care about drag events that begin within the hierarchy of a GameObject which contains a “BattlefieldCardView” component. If either of those expectations are not met, we will abort early.
If we did manage to find what we wanted, we will assign the view’s card to our owner so that other states can also work with it. We set the “Game” to a “PlayerInputState” so that no other conflicting input flows or game actions will be able to occur, and we set our own local state to the “FinishInputState” to wait and see where the drag will go.
[csharp]
private class FinishInputState : BaseControllerState, IEndDragHandler {
public void OnEndDrag (PointerEventData eventData) {
var hover = eventData.pointerCurrentRaycast.gameObject;
var view = (hover != null) ? hover.GetComponentInParent
if (view != null)
owner.defender = view.card;
owner.stateMachine.ChangeState
}
}
[/csharp]
The “FinishInputState” class responds to the “End” portion of a drag event. Note that when a drag event ends, that we can’t use the same fields on the eventData – afterall we just “released” a “press” so it no longer applies. Not to worry, we can still determine what the pointer was “over” by referring to a “pointerCurrentRaycast” field instead. Much like before, we are looking to obtain a “BattlefieldCardView” component from the object hierarchy and if found, will assign the view’s card to the owner’s defender field. Regardless of if a card was found or not, we will complete this flow by changing the state machine to the “CompleteState”.
[csharp]
private class CompleteState : BaseControllerState {
public override void Enter () {
owner.gameStateMachine.ChangeState
if (owner.attacker != null && owner.defender != null) {
var action = new AttackAction (owner.attacker, owner.defender);
owner.gameContainer.Perform (action);
}
owner.attacker = owner.defender = null;
owner.stateMachine.ChangeState
}
}
[/csharp]
Our final state, “CompleteState”, doesn’t respond to any input events. It merely handles the outcome of our flow, and resets things to normal. When the state enters, we reset the game’s state machine to the Idle State (otherwise we wouldn’t be able to play an action). If both an attacker and defender have been found, then we submit a new attack action. Any needed validation for the action will be performed by the action system. Finally we reset the attacker and defender fields and return to the “WaitingForInputState”.
Viewer Preparation
Before I create the “viewer” for our new attack action, I want to add a few methods to help make it easier to grab a GameObject that represents one of our game models. We will begin at the “BoardView” with a method that will then pass the call onto its children until a match is found.
Board View
Add the following public method to our Board View so that we can find the matching GameObject that is used to represent any given Card model:
[csharp]
public GameObject GetMatch (Card card) {
var playerView = playerViews [card.ownerIndex];
return playerView.GetMatch (card);
}
[/csharp]
In our scene hierarchy, the “BoardView” GameObject will be the parent of two “PlayerView” GameObjects. In this case, we look for the “ownerIndex” of the card to determine which of the two “PlayerView” GameObjects should hold the match.
Player View
The “PlayerView” holds several children. Generally speaking, I would have one child GameObject for each of the Zone types a player’s cards could be located within.
[csharp]
public GameObject GetMatch (Card card) {
switch (card.zone) {
case Zones.Battlefield:
return table.GetMatch (card);
case Zones.Hero:
return hero.gameObject;
default:
return null;
}
}
[/csharp]
The “GetMatch” method for this class will consider the zone of the card passed in the parameter and then use a switch statement to return a result accordingly. In this case I have only implemented two of the zones. The hero zone can be returned immediately because there is only one GameObject to represent it, but the Battlefield could hold several minions and will need another call.
Table View
The “TableView” component represents the BattleField zone of a player and is where all of a players minion cards will appear. We will need one final “GetMatch” method here to loop through the list of minions until a match is found:
[csharp]
public GameObject GetMatch (Card card) {
for (int i = minions.Count – 1; i >= 0; –i) {
if (minions [i].minion == card)
return minions [i].gameObject;
}
return null;
}
[/csharp]
Attack Viewer
Now I am finally ready to add a new component to serve as the “viewer” of the attack action. Overall the script is pretty simple. It uses the OnEnable and OnDisable as opportunities to handle the registration of notifications. We observe the “Validate” notification of the “AttackAction” as an opportunity to attach a perform phase viewer. The viewer method itself, “OnPerformAttack”, causes the attacker GameObject to move over the target GameObject using an animated tween. At that point we apply the key frame of the phase, bounce back a bit to make the impact look somewhat forceful, and then return the view to its starting position.
[csharp]
public class AttackViewer : MonoBehaviour {
void OnEnable () {
this.AddObserver (OnValidateAttack, Global.ValidateNotification
}
void OnDisable () {
this.RemoveObserver (OnValidateAttack, Global.ValidateNotification
}
void OnValidateAttack (object sender, object args) {
var action = sender as AttackAction;
action.perform.viewer = OnPerformAttack;
}
IEnumerator OnPerformAttack (IContainer game, GameAction action) {
var attackAction = action as AttackAction;
var board = GetComponent
var attacker = board.GetMatch (attackAction.attacker);
var target = board.GetMatch (attackAction.target);
if (attacker == null || target == null)
yield break;
var startPos = attacker.transform.position;
var toTarget = target.transform.position – startPos;
var tweener = attacker.transform.MoveTo (target.transform.position + new Vector3 (0, 1, 0), 0.5f, EasingEquations.EaseInBack);
while (tweener != null)
yield return false;
// Apply the attack damage here
yield return true;
var bounceBack = (toTarget.normalized * (toTarget.magnitude – 0.5f)) + startPos;
tweener = attacker.transform.MoveTo (bounceBack, 0.2f, EasingEquations.EaseOutQuad);
while (tweener != null)
yield return false;
tweener = attacker.transform.MoveTo (startPos, 0.25f);
while (tweener != null)
yield return false;
}
}
[/csharp]
We will need to attach this component to the “Board” GameObject in the Scene Hierarchy as well. Make sure you save the scene too.
Game View System
One more fix before we’re done. Our somewhat sloppy, and temporary implementation for building a deck of cards did not bother to assign the “ownerIndex” or “zone” of our hero cards. Add the following statements to fix that:
[csharp]
hero.ownerIndex = p.index;
hero.zone = Zones.Hero;
[/csharp]
Demo
Save the scene and project and then run the game. The turn after summoning a minion, you should be ready to attack and see the same highlight we added from the last lesson. Now try dragging one of your minions to the enemy hero. You should see it attack, and should notice that the hero’s hitpoints will drop by the same amount of damage as the attacking minions attack stat. You can attack with your minions in any order, but you can’t perform invalid attacks such as making any minion attack an ally, or making a minion attack more times than it is allowed.
Summary
We had a pretty long lesson this time. We covered the creation of a new attack action, handled its implementation with a game system and viewer, and also created an input controller to allow a user to trigger it all. A little refactoring was also done along the way to help keep the code simple and reusable. We ended with a playable demo, complete with the ability to summon minions and make them attack and ultimately knock out the enemy hero, leading to a much earlier victory than by fatigue damage only.
You can grab the project with this lesson fully implemented right here. If you are familiar with version control, you can also grab a copy of the project from my repository.
If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!