Make a CCG – Action Viewer

In the previous lesson, we implemented the systems and models necessary to change turns. However, outside of watching a checkmark appear in a unit test, there was no visible evidence of anything occuring. Having a “view” to represent the data is an important step of the process, but is something that really can be anything you want, and therefore should be separated as much as possible. The final result could be 2D or 3D. It doesn’t have to be animated, though it definitely could be, and might even include stuff like flashy particles, who knows? The important thing is that a user can understand and interact with the game.

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.

Game Factory

In this lesson we will be doing a lot of the setup work so that Unity can serve as the “view” of our game. Some of the code will look familiar to what you might have already seen in a unit test, but this will be the “official” implementation that we are doing now. To begin with, we want a single place where we create and configure our game container with all of the systems and various other aspects that might need to be used. This can be done in a static factory class that looks like this:

public static class GameFactory {

	public static Container Create () {
		Container game = new Container ();

		// Add Systems
		game.AddAspect<ActionSystem> ();
		game.AddAspect<DataSystem> ();
		game.AddAspect<MatchSystem> ();

		// Add Other
		game.AddAspect<StateMachine> ();
		game.AddAspect<GlobalGameState> ();

		return game;
	}
}

Note that this code wont compile yet, but it will once you’ve finished implementing the new classes in a bit. Otherwise, there is nothing much special here. You should already understand how to add aspects to a container. As we add more systems in the future, we will need to remember to attach them here.

Game View System

Next, we are going to create a special kind of system. The purpose of this system is to serve as a sort of bridge between the Unity view hierarchy and the data that drives it all. Once you’ve created it, you will want to add it as a component to the “Game” root object in the Unity scene.

public class GameViewSystem : MonoBehaviour, IAspect {

	public IContainer container { 
		get {
			if (_container == null) {
				_container = GameFactory.Create ();
				_container.AddAspect (this);
			}
			return _container;
		}
		set {
			_container = value;
		}
	}
	IContainer _container;

	ActionSystem actionSystem;

	void Awake () {
		container.Awake ();
		actionSystem = container.GetAspect<ActionSystem> ();
	}

	void Start () {
		Temp_SetupSinglePlayer ();
		container.ChangeState<PlayerIdleState> ();
	}

	void Update () {
		actionSystem.Update ();
	}

	void Temp_SetupSinglePlayer() {
		var match = container.GetMatch ();
		match.players [0].mode = ControlModes.Local;
		match.players [1].mode = ControlModes.Computer;
	}
}

Note that this class inherits from “MonoBehaviour” so it can be attached as a “Component” to a “GameObject”. It also implements the “IAspect” interface so it can be attached to the same “IContainer” as our other game systems. This is one of the strengths of designing the Aspect-Container architecture around interfaces instead of abstract classes. Had I only made a base class for an Aspect, then this implementation wouldn’t have been possible because any given class can only inheret from a single parent class.

In order to implement the “IAspect” interface, I needed to provide an “IContainer” property with both a getter and setter. I did that here using a pattern called a lazy-loading. What that means is that the “IContainer” itself wont be created until something accesses the property. At a minimum the property will be accessed in the “Awake” method, but it is possible that other “MonoBehaviour” based scripts will run their own “Awake” method first and will already want to cache their own reference to this container. Regardless of when it is made, I know that it will exist when I need it.

In the “Awake” method, I use the Container extension method to “Awake” any other non-MonoBehaviour systems that are attached to the container. Then I cache a reference to the “ActionSystem” of our container, which I will later use in the “Update” method to allow it to play out over time.

In the “Start” method, I call a simple and temporary method (“Temp_SetupSinglePlayer”) which configures our game as a single-player game. This won’t be the final resting place of such code, and is really only there so that we can demo a working product faster. I also set our state machine’s current state to be a “PlayerIdleState”. Both the StateMachine and States will be created in a bit, but for now it is enough to know that in this lesson I will only allow input on the views when it is the local player’s turn AND the game state is idle.

State Machine

