Since we follow the Model/View/Controller design pattern, the game model stores the official and complete state of the game. The view can always be re-drawn from the state of the model and the model is only changed by the Events executed by the Controller. The details of the model depends on the game, but most games share some basic features.
I start all new game projects with the following set of model classes.
Player
The player class represents the players of the game. Each player is uniquely and primarily identified by their table position. All the events in the control system refer to players by table position. Along with table position, all players have a color and a “Place” which is their final score order and is used to show place ribbons at the end of the game.
I also keep game statistics per player. These statistics are game dependent, and the idea is to provide information about how each player played the game for review at the end of the game. A statistic that I always collect is “Thinking time”.
public class Player { public enum PlayerColors { RED, GREEN, BLUE, YELLOW, CYAN, PURPLE, ORANGE } public enum Statistic { NUM_TYPES } #region State #endregion #region Statistics public float ThinkingTime = 0f; public Dictionary<Statistic, int> Statistics = new Dictionary<Statistic, int>(); #endregion public int Position; public int Place; public PlayerColors Color; public Player() { for (int i = 0; i < (int)Statistic.NUM_TYPES; ++i) Statistics[(Statistic)i] = 0; } public Color veryLightColor() { return GameGUI.VeryLightColors[(int)Color]; } public Color lightColor() { return GameGUI.LightColors[(int)Color]; } public Color solidColor() { return GameGUI.SolidColors[(int)Color]; } public int totalScore(bool includeCutpurse = true) { return 0; } public float totalScoreWithTieBreakers() { return totalScore() + 0; } }
PlayerList
This is the list of players and is used to keep track of the play order and to lookup a player by their table position. This class is mostly convenience functions. It is a MonoBehaviour and a singleton and is added to a top-level game object in the scene. This class also stores the supported number of players which is used by the MainMenu to determine if the “Start Game” button should be enabled.
public class PlayerList : MonoBehaviour { static PlayerList thePlayerList = null; public static List<Player> Players { get { return thePlayerList._players; } set { thePlayerList._players = value; } } List<Player> _players = new List<Player>(); public const int MIN_PLAYERS = 0; public const int MAX_PLAYERS = 0; void OnEnable() { thePlayerList = this; } void OnDestroy() { thePlayerList = null; } public static void shuffle() { Players.Shuffle(); } public static Player playerAtPosition(int position) { return Players.Find(p => p.Position == position); } public static Player nextPlayer(Player player) { return playerLeftOfPlayer(player); } public static Player playerLeftOfPlayer(Player player) { int pIndex = Players.IndexOf(player); int index = (pIndex == Players.Count - 1) ? 0 : pIndex + 1; return Players[index]; } public static Player playerRightOfPlayer(Player player) { int pIndex = Players.IndexOf(player); int index = (pIndex == 0 ? Players.Count - 1 : pIndex - 1); return Players[index]; } public static void setOrderToClockwiseWithStartAt(Player player) { thePlayerList._players = thePlayerList._players.OrderBy( p => (p.Position - player.Position+MAX_PLAYERS) % MAX_PLAYERS).ToList(); } public static void setScoreOrder() { float prevScore = float.MaxValue; int currPlace = 0; int realPlace = 0; foreach (Player p in thePlayerList._players .OrderByDescending(p => p.totalScoreWithTieBreakers())) { if (p.totalScoreWithTieBreakers() != prevScore) { // First player will always trigger this code. prevScore = p.totalScoreWithTieBreakers(); currPlace = realPlace; } p.Place = currPlace; ++realPlace; } } }
Game
The Game stores any model data that isn’t Player dependent. In most games, this class contains most of the state data and will often have sub-classes for different regions of the board.
This class also contains enumerations of all the different types of pieces, the list of things that get stored in PlayerPrefs and the different states that the game can be in.
The list of different pieces is surprisingly useful. I typically end up giving the Player class a Dictionary<PieceType, int> to keep track of the resources a player currently has. I make static lists of categories of pieces so that I can quickly iterate over all the pieces of a type.
The enumeration of game state is how I usually keep track of what “State” the game is in. This is a description of what is currently happening in the game, and is usually limited to points where the game is waiting for a player input. Some games have more complicated state, and I’m currently experimenting with storing state in custom classes. In a simple game, the Game class will then have a member variable CurrentGameState. In a more complex game, the game state will be stored as a Stack of states.
The PlayerPrefs enumeration holds things like the name of the last savegame so that I can “Resume Game” and any game options so that the main menu can default to the last used options.
public class Game : MonoBehaviour { public enum PlayerPrefSettings { LAST_FILE_LOADED } public enum PieceType { INVALID = -1, NUM_TYPES } public enum GameState { INVALID = -1, LOGIN, PLAY, GAME_OVER } #region GameOptions #endregion public static Game theGame = null; public Player CurrentPlayer = null; public GameState CurrentGameState = GameState.INVALID; public void init() { // Reset any state here. When we undo, all the events are re-executed and the first event will // call this function to cleanup the old game state. } public void OnEnable() { theGame = this; } public void OnDestroy() { if (theGame == this) theGame = null; } }