“Attack him where he is unprepared, appear where you are not expected.” – Sun Tzu
Overview
The ability to “Attack” can be deceptively complex. To understand the basic rules, I would recommend reading the guide, but I will give a quick overview, especially of what we will try to implement in this lesson.
The first step in launching an attack is to make an attack roll. This first roll is used to help determine whether or not the attack actually hits the target or not. As with other checks, this can be a success or failure, including critically. The simplest form of this formula is to make a D20 Roll and add a Strength modifier and proficiency bonus. Other bonuses or penalties could apply for a variety of reasons, and we will only focus on simple details for now.
Every “check” has a difficulty check (DC). In this case the DC comes from something called the armor class (AC) of the intended target. The formula for calculating this starts with a constant of 10 (instead of a D20 roll), adds the Dexterity modifier, and adds a proficiency bonus based on your type of armor. Like before, other bonuses and penalties could apply.
Once the check is made, the result of the attack can be specified. A Success means that you “hit” the opponent and can do damage. The “source” of the damage can come from different places. If you were attacking with a sword, then the sword would say how much damage it could inflict. On a critical success, the amount of damage may be doubled, but again, there are exceptions. A failure or critical failure mean that you have missed your target and no damage will be dealt.
Every turn, the current combatant is allowed to use up to three actions. However there is a “cost” to using attack more than once in a turn. This multiple attack penalty is different depending on the type of weapon used, but could be a penalty of 4 or 5 per extra attack. While you may be likely to hit the first time, consecutive strikes are harder to achieve.
Getting Started
Feel free to continue from where we left off, or download and use this project here. It has everything we did in the previous lesson ready to go.
Turn System
In order to handle the idea of an attack combo, we will need to flesh out our turn system a bit more. Specifically, we need to be able to keep track of whose turn it is, and also must be able to take more than one action within the same turn. In addition we need to be able to track the number of times a character has attacked this turn. Open up the TurnSystem.cs script and replace the contents with the following:
public interface ITurnSystem : IDependency<ITurnSystem> { Entity Current { get; } int ActionsRemaining { get; } int AttackCount { get; } bool IsComplete { get; } void Begin(Entity entity); void TakeAction(int actionCost, bool isAttack); } public class TurnSystem : ITurnSystem { public Entity Current { get { return current; } } Entity current; public int ActionsRemaining { get { return actionsRemaining; } } int actionsRemaining; public int AttackCount { get { return attackCount; } } int attackCount; public bool IsComplete { get { return actionsRemaining == 0; } } public void Begin(Entity entity) { current = entity; actionsRemaining = 3; attackCount = 0; } public void TakeAction(int actionCost, bool isAttack) { actionsRemaining -= actionCost; if (isAttack) attackCount++; } }
You might wonder why I would choose to track the “attack” count in a “turn” object. I could after all put it in the same system that handles the attack itself. There are two reasons I chose to place it here. First, any action that has an “attack trait” will need to be tracked – this could include things like spell attacks or skill attacks like shove. Each different type of attack will be implemented in its own system, so it makes sense to keep the overall attack tally somewhere externally. Second, the attack accumulation is in the context of a turn, and resets at each new turn, therefore it made sense to keep it here.
Armor Class
Every combatant will have a different Armor Class (AC) which is basically a stat that indicates how hard it is to deal damage to them. The stat could be higher due to a good Dexterity ability score, or to nice armor and proficiency with that armor etc. For the sake of this lesson, I am not handling any of the logic that determines how this stat is calculated, but am merely providing a system that returns the stat for each character.
Create a new C# script at Scripts -> Component named ArmorClassSystem and add the following:
public partial class Data { public CoreDictionary<Entity, int> armorClass = new CoreDictionary<Entity, int>(); } public interface IArmorClassSystem : IDependency<IArmorClassSystem>, IEntityTableSystem<int> { } public class ArmorClassSystem : EntityTableSystem<int>, IArmorClassSystem { public override CoreDictionary<Entity, int> Table => IDataSystem.Resolve().Data.armorClass; } public partial struct Entity { public int ArmorClass { get { return IArmorClassSystem.Resolve().Get(this); } set { IArmorClassSystem.Resolve().Set(this, value); } } }
Next open the ComponentInjector script and add the following to its Inject method:
IArmorClassSystem.Register(new ArmorClassSystem());
Armor Class Provider
Let’s use an attribute provider to assign an armor class to each of our combatants so that we won’t need to understand how to calculate those values yet. Add a new C# script to Scripts -> AttributeProvider named ArmorClassProvider and add the following:
using UnityEngine; public class ArmorClassProvider : MonoBehaviour, IAttributeProvider { [SerializeField] int value; public void Setup(Entity entity) { entity.ArmorClass = value; } }
Select the “Hero” Entity Recipe project asset, and add the ArmorClassProvider. Set its “Value” to 18.
Select the “Rat” Entity Recipe project asset, and add the ArmorClassProvider. Set its “Value” to 14.
Attack Roll System
Start by creating a new C# script at Scripts/Combat/Actions named AttackRollSystem and add the following:
public struct AttackRollInfo { public Entity attacker; public Entity target; public int attackRollBonus; public int comboCost; }
I have created a struct called AttackRollInfo that will hold everything I need to know to carry out an attack roll. These are the bits of data that must be provided to the system ahead of time. The system has no way to determine on its own WHO is attacking. Likewise, even if the attack system knows WHO is attacking, it may not be able to determine with WHAT they are attacking. Is the hero swinging his sword or his dagger? A different choice of weapon would likely change the passed in attackRollBonus.
For now, the bits I want to deal with are who is performing the attack, who is the target of the attack, what is the attack roll bonus to give when trying to attack, and what combo cost should be applied if there is a multiple attack penalty.
public interface IAttackRollSystem : IDependency<IAttackRollSystem> { Check Perform(AttackRollInfo info); }
Here I have defined the interface for the new attack roll system. It has only a single method – which can “Perform” an attack roll based on the info that is passed as a parameter. The alternative would have been to pass each of the fields of the struct as a separate parameter, but after about 3 parameters I feel that methods start to get hard to manage. Furthermore, if I should ever need to add, remove or change one of those parameters, then the interface, class, and anything working with this system would need to be refactored. By using the struct, I can add, remove, or change members of the struct but the interface for this method remains the same – no refactoring needed. The Perform method returns a Check which represents the success level of the attack attempt.
Now add the class that implements the interface:
public class AttackRollSystem : IAttackRollSystem { public Check Perform(AttackRollInfo info) { var turnSystem = ITurnSystem.Resolve(); var isAttackerCurrent = turnSystem.Current == info.attacker; var multipleAttackPenalty = isAttackerCurrent ? turnSystem.AttackCount * info.comboCost : 0; var modifier = info.attackRollBonus - multipleAttackPenalty; var dc = info.target.ArmorClass; var result = ICheckSystem.Resolve().GetResult(modifier, dc); if (isAttackerCurrent) turnSystem.TakeAction(1, true); return result; } }
In the Perform method we start by grabbing a reference to the turn system. This is because some of the logic around applying an attack roll depends on if it is the attacker’s turn or not. In particular we will apply a “multiple attack penalty” if it is the attackers turn. That penalty is based on the “comboCost” multiplied by the number of attacks that have been performed. The modifier we will make the “check” with will be the “attackRollBonus” minus the “multipleAttackPenalty”. The difficulty of the “check” comes from the target’s armor class. Finally we plug those values into the CheckSystem and get our result.
Performing an attack roll means that the combatant has actually taken an action, regardless of whether the action is a success. Therefore, we also update the turn system accordingly.
Open the CombatActionsInjector and add the following to its Inject method:
IAttackRollSystem.Register(new AttackRollSystem());
Attack Presenter
Create another new script in the same folder named AttackPresenter and add the following:
using UnityEngine; using Cysharp.Threading.Tasks; public struct AttackPresentationInfo { public Entity attacker; public Entity target; public Check result; } public interface IAttackPresenter : IDependency<IAttackPresenter> { UniTask Present(AttackPresentationInfo info); } public class AttackPresenter : MonoBehaviour, IAttackPresenter { public async UniTask Present(AttackPresentationInfo info) { var view = IEntityViewProvider.Resolve().GetView(info.attacker, ViewZone.Combatant); var combatant = view.GetComponent<CombatantView>(); await ICombatantViewSystem.Resolve().PlayAnimation(combatant, CombatantAnimation.Attack); } private void OnEnable() { IAttackPresenter.Register(this); } private void OnDisable() { IAttackPresenter.Reset(); } }
This script is similar to the presenter we implemented for the stride action. It has a struct of info for all the relevant information that needs to be provided for the system to function. In this case I have added the “target” as relevant information even though I haven’t done anything with the target in this demo. This is because I could see additional polish where if the attack is a “miss” that you might see the target do an evasion. Another option might be that we would want to move the attacker to the target, or make sure it at least faces the correct direction for the attack.
Note that this is a subclass of MonoBehaviour, which means you will add it to the Encounter scene by attaching it to the “Presenters” GameObject.
Solo Adventure Attack
Now we will create the “recipe” script that puts together the necessary bits of information to perform an attack – one specific to our solo adventure. In the future we may create one or more robust attacks that provide more control on whom to attack for example, but for now, we will use a very simple implementation to get an attack working. Create a new C# script at Scripts/SoloAdventure/Encounter named SoloAdventureAttack and add the following:
using UnityEngine; using Cysharp.Threading.Tasks; using System.Linq; public class SoloAdventureAttack : MonoBehaviour, ICombatAction { [SerializeField] int attackRollBonus; [SerializeField] int comboCost; [SerializeField] DiceRoll damage; public async UniTask Perform(Entity entity) { var attacker = ITurnSystem.Resolve().Current; var target = ICombatantSystem.Resolve().Table.First(c => c.Party != attacker.Party); // Perform the Attack Roll var attackInfo = new AttackRollInfo { attacker = attacker, target = target, attackRollBonus = attackRollBonus, comboCost = comboCost }; var attackRoll = IAttackRollSystem.Resolve().Perform(attackInfo); // Present the Attack IAttackPresenter presenter; if (IAttackPresenter.TryResolve(out presenter)) { var presentInfo = new AttackPresentationInfo { attacker = attacker, target = target, result = attackRoll }; await presenter.Present(presentInfo); } // TODO: Apply Damage if applicable switch (attackRoll) { case Check.CriticalSuccess: Debug.Log(string.Format("Critical Hit for {0} Damage!", damage.Roll() * 2)); break; case Check.Success: Debug.Log(string.Format("Hit for {0} Damage!", damage.Roll())); break; default: Debug.Log("Miss"); break; } } }
This script can be attached to the recipe objects for the attack action assets. The “attackRollBonus” represents both a stat modifier and proficiency already combined. The “comboCost” represents whether the attack is made with an agile weapon or not, and the “damage” represents both a base attack strength for a weapon and a bonus from the relevant stat modifier. In the future, we would want more control over all of these values, but as a simple way to get up and running for our demo campaign, hard coding these values replaces a lot of systems that would otherwise be necessary.
When we Perform the attack roll, we will always assume that the attacker is the Entity whose turn it is now. We will also automatically select the target as the first combatant found with a different Party. Since I have only one hero and one monster in my scene, this simple logic is just fine. Note that there is no validation logic in place yet, such as making sure the hero and rat are adjacent before attacking.
In the future, I will want to support whole parties of heroes and monsters, and so will need more control over picking the target. When that time comes, we could use something like the IPositionSelectionSystem to help us select the target to attack.
After making the attack roll, we grab the Attack presenter to play the animation for the attack. Finally I added a placeholder showing how damage could be calculated. This version simply doubles the damage for a critical attack, but some attacks may roll extra dice or different dice instead. Later we will provide another system to actually apply damage, but we need a concept of health or hit points first.
Action Recipe
We need to create new prefab assets based on empty GameObjects to represent the attacks that our hero and monster can perform. Save them to Assets -> Objects -> CombatAction and name one of them “Strike (Shortsword)” (for the hero) and the other “Bite” (for the monster).
Select the asset for the hero and attach the SoloAdventureAttack script with the following:
- Attack Roll Bonus: 7
- Combo Cost: 5
- Damage:
- Count: 1
- Sides: 6
- Bonus: 4
Select the asset for the monster and attach the SoloAdventureAttack script with the following:
- Attack Roll Bonus: 5
- Combo Cost: 5
- Damage:
- Count: 1
- Sides: 6
- Bonus: 2
Make sure to mark both assets as Addressable, then save the project.
Monster Action Flow
Let’s go ahead and make it so that the monster will actually take actions on its turn as well. Open up the MonsterActionFlow script and replace the implementation of its Play method with the following:
public async UniTask<CombatResult?> Play() { var current = ITurnSystem.Resolve().Current; var actionName = current.EncounterActions.names[0]; var action = await ICombatActionAssetSystem.Resolve().Load(actionName); await action.Perform(current); return ICombatResultSystem.Resolve().CheckResult(); }
Stride System
One more quick update – we need to make sure that our other action, stride, is counted as a turn action just like our attack is. Open the StrideSystem and add the following to the end of its Perform method:
ITurnSystem.Resolve().TakeAction(1, false);
Demo
Play the game from the beginning. When you reach the combat scene, try different combinations of attacking and striding. Make sure that after three actions, regardless of what kind they were, that the turn will change to the next combatant. Watch the console to see that you see critical hits, normal hits and also misses and see what kind of damage is potentially dealt from each attack.
Summary
In this lesson we fleshed out the turn system so that we could take multiple actions at a time. This let us attack and see how a multiple attack penalty could change the likelihood of a success. We implemented the Attack Roll and even show an animation for either combatant that attacks. We added Armor Class to indicate how easy a target is to hit. You could even say we implemented a sort of A.I. because the monster now attacks us back on its turn.
If you got stuck along the way, feel free to download the finished project for this lesson here.
If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!