Wednesday, January 2, 2008

State Design Pattern

For this example, let us assume that we would like to design an application that simulates the interaction in a single character game. The character, in this game, can be moving on the ground, swimming under water, flying an airplane or driving a car. Each gaming episode may involve the character in any one of these conditions. The character is expected to navigate in these different environments using the same navigation keys on the keyboard. Additionally, the character may use the Action Key (say Control key on the keyboard) to perform actions that are relevant to each environment. While on ground, the Action Key is used to talk; while in water, it is used to float up for a breath of air, while in an airplane, to get directions and while in the car, to use the GPS.
A map of the keys versus environments is listed here

Key Ground Underwater Airplane Car 
Front Direction Move Forward Swim Forward Increase Throttle Accelerate 
Back Direction Move Backward Swim Backward Decrease Throttle Decelerate 
Right Direction Move Angular Right Swim Angular Right Change Direction to the Right Turn Car to the Right 
Left Direction Move Angular Left Swim Angular Left Change Direction to the Left Turn Car to the Left 
Action Talk Surface for Air Get directions Use GPS 

A first take at this design would be by starting off creating a GameCharacter class. The interactions that are possible would be based on the key strokes that the GameCharacter supports. Let me map them to the normal gaming keys on the keyboard – UpArrowPressed, DownArrowPressed, RightPressed and LeftPressed to cover the navigation areas and the ActionPressed method for supporting the character’s action feature. The State of the environment can be stored in an instance value that holds an enumeration.

GameCharacter Class Listing

Public Class GameCharacter

  Private Enum EnvironmentStates

    Land

    Underwater

    Airplane

    Car

  End Enum

 

  Private _EnvironmentState As EnvironmentStates

 

  Public Sub New()

    'All episodes start with the character on Land

    _EnvironmentState = EnvironmentStates.Land

  End Sub

 

  Public Sub UpArrowPressed()

    Select Case _EnvironmentState

      Case EnvironmentStates.Land

        Console.WriteLine("Character is moving forward")

      Case EnvironmentStates.Underwater

        Console.WriteLine("Character is swimming forward")

      Case EnvironmentStates.Airplane

        Console.WriteLine("Character is increasing throttle")

      Case EnvironmentStates.Car

        Console.WriteLine("Character is accelerating")

    End Select

  End Sub

 

  Public Sub DownArrowPressed()

    Select Case _EnvironmentState

      Case EnvironmentStates.Land

        Console.WriteLine("Character is moving backwards")

      Case EnvironmentStates.Underwater

        Console.WriteLine("Character is swimming backwards")

      Case EnvironmentStates.Airplane

        Console.WriteLine("Character is decreasing throttle")

      Case EnvironmentStates.Car

        Console.WriteLine("Character is decelerating")

    End Select

  End Sub

 

  Public Sub RightArrowPressed()

    Select Case _EnvironmentState

      Case EnvironmentStates.Land

        Console.WriteLine("Character is moving at an angular right")

      Case EnvironmentStates.Underwater

        Console.WriteLine("Character is swimming at an angular right")

      Case EnvironmentStates.Airplane

        Console.WriteLine("Character is changing directionto the right")

      Case EnvironmentStates.Car

        Console.WriteLine("Character is turning car to the right")

    End Select

  End Sub

 

  Public Sub LeftArrowPressed()

    Select Case _EnvironmentState

      Case EnvironmentStates.Land

        Console.WriteLine("Character is moving at an angular left")

      Case EnvironmentStates.Underwater

        Console.WriteLine("Character is swimming at an angular left")

      Case EnvironmentStates.Airplane

        Console.WriteLine("Character is changing directionto the left")

      Case EnvironmentStates.Car

        Console.WriteLine("Character is turning car to the left")

    End Select

  End Sub

 

  Public Sub ActionPressed()

    Select Case _EnvironmentState

      Case EnvironmentStates.Land

        Console.WriteLine("Character is talking")

      Case EnvironmentStates.Underwater

        Console.WriteLine("Character is surfacing for air")

      Case EnvironmentStates.Airplane

        Console.WriteLine("Character is getting directions")

      Case EnvironmentStates.Car

        Console.WriteLine("Character is using the GPS")

    End Select

  End Sub

