7

I have a state machine pattern implementation that's pretty straightforward, but I've got a design problem I don't know an elegant solution to. Here's some partly pseudo code to illustrate.

class MainRoomState:State

    public override void HandleJoinSuccess(string gameName, List<LobbyPlayer> players)
    {

        Context.CurrentState = new LobbyState(Context);
        //the players list needs to be in the LobbyState
        //that data is specific to the LobbyState
    }

Basically, the event triggering the state change also is providing data that belongs in the new state, and I don't know if there's a nice way to do it besides a shared class in the parent Context object, or by mangling the constructor for this special case.

Edit - in more detail

I have my states setup to encapsulate state specific data as well as logic, which includes, in this case, a GUI screen belonging to the current state. This seems to be a more elegant way of handling not only separation of logic, but also state specific data.

Here's what my abstract state looks like,

public abstract class GameState
{

    public GameState(Game context)
    {
        Context = context;
        Net = context.Net;
    }

    //Incoming events
    public abstract void HandleConnected();
    public abstract void HandleDisconnected(string reason);
    public abstract void HandleNetworkNotification(string msg);
    public abstract void HandleGameStarting();
    //there's about 20 more methods not shown here 

Here's my Game class, mainly functioning as a data holder, but it also controls the game loop timer, firing main events, etc.

public class Game
{
    public Game()
    {
       //...bunch of init code here
       CurrentState = new DisconnectedState(this);
    }
    public GameState CurrentState
    {
        get;
        set;
    }

    //more methods, etc
}

You'll see that I opted for composite containment of the CurrentState rather than implementing pass-through methods. That was mostly because I disliked the messiness of passing through the calls when I really didn't need to. Also, the whole state stores a reference to Context, rather than passing it per method.

I've got two questions/problems with the pattern.

  1. Every concrete state MUST implement in some fashion, EVERY method in the abstract State. This is ridiculous overkill, and currently most of my states only use 20-40% of the methods, leaving me with a couple pages of this:

public override void HandleGameStarting() { //not handled in this state }

  1. My second issue is that each state has it's own data that only needs to exist while in that state. I don't want to store a huge conglomeration of data in the Context object when a far more encapsulated way of handling it would be putting it in the state.

Looking over your example, I suppose one answer to my original question is that I could properly pass through the methods to the Current State, and just do a kind of hack with the data,

class Game

public override void HandleJoinSuccess(string gameName, List<LobbyPlayer> players)
{

    CurrentState = new LobbyState(this);
    CurrentState.HandleData(gameName,players);
    //the players list needs to be in the LobbyState
    //that data is specific to the LobbyState
}

1 Answer 1

6

I think you may be getting a little bogged down in the English meaning of state, when compared to the State Pattern (or Finite-State Machine, which is really a diagrammatic representation of a State Pattern). Both are appropriate here, but they shouldn't be confused.

The State Pattern is something which should, given various common stimuli, operate on the game state differently at different times in the game. So, as you've rightly concluded, the lobby stage (which I'm assuming to be while people are joining a game) is a State in that context.

The game state is a list of players, pieces, cards, dice, whatever else, and all the information required to simulate them. You might want to call that the context.

Each State should receive a UserClicked message from the application which receives the context and coordinates or area clicked and should operate on those accordingly. The information that you are trying to give ownership of to the State object does not belong there, it belongs in the game itself, alongside the State.

The state may also have a UserPressedKey method, a TimerTicked method or any other kind of stimulus on which it should act. But each of these should act on the game state rather than being the game state.

One important method of the State will be ScreenRefresh, which will draw the context for the user.

Here is a very rough example:

abstract class ApplicationState {

    void Begin(GameContext context);
    void UserClicked(GameContext context, int x, int y);
    void UserPressedKey(GameContext context, char key);
    void TimerTicked(GameContext context);
    void ScreenRefresh(GameContext context);

    protected void OnStateChanged(ApplicationState newState) {

        // inform interested parties that state has changed,
        // using the observer pattern

    }

}

class LobbyState : ApplicationState
{
    void Begin(GameContext context) {

        context.Players = new User[context.NoOfPlayers];

    }

