Learn VB .NET Through Game Programming [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

Learn VB .NET Through Game Programming [Electronic resources] - نسخه متنی

Matthew Tagliaferri

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
توضیحات
افزودن یادداشت جدید





Building an Arcade Game

With DirectDraw capabilities so easily within the grasp of the Visual Basic programmer, you’ll now write an arcade game and get some sprites interacting on the screen. The arcade game is called SpaceRocks, and it involves a little spaceship floating around on the screen and shooting at some asteroids. (Sound familiar? Not to me, either.) Figure 8-2 shows a stirring game of SpaceRocks in action.


Figure 8-2. SpaceRocks, ahoy!

The structure of this game isn’t unlike the structure of the DirectXDemo program previously described, but there’s one further level of abstraction between this program and the last. In the previous program, much of the coding happened at the form level, such as the storage of the DirectDraw Device and Surface variables and the Dice object array. In SpaceRocks, a reusable class named dxWorld sets up the DirectDraw surface and device objects and handles the basic functions such as clearing the back buffer to black and flipping the back buffer to the front. Think of this as the generic game class; any future games you write will be subclasses of this class. Listing 8-6 shows portions of the dxWorld class (with some already-discussed elements removed).


Listing 8.6: The Ancestor Class for Future Games, dxWorld



Public MustInherit Class dxWorld
Private FFrm As Form
Private FNeedToRestore As Boolean = False
Protected oRand As New Random
Protected oDraw As Microsoft.DirectX.DirectDraw.Device
Protected oFront As Microsoft.DirectX.DirectDraw.Surface
Protected oBack As Microsoft.DirectX.DirectDraw.Surface
Protected oJoystick As Microsoft.DirectX.DirectInput.Device
Public Sub New(ByVal f As Form)
MyBase.New()
FFrm = f
FFrm.Cursor.Dispose
AddHandler FFrm.KeyDown, AddressOf FormKeyDown
AddHandler FFrm.KeyUp, AddressOf FormKeyUp
AddHandler FFrm.Disposed, AddressOf FormDispose
InitializeDirectDraw()
InitializeJoystick()
InitializeWorld()
Do While FFrm.Created
DrawFrame()
Loop
End Sub
Protected Overridable Sub FormDispose(ByVal sender As Object, _
ByVal e As System.EventArgs)
If Not (oJoystick Is Nothing) Then
oJoystick.Unacquire()
End If
End Sub
ReadOnly Property WorldRectangle() As Rectangle
Get
Return New Rectangle(0, 0, WID, HGT)
End Get
End Property
'override for better keyboard handling
Protected MustOverride Sub FormKeyDown(ByVal sender As Object, _
ByVal e As System.Windows.Forms.KeyEventArgs)
'override for better keyboard handling
Protected Overridable Sub FormKeyUp(ByVal sender As Object, _
<similar to prior discussion, removed>
End Sub
Private Sub InitializeDirectDraw()
<similar to prior discussion, removed>
End Sub
'override to set up your world objects
Protected MustOverride Sub InitializeWorld()
'override when bitmaps have to be reloaded
Protected Overridable Sub RestoreSurfaces()
oDraw.RestoreAllSurfaces()
End Sub
Private Sub DrawFrame()
<similar to prior discussion, removed>
End Sub
'override. put all your drawing in here.
Protected Overridable Sub DrawWorldWithinFrame()
Try
oBack.ForeColor = Color.White
oBack.DrawText(10, 10, "Press escape to exit", False)
Catch oEX As Exception
Debug.WriteLine(oEX.Message)
End Try
End Sub
End Class



The constructor for the dxWorld class takes a form as a parameter, and this form is dynamically assigned event handlers for its KeyUp, KeyDown, and Dispose events. The form used as the parameter for this class needs to have almost no code in it at all, except for the code that sets up an instance of this dxWorld class (actually, an instance of a descendant of the dxWorld class because dxWorld itself is declared MustInherit). As shown in Listing 8-7, creating an instance of this game on the form happens in four lines of code on an empty form.

Listing 8.7: Creating a New dxWorld Instance



Public Class fMain
Inherits System.Windows.Forms.Form
Dim FWorld As dxWorld.dxWorld
Private Sub fMain_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
FWorld = New dxWorld.dxSpaceRocks(Me)
End Sub
End Class



All of the important variables in the dxWorld class are declared as protected so that they’ll be accessible in the descendant classes. This includes the Surface variables for the front and back surface and the DirectDraw Device object. There’s also a Random object instance set up so that random numbers can be generated from anywhere inside the class or its descendants.


Setting Up a Joystick


You might also notice a variable named oJoystick, which is of type Microsoft.DirectX.DirectInput.Device. Yes, the new game class will be able to handle joystick input as well as keyboard input. Getting the joystick ready happens in the InitializeJoystick method on the dxWorld class, as shown in Listing 8-8.

Listing 8.8: The InitializeJoystick Method



Private Sub InitializeJoystick()
Dim oInst As DeviceInstance
Dim oDOInst As DeviceObjectInstance
'get the first attached joystick
For Each oInst In Manager.GetDevices( _
DeviceClass.GameControl, EnumDevicesFlags.AttachedOnly)
oJoystick = New Microsoft.DirectX._
DirectInput.Device(oInst.InstanceGuid)
Exit For
Next
If Not (oJoystick Is Nothing) Then
oJoystick.SetDataFormat(DeviceDataFormat.Joystick)
oJoystick.SetCooperativeLevel(FFrm, _
Microsoft.DirectX.DirectInput. _
CooperativeLevelFlags.Exclusive Or _
Microsoft.DirectX.DirectInput.CooperativeLevelFlags.Foreground)
' Set the numeric range for each axis to +/- 256.
For Each oDOInst In oJoystick.Objects
If 0 <> (oDOInst.ObjectId And _
CInt(DeviceObjectTypeFlags.Axis)) Then
oJoystick.Properties.SetRange(ParameterHow.ById, _
oDOInst.ObjectId, New InputRange(-256, +256))
End If
Next
End If
End Sub



InitializeJoystick retrieves the first game control device that it finds attached to the machine and then sets the range of all Axis objects within that joystick to have a range from –256 to +256. The standard game pad will have an x-axis and ay-axis; some three-dimensional controllers, such as SpaceBall, may have an x-axis, y-axis, and z-axis to be defined. Based on Listing 8-8, all axis objects associated with the joystick will be found and have their range set.

You’ll see the code that shows how to poll the joystick for data and use it to update the game state later in the section “Setting Up the Ship Control Code.” You first need to see how to set up the game elements themselves.


Creating the dxSprite Class


The base information to keep track of an object on the screen is stored in a class named dxSprite, as shown in Listing 8-9. This class is somewhat similar in structure to the SimpleDie class defined for the DirectXDemo project.


Listing 8.9: The dxSprite Class Interface



Public MustInherit Class dxSprite
Public Event GetSurfaceData(ByVal sender As dxSprite, _
ByRef oSource As Microsoft.DirectX.DirectDraw.Surface, _
ByRef oRect As Rectangle)
Property Location() As PointF
Property Size() As Size
Overridable Property Frame() As Integer
ReadOnly Property BoundingBox() As Rectangle
ReadOnly Property WorldBoundingBox() As Rectangle
Property pShowBoundingBox() As Boolean
ReadOnly Property Center() As PointF
Public MustOverride Sub Move()
Public Sub Draw(ByVal oSurf As Microsoft.DirectX.DirectDraw.Surface)
End Class



Much of the definition of this class is straightforward and doesn’t require explanation. There are a few members, however, that do require a bit of clarification. The GetSurfaceData event is used as a callback so that the sprite class doesn’t have to store surface (source bitmap) data directly. The reason you might not want to store surface data with the sprite was hinted at in the DirectXDemo application earlier. First, a game might contain dozens (hundreds?) of instances of the same sprite, and you certainly don’t want to store multiple copies of the same bitmap data in each individual sprite instance.

Second, a single object may have several bitmaps to represent it depending on the state in which it might be. For the SpaceRocks game, for example, the ship object has three possible sprites: a ship with a fire trail, a ship without atrail, and an exploding ship for when it gets hit by a rock.

Using an event to retrieve the proper sprite based on the state of the object in question helps to decouple the sprite class from the game class. Note that there’s nothing directly relevant to an arcade space-shooting-rock-type game in the class definition shown in Listing 8-9. The goal is to keep the dxSprite class generic enough to reuse in different projects (but you’ll be creating SpaceRocks-specific subclasses for this game).

The Draw method of the sprite class is also nonstandard. As mentioned earlier in the chapter, DirectDraw doesn’t take kindly to copying surfaces off the edge of the destination surface. The copy fails miserably, in fact, and crashes the program. Even if this crash is handled gracefully with a structure exception handler, the sprite “winks” out of existence as it reaches the edge of the destination surface instead of smoothly scrolling off the screen.

You must do some nasty rectangle manipulation to fix this problem. If you want to draw a ship partially off the left side of the screen, for example, then the program has to clip off the left side of the sprite and draw only the right portion of the rectangle on the left side. If the ship sprite is moving left, then each frame will clip more and more of the left side of the ship until it disappears. Figure 8-3 shows asprite off the left side of the screen. The gray area is the area to be clipped.


Figure 8-3. Two sprites partially off the left side of the screen. The gray area must be clipped, and only the white area should be drawn.

The nasty clipping math must also adjust the bounding boxes of each sprite. The bounding box represents a rectangle that surrounds the sprite and helps to test for collision between two sprites that might hit each other (the ship with arock, for example). There are two representations of the bounding box stored for each sprite. One is declared in sprite coordinates, meaning that the upper-left corner in this bounding box is usually 0, 0. The second bounding box representation is stored in world coordinates, meaning that the upper-left corner is usually the same as the sprite’s Location property (the location on the screen). Listing 8-10 shows a portion of the Draw method.

Listing 8.10: A Portion of the Draw Method



Public Sub Draw(ByVal oSurf As Microsoft.DirectX.DirectDraw.Surface)
Dim oSource As Microsoft.DirectX.DirectDraw.Surface
Dim oRect As Rectangle
Dim oPt As Point
Dim iDiff As Integer
RaiseEvent GetSurfaceData(Me, oSource, oRect)
If oSource Is Nothing Then
Exit Sub
Else
Try
FWBB = Me.BoundingBox 'start w/ normal bbox
'start at the location
oPt = New Point(System.Math.Floor(Location.X), _
System.Math.Floor(Location.Y))
If oPt.X < 0 Then
'draw partial on left side
oRect = New Rectangle(oRect.Left - oPt.X, oRect.Top, _
oRect.Width + oPt.X, oRect.Height)
If oPt.X + FWBB.Left < 0 Then
FWBB = New Rectangle(0, FWBB.Top, _
FWBB.Width + (oPt.X + FWBB.Left), FWBB.Height)
Else
FWBB = New Rectangle(FWBB.Left + oPt.X, FWBB.Top, _
FWBB.Width, FWBB.Height)
End If
oPt.X = 0
End If
<lots of other rectangle-clipping code removed>
'should never happen, just in case
If oRect.Width <= 0 Or oRect.Height <= 0 Then Return
'offset the bounding box by the world coordinates
FWBB.Offset(oPt.X, oPt.Y)
'draw the sprite
oSurf.DrawFast(oPt.X, oPt.Y, oSource, oRect, _
DrawFastFlags.DoNotWait Or DrawFastFlags.SourceColorKey)
'draw the bounding box
If Me.pShowBoundingBox Then
oSurf.ForeColor = Color.Red
oSurf.DrawBox(FWBB.Left, FWBB.Top, FWBB.Right, FWBB.Bottom)
End If
Catch oEx As Exception
Debug.WriteLine("--------------------------------------")
Debug.WriteLine(oEx.Message)
End Try
End If
End Sub




Creating the dxSpaceRocks Class


The SpaceRocks game is implemented in the class named dxSpaceRocks, which is a descendant of the dxWorld class. This class contains the classes that store all of the game objects, including the ship, the rocks, and any bullets currently flying around. The rocks and bullets are stored in a different way because there can be multiple instances of these classes in the game at one time. The player’s ship is always a lone instance, so the class that contains the ship information has a much different structure.

Setting Up the Game Class


Listing 8-11 shows the declaration of the game class and the instantiation of the private variables that track all the game objects.

Listing 8.11: The dxSpaceRocks Class with the Game Object Class Definition and Initialization Code



Public Class dxSpaceRocks
Inherits dxWorld
Private FShip As dxShipSprite
Private FRocks As dxRockCollection
Private FBullets As dxBulletCollection
Protected Overrides Sub InitializeWorld()
Dim oRand As New Random
FShip = New dxShipSprite
FShip.Location = New PointF(100, 100)
FShip.Size = New Size(96, 96)
FShip.pShowBoundingBox = False
FRocks = New dxRockCollection
FRocks.pShowBoundingBox = False
FBullets = New dxBulletCollection
FBullets.pShowBoundingBox = False
End Sub
Protected Overrides Sub RestoreSurfaces()
MyBase.RestoreSurfaces()
FShip.RestoreSurfaces(oDraw)
FRocks.RestoreSurfaces(oDraw)
FBullets.RestoreSurfaces(oDraw)
End Sub
<code removed>
End Class



The rock and bullet storage classes are collections, and their class names refer to them as such. The ship class, however, is a direct descendant of the dxSprite class, so its initialization is a bit different from the other two.

The procedure RestoreSurfaces, if you’ll recall, is called when bitmap surface objects have to be re-created. Because the game class itself isn’t storing any source surface objects, each game class has its own RestoreSurfaces method, and this method is called from the game’s method of the same name. This procedure was originally declared as protected and Overrideable in the base dxWorld class, which gives you the ability to access it and override it in the subclass.

Setting Up the Game Class Drawing and Movement


Drawing for all descendants of the dxWorld class happens by overriding the protected method DrawWorldWithinFrame. Listing 8-12 shows that method.


Listing 8.12: The DrawWorldWithinFrame Method



Protected Overrides Sub DrawWorldWithinFrame()
Dim p As New Point((WID / 2) - 40, 10)
MyBase.DrawWorldWithinFrame()
'joysticks don't generate events, so we update the ship
'based on joystick state each turn
UpdateShipState()
FShip.Move()
FRocks.Move()
FBullets.Move()
FBullets.Draw(oBack)
FShip.Draw(oBack)
FRocks.Draw(oBack)
FBullets.BreakRocks(FRocks)
oBack.ForeColor = Color.White
Select Case FShip.Status
Case ShipStatus.ssAlive
oBack.DrawText(p.X, p.Y, "Lives Left: " & _
FShip.LivesLeft, False)
If FRocks.CollidingWith(FShip.WorldBoundingBox, _
bBreakRock:=False) Then
FShip.KillMe()
End If
Case ShipStatus.ssDying
oBack.DrawText(p.X, p.Y, "Oops.", False)
Case ShipStatus.ssDead
If FShip.LivesLeft = 0 Then
oBack.DrawText(p.X, p.Y, "Game Over", False)
Else
oBack.DrawText(p.X, p.Y, _
"Hit SpaceBar to make ship appear " + _
"in middle of screen", False)
End If
End Select
End Sub



The DrawWorldWithinFrame method runs once per every “clock tick” of the game engine. It controls both object movement and object drawing. At the start of the method is a call to a procedure named UpdateShipState. This procedure (described next) changes the state of the ship based on what joystick buttons are being pressed. Then, the program calls a Move method on the ship class and the rock and bullet collections. The Move method updates the position of every game object based on its current location, the direction it’s traveling, and the speed at which it’s traveling.

Once all the game objects have been moved, the Draw method of the three game class objects is called, passing in the variable that holds the back buffer DirectDraw surface. You’ve already seen the Draw method for the dxSprite class (with all the rectangle clipping logic), and the Draw method on the collection classes simply calls the Draw method for each dxSprite in their respective collections.

The remainder of the DrawWorldWithinFrame method handles the drawing of a text message at the top of the screen based on the current state of the player’s ship. The game will report the number of lives the player has left, report a simple Oops as the ship explodes because of collision with a rock, give instructions on how to make the ship reappear if the user has lives left, or report Game Over if no lives remain. One other task is handled within this Case statement, and that’s the collision check between the ship and the rocks (the CollidingWith method on the FRocks variable).


Setting Up the Ship Control Code


The remainder of the dxSpaceRocks class handles ship movement via keyboard or joystick. Listing 8-13 shows this code.

Listing 8.13: Ship Movement Code for the dxSpaceRocks Class



Public Class dxSpaceRocks
Inherits dxWorld
Private FLeftPressed As Boolean = False
Private FRightPressed As Boolean = False
Private FUpPressed As Boolean = False
Private FSpacePressed As Boolean = False
<some code removed>
Protected Overrides Sub FormKeyDown(ByVal sender As Object, _
ByVal e As System.Windows.Forms.KeyEventArgs)
Select Case e.KeyCode
Case Keys.Left
FLeftPressed = True
Case Keys.Right
FRightPressed = True
Case Keys.Up
FUpPressed = True
Case Keys.Space
FSpacePressed = True
Case Keys.B
FShip.pShowBoundingBox = Not FShip.pShowBoundingBox
FRocks.pShowBoundingBox = Not FRocks.pShowBoundingBox
FBullets.pShowBoundingBox = Not FBullets.pShowBoundingBox
End Select
End Sub
Protected Overrides Sub FormKeyUp(ByVal sender As Object, _
ByVal e As System.Windows.Forms.KeyEventArgs)
MyBase.FormKeyUp(sender, e)
Select Case e.KeyCode
Case Keys.Left
FLeftPressed = False
Case Keys.Right
FRightPressed = False
Case Keys.Up
FUpPressed = False
End Select
End Sub
Private Sub UpdateShipState()
Dim oState As New JoystickState
Dim bButtons As Byte()
Dim b As Byte
Dim p As PointF
If Not oJoystick Is Nothing Then
Try
oJoystick.Poll()
Catch oEX As InputException
If TypeOf oEX Is NotAcquiredException Or _
TypeOf oEX Is InputLostException Then
Try
' Acquire the device.
oJoystick.Acquire()
Catch
Exit Sub
End Try
End If
End Try
Try
oState = oJoystick.CurrentJoystickState
Catch
Exit Sub
End Try
'ship is turning if x axis movement
FShip.IsTurningRight = (oState.X > 100) Or FRightPressed
FShip.IsTurningLeft = (oState.X < -100) Or FLeftPressed
FShip.ThrustersOn = (oState.Y < -100) Or FUpPressed
'any button pushed on the joystick will work
bButtons = oState.GetButtons()
For Each b In bButtons
If (b And &H80) <> 0 Then
FSpacePressed = True
Exit For
End If
Next
Else
FShip.IsTurningRight = FRightPressed
FShip.IsTurningLeft = FLeftPressed
FShip.ThrustersOn = FUpPressed
End If
If FSpacePressed Then
Select Case FShip.Status
Case ShipStatus.ssDead
'center screen
FShip.BringMeToLife(WID \ 2 - FShip.Size.Width \ 2 , _
HGT \ 2 - FShip.Size.Height \ 2 )
Case ShipStatus.ssAlive
p = FShip.Center
p.X = p.X - 16
p.Y = p.Y - 16
FBullets.Shoot(p, FShip.Angle)
End Select
FSpacePressed = False
End If
End Sub
End Class



Keyboard state is stored in Boolean variables named FLeftPressed, FRightPressed, FUpPressed, and FSpacePressed. These variables are set to True in the KeyDown event and to False in the KeyUp event (if the appropriate key is indeed being pressed, that is). By storing the variables in this way, the game allows for object movement as long as the correct key is down. For example, once a user presses the up arrow, the ship should have its thrusters on until the key is released. The Boolean FUpPressed will stay True as long as the arrow is down.

The B key is the last key that affects the game—it turns the bounding boxes on and off for debugging purposes.





Note

This was especially useful to me as I slowly coded the “sprite-half-off-the-screen” code in the dxSprite’s Draw method (see Listing 8-10 to relive the pain).


The function UpdateShipState, called once per drawing frame, polls the joystick and keyboard Boolean variables for their states and updates the state of the ship accordingly. For example, if the joystick’s x-axis has a value that’s greater than 100, then the ship is turning clockwise. A move in the negative y direction on the joystick is the cue to turn on the thrusters. Pressing Button 1 on the joystick (or pressing the spacebar) either fires a bullet or brings a dead ship back to life.


Setting Up the Ship Class


The dxShipSprite class is a descendant of the dxSprite class discussed earlier. This class controls the player’s ship as it cruises around on the screen. There are three graphics required for the ship—one for the ship with thrusters off, one with thrusters on, and one for an explosion sequence for when the ship is biting the dust. Figure 8-4 shows one frame of each of the bitmaps.


Figure 8-4: The first frame of the each of the three ship graphics

Drawing the Ship


The two ship graphics consist of 24 frames. Each frame represents a different rotation of the ship in a circle. There are 15 degrees of rotation between each frame (360 degrees / 24 frames = 15 degrees per frame). The explosion sequence is only six frames and was designed by hand (and not very well; bear in mind that I don’t consider computer graphics design among my talents).

Drawing the correct graphic at the correct time is a function of what state the ship is in at the moment. There’s an enumerated type declared called ShipStatus that defines whether the ship is currently okay, in the middle of exploding, or dead and gone. If the ship is gone, then the program obviously doesn’t have to draw it at all. If the ship is in the middle of exploding, then the explosion graphic is chosen for display. If the ship is okay, then one of the two ship graphics are displayed, either with or without the thruster fire. The ship control code in Listing 8-13 hinted at the fact that the ship sprite has a property named ThrustersOn, and this property determines which of the two ship bitmaps to draw. Listing 8-14 shows the portion of the dxShipSprite class that loads the three bitmaps into DirectDraw Surface variables and the code that selects the correct surface to draw in a given frame.

Listing 8.14: Ship Sprite State and Graphics-Related Code



Public Enum ShipStatus
ssAlive = 0
ssDying = 1
ssDead = 2
End Enum
Public Class dxShipSprite
Inherits dxSprite
Private FShipSurfaceOff As Microsoft.DirectX.DirectDraw.Surface
Private FShipSurfaceOn As Microsoft.DirectX.DirectDraw.Surface
Private FShipSurfaceBoom As Microsoft.DirectX.DirectDraw.Surface
Public Sub New()
MyBase.new()
AddHandler Me.GetSurfaceData, AddressOf GetShipSurfaceData
End Sub
Private FStatus As ShipStatus
ReadOnly Property Status() As ShipStatus
Get
Return FStatus
End Get
End Property
'we can keep surfaces in the ship
'sprite class because there's only one of them
Public Sub RestoreSurfaces(ByVal oDraw As _
Microsoft.DirectX.DirectDraw.Device)
Dim oCK As New ColorKey
Dim a As Reflection.Assembly = _
System.Reflection.Assembly.GetExecutingAssembly()
If Not FShipSurfaceOff Is Nothing Then
FShipSurfaceOff.Dispose()
FShipSurfaceOff = Nothing
End If
FShipSurfaceOff = New Surface(a.GetManifestResourceStream( _
"SpaceRocks.ShipNoFire.bmp"), New SurfaceDescription, oDraw)
FShipSurfaceOff.SetColorKey(ColorKeyFlags.SourceDraw, oCK)
If Not FShipSurfaceOn Is Nothing Then
FShipSurfaceOn.Dispose()
FShipSurfaceOn = Nothing
End If
FShipSurfaceOn = New Surface(a.GetManifestResourceStream( _
"SpaceRocks.ShipFire.bmp"), New SurfaceDescription, oDraw)
FShipSurfaceOn.SetColorKey(ColorKeyFlags.SourceDraw, oCK)
If Not FShipSurfaceBoom Is Nothing Then
FShipSurfaceBoom.Dispose()
FShipSurfaceBoom = Nothing
End If
FShipSurfaceBoom = New Surface(a.GetManifestResourceStream( _
"SpaceRocks.Boom.bmp"), New SurfaceDescription, oDraw)
FShipSurfaceBoom.SetColorKey(ColorKeyFlags.SourceDraw, oCK)
End Sub
Private Sub GetShipSurfaceData(ByVal aSprite As dxSprite, _
ByRef oSurf As Surface, ByRef oRect As Rectangle)
Dim aShip As dxShipSprite = CType(aSprite, dxShipSprite)
Select Case aShip.Status
Case ShipStatus.ssDead
oSurf = Nothing
Case ShipStatus.ssDying
oSurf = FShipSurfaceBoom
Case ShipStatus.ssAlive
If aShip.ThrustersOn AndAlso _
oRand.Next(0, Integer.MaxValue) Mod 10 <> 0 Then
oSurf = FShipSurfaceOn
Else
oSurf = FShipSurfaceOff
End If
End Select
oRect = New Rectangle((aShip.Frame Mod 6) * 96, _
(aShip.Frame \ 6) * 96, 96, 96)
End Sub
End Class



The RestoreSurfaces code is similar to what you saw in the DirectXDemo application, except that there are three surfaces to load instead of one. The routine GetShipSurfaceData is special because it serves as the event handler for the GetSurfaceData event for this object. If you’ll recall, the GetSurfaceData event is raised from within the Draw method of the dxSprite class (see Listing 8-10 if you need a reminder). When the Draw method is ready to draw, it raises this event and expects the event handler to pass back the correct source Surface object that needs to be drawn, as well as a Rectangle object that indicates which portion of the source bitmap to draw. The routine GetShipSurfaceData does all of that work for the ship class. Based on the state of the ship and whether its thrusters are on or off, the appropriate bitmap is returned. The last line constructs a source rectangle based on the value of the Frame property, based on the knowledge that all of the ship graphics are 96-pixels wide and high.





Note

The game uses one additional trick when selecting a bitmap to display. Ten percent of the time, the GetShipSurfaceData routine returns the ship graphic without the thruster fire, even when thrusters are on. This gives the fire a little “flicker” effect.


Moving the Ship


The ship’s current location is stored in the Location property defined on the ancestor dxSprite class. The trick is figuring out how to move the location based on the current angle of the ship, whether the thrusters are currently on, and how long they’ve been on.

Properties control the velocity of the ship, which is how many pixels it moves per frame in both the x and y directions, and its acceleration, which controls how fast the velocity is increasing.

Listing 8-15 lists the Move method of the ship class, which is called once during every frame by the dxSpaceRocks game class.

Listing 8.15: The Move Method of dxShipSprite



Public Overrides Sub Move()
Dim dx, dy As Single
'we're only moving every x frames
FSkipFrame = (FSkipFrame + 1) Mod 1000
If FSkipFrame Mod 3 = 0 Then
Select Case Me.Status
Case ShipStatus.ssAlive
Turn()
If ThrustersOn Then
Acceleration += 1
dy = -Math.Sin(FAngle * Math.PI / 180) * Acceleration
dx = Math.Cos(FAngle * Math.PI / 180) * Acceleration
Velocity = New PointF(Velocity.X + dx, Velocity.Y + dy)
Else
Acceleration = 0
End If
Case ShipStatus.ssDying
Frame += 1
Velocity = New PointF(0, 0)
Acceleration = 0
'we're done drawing the boom
If Frame >= 6 Then
FStatus = ShipStatus.ssDead
End If
Case ShipStatus.ssDead
'nothing
End Select
End If
Location = New PointF(Location.X + Velocity.X, Location.Y + Velocity.Y)
End Sub



Note that there’s a “governor” of sorts on the Move class in the form of an integer variable named FSkipFrame. This variable updates in every execution of the Move method, but it allows actual velocity and acceleration to change in every third execution. Without this governor, the ship’s controls are far too touchy and hard to control.

The Acceleration property is an integer that keeps increasing as long as the ship’s thrusters are turned on. (Actually, there’s maximum acceleration defined in the property, so it does max out eventually.) The Acceleration variable, along with the current angle the ship is facing and some basic trigonometry, help determine the speed of the ship during this turn in both the x and y directions. This speed is stored in the Velocity property.

At the bottom of the Move method, the calculated velocity is added to the current location, which yields the new location of the ship.


Setting Up Rocks and Rock Collections


The rocks are simpler structures than the ship because they move at a constant speed and in a constant direction, and they aren’t (directly) affected by the game player’s control. This simplicity is counteracted by the fact that the game has to keep track of an undetermined number of them, however. Thus, a “manager” class keeps track of each rock.

The (rather cool) rock graphics themselves were created courtesy of POV-RAY models from Scott Hudson. The models represent digital representations of actual “potential earth-crossing” asteroids. Please visit the Web site [http://www.eecs.wsu.edu/~hudson/Research/Asteroids] for further information.





Note

You can find information on POV-RAY and raytracing in Appendix B, “Using POV-RAY and Moray.”


Creating the Rock Class


The rock class itself keeps track of the size of the rock (there are three possible sizes), the direction it’s moving, which of the two graphics to use, which direction it’s spinning, and how fast it’s spinning. Listing 8-16 shows the public interface for this class.


Listing 8.16: The dxRockSprite Class and Enumerated Type for Determining Rock Size



Public Enum dxRockSize
rsLarge = 0
rsMed = 1
rsSmall = 2
End Enum
Public Class dxRockSprite
Inherits dxSprite
Public Event RockBroken(ByVal aRock As dxRockSprite)
Property pAlternateModel() As Boolean
Property pSpinReverse() As Boolean
Property pRockSize() As dxRockSize
Property pRotSpeed() As Integer
Property Velocity() As PointF
Public Overrides Sub Move()
Public Sub Break()
End Class



Details of this class are mostly trivial and unworthy of you (who by this time is a nearly expert game programmer). The pRockSize property is mildly interesting in that the bounding box of the rock is different depending on the size of the rock.

Creating the Rock Collection Class


The dxRockCollection class is much more interesting than the rock class. This class keeps track of the six different DirectDraw Surface objects that store the rock graphics (two rock shapes in three sizes each). It also keeps the pointers to each individual rock class and handles all of the interaction between the game and the rocks (you can think of this class as a sort of “rock broker”). To that end, several methods on the collection class simply perform functionality upon each rock in the collection. The Draw method is one such method, shown in Listing 8-17, which merely calls the like-named method on each object in the collection.


Listing 8.17: The Draw Method (and Some Others)



Public Sub Draw(ByVal oSurf As Microsoft.DirectX.DirectDraw.Surface)
Dim aRock As dxRockSprite
For Each aRock In FRocks
aRock.Draw(oSurf)
Next
End Sub



Another interesting piece of functionality in the rock collection is the pair of overloaded AddRock methods, shown in Listing 8-18. These methods add a new rock to the collection. It also includes the code that runs when a rock is shot and split in two.

Listing 8.18: Adding a New Rock to the Game in One of Two Ways



Private Overloads Function AddRock()
Dim oPt As PointF
'start location along the edges
Select Case FRand.Next(0, Integer.MaxValue) Mod 4
Case 0
oPt = New PointF(0, FRand.Next(0, Integer.MaxValue) Mod HGT)
Case 1
oPt = New PointF(WID, FRand.Next(0, Integer.MaxValue) Mod HGT)
Case 2
oPt = New PointF(FRand.Next(0, Integer.MaxValue) Mod WID, 0)
Case 3
oPt = New PointF(FRand.Next(0, Integer.MaxValue) Mod WID, HGT)
End Select
Return AddRock(dxRockSize.rsLarge, oPt)
End Function
Private Overloads Function AddRock(ByVal pSize As dxRockSize, _
ByVal p As PointF) As dxRockSprite
Dim aRock As dxRockSprite
aRock = New dxRockSprite
With aRock
.pShowBoundingBox = Me.pShowBoundingBox
.pAlternateModel = FRand.Next(0, Integer.MaxValue) Mod 2 = 0
.pSpinReverse = FRand.Next(0, Integer.MaxValue) Mod 2 = 0
.pRotSpeed = FRand.Next(0, Integer.MaxValue) Mod 3
.pRockSize = pSize
Select Case pSize
Case dxRockSize.rsLarge
.Size = New Size(96, 96)
Case dxRockSize.rsMed
.Size = New Size(64, 64)
Case dxRockSize.rsSmall
.Size = New Size(32, 32 )
End Select
.Location = p
Do 'no straight up/down or left/right
.Velocity = New PointF(FRand.Next(-3, 3), FRand.Next(-3, 3))
Loop Until .Velocity.X <> 0 And .Velocity.Y <> 0
.Move() 'the first move makes sure they're off the edge
AddHandler .GetSurfaceData, AddressOf GetRockSurfaceData
AddHandler .RockBroken, AddressOf RockBroken
End With
FRocks.Add(aRock)
End Function
Private Sub RockBroken(ByVal aRock As dxRockSprite)
Select Case aRock.pRockSize
Case dxRockSize.rsLarge
AddRock(dxRockSize.rsMed, aRock.Location)
AddRock(dxRockSize.rsMed, aRock.Location)
Case dxRockSize.rsMed
AddRock(dxRockSize.rsSmall, aRock.Location)
AddRock(dxRockSize.rsSmall, aRock.Location)
Case dxRockSize.rsSmall
'nothing
End Select
FRocks.Remove(aRock)
End Sub



The first AddRock function is the one that’s used when a new, large size rock is to be added to the game. It takes no parameters. Its job is to select a random point along one of the four edges of the screen, and then it calls the second AddRock method, passing along the size of the new rock (always large) and the location it has selected.

The second AddRock method actually creates the new instance of the dxRockSprite class, sets up all of its properties, and then adds it to the ArrayList that holds all of the rock objects. This second AddRock method is used when arock is shot and splits into two smaller pieces. You can see this code in the RockBroken routine, which serves as the event handler for the rock class event of the same name. When a large rock is broken, two medium-sized rocks are spawned at the same location of the large rock, and then the large rock is removed from the ArrayList named FRocks (and thus from the game). When amedium rock is broken, two smaller rocks are spawned in the same location, and the medium rock is removed from the ArrayList.

The last interesting function in the rock collection class is the CollidingWith function, which determines if an outside agent has crashed into a rock and whether that rock should break as a result (see Listing 8-19).

Listing 8.19: The CollidingWith Function



Public Function CollidingWith(ByVal aRect As Rectangle, _
ByVal bBreakRock As Boolean) As Boolean
Dim aRock As dxRockSprite
For Each aRock In FRocks
If aRock.WorldBoundingBox.IntersectsWith(aRect) Then
If bBreakRock Then
aRock.Break()
End If
Return True
End If
Next
Return False
End Function



The collision code in the game relies on the bounding boxes of all of the game objects (ship, rocks, and bullets). The bounding boxes are all represented by .NET Framework Rectangle objects. One of the most useful methods built into the Rectangle class is the IntersectsWith class, which returns True if the current rectangle overlaps another passed-in rectangle parameter. The function shown in Listing 8-19 checks to see if the bounding box for each rock in the collection intersects with the rectangle that’s passed into the function. If it finds an intersection, the function returns True and the rock involved in the collision either breaks or doesn’t break, depending on the value of the bBreakRock parameter (collisions with bullets break the rock, and a collision with the ship leaves the rock intact).


Setting Up Bullets and Bullet Collections


Keeping with the pattern of discussing things in decreasing order of complexity, the bullet class is the simplest of the three major game elements. The bullet has only one graphic (with only a single frame) and can move in a single direction at a fixed speed. Like the rocks class, a “manager” class keeps track of multiple bullets on the screen.

Creating the Bullet Class


The bullet class is simple and short enough to list here in its entirety, as shown in Listing 8-20.

Listing 8.20: The Bullet Sprite Class



Public Class dxBulletSprite
Inherits dxSprite
Private FFrameAliveCount As Integer
Sub New()
MyBase.New()
FBoundingBox = New Rectangle(10, 10, 12 , 12 )
End Sub
Private FVelocity As PointF
Property Velocity() As PointF
Get
Return FVelocity
End Get
Set(ByVal Value As PointF)
FVelocity = Value
End Set
End Property
Public Overrides Sub Move()
Location = New PointF(Location.X + Velocity.X, _
Location.Y + Velocity.Y)
FFrameAliveCount += 1
End Sub
ReadOnly Property pFrameAliveCount() As Integer
Get
Return FFrameAliveCount
End Get
End Property
End Class



The bullet class keeps track of velocity and a property known as FrameAliveCount. This property determines when a bullet has traveled far enough and should be removed from the screen. The Move method is extremely simple. It changes the location of the sprite by the value of the Velocity property in both the x and y directions.

Creating the Bullet Collection Class


The collection class that keeps track of multiple bullets shares many features with the rock collection class already discussed. It uses an ArrayList to store multiple instances of the dxBulltetSprite class. Listing 8-21 shows the Shoot method, which brings a new instance of the bullet class into the world.

Listing 8.21: The Shoot Method



Public Sub Shoot(ByVal p As PointF, ByVal iAngle As Integer)
If FBullets.Count >= 4 Then Exit Sub
Dim dx, dy As Single
Dim aBullet As dxBulletSprite
aBullet = New dxBulletSprite
With aBullet
.pShowBoundingBox = Me.pShowBoundingBox
.Location = p
dy = -Math.Sin(iAngle * Math.PI / 180) * 6
dx = Math.Cos(iAngle * Math.PI / 180) * 6
.Velocity = New PointF(dx, dy)
.Move()
AddHandler .GetSurfaceData, AddressOf GetBulletSurfaceData
End With
FBullets.Add(aBullet)
End Sub



The Shoot method first checks that there are fewer than four bullets already floating around in space. If four bullets are already on the screen, then the method returns without firing. If this check succeeds, though, then a new dxBulletSprite object is instantiated, properties are set (including the Velocity property, calculated from the angle parameter pass into the function), and the bullet is added to the ArrayList.

The method BreakRocks, shown in Listing 8-22, is called once in each drawing loop to see if the bullet has found its target.

Listing 8.22: The Method BreakRocks



Public Sub BreakRocks(ByVal FRocks As dxRockCollection)
Dim aBullet As dxBulletSprite
Dim i As Integer
'check each bullet to see if it hits a rock
'have to use a loop so you don't skip over when deleting
i = 0
Do While i < FBullets.Count
aBullet = FBullets.Item(i)
If FRocks.CollidingWith(aBullet.WorldBoundingBox, _
bBreakRock:=True) Then
FBullets.Remove(aBullet)
Else
i = i + 1
End If
Loop
End Sub



The method BreakRocks uses the CollidingWith function discussed in Listing 8-19 to determine if any of the bullets in this collection have collided with any rock in the game. A slightly tricky loop is employed in this method that requires some explanation. Whenever a collection is iterated and the possibility exists that elements in the collection will be removed during that iteration, then the program should never use the standard For..Each method to iterate, or the result is that items in the collection will be skipped. Instead, you should use a loop such as the one shown in Listing 8-22. This loop uses an integer counter to keep track of the place in the iteration. The trick is that if an element in the collection is deleted (in this case, a bullet), then the loop counter isn’t incremented. Say the loop is an element 5 in a collection of 10, and this element is removed from the collection. After the removal, all of the elements after element 5 have “slid down” one place in the order, meaning the former element 6 is now element 5. By not incrementing the counter after a delete, the next iteration of the loop makes sure to check that next element.

/ 106