A state machine is a pretty commonly used pattern in games. It is a “state” as an “object” pattern which makes it really easy to respond to changes in state. I’ve created several reusable “StateMachine” implementations in my previous projects, so I won’t discuss the idea in-depth this time. However, I do want to point out that I am not using any of my previous versions because I decided to create a version that would work with the new Aspect-Container architecture.

Before you look at the StateMachine code, let’s first look at a State:

public interface IState : IAspect {
	void Enter ();
	bool CanTransition (IState other);
	void Exit ();
}

public abstract class BaseState : Aspect, IState {
	public virtual void Enter () {}
	public virtual bool CanTransition (IState other) { return true; }
	public virtual void Exit () {}
}

I created an “IState” interface which inherits from the “IAspect” interface. This means that a state will be able to be added to a container just like our systems. There are three methods expected of a State: we will call “Enter” when a state first becomes the “current” state of a state machine. Before transitioning to another state, we will call the “CanTransition” method. The “BaseState” implementaiton for this always returns true, though some subclasses may need special rules where a change of state should not be allowed. Finally, we will call “Exit” on a current state before transitioning to any new state.

Now look at the “StateMachine” implementaiton:

public class StateMachine : Aspect {

	public IState currentState { get; private set; }
	public IState previousState { get; private set; }

	public void ChangeState<T> () where T : class, IState, new () {
		IState fromState = currentState;
		T toState = container.GetAspect<T> () ?? container.AddAspect<T> ();

		if (fromState != null) {
			if (fromState == toState || fromState.CanTransition(toState) == false)
				return;
			fromState.Exit ();
		}

		currentState = toState;
		previousState = fromState;
		toState.Enter ();
	}
}

public static class StateMachineExtensions {
	public static void ChangeState<T> (this IContainer game) where T : class, IState, new () {
		var stateMachine = game.GetAspect<StateMachine> ();
		stateMachine.ChangeState<T> ();
	}
}

The StateMachine is pretty simple. It holds a current and previous state and can update those references via the generic “ChangeState” method. The generic constraints on the ChangeState method allow us to do a unique pattern where we first attempt to get the new target state from the container, but when one is not found, we create it and add it automatically. Note that a full transition is not gauranteed – if you set the next state to be the same as the current state, or if the state returns false for its “CanTransition” method, then no further action will take place.

Like we have done before, I also created an extension method so that the IContainer could appear to function as if it were a state machine.

Player Idle State

For our first real game state, I have created a “Player Idle State”. When the game is in this state, the current player should be allowed to perform any player actions such as playing a card from his hand, attacking, or ending his turn. Since the only action we have actually implemented so far is for changing turns, and also because we have not yet created an A.I., all this code does for now is to automatically change the turn if the current player is not the local player. Otherwise it does nothing, but this is because we merely need to wait for the player to interact with our “view” system for changing a turn.

public class PlayerIdleState : BaseState {
	public override void Enter () {
		Temp_AutoChangeTurnForAI ();
	}

	void Temp_AutoChangeTurnForAI () {
		if (container.GetMatch ().CurrentPlayer.mode != ControlModes.Local) {
			container.GetAspect<MatchSystem> ().ChangeTurn ();
		}
	}
}

Sequence State

Next, we will want another game state. The purpose of this state is to mark the duration of time that the ActionSystem is running. It doesn’t actually need to do anything as it is simply an indicator of what is going on.

public class SequenceState : BaseState {

}

Global State

We will also create a final state called the Globabl State (shhh its not actually a state). This is sort of a state for our game that is always active.

public class GlobalGameState : Aspect, IObserve {

	public void Awake () {
		this.AddObserver (OnBeginSequence, ActionSystem.beginSequenceNotification);
		this.AddObserver (OnCompleteAllActions, ActionSystem.completeNotification);
	}

	public void Destroy () {
		this.RemoveObserver (OnBeginSequence, ActionSystem.beginSequenceNotification);
		this.RemoveObserver (OnCompleteAllActions, ActionSystem.completeNotification);
	}

	void OnBeginSequence (object sender, object args) {
		container.ChangeState<SequenceState> ();
	}

	void OnCompleteAllActions (object sender, object args) {
		container.ChangeState<PlayerIdleState> ();
	}
}