    void UserClicked(GameContext context, int x, int y) {

        // Find a displayed player box which contains click coords

        int index = -1;
        for (i=0; i < context.Players.Length; i++) {
            if (PlayerBox[i].Contains(x, y)) {
                index = i;
            }
        }

        if (index > -1) {

            if (context.Player[index] == null) {
                context.Player[index] = context.ActivePlayer;
            } else if (context.Player[index] == context.ActivePlayer()) {
                context.Player[index] = null;
            } else {
                ErrorSound.Play();
            }

        } else if (CloseIcon.Contains(x, y)) {

            OnStateChanged(new ExittingState());

        } else {

            ErrorSound.Play();

        }

    }

    void UserPressedKey(GameContext context, char key) {

         if (key = 'J' or key = 'j') {

             if (! context.AddPlayerRandomly(context.ActiveUser)) {
                 ErrorSound.Play();
             }

             if (context.GameIsNowFull()) {
                 OnStateChanged(new InitializingState());
             }

         } else if key = Esc {

             if (!context.RemovePlayer(context.ActiveUser)) {
                 ErrorSound.Play();
             }

         } else {

             ErrorSound.Play();

         }

    }

    void TimerTicked(GameContext context) {

        // there is no use for a game timer while
        // we're trying to fill the game, but you
        // want one to tick anyway, because the
        // application doesn't know which state
        // it is in.

    }

    void ScreenRefresh(GameContext context) {

       DrawPlayerBoxes(context.Screen, context.Players);
       DrawExitIcon(context.Screen);

    }

}

class InitializingState : ApplicationState {

    private double waitSpinnerAngle = 0;

    void Begin(GameContext context) {

        context.RandomizePlayOrder();
        context.ShuffleCards();

        // all other game initialization rules here

        OnContextChanged(new PlayerUpState(context.Players[0]));

    }

    void UserClicked(GameContext context, int x, int y) {

        // Not responding to user input for a moment
        ErrorSound.Play();

    }

    void UserPressedKey(GameContext context, char key) {

        // Not responding to user input for a moment
        ErrorSound.Play();

    }

    void TimerTicked(GameContext context) {

        waitSpinnerAngle += 0.05;
        if (waitSpinnerAngle > 1) waitSpinnerAngle = 0;

    }

    void ScreenRefresh(GameContext context) {

         DrawWaitSpinner(waitSpinnerAngle);

    }

}

See where I'm going with this? The Application itself is then as simple as setting the initial State, hooking a listener into the OnStateChanged event and calling Begin. When the listener hears that the event is called, unhook your old State, hook in the new one, and call Begin again.

Everything else is triggered by events from the mouse, keyboard or timer, passed directly to the current State without knowledge of which State the game is currently in. If you are running this game across a network then you will also need an event for changes of state received from other players.

Everything in your State and Context is now very unit-testable and separation of concerns are observed. Although, you may want to refactor that LobbyState#UserClicked method a bit, among other things.

Remember, this is just a free example and you get what you paid for it. Don't try to apply it directly to your game. Just use it to understand how the State Pattern should work in the context of a game.

7
  • 1
    +1 - I'd probably stick the context in the constructor, but yes you need the State object that controls the logic and the context that holds the data.
    – user23157
    Feb 4, 2012 at 11:41
  • @TheMouthofaCow: Honestly, that was a close call. I don't have a strong preference. On one hand, if it's held in the State objects and the application, there is a risk of over-ownership; on the other hand, this way does mean passing context to every method.
    – pdr
    Feb 4, 2012 at 13:12
  • it's an implementation detail :)
    – user23157
    Feb 4, 2012 at 16:15
  • @pdr, thank you for the detailed example and explanation. I understand what you're saying. My implementation is a little different, I'm going to edit my question with more detail.
    – Kal_Torak
    Feb 4, 2012 at 21:36
  • @Kal_Torak: Please provide more info for a more specific answer. If you have a main room, I assume this is a client-server game. If so, where is your Context.CurrentState singleton? Client or Server application? Should it be a singleton, or should it be per-game? (The game is where a state is more logical - the main room is just a list of games with open spaces, no?)
    – pdr
    Feb 4, 2012 at 22:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.