End Class



Additionally, while in water, if the character reaches the edge of the water body, user will jump out of water and be on solid ground. Similarly, reaching the landing area, whilst in an airplane, will move the character on the ground. In a similar fashion, the character will change environments when he reaches the periphery of that environment. Ground is compatible with all the three environments and changes are triggered in both directions on reaching appropriate boundaries. Environment changes between other boundaries are not possible.

Environment Water boundary Airport boundary Parking Lot boundary
Land Jump underwater Jump inside Airplane Jump inside Car
Underwater Jump on Land* Not Applicable Not Applicable
Airplane Not Applicable Jump on Land* Not Applicable
Car Not Applicable Not Applicable Jump on Land*

* Next corresponding boundary

We can extend our design to add instance variables that can help define the boundary parameters. Note that while on land – a two dimensional co-ordinate system can be used to map the boundary, however while in water or in air, we need a three dimensional co-ordinate system to store the depth and height respectively in addition to the x and y co-ordinates.

Additional Instance variables

  Private _XCoord As Integer

  Private _YCoord As Integer

  Private _DepthCoord As Integer

  Private _HeightCoord As Integer

  Private _OpenLandXDimension As Integer

  Private _OpenLandYDimension As Integer

  Private _WaterXDimension As Integer

  Private _WaterYDimension As Integer

  Private _WaterDepthDimension As Integer

  Private _AirplaneXDimension As Integer

  Private _AirplaneYDimension As Integer

  Private _AirplaneHeightDimension As Integer

  Private _RoadXDimension As Integer

  Private _RoadYDimension As Integer



Modified constructor

  Public Sub New()

    'All episodes start with the character on Land

    _EnvironmentState = EnvironmentStates.Land

    ' ... at location 0,0; isn't that nice.

    _XCoord = 0 : _YCoord = 0 : _DepthCoord = 0 : _HeightCoord = 0

 

    'Define the dimensions of each environment

    _OpenLandXDimension = 100 : _OpenLandYDimension = 100

    _WaterXDimension = 100 : _WaterYDimension = 100 : _WaterDepthDimension = 50

    _AirplaneXDimension = 100 : _AirplaneYDimension = 100 : _AirplaneHeightDimension = 200

    _RoadXDimension = 50 : _RoadYDimension = 200

  End Sub



Boundary determining functions

  Private Function _IsWaterBoundaryEncountered() As Boolean

    'Use the layout map and the current X and Y coordinates and the Depth coordinate to determine if the water boundary is encountered

    'the depth coordinate is useful when the current state is in water

  End Function

 

  Private Function _IsAirportBoundaryEncountered() As Boolean

    'Use the layout map and the current X and Y coordinates as well as the Height coordinate to determine if the airport boundary is encountered

    'the height coordindate is useful when the current state is in the airplane

  End Function

 

  Private Function _IsParkingLotBoundaryEncountered() As Boolean

    'Use the layout map and the current X and Y coordinates as well as the coordinate to determine if the parking lot boundary is encountered

  End Function



Partially modified listing of the UpArrowPressed method

  Public Sub UpArrowPressed()

    Select Case _EnvironmentState

      Case EnvironmentStates.Land

        Console.WriteLine("Character is moving forward")

        _XCoord += 1

 

        Select Case True

          Case _IsWaterBoundaryEncountered()

            _EnvironmentState = EnvironmentStates.Underwater

            _XCoord = 0 : _YCoord = 0 : _DepthCoord = 1

 

          Case _IsAirportBoundaryEncountered()

            _EnvironmentState = EnvironmentStates.Airplane

            _XCoord = 0 : _YCoord = 0 : _HeightCoord = 1

 

          Case _IsParkingLotBoundaryEncountered()

            _EnvironmentState = EnvironmentStates.Car

            _XCoord = 0 : _YCoord = 0

        End Select

 

      Case EnvironmentStates.Underwater

        Console.WriteLine("Character is swimming forward")

        'Repeat similar code here

 

      Case EnvironmentStates.Airplane

        Console.WriteLine("Character is increasing throttle")

        'Repeat similar code here

 

      Case EnvironmentStates.Car

        Console.WriteLine("Character is accelerating")

        'Repeat similar code here

    End Select

  End Sub



