Sol's Graphics for Beginners

(ch22src.zip)

(prebuilt win32 exe)

22 - Adding the Dots on the Ball

Feel free to make a copy of the project, or continue from the last one.

In this chapter we'll be refactoring the code from chapter 20 - Simple 3d Transformations to be usable in our game.

First, copy the vertex structure to our gp.h, under the collectible structure.

Next, copy the definitions of gVtx and gRVtx to main.cpp (after the other globals), and also into the gp.h (after the other globals), and prefix the lines with 'extern' in the header file, like this:

// Original vertices
extern vertex *gVtx;
// Transformed vertices
extern vertex *gRVtx;

Next, let's add a couple of defines in the gp.h file. We'll change the ball's color a bit, and add defines for the dot colors:

// Color of player's ball
#define BALLCOLOR 0x003f9f
// Color of player's ball's foreground dots
#define BALLHICOLOR 0xffffff
// Color of player's ball's background dots
#define BALLLOCOLOR 0x7f7fff

The BALLCOLOR was there earlier; BALLHICOLOR and BALLLOCOLOR are new.

Since we might want to adjust the number of vertices, let's make a macro out of that as well:

// Number of vertices in the ball
#define BALLVTXCOUNT 10

Next, copy the contents of the init() function from chapter 20 to the beginning of our init() function, and replace all instances of the number '100' with the BALLVTXCOUNT macro, so that the arrays have that size and the for-loop only loops through the array size.

Still in the init() function, find the line that says 'int i, j, k;', and move that near the top, replacing the line that says 'int i;' that we just copied from the older chapter init() function.

Then we need a new source file, called 'ball.cpp'. The ball rendering and 3d math will go into that file. Create the file, start it off with the same 'include' block as all the other .cpp files, then copy the rotate_z(), rotate_y(), rotate_x() and render() functions into the file.

Go through the rotate-functions, replacing the '100' in the for-loops with BALLVTXCOUNT again.

Replace the render() function declaration with:

void drawball(int x, int y, int r, int colorb, int color0, 
              int color1, float roty, float rotz)

Remove all the lines from the beginning of the drawball() function until the drawcircle() function call. Replace the drawcircle() line with:

drawcircle(x, y, r, colorb);

Change the '100' on the memcpy line to BALLVTXCOUNT.

