The screens we have implemented so far appear and disappear immediately. That looks fine for some screens, but chances are good you will eventually want to add a bit more polish. In this lesson we will implement a couple more screens that animate onto and off of the screen. We will use a couple of different animations, and see how to continue in our flow after the animations complete.
Transitions
The work of actually animating a screen change is triggered in the various “Transition” classes that I already included with the “Scripts/Common/UI/Transition” libraries. They are built on top of the dynamic animation classes that I also wrote and already included in the project. I’ve used the animation classes multiple times across a variety of projects and first wrote about them here, so I won’t cover it again now. The transition classes are new to this project though, so let’s take a minute to glance over the code you’ll find within them.
Base Transition
All of the transitions inherit from the same base class, BaseTransition. This is an abstract class that serves as nothing more than an interface, but is actually a class because Unity’s implementation with classes is better than with true interfaces. Our BaseViewController class will have a field of this data type, and we will be able to connect any of the subclass implementations to it.
There are two methods we care about: the ability to Show and to Hide a screen. At this level, we don’t care “how” the screen transitions or even how long it takes, but we do want to be able to invoke an action whenever the transition has completed, so we pass that along as a parameter.
public abstract class BaseTransition : MonoBehaviour { public abstract void Show (Action didShow = null); public abstract void Hide (Action didHide = null); }
Scale Transition
In this project I use a scale transition to make screens grow from or shrink to the center of the screen such as the dialog saying whose turn it is. With the “Game” scene open, look in the hierarchy panel at the “Master Controller -> General -> Dialog Screen -> Panel” to see an implementation for this transition. Next, check out the “ScaleTransition” class to see how the code works.
The transitions each follow the same basic implementation. One of the first things you will see is a nested class – a simple serializeable data structure that represents “key frames” for an animation. Basically it will be there to hold the information for the “on-screen” and “off-screen” targets, and it is something we can configure in Unity’s inspector. For the scale transition, I want to know three bits of information at each key frame: what the scale should be, how long it should take, and what type of easing curve should be used in the tween.
[System.Serializable] public class Data { public Vector3 scale; public float duration; public Func<float, float, float, float> equation; }
For convenience sake, I have also created a default show and hide data for you. Whenever you first attach the component to a game object, you will see these two fields already available and preconfigured with the values I specified. You are free to modify them however you like in the inspector, but at least you have some starting values to help give a good idea of the proper use.
public Data showData = new Data { scale = Vector3.one, duration = 0.5f, equation = EasingEquations.EaseOutBack }; public Data hideData = new Data { scale = Vector3.zero, duration = 0.5f, equation = EasingEquations.EaseInBack };
Next, we need to implement the abstract portions of the base class. We will provide a Show and Hide implementation that tweens between the show and hide key frames using a Tweener. When the animation completes, we will then invoke the completion action assuming one was provided.
public override void Show (System.Action didShow) { transform.localScale = hideData.scale; StartCoroutine (Process (showData, didShow)); } public override void Hide (System.Action didHide) { transform.localScale = showData.scale; StartCoroutine (Process (hideData, didHide)); } IEnumerator Process (Data data, Action complete) { Tweener tweener = transform.ScaleTo (data.scale, data.duration, data.equation); while (tweener != null) yield return null; if (complete != null) complete(); }
Panel Transition
I also use the panel transition which allows screen elements to slide onto or off of the screen. The Panel component is another reusable script that I wrote and have been using repeatedly throughout my projects. I first wrote about it in my Tactics RPG project here. The Panel is already a sort of wrapper on a UI element that allows it to animate on and off the screen. Technically I don’t need to wrap it in a Transition class to get the functionality I want. However, by wrapping it, I keep a consistent feel to all of my screens including the ones that don’t use a panel for transition, or for ones which might use multiple animations simultaneously. A single interface offers flexibility as well. For example, if I decided to change from one type of transition to another, then I only need to modify the screen itself, and none of the other code (such as in the flow controller) will need to be changed.
Just like with the scale transition, we will create a nested data class to hold our key frame information. The only difference in this version is that we use a string representing a panel’s position instead of a vector3 for a target scale.
[System.Serializable] public class Data { public string position; public float duration; public Func<float, float, float, float> equation; }
Again, just like before, I also create default show and hide targets:
public Data showData = new Data { position = "Show", duration = 0.5f, equation = EasingEquations.EaseOutBack }; public Data hideData = new Data { position = "Hide", duration = 0.5f, equation = EasingEquations.EaseInBack };
To finish implementing the base class methods, I will also need a reference to the Panel component itself. Otherwise it will look pretty similar:
public Panel panel; public override void Show (System.Action didShow) { panel.SetPosition (hideData.position, false); StartCoroutine(Process(showData, didShow)); } public override void Hide (System.Action didHide) { panel.SetPosition (showData.position, false); StartCoroutine(Process(hideData, didHide)); } IEnumerator Process (Data data, Action complete) { Tweener tweener = panel.SetPosition(data.position, true); tweener.duration = data.duration; tweener.equation = data.equation; while (tweener != null) yield return null; if (complete != null) complete(); }
Group Transition
When the “Get Set” screen appears, part of it comes in from the top and part comes in from the bottom. This is simply a combination of two panel transitions utilized by a “group” transition. Look in the hierarchy panel at the “Master Controller -> Play -> Get Set Screen” to see an implementation for the Group Transition. This object has two children objects, each with a Panel Transition. By wrapping multiple transitions in a single transition, I can still simply call Show or Hide like I do with any other screen and don’t have to worry about the fact that this was a slightly more complex implementation.
The code for this class is pretty simple. I have a list to hold a reference to all of the transitions that I want to group together, and then loop through them all calling show or hide as necessary. When each of the transitions complete, I decrement a counter so that I can know when all of them have completed. Only after reaching zero do I invoke the final completion action.
public class GroupTransition : BaseTransition { public List<BaseTransition> transitions; public override void Show (Action didShow = null) { int count = transitions.Count; for (int i = 0; i < transitions.Count; ++i) { transitions [i].Show (delegate { count--; if (count == 0) { if (didShow != null) didShow(); } }); } } public override void Hide (Action didHide = null) { int count = transitions.Count; for (int i = 0; i < transitions.Count; ++i) { transitions [i].Hide (delegate { count--; if (count == 0) { if (didHide != null) didHide(); } }); } } }
Base View Controller
I am going to have many screens use animated transitions, so we will add some reusable code to the base view controller class. What I want is methods that we can call to “Show” or “Hide” a particular screen, and to pass a completion action to be run whenever the transition has finished. If the screen doesn’t use a transition, we can still call the same methods, they will simply complete immediately.
Note that in the code below I could have passed the completion action that the view controller receives to the transition’s invocation directly. However, I hold onto the action and call my own “DidShow” or “DidHide” method with that action instead. Subclasses of the view controller will be able to override these methods and perform additional logic either before or after invoking the completion action which gives me just a little bit more flexibility.
public virtual void Show (Action didShow = null) { gameObject.SetActive (true); if (transition != null) { transition.Show (delegate { DidShow (didShow); }); } else { DidShow (didShow); } } protected virtual void DidShow (Action complete) { if (complete != null) complete (); } public virtual void Hide (Action didHide = null) { if (transition != null) { transition.Hide (delegate { DidHide (didHide); }); } else { DidHide (didHide); } } protected virtual void DidHide (Action complete) { gameObject.SetActive (false); if (complete != null) complete (); }
Dialog View Controller
If you’ve followed along with grabbing the resources that I posted earlier, your screen should now look something like this (ignore the game board for now):
The dialog is a reusable screen that is simply an alert. It has a title and optional message that it displays to a user with a button for dismissing it. We use it in a few places in this game, and in this lesson will see it used whenever a new turn begins to provide information on whose turn it is. It serves as a sort of buffer to make sure that a user doesn’t accidentally roll for someone else.
public class DialogViewController : BaseViewController { public Action didComplete; [SerializeField] Text titleLabel; [SerializeField] Text messageLabel; public void Show (string title, string message, Action didShow = null) { titleLabel.text = title; messageLabel.gameObject.SetActive (!string.IsNullOrEmpty (message)); messageLabel.text = message; Show (didShow); } public void ContinueButtonPressed () { if (didComplete != null) didComplete(); } }
Get Set View Controller
If you’ve followed along with grabbing the resources that I posted earlier, your screen should now look something like this (ignore the game board for now):
This screen appears after dismissing the dialog alert that informed us whose turn it is. It is a point in the game where we can potentially branch the app flow – we may want to inspect the team or simply roll the dice to begin our journey. We can also use it as an opportunity to display other relevant information such as how many badges the player has earned so far.
There isn’t much new here, we’ve already seen branching screens such as the “IntroMenuViewController” and we are using the same sort of pattern here. I created an enum for each of the different branch options, and call a single “didFinish” action with the result of the users choice.
public class GetSetViewController : BaseViewController { public enum Exits { Roll, Team } public Action<Exits> didFinish; [SerializeField] Text playerNameLabel; [SerializeField] Sprite emptyBadge; [SerializeField] Image[] badges; void OnEnable () { var player = game.CurrentPlayer; playerNameLabel.text = player.nickName; // TODO: Show earned badges } public void OnRollPressed () { if (didFinish != null) didFinish (Exits.Roll); } public void OnTeamPressed () { if (didFinish != null) didFinish (Exits.Team); } }
Flow Control
We now have the ability to animate screen transitions as well as screens that implement the necessary components, but we still need to tie it all in to the rest of the applications flow. We will use the Flow Controller with some new states to accomplish this task.
Setup Complete State
In our last lesson, we left off by merely adding a stub for the setup complete state. It was just a field definition assigned to null. We can take it a little further now and point it to the “Play” state:
public partial class FlowController : MonoBehaviour { State SetupCompleteState { get { if (_setupCompleteState == null) _setupCompleteState = new State(OnEnterSetupCompleteState, null, "SetupComplete"); return _setupCompleteState; } } State _setupCompleteState; void OnEnterSetupCompleteState () { // TODO: Additional loading for the game board, pawns etc stateMachine.ChangeState (PlayState); } }
Play State
The play state is another state that simply serves as a better named wrapper state. I sort of think of this as a sub-state machine even though it is all flattened out. Anyway, this state currently serves no other purpose than to redirect the flow to the “real” first state of the play flow which is the “GetReadyState”:
public partial class FlowController : MonoBehaviour { State PlayState { get { if (_playState == null) _playState = new State(OnEnterPlayState, null, "Play"); return _playState; } } State _playState; void OnEnterPlayState () { stateMachine.ChangeState (GetReadyState); } }
Get Ready State
This state causes our dialog view controller to appear and inform the user whose turn it is now. It is the first time we are making use of our new animated transitions, so it looks a little more complex than some of our other states. This is because I am able to do everything I need right there in place with nested anonymous delegates. It may be slightly harder to read, but has some nice benefits including the ability to capture surrounding data, which we will make use of later.
Allow me to try and explain what’s going on when this state enters. I left a couple of placeholder “TODO” comments, because we will revisit this later. For now, I create a dynamic title for our alert based on the nickname of the current player. When we show the alert we will pass this title along. Then we call the “Show” method on the dialog view controller. You can think of this as an asynchronous method call because “showing” the dialog is an animated process. In order to make the next statements execute after the dialog has finished being shown, it is wrapped in an action.
So, if you are still with me, I wait until after the dialog is fully visible and only then do I register a new action to the dialog’s “didComplete” action. This means that if a player were to tap on the “OK” button while the alert was still appearing that nothing would happen. I wanted to be extra sure that the user actually sees the message.
Once the “didComplete” is invoked (because the user clicks the OK button), then we clear that action so it can’t be invoked again. This is especially important for the animated transitions because it would be possible, and fairly easy, for a user to tap the button a second time while the dialog is tweening to its hidden position. Next we tell the dialog view controller to “Hide” which accepts yet another action. When the dialog is fully hidden, we will use that action to move on to the next state – the Get Set State.
public partial class FlowController : MonoBehaviour { State GetReadyState { get { if (_getReadyState == null) _getReadyState = new State(OnEnterGetReadyState, null, "Get Ready"); return _getReadyState; } } State _getReadyState; void OnEnterGetReadyState () { // TODO: Save Game // TODO: Play Music var title = string.Format ("{0}'s Turn!", game.CurrentPlayer.nickName); dialogViewController.Show (title, null, delegate { dialogViewController.didComplete = delegate { dialogViewController.didComplete = null; dialogViewController.Hide (delegate { stateMachine.ChangeState(GetSetState); }); }; }); } }
Get Set State
Our next state looks a bit like the “Get Ready State”. We are able to show a screen, then hide it and move on in the flow all from a single nested block of code that begins when the state enters.
As I mentioned earlier, one of the benefits to working with these nested actions is their ability to capture surrounding data. This is used here because when the screen is finished it has two possible branches it could take – we are either rolling for our journey or managing our team of pokemon. The result that is passed along is “captured” and still available once we have completed the animation of hiding the screen and we didn’t have to add any fields to the class in order to store it for later.
Normally, this class will trigger the FlowController to move into either the “RollState” or “TeamState” – you can see some “TODO” statements that mark it accordingly. Rather than get stuck at some more stub states, I decided to temporarily bypass the intended flow and jump straight to another state, the “NextPlayerState” so that you could watch the screens animate in and out repeatedly if you want.
public partial class FlowController : MonoBehaviour { State GetSetState { get { if (_getSetState == null) _getSetState = new State(OnEnterGetSetState, null, "Get Set"); return _getSetState; } } State _getSetState; void OnEnterGetSetState () { getSetViewController.Show (delegate { getSetViewController.didFinish = delegate(GetSetViewController.Exits obj) { getSetViewController.didFinish = null; getSetViewController.Hide (delegate { switch (obj) { case GetSetViewController.Exits.Roll: stateMachine.ChangeState (NextPlayerState); // TODO: RollState break; case GetSetViewController.Exits.Team: stateMachine.ChangeState (NextPlayerState); // TODO: TeamState break; } }); }; }); } }
Next Player State
This state is normally called at the very end of a player’s journey and will cause the game to increment its current player (or loop back to the first player if necessary). Then we loop the flow back to the first state within the play state flow which is the “Get Ready State”
public partial class FlowController : MonoBehaviour { State NextPlayerState { get { if (_nextPlayerState == null) _nextPlayerState = new State(OnEnterNextPlayerState, null, "NextPlayer"); return _nextPlayerState; } } State _nextPlayerState; void OnEnterNextPlayerState () { game.NextPlayer (); // TODO: Move focus to next pawn stateMachine.ChangeState (GetReadyState); } }
Game System
To complete this lesson we need to implement the game system’s ability to change to the next player. It’s pretty simple thanks to the modulous operator “%” which allows you to keep the remainder of a division. Basically I calculate a value that is one higher than the current player index, and then divide that number by the number of players in the game and keep the remainder. That new value becomes the new current player index for the game.s
public static class GameSystem { public static void NextPlayer (this Game game) { var index = (game.currentPlayerIndex + 1) % game.players.Count; game.currentPlayerIndex = index; } }
Demo
We’re done, and finally have something to show for it. Go ahead and play the “Game” scene. Feel free to setup a new game with a few players and give them different names. When the setup is complete you will be able to continue on in the app flow and see the dialog appear for the “Get Ready” state using our nice new animated scale transition. Once it animates away you will also see our grouped panel animation on the “Get Set” state. Click either button at the bottom, and then it will change player turns and continue. You can do this as often as you like, but I suspect a couple of rounds will be enough to get the idea.
Summary
We have gotten a little further in our flow again, now just to the beginning stages of actually playing the game. Our new screens are more polished than before because they appear and disappear with animated transitions. The setup around these screens prevents any unexpected consequences that might otherwise occur by still being able to tap buttons on the screen when those animated transitions are in progress. Finally, we added a simple extension method to the game so that we could actually complete a mini sort of game loop where we are giving each player a turn and can repeat that loop as much as we want.
Don’t forget that there is a repository for this project located here. Also, please remember that this repository is using placeholder (empty) assets so attempting to run the game from here is pretty pointless – you will need to follow along with all of the previous lessons first.