Of course, besides navigating in these environments, the character performs a number of actions that are not relevant in the context of this example and I have deliberately avoided them (at least for now)
Well, we have a working design and things look good. A closer look at the code brings out the following issues –
  • The instance variable definition shows that a lot of variables only have contextual meaning based on the state of the character. For example, the _DepthCoord and the _HeightCoord have no meaning when the GameCharacter is on ground or while he is driving a car.

  • Each supported keystroke handler has the same “Select Case” code blocks to determine their action based on the state of the GameCharacter.

  • State transitions are interspersed in code making it almost impossible to understand when a transition takes place.

  • Adding a new state – say Sailing in a boat will require rework of each keystroke handler and additional rework on maintaining the correct state when the boundary is encountered. This implies that the entire GameCharacter class will need to be checked for changes and at least five methods will need to be altered in addition to adding instance variables to represent state specific behavior.

So how do we fix this? Quite easily, as a matter of fact, all we need to do is build a state interface that provides methods to address the keystrokes and implement each required state to handle the keystrokes as it sees fit in its environment. The GameCharacter simply holds the instance of the current state and delegates all keystrokes to the current state instance.

IEnvironmentState Interface Listing

Public Interface IEnvironmentState

  Sub UpArrowPressed()

  Sub DownArrowPressed()

  Sub RightArrowPressed()

  Sub LeftArrowPressed()

  Sub ActionPressed()

End Interface



Move the responsibility of holding and determining the environment boundaries to the GameEnvironmentManager class

GameEnvironmentManager Class Listing

Public Class GameEnvironmentManager

 

  Private _OpenLandXDimension As Integer

  Private _OpenLandYDimension As Integer

  Private _WaterXDimension As Integer

  Private _WaterYDimension As Integer

  Private _WaterDepthDimension As Integer

  Private _AirplaneXDimension As Integer

  Private _AirplaneYDimension As Integer

  Private _AirplaneHeightDimension As Integer

  Private _RoadXDimension As Integer

  Private _RoadYDimension As Integer

 

  Public Sub New()

    'Define the dimensions of each environment

    _OpenLandXDimension = 100 : _OpenLandYDimension = 100

    _WaterXDimension = 100 : _WaterYDimension = 100 : _WaterDepthDimension = 50

    _AirplaneXDimension = 100 : _AirplaneYDimension = 100 : _AirplaneHeightDimension = 200

    _RoadXDimension = 50 : _RoadYDimension = 200

  End Sub

 

  Public Function IsWaterBoundaryEncountered(ByVal XCoord As Integer, ByVal YCoord As Integer) As Boolean

    'Use the X and Y coord to determine if the Water boundary is encountered

  End Function

 

  Public Function IsWaterBoundaryEncountered(ByVal XCoord As Integer, ByVal YCoord As Integer, ByVal DepthCoord As Integer) As Boolean

    'Use the X and Y coord alongwith the depth to determine if the Water boundary is encountered

  End Function

 

  'Other functions to determine the other boundaries

End Class



Implement each state and control the state of the GameCharacter appropriately by creating classes for WalkingLand, SwimmingUnderWater, FlyingAirplane and DrivingCar

WalkingLand Class Listing

