Learn VB .NET Through Game Programming [Electronic resources]

Matthew Tagliaferri

نسخه متنی -صفحه : 106/ 53
نمايش فراداده

Creating the Player Class and Subclasses

Kinh Luan’s two-player game example abstracted the responsibilities of a game player into a series of interfaces and then implemented those interfaces in a series of classes. The game uses different classes for a human player, a computer-controlled player, and a local version of a network player.

Creating the Base Class

For this program, instead of using interfaces to define functionality, you’ll instead create a base class and then create subclasses from it. This enables you to put some commonly used code inside the base class without duplicating it. Listing 7-1 shows the base player class for the network Reversi game.

Listing 7.1: The ReversiPlayer Class, the Base Class for All Reversi Players

Public MustInherit Class ReversiPlayer 
Private FName As String 
Private FColor As Color 
Private FScore As Integer 
Private FMyTurn As Boolean 
Public Event IsMyTurn(ByVal oPlayer As ReversiPlayer) 
Sub New(ByVal cNm As String, ByVal cClr As Color) 
MyBase.New() 
FName = cNm 
'only two colors allowed 
Debug.Assert(cClr.Equals(Color.Red) Or cClr.Equals(Color.Blue)) 
FColor = cClr 
End Sub 
ReadOnly Property Name() As String 
Get 
Return FName 
End Get 
End Property 
ReadOnly Property Color() As Color 
Get 
Return FColor 
End Get 
End Property 
ReadOnly Property OpponentColor() As Color 
Get 
Return IIf(FColor.Equals(Color.Red), _ 
Color.Blue, Color.Red) 
End Get 
End Property 
Overridable Property MyTurn() As Boolean 
Get 
Return FMyTurn 
End Get 
Set(ByVal Value As Boolean) 
FMyTurn = Value 
If Value Then 
RaiseEvent IsMyTurn(Me) 
End If 
End Set 
End Property 
Property Score() As Integer 
Get 
Return FScore 
End Get 
Set(ByVal Value As Integer) 
FScore = Value 
End Set 
End Property 
End Class 

The Reversi player class contains properties for the player name, the color of the player’s pieces on the board (currently constrained to blue or red), the color of the opponent’s pieces on the board (if this player is red, then the opponent is blue, and vice versa), the player’s score (how many pieces of her color are on the board), and a Boolean property named MyTurn that represents if it’s currently her turn. There’s also an event raised whenever the MyTurn property is set to True. The Name and Color properties are defined as ReadOnly, and they’re set in the constructor for this class. This structure makes sure that an instance of this class can’t change its name or color after it has been initially created.

Creating the Human Player Class

The first variant of the player class to discuss is the player controlled by a human. There’s only a small amount of specialized code beyond the base player class; Listing 7-2 shows that code.

Listing 7.2: The HumanReversiPlayer Class

Public Class HumanReversiPlayer 
Inherits ReversiPlayer 
Private FForm As Form 
Public Event MyMoveLoc As MoveToLocationDef 
Sub New(ByVal cNm As String, ByVal cClr As Color, ByVal f As Form) 
MyBase.New(cNm, cClr) 
FForm = f 
End Sub 
Overrides Property MyTurn() As Boolean 
Get 
Return MyBase.MyTurn 
End Get 
Set(ByVal Value As Boolean) 
If Value Then 
AddHandler FForm.MouseDown, AddressOf OnMouseDown 
Else 
RemoveHandler FForm.MouseDown, AddressOf OnMouseDown 
End If 
MyBase.MyTurn = Value 
End Set 
End Property 
Private Sub OnMouseDown(ByVal sender As Object, _ 
ByVal e As System.Windows.Forms.MouseEventArgs) 
If Not MyTurn Then Exit Sub 
If Not e.Button = MouseButtons.Left Then Exit Sub 
RaiseEvent MyMoveLoc(Me, e.X, e.Y) 
End Sub 
End Class 

The first thing that differs in the human-specific version of the player class is the constructor. This constructor takes a form variable as well as a Color property and a Name property. This passed-in form variable is equal to a private variable, so the class can access it later.

