We are nearly feature complete with this project. There is enough in place that we can complete the entire game flow now. This means we will actually add the “game over” state, and declare a winner. We will also add the necessary functionality to save and load a game so you can come back to it later.
Data Controller
Let’s start by finishing out the Data Controller. We will be adding the logic necessary to persist a game’s data between play sessions. There are a lot of options on how you can save game data. Complex games may choose to write data to a database which we touched on in the earlier SQLite lesson. You could also write files to disk containing all of, or portions of, your game data. I discussed this approach at the end of an old tutorial here. This game is so simple that I decided it would be okay to use player prefs. In my opinion this is the easiest of all the options.
Unity’s PlayerPrefs allows you to write a variety of simple data types like “Int”, “String”, and “Float”. Each bit of data is saved through a key – you can imagine the player prefs as a dictionary where a key can return a value. I will serialize the entire game as a single JSON string so I will only need one key:
const string saveDataKey = "GameSaveData";
We will be able to use this key to see if there is any saved game data available. For example, on the menu screen we can check for the presence of this key, and when it is found we can either enable or disable the “Load” button accordingly.
public bool HasSavedGame () { return PlayerPrefs.HasKey (saveDataKey); }
We already made the effort to make the Game model and its various fields like the Gyms, Players and Pokemon to already be Serializeable. This will allow us to convert the entire Game hierarchy into a JSON string with a single line of code. We can then use our key to write that value to the Player Prefs.
public void SaveGame () { var json = JsonUtility.ToJson (game); PlayerPrefs.SetString (saveDataKey, json); }
We can load the game by grabbing the value from the Player Prefs using the same key. Then we will use the GameFactory to handle the creation of a game from that JSON data.
public void LoadGame () { var json = PlayerPrefs.GetString (saveDataKey); game = GameFactory.Create (json); }
After a game is completed (or for some other game design reason of your choice), you may wish to clear the saved game data. This is as simple as removing that key from player prefs:
public void ClearSavedGame () { PlayerPrefs.DeleteKey (saveDataKey); }
Note that all of the rest of my code has no idea that I chose to use PlayerPrefs to persist game data. Because I wrapped the functionality in my own interface, I can later choose to adopt a different persistance method. For example, I could choose to write files to disk instead of using PlayerPrefs and would only need to change the implementation code in this file.
Intro Menu View Controller
Now that we have an interface for determining whether saved game data is available, we can finsih implementing the menu screen. In the OnEnable method, we can now set the load button’s interactable field based on the result of a call to the DataController’s HasSavedGame method like this:
void OnEnable () { loadButton.interactable = DataController.instance.HasSavedGame (); }
Load State
When a user clicks the load button from the menu screen, our flow will come to the “Load” state. Here we will tell the DataController to LoadGame and our “Setup” portion of the flow will be complete!
public partial class FlowController : MonoBehaviour { State LoadState { get { if (_loadState == null) { _loadState = new State(OnEnterLoadState, null, "Load"); } return _loadState; } } State _loadState; void OnEnterLoadState () { DataController.instance.LoadGame (); stateMachine.ChangeState(SetupCompleteState); } }
Get Ready State
Now we need to actually trigger a save of our game data. I’ve decided that the best place to do this is at the “GetReadyState” which is the very beginning of any player’s turn. This way, when you return to a game later it can start at that point and we will already have the code in place for the game to say whose turn it is. Change the comment at the top of the OnEnterGetReadyState method from this:
// TODO: Save Game
… to this:
DataController.instance.SaveGame ();
Check Destination State
Finally, let’s wrap up the logic for our game states so that whenever a player has won the game that we can reach the Game Over state. The first thing we will need to add is something to break out of a “Journey” flow when a player has won. If we don’t do this, then after a final gym battle, a player could have a remaining number of tiles to move based on his roll. It would look kind of silly to continue moving once you already won. Update the “OnEnter…” method to this:
void OnEnterCheckDestinationState () { Player current = game.CurrentPlayer; if (GameSystem.IsGameOver(game) || current.tileIndex == current.destinationTileIndex) { stateMachine.ChangeState (CheckWinState); } else { stateMachine.ChangeState (MoveState); } }
We added an extra check of “IsGameOver” and directecd to a new state called “CheckWinState” instead of the “NextPlayerState” we had been using.
Check Win State
This state allows us an opportunity to break out of a “Play” flow. It only allows the next player to take a turn if the current player hasn’t already won.
public partial class FlowController : MonoBehaviour { State CheckWinState { get { if (_checkWinState == null) _checkWinState = new State(OnEnterCheckWinState, null, "CheckWin"); return _checkWinState; } } State _checkWinState; void OnEnterCheckWinState () { if (GameSystem.IsGameOver(game)) { stateMachine.ChangeState (GameOverState); } else { stateMachine.ChangeState (NextPlayerState); } } }
Game Over State
Finally, we can add the Game Over State, which is responsible for showing a message when a player has finally won. Note that when this state enters I also clear the saved game data from the data controller.
I have provided the simplest sort of implementation which is nothing more than a dialog box to declare who the winner is. Dismissing the dialog simply leads back to the Intro state so a whole new game can begin. I’d imagine that in most games you would want to spend a lot more effort on this screen to help celebrate the player’s victory, but since I dont have an artist handy I have only provided a placeholder.
public partial class FlowController : MonoBehaviour { State GameOverState { get { if (_endState == null) _endState = new State(OnEnterGameOverState, null, "GameOver"); return _endState; } } State _endState; void OnEnterGameOverState () { DataController.instance.ClearSavedGame (); // musicController.PlayGameOver(); var title = "Game Over"; var message = string.Format ("{0} wins!", game.CurrentPlayer.nickName); dialogViewController.Show (title, message, delegate { dialogViewController.didComplete = delegate { dialogViewController.didComplete = null; dialogViewController.Hide(delegate { stateMachine.ChangeState(IntroState); }); }; }); } }
Demo
Try a new game with at least a couple of players. Play a few rounds and then stop the game. You can even quit Unity and come back later if you want. Play again, but choose to “Load” from the menu and the game should be exactly as it was when you left it. You should see the same number of players, pokemon, stats and all game data perfectly restored. Now continue playing until one of the players has actually defeated all four gyms. Feel free to cheat if you like – I often boost the attack, defense, and stamina stats of my Pokemon in the Inspector to make the battles very easy during play testing. After winning your fourth gym battle you should see the Game Over screen appear, and then after dismissal should be able to start a new game.
Summary
Our game is nearly feature complete now. We can actually “complete” a full game and can also save and load games. Thanks to some of Unity’s tools like the JsonUtility and PlayerPrefs this whole process was actually very simple.
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.
If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!