It’s time to take our combat actions in stride.
Overview
In the previous lesson we added an action menu to let a user control the hero’s actions. We added two placeholder actions, one of which is named “Stride”. You can read more about Stride and movement in encounters at the following links:
To summarize in my own words, characters have a stat called “Speed” which is a distance in feet that they can move with a single action. On a game board, each square usually represents 5 feet. So if your character has a speed of 25 feet, then they would be allowed to move 5 squares for a single action. There are different actions for moving, and is why you see “Stride” vs “Step”. A step is only a single tile of movement and can avoid acts of opportunity, whereas a Stride can move much further, but does provoke acts of opportunity.
The goal of this lesson is to begin implementing the Stride action. After the user selects Stride from the action menu, the user should then be able to move a cursor around on the board to chose the location they wish the character to move to. Upon confirming the destination, both the model data and corresponding view should update accordingly. This means we will see the hero game object tween across the board while playing its “walk” animation.
Things outside the scope of this lesson:
- Actually adding the speed stat or relevant system.
- Validation such as keeping the move within the range of the characters speed, and to give consideration to any pathfinding necessary to reach the location.
- Acts of opportunity that could occur based on the movement.
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.
Point
The first thing we need to add is a simple structure that represents an (x,y) coordinate. We will create something custom, because I like to work with integers rather than floats (otherwise I could use a Vector2). This Point struct will be used to represent where entities are positioned in the world, or where we might like to move them to in actions, or where we might want to target with a spell, etc.
Create a new folder at Assets -> Scripts -> Component named Position. Then create a new C# script inside it named Point and add the following:
using System; using UnityEngine; [System.Serializable] public struct Point : IEquatable<Point> { public int x; public int y; public Point(int x, int y) { this.x = x; this.y = y; } public static Point operator +(Point lhs, Point rhs) => new Point(lhs.x + rhs.x, lhs.y + rhs.y); public static Point operator -(Point lhs, Point rhs) => new Point(lhs.x - rhs.x, lhs.y - rhs.y); public static bool operator ==(Point lhs, Point rhs) => lhs.x == rhs.x && lhs.y == rhs.y; public static bool operator !=(Point lhs, Point rhs) => !(lhs == rhs); public static explicit operator Point(Vector3 v) => new Point(Mathf.RoundToInt(v.x), Mathf.RoundToInt(v.y)); public static implicit operator Vector3(Point p) => new Vector3(p.x, p.y, 0); public override bool Equals(object obj) { if (obj is Point) { Point p = (Point)obj; return this == p; } return false; } public bool Equals(Point p) { return this == p; } public override int GetHashCode() { return (x, y).GetHashCode(); } public override string ToString() { return string.Format("Point(x:{0}, y:{1})", x, y); } }
This simple structure simply holds two fields for the ‘x’ and ‘y’ coordinate it represents. I added conformance to IEquatable, and also provided several operators for convenience. This allows us to do things like add or subtract Points, check for equality between Points and convert between a Point and Vector3.
Position System
I want each Entity to be able to have a “position” within their world. The position is implemented as a Point. To manage this new data we will of course create a new system. Create a new C# script within the same folder named PositionSystem and add the following:
public partial class Data { public CoreDictionary<Entity, Point> position = new CoreDictionary<Entity, Point>(); } public interface IPositionSystem : IDependency<IPositionSystem>, IEntityTableSystem<Point> { } public class PositionSystem : EntityTableSystem<Point>, IPositionSystem { public override CoreDictionary<Entity, Point> Table => IDataSystem.Resolve().Data.position; } public partial struct Entity { public Point Position { get { return IPositionSystem.Resolve().Get(this); } set { IPositionSystem.Resolve().Set(this, value); } } }
Nothing special here, we have simply followed the same basic pattern we have used for names, ability scores, skills, etc. where we create a subclass of an Entity Table System. This system maps from an Entity to a Point and wraps the Data named “position”.
To make sure this system is properly injected, open the ComponentInjector and add the following to its Inject method:
IPositionSystem.Register(new PositionSystem());
Position Selection System
Once the user selects “Stride” from the menu, they will need to chose WHERE to stride to. This need to select a position is something that we should make easily reusable because we will probably need it for many different actions.
Create a new C# script at Assets -> Scripts -> SoloAdventure -> Encounter named PositionSelectionSystem and add the following:
using Cysharp.Threading.Tasks; public interface IPositionSelectionSystem : IDependency<IPositionSelectionSystem> { UniTask<Point> Select(Point start); } public class PositionSelectionSystem : IPositionSelectionSystem { public async UniTask<Point> Select(Point start) { var result = start; var indicator = ICombatSelectionIndicator.Resolve(); indicator.SetPosition(start); var input = IInputSystem.Resolve(); while (true) { await UniTask.NextFrame(); if (input.GetKeyUp(InputAction.Confirm)) break; result += new Point( input.GetAxisUp(InputAxis.Horizontal), input.GetAxisUp(InputAxis.Vertical) ); indicator.SetPosition(result); } return result; } private void OnEnable() { IPositionSelectionSystem.Register(this); } private void OnDisable() { IPositionSelectionSystem.Reset(); } }
This is a system with only a single exposed method named Select. This method produces a task that should ultimately return a Point. It begins by getting a reference to a visual element which we name “indicator” (we will add this soon). It makes the indicator visible and positions it at the starting point which was passed as a parameter. As the user provides input, the indicator will move around the board, and whenever a new position is confirmed then the task will be completed.
Open the SoloAdventureInjector and add the following to its Inject method:
ICombatantViewSystem.Register(new CombatantViewSystem()); IPositionSelectionSystem.Register(new PositionSelectionSystem());
Note that in addition to the PositionSelectionSystem that we also handled the injection of the CombatantViewSystem because I forgot to do it when I introduced the class in a previous lesson.
Combat Actions
Regardless of the action a user can select in the action menu, I want to be able to use the same code to trigger it. This means they will all need to conform to the same interface.
Create a new folder at Assets -> Scripts -> Combat named Actions. Then create a new C# script in the same folder named ICombatAction and add the following:
using Cysharp.Threading.Tasks; public interface ICombatAction { UniTask Perform(Entity entity); }
This is the shared interface that all combat actions should implement.
Stride
Each combat action will be implemented using a script on a GameObject. It will handle the flow of code necessary to obtain the information needed by a system to apply the action. In this case, a Stride action can only be applied by a system if it knows what to move and where to move it.
Create another C# script in the same folder named Stride and add the following:
using UnityEngine; using Cysharp.Threading.Tasks; public class Stride : MonoBehaviour, ICombatAction { public async UniTask Perform(Entity entity) { // TODO: differentiate between user-controlled and AI controlled entities var position = await IPositionSelectionSystem.Resolve().Select(entity.Position); var info = new StrideInfo { entity = entity, destination = position }; await IStrideSystem.Resolve().Apply(info); } }
The entity performing this action is passed as a parameter. The destination must then be chosen as part of this scripts flow. In this case, I have only applied logic to acquire a position for a user-controlled entity. Later on we may add logic for A.I. controlled units as well. We will need additional systems to differentiate between types of entities, so that will have to come later.
Stride System
Once we have acquired the necessary information to perform a stride action, we need to actually apply it. This means updating the game data so that an Entity’s Position is changed. This system should also know when to “Present” the changed data on screen, though another system will be responsible for handling that.
Create a new C# script in the same folder named StrideSystem and add the following:
using Cysharp.Threading.Tasks; public struct StrideInfo { public Entity entity; public Point destination; } public interface IStrideSystem : IDependency<IStrideSystem> { UniTask Apply(StrideInfo info); } public class StrideSystem : IStrideSystem { public async UniTask Apply(StrideInfo info) { // TODO: Check for act of opportunity before leaving current square await Present(info); Perform(info); // TODO: Check for act of opportunity after arriving at new square } private async UniTask Present(StrideInfo info) { IStridePresenter presenter; if (IStridePresenter.TryResolve(out presenter)) { var presentInfo = new StridePresentationInfo { entity = info.entity, fromPosition = info.entity.Position, toPosition = info.destination }; await presenter.Present(presentInfo); } } private void Perform(StrideInfo info) { var entity = info.entity; entity.Position = info.destination; } }
Here we have defined a new struct that holds the information necessary for our system to actually perform a stride action. Specifically, we need to know what entity to move and where to move it to. Then we added a new interface for our system with a single method named “Apply”. It accepts the info stuct and returns a Task, indicating that the application of the action could occur over time. This is because when we implement it we can also implement a system to Present the action and will do so with animation.
In the concrete system, I have left a couple of “TODO” statements where acts of opportunity may need to be handled. We don’t have enough in place to implement that yet, so I have just left a reminder to myself that it needs to be revisited later. We are able to actually Present and Perform the action.
The presentation of the action will be handled by yet another system, which also wants its own information. In that case I decided I wanted to know what is moving, where it moved from and also where it should move to. I did this so that the presenter would not need to make assumptions about whether or not the data had been applied before or after it was presented.
The performing of the action merely updates the data by assigning the Entity’s Position to be the destination that was included with the info.
Stride Presenter
Next we want a system that is solely devoted for handling what it “looks” like when a stride action is triggered. One benefit of keeping this separated from the StrideSystem is that you could more easily switch from 2d to 3d, or to another engine entirely, and would only need to update the code that was specific to the parts that changed anyway.
Create a new C# script in the same folder named StridePresenter and add the following:
using UnityEngine; using Cysharp.Threading.Tasks; public struct StridePresentationInfo { public Entity entity; public Point fromPosition; public Point toPosition; } public interface IStridePresenter : IDependency<IStridePresenter> { UniTask Present(StridePresentationInfo info); } public class StridePresenter : MonoBehaviour, IStridePresenter { [SerializeField] float speedMultiplier = 0.25f; public async UniTask Present(StridePresentationInfo info) { Vector3 delta = info.toPosition - info.fromPosition; var view = IEntityViewProvider.Resolve().GetView(info.entity, ViewZone.Combatant); var combatant = view.GetComponent<CombatantView>(); ICombatantViewSystem.Resolve().SetAnimation(combatant, CombatantAnimation.Walk); await view.transform.MoveTo(info.toPosition, speedMultiplier * delta.magnitude).Play(); ICombatantViewSystem.Resolve().SetAnimation(combatant, CombatantAnimation.Idle); } private void OnEnable() { IStridePresenter.Register(this); } private void OnDisable() { IStridePresenter.Reset(); } }
We start out by defining a new struct that holds each of the bits of data I thought I might like to know while presenting this action. Specifically, I want to know what is moving, where it moved from and where it is moving to. Having both the “from” and “to” means I don’t need to worry about whether or not the game data will be updated before or after this code runs. It also makes it easier for me to determine the distance the view will actually move so I can calculate how long it should take in a tweened animation.
As a pattern, I will probably add my “Presenter” scripts to the scene so that I can more easily tweak the way things look. For example, a “speed multiplier” is exposed as a serialized field. In the event that I also wanted sounds, particles, etc, being a MonoBehaviour could allow me to more easily obtain references to components or assets that I might need.
Combat Actions Injector
Over time, there will be many actions created for a game like this, so let’s add a new Injector script to handle them all. Create a new C# script in the same folder named CombatActionsInjector and add the following:
public static class CombatActionsInjector { public static void Inject() { IStrideSystem.Register(new StrideSystem()); } }
Then, we will need to invoke our new injector in a parent injector. Open CombatInjector and add the following to its Inject method:
CombatActionsInjector.Inject();
Combat Action Asset System
In practice, I will create each action as a GameObject which can be loaded as an addressable asset. As I have done for other assets, let’s make a system that specializes in loading these action assets and returns it according to the correct interface.
Create a new C# script at Assets -> Scripts -> AssetManager named CombatActionAssetSystem and add the following:
using UnityEngine; using Cysharp.Threading.Tasks; public interface ICombatActionAssetSystem : IDependency<ICombatActionAssetSystem> { UniTask<ICombatAction> Load(string assetName); } public class CombatActionAssetSystem : ICombatActionAssetSystem { public async UniTask<ICombatAction> Load(string assetName) { var assetManager = IAssetManager<GameObject>.Resolve(); var key = string.Format("Assets/Objects/CombatAction/{0}.prefab", assetName); var prefab = await assetManager.LoadAssetAsync(key); return prefab.GetComponent<ICombatAction>(); } }
We will also need to handle injection for this, so open the AssetManagerInjector and add the following to its Inject method:
ICombatActionAssetSystem.Register(new CombatActionAssetSystem());
Stride Action Asset
Create a new folder at Assets -> Objects named CombatAction. Create an Empty GameObject named “Stride”, and convert it to a prefab asset saved in this new folder. Attach the Stride script component. Finally, check the box to make sure it is Addressable. Save your project.
Hero Action Flow
The Hero Action Flow is responsible for showing the Action Menu and then triggering the selected action. Open the script and modify the Play method to look like this:
public async UniTask<CombatResult?> Play() { var hero = ISoloHeroSystem.Resolve().Hero; ICombatSelectionIndicator.Resolve().SetPosition(hero.Position); ICombatSelectionIndicator.Resolve().SetVisible(true); var menu = IActionMenu.Resolve(); await menu.Setup(); await menu.TransitionIn(); var actionName = await menu.SelectMenuItem(); await menu.TransitionOut(); var action = await ICombatActionAssetSystem.Resolve().Load(actionName); await action.Perform(hero); ICombatSelectionIndicator.Resolve().SetVisible(false); return ICombatResultSystem.Resolve().CheckResult(); }
We start by grabbing the hero reference and moving our indicator to its location. After the action menu is dismissed we then load the relevant asset and then “Perform” it. After the action completes, we can hide the indicator until it is needed again.
Encounter Scene
Download then unzip and import the contents from this package. It holds the combat selection indicator: the sprites, animations, and scripts.
Open the Encounter scene then add an instance of our newly imported prefab, Selection_Indicator, as a child of the Demo GameObject in the scene.
Next, create an empty GameObject named “Presenters” as a child of the Demo GameObject. Add the StridePresenter script as a component to this new object.
Demo
Run the game from the loading screen. When you reach the Encounter scene select the “Stride” action. You should see an indicator appear. Move the indicator to a new location using the arrow keys on your keyboard, then hit the Return key to confirm it. You should see the menu dismiss, then watch as the warrior walks to the new position. You can repeat this as often as you like.
Note that there are a few “bugs” (really just features that are not implemented yet). For starters, when the indicator appears the very first time, it does not appear over the warrior. This is because we never told the “model” to match where we had placed the “view”, so the game thinks the position of the warrior is actually at the default value of (0,0). We will fix this when we place the hero and monster views dynamically.
Another more serious issue is that if you select the attack option from the menu, then the game will crash with some errors. This is because we are trying to load an asset that doesn’t yet exist, and then attempt to grab a component from a null object. This points out the importance of error-handling, which I have not addressed yet for this project. Generally speaking you don’t want your game to crash, but you need a plan on how to handle it. What “should” you do if an asset isn’t available? We will eventually add the the other action asset, so the problem will go away.
Summary
In this lesson, we implemented several new systems and data structures necessary to hook up our first combat action – Stride! A hero can now move freely around the game board. 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!