The next extension to the human player class is an overridden version of the MyTurn property. Some interesting syntax exists here. Your intent is to store the MyTurn variable data in the same way but to selectively attach or detach a MouseDown event handler to the form variable depending on whether it’s currently the human player’s turn. By doing this, you can disable the ability for the human player to click the form when it’s not her turn.

The property Get function returns the value of MyBase.MyTurn, which is another way of saying “The value of MyTurn in the ancestor class ReversiPlayer.” Likewise, the last line of the Set statement for this property writes the intended value to MyBase.MyTurn, meaning that the underlying property is still being used to hold the MyTurn variable data.

The remainder of the MyTurn property’s Set statement adds or removes the MouseDown handler of the private form variable, depending on the new value of the property. If MyTurn is True, then the MouseDown event handler becomes active. If MyTurn is False, then the MouseDown event handler is unattached.

The MouseDown event itself, named OnMouseDown, raises an event named MyMoveLoc when it’s called. Look at the definition of the MyMoveLoc event earlier in the HumanReversiPlayer class definition:

Public Event MyMoveLoc As MoveToLocationDef 

This event declaration looks just like a variable declaration, doesn’t it? It contains an access specifier (Public), a name (MyMoveLoc), and a type (MoveToLocationDef). So exactly what type is MoveToLocationDef? Is it an integer, a string, or a class?

In truth, MoveToLocationDef describes a type of function. Think about that statement for a second. Can a function be of a certain type? Or, perhaps more clearly put, can two functions be of different types? The answer is obviously “yes”; a function is of a specific type, and that type is described by the parameters sent to the function. So, if function A accepts a single integer parameter, then you could say that the function is of type A, and if function B accepts an integer and a string parameter, then that function is of type B.

The description of a function type happens in Visual Basic .NET through a delegate.Adelegate is a function type declaration. It describes a class of functions that are related from the standpoint that they all pass the same parameter lists. You can declare the delegate declaration for the MoveToLocationDef event as follows:

Public Delegate Sub MoveToLocationDef(ByVal sender As ReversiPlayer, _ 
ByVal x As Integer, ByVal y As Integer) 

This declaration declares a type of function (actually, a Sub in this case) that takes a ReversiPlayer parameter and two integer parameters named x and y. Once you make this declaration, you can declare an event as having this type:

Public Event MyMoveLoc As MoveToLocationDef 

What this means is that the MyMoveLoc event will pass back the same three parameters as specified in the delegate declaration—a ReversiPiece variable and two integers. Looking back at the MyTurn property declaration in Listing 7-2, you can see that this is indeed the parameter list when the program raises the event:

RaiseEvent MyMoveLoc(Me, e.X, e.Y) 

Creating the Computer Player Class

The class that controls the computer player is similarly simple. It changes little from the base class, as shown in Listing 7-3.

Listing 7.3: The ComputerReversiPlayer Class

Public Delegate Sub MakeBestMoveDef(ByVal sender As ReversiPlayer) 
Public Class ComputerReversiPlayer 
Inherits ReversiPlayer 
Public Event MyMakeBestMove As MakeBestMoveDef 
Sub New(ByVal cNm As String, ByVal cClr As Color) 
MyBase.New(cNm, cClr) 
End Sub 
Overrides Property MyTurn() As Boolean 
Get 
Return MyBase.MyTurn 
End Get 
Set(ByVal Value As Boolean) 
MyBase.MyTurn = Value 
If Value Then 
RaiseEvent MyMakeBestMove(Me) 
End If 
End Set 
End Property 
End Class 

This class declares a second delegate for use by the computer player class. This delegate, named MakeBestMoveDef, describes a function that takes a single parameter of type ReversiPlayer. It also declares an event within the ComputerReversiPlayer class as being of this type, and it raises this event whenever the MyTurn property of this class is set to True. In other words, the event executes whenever it becomes the computer’s turn.

Creating the Network Player Class

The player class that represents a network player is sort of a “proxy” class. Imagine two people who are going to play the game over a network, player A and player B. On the machine being used by player A, a HumanReversiPlayer class instance will be created to represent and handle the responsibilities of player A, and NetworkReversiPlayer will be created to send and receive the move information over the network to the computer of player B. On player B’s machine, the opposite is true. Player B will have a HumanReversiPlayer instance to control her own moves and a NetworkReversiPlayer instance to send and receive move data from player A. Listing 7-4 shows the NetworkReversiPlayer class, which contains the send and receive move code.

