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 is to successfully return the ball as many consecutive times as possible without dropping off the bottom.

Requirements

The game board should be set up like this (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; 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 ultimatly, 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 book that was suggested for the class.

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

We would like to see how creative you can be with this assignment.  If you would like, you can come up the graphics and sounds to be used in this assignment.  T ry 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 Hints

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

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_objDDBackSurface.SetFontTransparency True
g_objDDBackSurface.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 transparently 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

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

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

We can grasp this intuitively through a real-world example.  Suppose a car is travelling in a straight line at 50 miles/hour.  That is, after travelling for 1 hour, the call will be 50 miles from its starting point.  If the car is travelling for, say, 4 hours, the car is now at a distance of 4*50 = 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/hour, the car is now at the 500 + 4*50 = 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 right 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.

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 interpenetrate.

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 perpindicular 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)

Keyboard Input

You will use the DirectInput library for receiving keyboard input, which is documented on page 470 of the DirectX book.  While it is possible 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.  In this example, we are trying to determine if the left-arrow key has been pressed, but this can be generalized to any key.

'Common declarations.
Dim objDirectX As 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.  In fact, there is very few places where you should have ANY code in the module.  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 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).

We have included some sample ball and paddle images below.  The ones in the web page are GIFs, but you can get the complete set as .bmps 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

Note that we use DirectX8 for the 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 reference to DirectX8 in the project.

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.

Blocks / Other Gameplay Features (10 points)

A common type of game (or at least it used to be common...) has the player bouncing a ball with a paddle to destroy a set of blocks.  When the ball hits a block, it is reflected off, and if that block has been hit enough times, the block is destroyed.  When all the blocks are destroyed, the player moves to the next level.

For a substantial amount of extra credit, implement a block-based game as described, rather than just the simple racquet-pong.  There are some sample screenshots below from the game Arkanoid that should give you the idea.

  

A reasonable technique for implementing this is to store a 2-dimensional array that represents the grid of blocks on the board, along with an array of sprite objects for each type of block (not for each individual block).  A number indicates which type of block is present for each grid cell (or zero for destroyed/non-existent).  Cycle through the array and for each block, render the sprite for that block type, at the correct position.  You will also have to extend your collision detection to test each block, though it may be necessary for performance reasons to avoid performing exhaustive tests against every block (like partitioning the field into several regions, and only testing the region that contains the ball).  Contact the TA regarding ideas, if necessary.

Submision Instructions