We’ve laid some nice foundations in the past few lessons, and now its time to actually start building up the game on top of it all. We’ll start with somthing pretty simple to help drill in the basic ideas, while also adding a little more functionality. Since we are making a turn based game, we’ll begin by learning to change turns.
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.
Data System
We’re going to want to change a turn, and the way we will accomplish it is by changing the relevant value of a match. This means that before I can actually “change turns”, I’m going to need an instance of a match to exist somewhere. We can maintain a bit of organization by keeping all persistent data in a single location. In this case, that location will be a system whose job it will be to also handle saving and loading that data in the future. We don’t need to worry about all of that yet – let’s just add an instance of the match for now.
[csharp]
public class DataSystem : Aspect {
public Match match = new Match();
}
public static class DataSystemExtensions {
public static Match GetMatch (this IContainer game) {
var dataSystem = game.GetAspect
return dataSystem.match;
}
}
[/csharp]
As you can see it is quite a simple setup. The key points are that the system inherits from “Aspect” so that it can be added to the same container as our other systems, and that it holds an instance of a match.
Because the data held by the data system will need to be accessed so frequently, I also added a simple extension method to the “IContainer” so that it would feel like the container holds the match directly. It wasn’t a necessary step, but it could save a line or two of code in enough places that it felt like a nice feature.
If you like the idea of the extension class we used here, you may also consider adding it in many more places. For example, the Action System will be used frequently enough that you may want to expose the abiltiy to perform actions and reactions as though they were also available directly from the container:
[csharp]
public static class ActionSystemExtensions {
public static void Perform (this IContainer game, GameAction action) {
var actionSystem = game.GetAspect
actionSystem.Perform (action);
}
public static void AddReaction (this IContainer game, GameAction action) {
var actionSystem = game.GetAspect
actionSystem.AddReaction (action);
}
}
[/csharp]
Change Turn Action
The purpose of our game action model is to provide context to some system that will actually apply the logic to the game model(s) as necessary. In order for a system to know how to change a turn, I technically don’t need any extra information – or at least I wouldn’t if it is safe to assume that control will ALWAYS pass from one player to the next. In truth, I think that is probably the way it actually is in Hearthstone.
Regardless, I decided to demonstrate a slightly more flexible action. We will specify the next player’s index in the action itself. This gives notificaiton observers a chance to modify whose turn will actually come next. If we were playing something like Uno, then playing a “skip” or “reverse” card could change the normal flow of player turns. In the same way, I can imagine special cards in our CCG that might also allow a player to take another turn – though they would need to be used sparingly because I would imagine it to be very powerful. If you’re curious, the unit testing code includes a test to override the default value and allow a player an extra turn.
[csharp]
public class ChangeTurnAction : GameAction {
public int targetPlayerIndex;
public ChangeTurnAction (int targetPlayerIndex) {
this.targetPlayerIndex = targetPlayerIndex;
}
}
[/csharp]
Thanks to everything we packed into the base class, there is almost nothing here. Technically I didn’t even have to include the custom constructor. We really just need a public field to hold the target player index. We assign it the default value for the next player, but any other system may change the value via a card’s special ability, etc. before the action actually gets performed.
Observers
When running our new game action through the action system, notifications for its preparation and performance phases will be posted. It will be the job of another system to observe each notification and implement any necessary logic. While the notification code itself is super simple to use, it also requires a degree of discipline.
Adding a notification observer is easy and can be done just about anywhere, including in a constructor. The removal of the notification observer is not as easy. You can’t, for example, rely on a destructor for this purpose. This is because the notification center maintains strong pointers which will prevent the object from being destroyed.
If we were working with MonoBehaviour there would be obvious locations to both add and remove observers such as Awake / Destroy, or OnEnable / OnDisable. Although I don’t want to use a MonoBehaviour, I don’t have anything against copying some of the patterns I actually liked. In my opinion we can even improve on the idea. The MonoBehaviour uses reflection to figure out what methods are actually implemented, which means that you don’t get auto-complete in your code editor when typing out your method names. You may forget if it was supposed to be “OnEnable” vs “Enable” or “Start” vs “OnStart”. Instead of reflection, we can have our systems implement an interface for whichever features we want to support. Take a look at a sample which gives us “Awake” functionality:
[csharp]
public interface IAwake {
void Awake();
}
public static class AwakeExtensions {
public static void Awake (this IContainer container) {
foreach (IAspect aspect in container.Aspects()) {
var item = aspect as IAwake;
if (item != null)
item.Awake ();
}
}
}
[/csharp]
So now, any system that implements “IAwake” can have an “Awake” method called. I created an extension method of a container to loop through all of its aspects checking for the implementation of this interface, and invoking it if found. In practice, after building the game container through some sort of factory method, I would next use this extension. I would know that all of the systems already existed before running “Awake” in case one of the systems wanted to cache a reference to another, or connect notifications, etc.
Since I mentioned wanting a way to also remove notifications, let’s mimic the “Destroy” functionality of a MonoBehaviour too:
[csharp]
public interface IDestroy {
void Destroy();
}
public static class DestroyExtensions {
public static void Destroy (this IContainer container) {
foreach (IAspect aspect in container.Aspects()) {
var item = aspect as IDestroy;
if (item != null)
item.Destroy ();
}
}
}
[/csharp]
As you might imagine, if you continued creating a new interface for every single method, then your class definitions would start to get a bit unwieldly. We can help by making use of interface inheritance. If I know that I want both an “Awake” method for adding notifications and a “Destroy” method for removing notifications, then I could define a new interface that requires both like so:
[csharp]
public interface IObserve : IAwake, IDestroy {
}
[/csharp]
And now any class which implements the single “IObserve” interface will have both an “Awake” and “Destroy” method.
Match System
Let’s do a quick recap. We have a system that holds our game models (at the moment this is just a single match object). We have created a game action with a context for modifying a game model. The game action will be passed through the action system which will also cause notifications to be posted. A system is supposed to “observe” these notifications and actually apply the logic. This last step is what we are creating now. Since the action is modifying a match object, I decided to create a match system to handle the process.
[csharp]
public class MatchSystem : Aspect, IObserve {
public void ChangeTurn () {
var match = container.GetMatch ();
var nextIndex = (1 – match.currentPlayerIndex);
ChangeTurn (nextIndex);
}
public void ChangeTurn (int index) {
var action = new ChangeTurnAction (index);
container.Perform (action);
}
public void Awake () {
this.AddObserver (OnPerformChangeTurn, Global.PerformNotification
}
public void Destroy () {
this.RemoveObserver (OnPerformChangeTurn, Global.PerformNotification
}
void OnPerformChangeTurn (object sender, object args) {
var action = args as ChangeTurnAction;
var match = container.GetMatch ();
match.currentPlayerIndex = action.targetPlayerIndex;
}
}
[/csharp]
Note that the class inherits from “Aspect” – this is what allows it to be connected to the same container as the “DataSystem” and “ActionSystem” so that all three systems can work together. It also implements the “IObserve” interface – this is what let’s us know to invoke the “Awake” and “Destroy” methods to give the class a chance to add and remove its notification observers.
I provided a public “ChangeTurn” method that initiates the process of changing a turn (but note that the turn wont actually change until the action system goes through its phases). You might call this method after a user presses a button to end his turn. I also provided an overloaded version which accepts an index. This may not be necessary in a pure Hearthstone clone, but could be useful in the Uno example from before.
Once the perform phase runs, and the notification is observed, we will use the “OnPerformChangeTurn” handler to actually apply the logic of changing the turn based on whatever is now available in the action’s context.
Demo
There isn’t a “real” demo for this lesson outside of being able to look at the various unit tests that I have included in the project for you. You can always run them and see that they pass, but admittedly it’s not terribly exciting to see a green checkmark appear. This is largely due to the fact that in a way, we have really only implemented “half” of the action – the data and systems are working. The next half to implement is some sort of “viewer” to display to the user what is happening behind the scenes. We’ll add support for this next, so stay tuned!
Summary
In this lesson we tied together several systems to demonstrate a simple task – the ability to change turns. We actually created a placeholder for the DataSystem which holds the match, we created our first Game Action subclass, and we also created a Match System to handle the new action. We also added some more interfaces related to the observation of notifications so that it would be easy to both add and remove our notificaiton handlers at appropriate times.
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!
Really cool tutorials.
Btw I don’t know if you are aware, but your design pattern looks a lot like ECS.
You should take a look at Entitas for Unity, I think you will like it:
https://github.com/sschmid/Entitas-CSharp
Cheers
Thanks, I’m glad you like them. I am familiar with ECS – I used a variant by Adam Martin in my previous project – the unofficial pokemon board game. After being influenced by his rather strong opinions on the topic, some of the design decisions I saw in Entitas didn’t seem to match well so I may not have given it a fair chance. Perhaps I’ll take another look later.
Thank you for posting such quality work, i have a question about your Tactics Project is it appropriate to ask here or is there a better place for such questions?
Glad you liked it. Feel free to ask questions here or on my forum. I personally think its a little easier to understand detailed Q&A on the forum but simple questions are fine here.