V22.0480 - ASSIGNMENT 3

Racquet-Pong


One of the earliest commercial video games was the simple, yet addictive (for the time) Pong, based on ping-pong. Two players, each controlling a paddle at opposite ends of the playing board, would hit a ball back and forth.  The ball would bounce off sides and off the paddles themselves, and the goal was to get the other player to miss the ball, while not allowing the ball to pass your own paddle.

The original Pong home machine (this was a game console with exactly one game) required two people to play - there was no AI for the other paddle.  Because we'd like you to be able to play your game with just one player, we will recreate Pong, but with a twist: the side opposite you is a wall, and you will try to continually bounce the ball to yourself, trying not to miss - a bit like racquetball, hence the name, Racquet-Pong.  To make things challenging, the ball speeds up every time you hit it, and your goal as a player is to successfully return the ball as many consecutive times as possible without dropping off the bottom.

Requirements

You will create a windowed application (i.e. DirectDraw in normal mode) using DirectInput to handle the input. The game board should be set up as follows (note that you are not required to maintain the exact dimensions, board orientation, or relative sizes of the components; this is just one possibility):

The paddle can move back and forth across the bottom of the playing board, but must be prevented from going off the sides.  The ball bounces off the walls and the paddle, and continually accelerates after each hit.  Of course, the ball must not fly off the playing board.  On every hit, a score meter will display the number of consecutive hits so far, as well as the current top score.  If the ball hits the bottom of the playing field, it is out, and the game will start over.

The paddle, ball, and field (background) must be represented by bitmapped images.  (Sample images are provided below; you may try to come up with more interesting images for your final submission).  Because the ball should be round, while a standard bitmap is rectangular, you will need to use transparency when drawing your sprites to the screen (see implementation hints).  These images must move without any noticeable "flicker" artifact, so you will want to use double-buffering.

Your game can have sound effects (see extra credit).  When you hit the ball, when the ball hits a wall, when the game ends, or when you break the record, the game should play a sound to acknowledge this event.  Sound makes the game more immersive, and ultimately, more fun to play.  We give you a lot of the basic code for producing sounds from .wav files- for more information, you should consult the DirectX SDK documentation or the DirectX book that was suggested for the class.

Control will be performed with the keyboard utilizing DirectInput.  You should use the left and right arrow keys to move the paddle back and forth.

If you would like, you can create your own graphics and sounds to be used in this assignment.  HOWEVER, make sure your assignment is complete with the existing artwork before you try to customize it, so you avoid losing points if you run out of time.  You may try to theme your background, paddle, ball, etc. You have full flexibility on what images to use for each component. Try to be creative and come up with an interesting, consistent, and artistic look and feel for your game.

Finally, the game should be challenging.  It should not be possible to hit the ball into such a path that it will always bounce back to the paddle, allowing the user to get an infinite number of hits without moving.  The best way to avoid this is through some element of randomness, and ideas are given below.

Implementation Notes

Drawing Graphics

You will have at least 5 DirectDraw surfaces in your application. This application will be displayed in a window (that is, not fullscreen).  Also, you will not be using a flipping chain.

With the exception of the title bar and menus (if you need to include menus for some reason) you will draw graphics with DirectDraw.  This will be essential for later, when you are using fullscreen mode.  As with the previous assignment, you must not use a picture box (or an image box) and must instead draw directly on the form.

You should encapsulate all sprite code in a single class CSprite, which has a current position, velocity and size, as well as all of the pertinent DirectX objects relating to that sprite (surface, SURFDESC, etc.).  This class will be able to draw its sprite at its particular location on the screen.  You might also want to further encapsulate your objects into CBall, CPaddle, etc. which will help you to better manage and organize your code. You will find it helpful to look carefully at Class Notes 5 and build sprite classes as recommended.

To draw the current and top score, you can use the DrawText function of the DirectDrawSurface object, in the following manner

sStatus = "Score: " & g_dScore & " Top Score: " & g_dTopScore
g_sfcBufferBack.SetFontTransparency True
g_sfcBufferBack.DrawText 5, 10, sStatus , False

This creates a string for the score display, and draws it onto the specified buffer at x=5, y=10.  The font will be drawn over the background.  Passing "False" as the last parameter tells DirectDraw to draw the string at the location specified by the other arguments (as opposed to the current cursor location).  Additionally, you can use the setFont method to indicate the typeface.  You probably want to make sure to do this part after you draw the sprites, so that the text is on top of the sprites, rather than the other way around.

