Unofficial Pokemon Board Game – Title Screen

Now it’s time to provide a concrete example of our flow in motion. We will handle the first couple of states, the “Intro” and “Setup – Menu” state. We will show how to display one screen after another, both based on an app event (launch) and based on user input. The Flow Controller handles both cases like a champ.

Prefabs

In this project I have already created all of the prefabs. You are of course free to remake your own using the screenshots or reference from the project, but I wanted to focus more on the code than on the Unity editor itself. If you open the “Game” scene, then look in the Hierarchy pane, you should find a game object named “Master Controller” and a child container game object called “Intro”. This has three prefab instances, one holds the canvas setup to show the Title graphic, one holds the “Play” button, and one holds the menu for the setup options “New” or “Load”. You can toggle these objects on and off to see what each one looks like.

Intro Logo Screen

This prefab is just the logo which says “Unofficial Pokemon Board Game” – it has a canvas with an image positioned near the top to allow for the other buttons. Super simple. It has a custom script on it, but the script is currently unused and has an empty body. If you wanted to do some logo animation this might be a good place to add it.

Intro Title Screen

This screen stacks on top of the logo screen and adds the interactive portion of the screen that appears during the “Intro” state. It is nothing more than a canvas with a “Play” button. The root game object also has a script (IntroTitleViewController) which is already configured with a public method that is the target of the Play button’s “On Click” event handler. I only left a stub so that the reference in the inspector could be made, and all we have to worry about in this lesson is updating the code. Actually, the fully implemented code isn’t much more complex, just add the following:

public class IntroTitleViewController : MonoBehaviour {

	public Action didFinish;

	public void PlayButtonPressed () {
		if (didFinish != null)
			didFinish();
	}
}

You might wonder what the purpose of the indirection here is. I could have just provided a public reference to the button itself and allowed “whatever” it is that uses the “didFinish” delegate to use the button’s “On Click” event directly. In this case I would agree, but the code here follows the same pattern that I use in other scripts which are a bit more complex, and the code consistency is also nice. In addition, exposing the button is a greater risk, because other code will have access to a larger set of features than you might have intended to give. Furthermore, you may later change the design and the point of completion may not be the press of the button, but could be an animation or just about anything else instead. If I use the indirection I can accomodate any of those changes without needing to modify the listener’s code later.

The main purpose of the “didFinish” delegate is that I can register when the consumer state enters, and unregister when the state exits. I wont need to worry about disabling the Play button after clicking it, because by itself, it doesn’t perform any action. This way I don’t have to worry about subsequent button events potentially confusing my flow controller or putting it into an unexpected state. For this screen it wouldn’t be possible anyway because the whole game object is also activated and deactivated based on the current state. Other screens use animated transitions though, and could receive additional taps during that time.

Intro Menu Screen

In my flow charts, this screen is actually the first step of the Setup process. However, visually it still displays the logo, and feels a bit more like an intro screen, and so I left it grouped together here.

public class IntroMenuViewController : MonoBehaviour {

	public enum Exits {
		New,
		Load
	}

	public Action<Exits> didFinish;

	[SerializeField] Button loadButton;

	void OnEnable () {
		loadButton.interactable = false;
	}

	public void OnCreateButton () {
		if (didFinish != null)
			didFinish (Exits.New);
	}

	public void OnLoadButton () {
		if (didFinish != null)
			didFinish (Exits.Load);
	}
}

This screen is basically the same as the last – there are already a couple of public method stubs that had been connected to the buttons in the prefab for you. However, the previous screen had only one exit, but this state includes a branching path. In order to let the flow controller follow the input decision of the user, I have provided an enum parameter in the “didFinish” delegate so that we can know which option they wanted.

One of my most frequently encountered bugs in past projects were due to multiple simultaneous taps on a mobile screen. The problem was far more common on a screen like this one, when one screen has two or more buttons, each with a branching path. On a mobile device, the user might press both at the same time (whether on purpose or not who can say). With the delegate approach I use here, I unsubscribe when changing state, so pressing multiple buttons at the same time wont cause a problem – it will merely resolve one of the events first and the other will be “ignored” in that there is no longer a delegate action to invoke.

Eventually, the “OnEnable” method will control the interactability of the load button based on whether or not there is any saved data to load. We haven’t provided that system yet, so for now I simply turned it off.

Flow Controller

Now that we have some view controllers ready, we need to implement the Flow Controller’s states and state machine to tie everything together. Open the main FlowController script located at Scripts/Controller/FlowController/FlowController.cs and add the following after all of the existing field declarations:

