Thanks to all the work we did creating our data models in the last lesson, we now have enough that we can continue on with the game flow into the setup screens. In this lesson we will implement two new screens. The first screen will determine the number of players, and the next screen is for configuring each player with a name and starter Pokemon.
Data Controller
Before we dive in, I want to make sure that the new data models we created in the previous lesson can be available when we need them. The Data Controller is a singleton which can be reached easily from anywhere, so it will be used to hold the game instance too. Add a field to the class to hold it like this:
public Game game;
Flow Controller
I want to add a direct reference to the Data Controller’s singleton so that the data it holds (the game in particular) will be able to be serialized and appear in a Unity inspector window. We will add a new field for this purpose:
[SerializeField] DataController dataController;
We can assign our new reference it in the “Start” method. While we are here we can also update the code to allow the Data Controller to finish loading before beginning the application flow.
void Start () { dataController = DataController.instance; dataController.Load (delegate { stateMachine.ChangeState(IntroState); }); }
The Flow Controller had held a field for the game, but now that the Data Controller owns it, we can convert the field to a property that simply wraps the main instance instead:
Game game { get { return dataController.game; } set { dataController.game = value; } }
Create State
When we implemented the Title Screen, we also added a stub for the Create state which would be triggered by starting a new game. This state is a simple wrapper with a name that is easier to remember. We used this pattern before, but feel free to strip it out if you prefer. The only purpose for this state is to get to the real first state that should happen when we take the create branch in our flow.
public partial class FlowController : MonoBehaviour { State CreateState { get { if (_createState == null) _createState = new State(OnEnterCreateState, null, "Create"); return _createState; } } State _createState; void OnEnterCreateState () { stateMachine.ChangeState (PlayerCountState); } }
Player Count State
This state begins the process of creating a new game. The first question we need to resolve is how many people will be playing. For this we will display a new screen that has several buttons allowing this configuration step. All this state really needs to do is cause that screen to appear, and then when that screen has finished getting the info it needs, we allow the game factory to create our game with the specified number of players, and then continue to the next step in the setup flow.
public partial class FlowController : MonoBehaviour { State PlayerCountState { get { if (_playerCountState == null) _playerCountState = new State(OnEnterPlayerCountState, OnExitPlayerCountState, "Player Count"); return _playerCountState; } } State _playerCountState; void OnEnterPlayerCountState () { playerCountViewController.gameObject.SetActive(true); playerCountViewController.didComplete = delegate(int count) { game = GameFactory.Create(count); stateMachine.ChangeState(PlayerConfigureState); }; } void OnExitPlayerCountState () { playerCountViewController.didComplete = null; playerCountViewController.gameObject.SetActive(false); } }
Note that this code wont compile yet because we havent finished adding the code for the playerCountViewController class. That will come in a bit.
Player Configure State
The next step in the flow will be to assign the names and starter Pokemon for each player. The code here should start to look pretty familiar. We are simply showing a new screen, registering for delegates to know what options a user selects, and then continuing with the flow accordingly. In this case, there is a branch – the user might have set the wrong number of players, so we allow the option to go back to the previous step and try again. If they finish configuring all of the players and then continue, they will have completed the setup.
public partial class FlowController : MonoBehaviour { State PlayerConfigureState { get { if (_playerConfigureState == null) _playerConfigureState = new State(OnEnterPlayerConfigureState, OnExitPlayerConfigureState, "Player Configure"); return _playerConfigureState; } } State _playerConfigureState; void OnEnterPlayerConfigureState () { playerConfigureViewController.gameObject.SetActive(true); playerConfigureViewController.didComplete = delegate { stateMachine.ChangeState(SetupCompleteState); }; playerConfigureViewController.didAbort = delegate { stateMachine.ChangeState(PlayerCountState); }; } void OnExitPlayerConfigureState () { playerConfigureViewController.didComplete = null; playerConfigureViewController.didAbort = null; playerConfigureViewController.gameObject.SetActive(false); } }
Setup Complete State
Because there was a branch in the setup flow I decided to bring everything together to a common completion state. We can do additional logic here that would be the same in either branch, and then when continuing I only have to worry about setting the next target state in a single place. Fow now, just use a stub state, and we’ll come back to it in a future lesson:
State SetupCompleteState = null;
Player Count View Controller
If you’ve followed along with grabbing the resources that I posted earlier, your screen should now look something like this:
The code for this screen couldn’t be much simpler. Most of the heavy lifting was already configured when creating the prefab. Each button already has a number assigned to it, and when clicking it, we pass that number along as part of our completion action.
public class PlayerCountViewController : MonoBehaviour { public Action<int> didComplete; public void SetPlayerCount (int count) { if (didComplete != null) didComplete(count); } }
Base View Controller
Several of our screens will have repeated functionality and data reference requirements. So that I don’t have to repeat my code in multiple places, I decided to use a shared base class so that my other classes get the functionality for free through inheritance. Add the following property to the BaseViewController class:
protected Game game { get { return DataController.instance.game; } }
Player Configure View Controller
If you’ve followed along with grabbing the resources that I posted earlier, your screen should now look something like this:
This screen actually has a good bit of code to discuss, so I’m breaking it down into lots of little pieces…
public Action didAbort; public Action didComplete;
At the top I added two actions which our state class will use as points of completion to continue on in the application flow. Whenever I invoke either of these, I am saying that I am done with whatever my purpose was on this screen.
Entity buddy; Entity[] pokemon; int playerIndex = 0;
I added three additional private fields: the buddy entity represents which of the starter options is currently selected, the pokemon array holds the options for starter Pokemon that you can pick from, and the playerIndex is used to keep track of which player is currently being configured.
void Awake () { pokemon = new Entity[3]; var connection = DataController.instance.pokemonDatabase.connection; pokemon[0] = connection.Table<Entity>().Where(x => x.id == 1).FirstOrDefault(); pokemon[1] = connection.Table<Entity>().Where(x => x.id == 4).FirstOrDefault(); pokemon[2] = connection.Table<Entity>().Where(x => x.id == 7).FirstOrDefault(); }
I use the Awake method to do some initialization that only needs to be performed once. Basically I create an array to hold the three starter Pokemon, then fetch them and assign them to the slots. I used their id numbers directly which some might complain about. These can be “magic” numbers where people have no idea what they are used for. In this case, creating a separate constant for each seemed unnecessary since in context it should be obvious that I am using the id of a Pokemon and it ultimately doesn’t matter which Pokemon I choose to use.
void OnEnable () { SetPlayerIndex (0); }
The OnEnable method could in theory be called multiple times. Whenever the state for configuring players is active, this gameObject will also become active, and this method will be called. For example, imagine that in a single session, your players create and finish a game and then begin another. When this screen appears the second time, you will want to make sure that we “start over” by configuring all of the players from the beginning. I set the player index to ‘0’ because I am referring to a list of players which are accessed starting at index ‘0’.
public void OnContinueButton () { SavePlayer (); SetPlayerIndex (playerIndex + 1); }
Whenever the continue button on the screen is clicked, we will apply the settings of the screen to the current player using the SavePlayer method, and then we will set the player index to the next index.
public void OnBackButton () { SetPlayerIndex (playerIndex - 1); }
If the back button is pressed instead, then I simply go to the previous player index. There is no reason to record any of the values on the screen because we can consider the current step to be aborted.
public void SelectPokemon (int index) { buddy = pokemon[index]; for (int i = 0; i < pokemon.Length; ++i) { Poses pose = buddy == pokemon[i] ? Poses.Front : Poses.Back; pokemonAvatars[i].sprite = PokemonSystem.GetAvatar(pokemon[i], Genders.Male, pose); } }
Each of the three starter Pokemon has a button on the screen to represent them. The button is also configured with an index so they can all call the same method and just pass their value along. The selected “buddy” becomes the Pokemon at the index which the button held. Next, I loop through all of the images for the buttons and upate them to either show the front or back of a Pokemon based on whether or not it is the selected buddy. This is used as visual feedback for which button is selected instead of an outline or color shift etc.
void SetPlayerIndex (int index) { if (index < 0) { if (didAbort != null) didAbort (); } else if (index >= game.players.Count) { if (didComplete != null) didComplete (); } else { playerIndex = index; LoadPlayer (); } }
I call this method in several places throughout the class. I use a branch for a few outcomes. In the event that we pass a value less than zero, it indicates that the user wants to back out of this screen completely, so we call the didAbort delegate. Another exit case is if the index would be out of bounds of the player array – this case means that the user has finished configuring all of the players. The final “else” statement is only invoked when the index could correspond to an actual player in the game’s players list. If so, we update the playerIndex field and then call a method to cause the screen to reset for the new player.
public void LoadPlayer () { playerLabel.text = string.Format("Player {0}", (playerIndex + 1)); nameInput.text = string.Empty; int random = UnityEngine.Random.Range(0, 3); SelectPokemon(random); }
The LoadPlayer method configures the screen for the display of a new player. We begin with a default label indicating which player it is that needs to be configured and clear out the input field so it is easy for a new name to be entered. I also select a random pokemon as a suggestion.
void SavePlayer () { var player = game.players[playerIndex]; player.nickName = string.IsNullOrEmpty(nameInput.text) ? playerLabel.text : nameInput.text; player.pokemon.Clear (); player.pokemon.Add( PokemonFactory.Create(buddy, 4) ); }
The SavePlayer method takes the values that appear on screen and applies it to the corresponding player in the game. In the event that the user didn’t actually provide a name, I use the same value we had displayed in the label – something like “Player 1”. Before I add the buddy pokemon to the player’s pokemon list, I first clear it. This is just in case the user had gone back and forth a couple of times, and will make sure that they dont end up starting the game with more than one.
Demo
Go ahead and run the Game scene. You can now complete the setup screens of a new game. When you do (and are looking at a blank screen again) take a moment to look in the inspector pane. With the “Master Controller” object in the hierarchy pane selected, you can see the “Flow Controller” scripts fields in the inspector pane. Among the values will be an object hierarchy for the Data Controller. You can expand that to see the Game, its Players, and each Player’s Pokemon, all of which should match whatever you selected on screen.
Summary
In this lesson we put all of our models from the previous lesson to good use. We were able to actually create a game, populate it with players, and then configure the players with a name and buddy. We also included a demo that showed how our models could be viewed and edited in an inspector window.
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.
During runtime, when I click on the number of players, I get:
NotSupportedException: Cannot compile: Parameter
Have you tried the code in the repository? It’s possible that you have missed something, so it can be helpful to compare. Usually the error messages provide additional helpful information such as the name of the script and even the line of the problem. I googled and saw similar error messages related to SQLite so it might be related to that.
I am fairly certain you are correct, it is a problem with the newest unity and SQLite. I rolled back to the version you used, 5.5, and have no problems now. This is perfect code for me to get the right mindset on for storing data, thank you.