Listing 7.4: The NetworkReversiPlayer Class

Public Class NetworkReversiPlayer 
Inherits ReversiPlayer 
Private FStream As NetworkStream 
Public Event MyMoveLoc As MoveToLocationDef 
Sub New(ByVal cNm As String, ByVal cClr As Color, _ 
ByVal oStream As NetworkStream) 
MyBase.New(cNm, cClr) 
FStream = oStream 
End Sub 
Public Sub LookForTurn() 
Do 
If FStream.DataAvailable Then 
Dim cPiece As String 
Dim oPiece As New ReversiPiece 
Dim oSer As New  XmlSerializer(oPiece.GetType) 
Dim oRead As StreamReader 
oRead = New StreamReader(FStream) 
cPiece = oRead.ReadLine 
oPiece = oSer.Deserialize(New StringReader(cPiece)) 
RaiseEvent MyMoveLoc(Me, _ 
oPiece.Location.X, oPiece.Location.Y) 
End If 
System.Threading.Thread.Sleep(250) 
Loop Until False 
End Sub 
Public Sub SendMyTurnToOpponent(ByVal aP As ReversiPiece) 
Dim oSer As New XmlSerializer(aP.GetType) 
Dim oSW As New StringWriter 
Dim oWriter As New XmlTextWriter(oSW) 
Dim oByte() As Byte 
Dim cSend As String 
oSer.Serialize(oSW, aP) 
oWriter.Close() 
cSend = oSW.ToString 
cSend = cSend.Replace(Chr(10), ") 
cSend = cSend.Replace(Chr(13), ") 
'add crlf so READLINE works 
cSend &= Microsoft.VisualBasic.vbCrLf 
Try 
oByte = System.Text.Encoding.ASCII.GetBytes(cSend.ToCharArray()) 
FStream.Write(oByte, 0, oByte.Length) 
Catch oEX As SocketException 
MsgBox(oEX.Message) 
End Try 
End Sub 
End Class 

There’s no overridden functionality in the network player class, only new functionality. It declares an event named MyMoveLoc, which is raised within the LookForTurn method once it’s determined that this network player has moved somewhere. This LookForTurn method is an endless loop that polls for data on an object called NetworkStream. This class sends data over a network from one program to another. The section “Sending Game Data” discusses the format of the data; let’s focus on the class structure of the program for the time being. The new SendMyTurnToOpponent method also uses this NetworkStream object instance to send the information of a move over the wire to the other player.

Creating “Thin” Player Classes?

You might be asking yourself “Where’s the beef?” after studying the player classes. In other words, the player classes don’t seem to do much in the way of game playing, do they? They contain no information about the board, for instance, or any logic to decide what move might be the best to make. In fact, all they seem to do is store some player-specific information and raise some events.

This design choice was made, uh, by design. The player classes were kept “thin” purposefully so that they know as little as possible about the specific game being played. Take a second look at the player classes, and you’ll probably agree that you could use these same classes for a number of different games, not only for the Reversi game being discussed here.

This is what’s known as keeping the coupling between classes loose. Coupling (apart from a hilarious sitcom on the BBC) describes how “linked” classes are to one another. Listing 7-5 shows a sample definition of a more tightly coupled player class.

Listing 7.5: A Tight Coupling of a Player and Game Class

Public Class TightlyCoupledComputerReversiPlayer 
Inherits ReversiPlayer 
Private FReversiGame as ReversiGame 
Sub New(ByVal cNm As String, ByVal cClr As Color, g as ReversiGame) 
MyBase.New(cNm, cClr) 
FreversiGame = g 
End Sub 
<more stuff here> 
End Class 

This version of the class passes in an instance of a ReversiGame class, probably so that the code that determines the best move to make resides inside the computer player class. Although this makes sense from a “responsibilities” standpoint, it doesn’t make sense from a “loose coupling” standpoint because the player class and the game class are now forever linked to one another. Such linkage is bad because it prevents the reuse of the player class in another game without modification.