This state is already added to the Container by the Factory. It is an observer that listens to notifications from the Action System for when a new sequence begins, and for when the root sequence ends. It handles the transitions between the “PlayerIdleState” and “SequenceState” and may handle additional logic in the future.

Change Turn Viewer

The setup work is done. Now let’s focus on actually showing, and interacting with, the Change Turn action. Of course you can ultimately show whatever you want for this, but since this project is inspired by Hearthstone we will generally mimic their UI. On the right edge of the screen they have a button for changing turns. If it is your turn it says “End Turn” – because that is what it will do when you click it. Once pressed, it flips over and reads “Enemy Turn” and clicking it wont do you any good. Just in case that wasn’t obvious enough, they also show a big banner right in the middle of the screen when it is “Your Turn” so you will know to start doing stuff.

My only “slightly” less attractive programmer art equivalent appears below:

I’m not going to cover the details of how to create your own really amazing box and canvas with text, because I am assuming that you have a certain understanding of Unity already. Of course, if for whatever reason you are struggling with it, feel free to refer to and/or directly use my sample project here.

In addition to the new GameObjects I added to the scene, I created two new component scripts as well. First I created a component on the button hieararchy called “ChangeTurnButtonView”. All that this script does is to maintain convenient references. I parented a “cube” and the top and bottom canvases (that say “End Turn” or “Enemy Turn”) all to a single object called the “rotationHandle” so that I could rotate everything at once. The references to the UI Text components are provided in case the text should ever need to change (maybe you want to localize the app or something).

public class ChangeTurnButtonView : MonoBehaviour {
	public Transform rotationHandle;
	public Text allyText;
	public Text enemyText;
}

The other script is where most of the work takes place. This script is called the “ChangeTurnView” which appears in snippets below:

public class ChangeTurnView : MonoBehaviour {
	[SerializeField] Transform yourTurnBanner;
	[SerializeField] ChangeTurnButtonView buttonView;
	IContainer game;
	
	// Add next code snippets here
}

The ChangeTurnView inherits from MonoBehaviour because it is a component that needs to be attached to an object in the scene. There are three fields, two of which are exposed to the inspector thanks to the [SerializeField] tag. The “yourTurnBanner” is a reference to a Transform which holds the canvas that displays the banner that tells you when it is “Your Turn” with really big words so you hopefully don’t miss it. The “buttonView” is a reference to the script on the button hieararchy that flips over depending on whose turn it is. The “game” is a reference to the container that holds all our systems – it will be cached on Awake.

public void ChangeTurnButtonPressed () {
	if (CanChangeTurn ()) {
		var system = game.GetAspect<MatchSystem> ();
		system.ChangeTurn ();
	} else {
		// TODO: Play an error input sound effect?
	}
}

Next I have provided a public “ChangeTurnButtonPressed” method which is the target of a “Button” component that is attached to the Cube. It will be invoked if you click the button such as by using your mouse in Play mode. In the method, we check to see if we are allowed to change the turn or not. If so, we call the relevant method on the MatchSystem. If not, I have an “else” block just waiting to be filled with something (which I am leaving up to you).

bool CanChangeTurn () {
	var stateMachine = game.GetAspect<StateMachine> ();
	if (!(stateMachine.currentState is PlayerIdleState))
		return false;

	var player = game.GetMatch ().CurrentPlayer;
	if (player.mode != ControlModes.Local)
		return false;

	return true;
}

To actually determine whether or not the user should be able to take an action, I provided the “CanChangeTurn” method. It checks to make sure that the StateMachine’s current state is the “PlayerIdleState” and also requires the current player to be a local player.

void Awake () {
	game = GetComponentInParent<GameViewSystem> ().container;
}

void OnEnable () {
	this.AddObserver (OnPrepareChangeTurn, Global.PrepareNotification<ChangeTurnAction> (), game);
}

void OnDisable () {
	this.RemoveObserver(OnPrepareChangeTurn, Global.PrepareNotification<ChangeTurnAction> (), game);
}