Updating the World

Again, you will find it helpful to look carefully at Class Notes 5 and build sprite classes as recommended.  Pay special attention to Slide 10 (updating spaceship probe's sprite movement) and its attendant discussion regarding updating the object's position.  Each sprite should have a position (x and y coordinates) and a velocity vector (rate of change in x and y per some unit of time). You may store the sprite position as PositionX and PositionY and the velocity in each of those directions as VelocityX and VelocityY, respectively. On every frame, we must use the velocity of the sprite to update the position - this is animation.  The general rule for updating position is very simple, and can be expressed in terms of vector operations, where the vectors can be of arbitrary dimensions.

p' = p + v*t = [ p.x + v.x*t , p.y + v.y*t ]    (in 2 dimensions)

For example, suppose a car is traveling in a straight line at 50 miles per hour.  After 4 hours, the car is now at a distance of 50 * 4 = 200 miles from the starting point.  To get the absolute position, we add this computed distance to the original position.  So, for example, if the car was at the 500 mile marker on I-95, after 4 hours of driving at 50 miles per hour, the car is now at the 500 + 50 * 4 = 700 mile marker (making the assumption that I-95 is perfectly straight).

In our case, we generalize this into a 2-dimensional space by specifying the position in x and y (distance from the left and bottom edges of the window) and specifying the velocity as a rate of change per unit of time in both x and y.  We then use the same vector equation to compute the updated position of the ball, just as we did for the car.

For each frame, we will need to incrementally update the position of the ball, by considering the current position and velocity, and updating the system by the time elapsed since the last update.  So if 1/10th of a second has passed since the last frame, the new position will be p' = p + v*0.1.  It will be very important to use a timer in order to ensure smooth animation, as well as consistency between machines.  When the amount of work done by the system varies from frame to frame, the interval between frames will not be constant.  Therefore, it will be essential to use the Timer to measure the exact time interval, and update the system accordingly.  Additionally, on slower machines (or faster machines to come in the future), the time interval may be very different, and use of the timer is again critical to maintain consistency.

The above equation and implementation is described in the pseudocode on Slide 10 of Class Notes 5, so it's in your best interest to review those notes.

To make it easier to modify the ball speed, you might find it convenient to store the velocity as both a normalized vector (which represents the direction, and has length 1), and a speed factor, which is used to scale the velocity.  The speed can then be updated independent of direction.

To test collision of the ball against the walls and the paddle, we can perform very simple tests between the center point of the ball, and the axis-aligned line segments of the walls and paddle.   Suppose you have the following game board:

The left and right walls are defined by the lines x=0 and x=50, the top wall and bottom are defined by y = 100 and y = 0, and the paddle is on the line y = 10, for x in [40,60].  If the center point of the ball is above 100, we have hit the top.  If it is below 0, we have hit the bottom.  This is similarly true for the left and right walls when x < 0 or x > 50.  For the paddle, we do only a slightly more complicated test, and check to see if the ball would intersect y = 10 between x =40 and x = 50.  You might want to actually test for collision at a slight distance out from the walls, so that the ball and the walls do not overlap.

When a ball hits a wall or paddle, it should bounce back. Generally, the angle of reflection from the boundary will be equal to the the angle of incidence (the acute angle between the ball's direction and the boundary line). Given the ball's velocity vector, v, and the normal vector of the line, n (more on that in a minute) we can compute the ball's new, reflected velocity vector as:

v' = -2(v dot n)n + v OR r = [-2(v.x*n.x + v.y*n.y)*n.x + v.x, -2(v.x*n.x + v.y*n.y)*n.y + v.y ]

The normal of a line is a unit-length vector that is perpendicular to that line, and in our case, would point into the playing field. So for example, if the bottom left corner were (0,0), with dimensions increasing toward the top and right (as in the standard cartesian graph) the normal of the lines are: bottom (0, 1), top (0, -1), left (1, 0), right (-1, 0) For a geometric justification, see below, or: http://www.tdb.uu.se/~grafik/ht02/lectures/shading2.pdf

To add to the challenge and add randomness in the gameplay, I randomly scale the normal by a small amount (so that it no longer is normal).  This has the effect of changing the angle of reflection, making it either wider or narrower.  This will help to prevent the situation where the ball bounces back and forth on the same path endlessly.

You might, however, want to make reflection off the paddles operate a bit differently.  If we applied the same formula, then it would be impossible to aim the ball - the direction of the balls return would be entirely determined by the incoming direction. The ball would always bounce off all surfaces with the same angle, which gets uninteresting.  In similar games, like the classic Arkanoid, the direction was determined by the location on the paddle hit by the ball. The farther to the outer edge, the wider the angle in that direction.

One suggestion is shown above.  As the diagram shows, when the ball hits the paddle at one of the grey points, the ball would bounce off in the direction indicated.  We can model this as a rotation of the paddle's normal vector, represented by the grey dashed line.   The velocity v is equal to the rotation matrix for a given angle theta, applied to the normal, (0,-1), using the standard coordinate system.  The angle itself can vary between some minimum and maximum, 70 degrees in this example.  The angle theta is determined through linear interpolation with an interpolant alpha, which can range from 0 to 1.   The closer alpha is to 0, the closer the result will be to -70; the closer alpha is to 1, the closer the result will be to +70.  Finally, the interpolant is equal to the relative position of the point of collision in the paddle.  (Note: In the figure above, paddlex is the x coordinate of the left of the paddle)

Sample Code

Direct Draw Code

There is sample code you may refer to within DirectDraw Tutorial 2 located in

    mssdk\samples\Multimedia\VBsamples\DirectDraw\Tutorials\Tut2

mssdk is the DirectX directory created when you installed DirectX.

I would suggest that you not use the tutorial as a starting point; instead, reference the tutorial code as needed to see how certain techniques are implemented.

The more you employ good object-oriented design principles and avoid duplicate code, the better the grade.  Style will count also, so please pay attention to comments, descriptive (not cryptic) variable names, formatting, etc.  You do not have to comment before each property if your property names are clear (just have one line signifying where your group of property definitions begin and another line for when you're finished with all your property definitions).

Direct Input Code

You will use the DirectInput library for receiving keyboard input, which is documented in the class notes, SDK Help files and on page 470 of the DirectX book.  While it is possible (though not for this assignment) to use the KeyDown / KeyUp events of the form to receive keyboard input, that method would tie you to the Windows message queue.  Often in the processing of the game loop, you would like to have a quick check to see if one key is pressed.  With DirectInput, you have a low-latency keyboard library that will allow you to quickly query to see if individual keys have been pressed, and which allows for greaater flexibility than the event-based interface of KeyDown/KeyUp.

You can use the following code as a starting point for your use of DirectInput. You may also use the tutorial we reviewed in class, which is in the following directory

    mssdk\samples\Multimedia\VBsamples\DirectInput\Keyboard

If you do use the tutorial code, you must remove the code from the form and place it in its own module or class.

In the following example, which you may use, we are trying to determine if the left-arrow key has been pressed, but this can be generalized to any key.

'Common declarations.
DimobjDirectXAs New DirectX8                'Top-level DirectX8 object
Dim g_objDirectInput As DirectInput8          'Primary DirectInput object
Dim g_objKeyboardDevice As DirectInputDevice8 'Device object, represents keyboard
Dim g_ksKeyState As DxVBLibA.DIKEYBOARDSTATE  'Contains an array that indicates pressed keys

'Create the DirectInput object and the keyboard device
Set g_objDirectInput = g_objDirectX8.DirectInputCreate
Set g_objKeyboardDevice = g_objDirectInput.CreateDevice("GUID_SysKeyboard")

'Set the properties of the keyboard device, and aquire (makes it readable)
'This can be done once at initialization
g_objKeyboardDevice.SetCommonDataFormat DIFORMAT_KEYBOARD
g_objKeyboardDevice.SetCooperativeLevel frmMain.hWnd, DISCL_BACKGROUND Or DISCL_NONEXCLUSIVE
g_objKeyboardDevice.Acquire

'Retrieve the array of currently pressed keys; place into g_ksKeyState
'This can be done once per frame; g_ksKeyState contains a standard array
g_objKeyboardDevice.GetDeviceStateKeyboard g_ksKeyState

'Query the array to see if the left arrow key has been pressed
'If greater than zero, it has been pressed
'Constants are given in the CONST_DIKEYFLAGS enum
'This is done everytime the state of the key needs to be determined
bLeftButtonPressed = g_ksKeyState.Key(CONST_DIKEYFLAGS.DIK_LEFT) > 0

Code Structure

In many games, it is typical that the program has a main "game loop", which loops constantly.  On each iteration (or "frame") the game loop will process input, update the state of the world, and then draw the scene.   You should probably follow a similar structure.  However, to allow the basic windowing events to occur, you need to call the Visual Basic DoEvents function at least once per iteration.

At the beginning of each frame, you should use the Timer function to determine how much time has elapsed since the beginning of the last frame.  This time delta will then be used to update the scene incrementally, and in a frame-rate independent manner.  It may be more convenient for you to retrieve the time as an integral number of milliseconds, for which you can use the GetTickCount function.  To use this function, place the following line at the top of your module.

Declare Function GetTickCount Lib "kernel32" () As Long

If you find that GetTickCount does not have the timer resolution that you need, consult the Microsoft documentation on the function QueryPerformanceCounter.

As before, make sure that the majority of your code is in the module and/or classes.  In fact, there are very few places where you should have ANY code in the form.  In my solution, I have just one statement, to handle the Form_Unload event.  You can either call the "End" statement, or call a routine in the module that will cause the game loop to terminate by setting a flag.
 

Sample Artwork


We have included some sample ball and paddle images below.  Click on the images directly to download .bmp files (the ones in the web page are GIFs).  Get the complete set here: (sample_images.zip)

Extra Credit

We would like to encourage those that finish early to go above and beyond the basic requirements, and here are some options for additional credit.  If someone has an interesting idea for a feature that they would REALLY like to work on, contact the TA at cdecoro@cat.nyu.edu regarding extra credit for that idea.

Playing Sounds (5 points)

Sounds make a game more immersive, and provide feedback to game events.  You should have sound effects for the collision of a ball with the walls or with the paddle, as well as when the ball goes out of play.  Additionally, a good game has background music; something that sets the tone, without being distracting enough to get in the way of the gameplay.

For sound, your best option is to use the DirectSound library.  This is a low-latency, high-performance interface that gives a lot of control over the process of playing sound.  It is documented in Page 424 of the DirectX book.  Essentially, you will need to create a DirectSound object, and a Secondary Buffer (the Primary Buffer represents the sound currently playing; secondary buffers are mixed into the primary buffer when they are played).  The general overview of playing a sound with DirectSound is the following:

'Standard declarations, notice objDirectX is only one declared as New
'You already have objDirectX for DirectDraw
Dim objDirectX As New DirectX8
Dim objDirectSound As DirectSound8
Dim objSound As DirectSoundSecondaryBuffer8
Dim dsBuf As DxVBLibA.DSBUFFERDESC

'Indicate that we want a hardware buffer, if possible
dsBuf.lFlags = DSBCAPS_STATIC

'Create the other two objects, you would generally do this only once,
'  unless the sound changes
Set objDirectSound = objDirectX.DirectSoundCreate("")
Set objSound = objDirectSound.CreateSoundBufferFromFile("myfilename.wav", dsBuf)

'Play the sound once (non-looping)
'This would be called whenever you change the image
objSound.Play DSBPLAY_DEFAULT

I would suggest creating a CSound class.  This class represents a particular sound effect, and contains the filename of the sound to be played, along with the relevant DirectSound objects.  This object can then be requested to play its sound at a particular time.

Important note on using DirectInput with DirectDraw

Note that we use DirectX8 for the input (and sound) objects, while we use DirectX7 for the graphics.  This is completely reasonable, and is even suggested by the DirectX book, as the DirectX runtime is backwards compatible.

Just remember to set the Project Reference to DirectX8 in the project in addition to DirectX7.  Note that if the project reference for DirectX7 is higher up in the list than that for DirectX8, you need to preface your DirectX8 constant types with DxVBLibA as in Dim dsBuf As DxVBLibA.DSBUFFERDESC (see Sounds sample code above). If DirectX8 is higher in the list, the prefix necessary to access DirectX7 constant types is DxVBLib.  If you encounter any difficulties, please post a question to the list.
 

Submision Instructions