StateMachine stateMachine = new StateMachine();

void Start () {
	stateMachine.ChangeState(IntroState);
}

This creates the state machine which will hold and transition between all of the application states that were defined in my flow charts. I use the “Start” method to set the initial state that should become active once the application begins running. It begins with the “Intro” state:

Intro State

Copy the following into the Scripts/Controller/FlowController/States/IntroState.cs script

public partial class FlowController : MonoBehaviour {
	
	State IntroState {
		get {
			if (_introState == null)
				_introState = new State(OnEnterIntroState, OnExitIntroState, "Intro");
			return _introState;
		}
	}
	State _introState;

	void OnEnterIntroState () {
		introLogoViewController.gameObject.SetActive (true);
		introTitleViewController.gameObject.SetActive (true);
		introTitleViewController.didFinish = delegate {
			stateMachine.ChangeState (SetupState);
		};
	}

	void OnExitIntroState () {
		introTitleViewController.didFinish = null;
		introTitleViewController.gameObject.SetActive (false);
	}
}

The code is pretty simple. When the “Intro” state enters, I enable two game objects: the one for the logo, and the one with the “play” button. Then, I provide a delegate handler for the view controller, which currently will be invoked when pressing the play button. The delegate will cause the state machine to change state to the “Setup” state, but as the state machine performs a transition, it will call an “Exit” action on the current state. This will allow me to unregister from the delegate and hide the object with the play button. Note that I don’t yet hide the logo object because I want it to remain visible during the setup menu.

Setup State

Even though the “Setup” state in the flow chart is made up of another flow chart, I created an actual state to represent it. I think of it as the entry of the flow chart because it is a name that is easier to remember than whatever other state will end up being the first state in the flow.

public partial class FlowController : MonoBehaviour {

	State SetupState {
		get {
			if (_setupState == null)
				_setupState = new State(OnEnterSetupState, null, "Setup");
			return _setupState;
		}
	}
	State _setupState;

	void OnEnterSetupState () {
		stateMachine.ChangeState (MenuState);
	}
}

All the code really does is to make sure the “real” setup flow begins. It passes the control off to another state whose name matches the first node in the “Setup” flow chart, which is the “Menu” state. It would be fine if you wanted to skip this one and have the “Intro” state change directly to the “Menu” state instead.

Menu State

public partial class FlowController : MonoBehaviour {

	State MenuState {
		get {
			if (_menuState == null)
				_menuState = new State(OnEnterMenuState, OnExitMenuState, "Menu");
			return _menuState;
		}
	}
	State _menuState;

	void OnEnterMenuState () {
		introMenuViewController.gameObject.SetActive (true);
		introMenuViewController.didFinish = delegate(IntroMenuViewController.Exits obj) {
			switch (obj) {
			case IntroMenuViewController.Exits.New:
				stateMachine.ChangeState (CreateState);
				break;
			case IntroMenuViewController.Exits.Load:
				stateMachine.ChangeState (LoadState);
				break;
			}
		};
	}

	void OnExitMenuState () {
		introMenuViewController.didFinish = null;
		introMenuViewController.gameObject.SetActive (false);
		introLogoViewController.gameObject.SetActive (false);
	}
}

When the menu state enters I make sure to enable the menu screen and register for the “didFinish” delegate. Depending on the exit value which was passed along, we will then change state to one of two branching options.

When the menu state exits I will disable the menu object and the logo object as well. I also make sure to clear out the delegate.

Stubs

You could just about run the scene and see the first two screens now. However, because we haven’t provided an implementation of the “Create” or “Load” state yet, there would be two compiler errors. For now, we can add a stub placeholder like these (Note that I placed each in its own file according to its name):

State LoadState = null;
State CreateState = null;

Demo

Now run the “Game” scene and you should see the Logo and Play button appear. If you click “Play” then the “Menu” will appear. Clicking the menu buttons won’t have the desired result yet – but you will still get a sense that it has moved on in the flow. Since the menu screen state will no longer be active, its exit action will have been invoked and the logo and menu objects will become inactive. The final result is just a blank screen, but we will be adding more soon.

Summary

In this lesson, we started putting the Flow Controller to the test. We implemented the first couple of states and actually got the flow working and traversable. By continuing the pattern we setup here, each of the rest of the screens can snap in place much like the bricks of a structure. One little bit at a time and before you know it you have something much more impressive.

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.

Leave a Reply

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