Next I have implemented a few common MonoBehaviour methods. In “Awake”, I cache a reference to the game container which I am able to retrieve from the “GameViewSystem” thanks to the hierarchy of my game objects in the scene. I use the “OnEnable” and “OnDisable” methods as opportunities to handle the adding and removing of my notification observer.

void OnPrepareChangeTurn (object sender, object args) {
	var action = args as ChangeTurnAction;
	action.perform.viewer = ChangeTurnViewer;
}

In this case, I don’t need any “viewer” attached to the “prepare” phase of the “ChangeTurnAction” so I can simply listen to that notification and attach a viewer to the “perform” phase. If I had needed a viewer for both phases, I could still listen to the notification for the beginning of the action sequence, but I would need to compare the action’s id to make sure it was the right kind of sequence.

IEnumerator ChangeTurnViewer (IContainer game, GameAction action) {
	var dataSystem = game.GetAspect<DataSystem> ();
	var changeTurnAction = action as ChangeTurnAction;
	var targetPlayer = dataSystem.match.players [changeTurnAction.targetPlayerIndex];

	var banner = ShowBanner (targetPlayer);
	var button = FlipButton (targetPlayer);
	var isAnimating = true;

	do {
		var bannerOn = banner.MoveNext ();
		var buttonOn = button.MoveNext ();
		isAnimating = bannerOn || buttonOn;
		yield return null;
	} while (isAnimating);
}

The “ChangeTurnViewer” functions pretty similarly to a Coroutine and displays the “perform” state of the action for changing turns. The animations that play out will be different depending on whose turn it will be. For example, I wont even show the banner when the target player is the “enemy” player. However, I will show animations for the banner and for the button if the target player is the “local” player. Since the animations play out at different durations, I simply loop while either of them is still going.

IEnumerator ShowBanner (Player targetPlayer) {
	if (targetPlayer.mode != ControlModes.Local)
		yield break;

	var tweener = yourTurnBanner.ScaleTo(Vector3.one, 0.25f, EasingEquations.EaseOutBack);
	while (tweener.IsPlaying) { yield return null; }

	tweener = yourTurnBanner.Wait (1f);
	while (tweener.IsPlaying) { yield return null; }

	tweener = yourTurnBanner.ScaleTo (Vector3.zero, 0.25f, EasingEquations.EaseInBack);
	while (tweener.IsPlaying) { yield return null; }
}

The ShowBanner sequence aborts early if the target player is not the local player. Otherwise it plays a sequence of animations that cause the banner to scale from zero to its full size, hold on screen for a second, and then shrink back to zero so that it is hidden again. Note that you could also have created a Unity animation asset for this and waited for a keyframe or something instead.

IEnumerator FlipButton (Player targetPlayer) {
	var up = Quaternion.identity;
	var down = Quaternion.Euler (new Vector3(180, 0, 0));
	var targetRotation = targetPlayer.mode == ControlModes.Local ? up : down;
	var tweener = buttonView.rotationHandle.RotateTo(targetRotation, 0.5f, EasingEquations.EaseOutBack);
	while (tweener.IsPlaying) { yield return null; }
}

The FlipButton sequence causes the button to rotate to face up or face down depending on the player control mode. In this case I am animating the rotation using quaternions instead of euler angles. In case you wonder why, check out this little video that compares both approaches:

The quaternion animation rotates nice and smooth, exactly as I had expected. The rotation by euler angles does fine in one direction, but looks a little drunk on the return trip. Can you guess which is which?

Demo

Play the game scene and tap the “End Turn” button. It should flip over to the “Enemy Turn” side, and then immediately flip back as it becomes your turn again. On your turn you should also see a large banner appear in the center of the screen which says that it is “Your Turn”. Since the input is constrained by the State Machine and current player’s control mode, you can “spam” tap the button without worry. It wont actually cause multiple “ChangeTurnActions” to be queued up simultaneously. You wont actually be able to trigger another action until all of the animation has stopped and the game state is idle on your turn.

Summary

In this lesson we set up a lot of architecture necessary for Unity to become the view for our game. Then we demonstrated how to use scene input to interact with our game systems and then watch an animation as the actions are actually carried out.

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!

Leave a Reply

Your email address will not be published. Required fields are marked *