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 ListingPublic 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 ListingPublic 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 ListingPublic 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 ListingPublic 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 ListingPublic 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 ListingModule 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