Remove the rotz and roty definition lines (we'll use the ones from the function parameters instead).

The for-loop has the final '100', which we'll replace with BALLVTXCOUNT. Set the 'c' variable to color1 by default and color0 if the z is below zero.

The drawcircle() call inside the for-loop changes to:

drawcircle((int)(gRVtx[i].x * (r - 2) + x),
            (int)(gRVtx[i].y * (r - 2) + y),
            2, c);

The lines after the rendering loop are not needed anymore, so you should remove them, including the last drawcircle() call. We're not quite done with the ball rendering code yet, but let's see what it looks like.

Add the following in gp.h, after the other function externs:

extern void drawball(int x, int y, int r, int colorb, int color0, 
                     int color1, float roty, float rotz);

Go to game.cpp, and find the comment that says 'draw the player object'. Replace the two drawcircle() calls with:

drawball((int)gXPos, (int)gYPos, RADIUS, BALLCOLOR, 
          BALLLOCOLOR, BALLHICOLOR, tick/100.0f, 
          (float)atan2(gYMov, gXMov));

Compile and run. We're 'rolling' the ball with the tick currently, but you should see how just seeing the ball's motion direction affects the playability.

Let's change the ball's rolling so that it shows the ball's speed as well. Add the following variable to main.cpp, and also to gp.h with the extern-prefix.

// Player's ball's roll value
float gRoll;

In game.cpp, go to reset() and reset the new variable to zero. Then, in rendergame(), find the comment that says 'Collision with the screen borders', and add the following line just before the comment:

gRoll += (float)sqrt(gXMov * gXMov + gYMov * gYMov);

The above line calculates the length of the motion vector using the pythagoras theorem, and adds it to the roll value.

Finally, go back to where we're calling the drawball() function, and replace the "tick/100.0f" with "gRoll/10.0f". Compile and run. Not perfect, but much better.

At this point the ball has a couple of glitches. First is that the 'background' dots sometimes draw on top of the 'foreground' dots. We'll deal with that next.

The second is that the ball's motion is not continuous; if you do a 180 turn, the ball will 'flip' around. Solving that properly would require some heavy math, but luckily we'll get away with a tradeoff.

Anyway, let's solve the dot sorting problem.

Go back to ball.cpp. We could create a sort index for all the vertices in the ball, sort them by their z values, and then draw them in that order, but that would be an overkill in this case. Instead, we'll replace the for-loop with two, first drawing all the dots with z below zero, and then the rest, like this:

for (i = 0; i < BALLVTXCOUNT; i++)
{
  if (gRVtx[i].z < 0)
    drawcircle((int)(gRVtx[i].x * (r - 2) + x),
                (int)(gRVtx[i].y * (r - 2) + y),
                2, color0);
}

for (i = 0; i < BALLVTXCOUNT; i++)
{
  if (gRVtx[i].z >= 0)
    drawcircle((int)(gRVtx[i].x * (r - 2) + x),
                (int)(gRVtx[i].y * (r - 2) + y),
                2, color1);
}

There. It's a small thing, but small things do matter. Compile and run.

Next, the continuity problem. Basically the correct way to solve it would be to keep the ball's rotation as a quaternion or a rotation matrix, and adjust that each frame, and then rotate the original vertices with the current rotation. Since we can normalize the quaternion, this would minimize the error that creeps in.

If you're wondering what's this error that I'm talking about, floating point numbers are designed to be inaccurate, but to cover a large range of numbers. That means that basically, each time you add, substract, multiply, etc. a floating point value, you're very likely throwing away some accuracy. Thus, in many cases, a+b-b != a.

Thus, for example, if you rotate unit vectors, you may not end up with unit vectors. This is true especially if you rotate them multiple times, as the errors tend to accumulate. Luckily, the vector will most likely point at about the correct direction after the rotations, and you can normalize (i.e. make unit length) the vectors by dividing its components with the vector length.

I'm not entirely sure if you can normalize a rotation matrix, but I suppose that might also be possible.

In any case, we can get away with doing things in a less-correct way, i.e. rotate the vertices each frame from where they were left in the last frame, since nobody will be able to see the difference even if the coordinates do change a bit.

Back to work.

Go to ball.cpp, and find the memcpy line where we're copying the original vertices over the rotated ones. Move this line to game.cpp:s reset() function.

If you compile and run, you'll find that the spots are moving quite fast. This is because we're rotating the dots with all the accumulated 'roll'.

Go to game.cpp, find the call to drawball() in render(), and reset the gRoll to zero after the call. Compile and run; no, we're still not done. If you roll straight to the right, it works, but everything else blows up.

Go to ball.cpp, and change the rotation calls as follows:

rotate_z(-rotz);
rotate_y(roty);
rotate_z(rotz);

First we rotate the ball so that if the whole world moved with the ball, the ball would be rolling straight to the right. Then, we rotate the ball, and move the world back like it was.

Compile, run, and play for a while. It's surprising how much better the controls feel now that you can see the ball's rotation. The gameplay itself is still rather horrible, and to make things worse, you learn to play with the horrible gameplay while you're developing the game. Let someone else try it for a bit, and watch; you'll see that the ball is still very hard to control.

Since we'll be adding several things that will also affect gameplay, we'll postpone the tuning for a while.

Next up is the addition of the 23 - Background Tunnel.

Having problems? Improvement ideas? Just want to discuss this tutorial? Try the forums!

Any comments etc. can be emailed to me.