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.
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.
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
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.
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.
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).
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.
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
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. |
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.
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.” |
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.
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).
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.
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.
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.