Public Class WalkingLand

  Implements IEnvironmentState

 

  Private _oGameCharacter As GameCharacter

  Private _oGEM As GameEnvironmentManager

  Private _XCoord As Integer

  Private _YCoord As Integer

 

  Public Sub New(ByVal oGameCharacter As GameCharacter, ByVal oGEM As GameEnvironmentManager)

    _oGameCharacter = oGameCharacter

    _oGEM = oGEM

    _XCoord = 0

    _YCoord = 0

  End Sub

 

  Public Sub UpArrowPressed() Implements IEnvironmentState.UpArrowPressed

    Console.WriteLine("Character is moving forward")

    _XCoord += 1

    _HandleIfBoundaryEncountered()

  End Sub

 

  Public Sub DownArrowPressed() Implements IEnvironmentState.DownArrowPressed

    Console.WriteLine("Character is moving backwards")

    _XCoord -= 1

    _HandleIfBoundaryEncountered()

  End Sub

 

  Public Sub RightArrowPressed() Implements IEnvironmentState.RightArrowPressed

    Console.WriteLine("Character is moving at an angular right")

    _XCoord += 1 : _YCoord -= 1

    _HandleIfBoundaryEncountered()

  End Sub

 

  Public Sub LeftArrowPressed() Implements IEnvironmentState.LeftArrowPressed

    Console.WriteLine("Character is moving at an angular left")

    _XCoord += 1 : _YCoord += 1

    _HandleIfBoundaryEncountered()

  End Sub

 

  Public Sub ActionPressed() Implements IEnvironmentState.ActionPressed

    Console.WriteLine("Character is talking")

  End Sub

 

  Private Sub _HandleIfBoundaryEncountered()

    Select Case True

      Case _oGEM.IsWaterBoundaryEncountered(_XCoord, _YCoord)

        _oGameCharacter.EnvironmentState = New SwimmingUnderwater(_oGameCharacter, _oGEM)

 

        'Other boundary encountered statements go here

    End Select

  End Sub

End Class



GameCharacter Class Listing

Public Class GameCharacter

  Private _oEnvironmentState As IEnvironmentState

  Private _oGEM As GameEnvironmentManager

 

  Public Sub New()

    _oGEM = New GameEnvironmentManager

    'All episodes start with the character on Land

    _oEnvironmentState = New WalkingLand(Me, _oGEM)

  End Sub

 

  Public Sub UpArrowPressed()

    _oEnvironmentState.UpArrowPressed()

  End Sub

 

  Public Sub DownArrowPressed()

    _oEnvironmentState.DownArrowPressed()

  End Sub

 

  Public Sub RightArrowPressed()

    _oEnvironmentState.RightArrowPressed()

  End Sub

 

  Public Sub LeftArrowPressed()

    _oEnvironmentState.DownArrowPressed()

  End Sub

 

  Public Sub ActionPressed()

    _oEnvironmentState.ActionPressed()

  End Sub

 

  Public WriteOnly Property EnvironmentState() As IEnvironmentState

    Set(ByVal value As IEnvironmentState)

      _oEnvironmentState = value

    End Set

  End Property

End Class



I have ignored the details of the other classes to reduce the code bulk in this example. This design completely separates the GameCharacter from its constituent states and the behavior of the GameCharacter is delegated to the individual states. Interestingly, each state may need to be aware of other related transitioning states in the system so that a state transition can be effectively addressed. An alternative to our design is to have these states predefined in the GameCharacter class and simply trigger the GameCharacter class via public methods to set its next state appropriately. This alternate design will eliminate any need for the implementing EnvironmentStates to know about each other and the entire controlling mechanism is built in the GameCharacter class.
Selecting an appropriate design choice depends on how best you feel about handling additional states or packaging components.



I have built a test harness to test our GameCharacter and its reaction based on Keystrokes. For the sake of keeping the example short, I have forced a return value of true for the boundary condition tests when the GameCharacter moves forward and when it surfaces for air after swimming backward.

modMain Module Listing

Module modMain

  Sub Main()

    Dim oGameCharacter As GameCharacter = New GameCharacter

    oGameCharacter.UpArrowPressed()  'This will trigger Swimming in water

    oGameCharacter.DownArrowPressed()

    oGameCharacter.ActionPressed() 'This will trigger Jumping on Land

    oGameCharacter.UpArrowPressed()  'This will trigger Swimming in water again, but we wont test it

    Console.ReadLine()

  End Sub

End Module



And the results

3 comments:

Hrishi said...

Gaonjeevan :P

Java rocks. its the best. .NET sucks.
its the language of the devil

;)

hehehehehehe

Alvin Menezes said...

I am impressed that all this made some sense to you, of course, in your own queer way

Hrishi said...

:)

Actually it didn't :)