The code for colliding marbles is built on top of earlier changes to the
DDEX3 sample. Look
here,
here and
here for more information. There are a few changes I made in the code for making the marbles collide and I hope you have already gone through Prof. Fu-Kwun Hwang
post on how to make stuff bounce off each other.
Create a structure to store the information about a marble:
/*
This structure stores the info related to a ball.
Its center (xPos, yPos)
Its velocity along both axes (dwVelX, dwVelY)
Its radius in pixels
*/
typedef struct _ballinfo
{
int xPos;
int yPos;
int dwVelX;
int dwVelY;
int radius;
} BALLINFO;
And here are the globals we use in the source file:
#define MAX_BALLS 3
//we’ll be bouncing 3 balls around here.
BALLINFO g_BallInfo[MAX_BALLS];
//to store the rects of each ball
RECT g_Rect[MAX_BALLS];
To add more marbles to the program you just need to change the MAX_BALLS macro and recompile the code. Well, the terms balls and marbles are used interchangeably (:
We initialize our global data in InitApp() function, we call InitializeData() just before InitApp() returns:
void InitializeData()
{
int radius = 22;
for (int i=0; i < MAX_BALLS; i++)
{
g_BallInfo[i].xPos = RandInt(radius, 216);
//switch on i to decide on yPos so that the balls dont overlap when they are created
switch(i)
{
//dividing the screen into 3 horizontal sections with one ball in each
case 0:
g_BallInfo[i].yPos = RandInt(radius, 100-radius);
break;
case 1:
g_BallInfo[i].yPos = RandInt(100+radius, 200-radius);
break;
case 2:
g_BallInfo[i].yPos = RandInt(200+radius, 300-radius);
break;
default:
g_BallInfo[i].yPos = RandInt(radius, 320-radius);
}
g_BallInfo[i].dwVelX = RandInt(-6, 6);
g_BallInfo[i].dwVelY = RandInt(-6, 6);
g_BallInfo[i].radius = radius;
printf("Ball %d details:\n", i+1);
printf("X=%d Y=%d VelX=%d VelY=%d\r\n", g_BallInfo[i].xPos, g_BallInfo[i].yPos,
g_BallInfo[i].dwVelX, g_BallInfo[i].dwVelY);
}
}
Here we initialize all the data related to each marble, its center, velocity and the radius. The switch case makes sure that the balls do not overlap initially, currently I am only checking for three marbles in the switch. So if there are more than 3, there is still some possibility of the balls overlapping. I’ll leave that part for later and lets focus on more important things.
The RandInt() function is simple, it returns a random value between the two ranges specified:
DWORD RandInt(DWORD low, DWORD high)
{
DWORD range = high – low;
DWORD num = Random() % range;
return (num + low);
}
Next, we look at the UpdateFrame() function:
static void UpdateFrame(HWND hWnd)
{
HRESULT hRet;
DDBLTFX ddbltfx;
int i = 0, j = 0;
memset(&ddbltfx, 0, sizeof(ddbltfx));
ddbltfx.dwSize = sizeof(ddbltfx);
ddbltfx.dwFillColor = 0;
ddbltfx.dwROP = SRCCOPY;
//clear the back buffer (color fil with black)
g_pDDSBack->Blt(NULL, NULL, NULL, DDBLT_COLORFILL | DDBLT_WAITNOTBUSY, &ddbltfx);
for (i = 0; i < MAX_BALLS; i++)
{
g_Rect[i].top = g_BallInfo[i].yPos – 24;
g_Rect[i].left = g_BallInfo[i].xPos – 24;
g_Rect[i].bottom = g_BallInfo[i].yPos + 24;
g_Rect[i].right = g_BallInfo[i].xPos + 24;
}
ddbltfx.ddckSrcColorkey.dwColorSpaceLowValue = RGB(0, 0, 0);
ddbltfx.ddckSrcColorkey.dwColorSpaceHighValue = RGB(0, 0, 0);
//blt all the balls on the screen
for (i = 0; i<MAX_BALLS; i++)
{
while (TRUE)
{
hRet = g_pDDSBack->Blt(&g_Rect[i], g_pDDSOne, NULL, DDBLT_ROP | DDBLT_KEYSRCOVERRIDE, &ddbltfx);
if (hRet == DD_OK)
break;
if (hRet == DDERR_SURFACELOST)
{
hRet = RestoreAll();
if (hRet != DD_OK)
break;
}
if (hRet != DDERR_WASSTILLDRAWING)
break;
}
}
//update the balls positions and check for bounds
for (i = 0; i<MAX_BALLS; i++)
{
g_BallInfo[i].xPos = g_BallInfo[i].xPos + g_BallInfo[i].dwVelX;
g_BallInfo[i].yPos = g_BallInfo[i].yPos + g_BallInfo[i].dwVelY;
if(g_BallInfo[i].xPos <= 24)
{
g_BallInfo[i].xPos = 24;
g_BallInfo[i].dwVelX = -g_BallInfo[i].dwVelX;
}
if(g_BallInfo[i].xPos >= (240 – 24))
{
g_BallInfo[i].xPos = 240 – 24;
g_BallInfo[i].dwVelX = -g_BallInfo[i].dwVelX;
}
if(g_BallInfo[i].yPos <= 24)
{
g_BallInfo[i].yPos = 24;
g_BallInfo[i].dwVelY = -g_BallInfo[i].dwVelY;
}
if (g_BallInfo[i].yPos >= (320 – 24))
{
g_BallInfo[i].yPos = 320 – 24;
g_BallInfo[i].dwVelY = -g_BallInfo[i].dwVelY;
}
}
//check for collisions between all the balls
for (i = 0; i < MAX_BALLS-1; i++)
{
for (j = i+1; j < MAX_BALLS; j++)
{
if(Collision(&g_BallInfo[i], &g_BallInfo[j]))
{
printf("Balls HIT!\n");
CalculateNewVelocities(&g_BallInfo[i], &g_BallInfo[j]);
MessageBeep(MB_OK);
}
}
}
}
This is more or less the same as before except that the same things are done for all the three marbles here. Calculate the RECTs for each marble in g_Rect[]. Blt the marbles, update the marbles’ position and check for collisions between marbles. This is the interesting part.
Let me digress a bit here. How do you find out if the marbles are colliding? The presence of
RECTs tricks your mind into thinking that you can check if the two
RECTs overlap, which is what I did initially and which was really dumb, really. I used
IntersectRect() api to find out if the two
RECTs overlap, and if they did I decided its a collision! The problem here is that the two
RECTs may overlap but there might actually be no collision for example when only the corners of the two
RECTs intersect. And that happens way too many times, and watching the program run like that leaves you baffled, things dont make sense! It didn’t take me long to figure out a better way, instead of checking for overlapping
RECTs, you had to check if the two circles (marbles) overlapped. Let the radius of the two circles be
R1 and
R2, calculate the distance between the centers of the two marbles, lets call it
D. The marbles collide when
D <= R1 + R2. Well, they actually collide when
D = R1 + R2, but because our program updates the frames by a certain delta everytime we need to check for
D <= R1 + R2.
The Collision() function takes two BALLINFO structures as input and determines if they collide, returns TRUE if they do and FALSE otherwise:
BOOL Collision(const BALLINFO *ball1, const BALLINFO *ball2)
{
int distance = 0, temp = 0;
int MinDistance = ball1->radius + ball2->radius;
//calculate the distance between the centers of the two balls
int dx = ball2->xPos – ball1->xPos;
int dy = ball2->yPos – ball1->yPos;
temp = dx*dx + dy*dy;
distance = sqrt(temp);
//printf("COLLISION: distance=%d, 2*R=%d\n", distance, MinDistance);
if(distance <= MinDistance)
{
return TRUE;
}
return FALSE;
}
We use the standard formula to calculate the distance between two points, Distance = SQRT((x2-x1)2 + (y2-y1)2). If the marbles do collide then we have to calculate the new velocities for the marbles, and this is how we do it:
void CalculateNewVelocities(BALLINFO *ball1, BALLINFO *ball2)
{
int ed = 1, mass1 = 20, mass2 = 20;
double velX1, velY1, velX2, velY2;
double dx = ball2->xPos – ball1->xPos;
double dy = ball2->yPos – ball1->yPos;
//calculate the distance between their centers
double distance = sqrt(dx*dx+dy*dy);
double vp1 = ball1->dwVelX*dx/distance + ball1->dwVelY*dy/distance;
double vp2 = ball2->dwVelX*dx/distance + ball2->dwVelY*dy/distance;
double dt = (ball1->radius + ball2->radius – distance)/(vp1 – vp2);
//printf("CENTERS BEFORE: X1=%d, Y1=%d X2=%d, Y2=%d\n", xPos1, yPos1, xPos2, yPos2);
//the centers of the ball when they actually collided
ball1->xPos = ball1->xPos – floor(ball1->dwVelX*dt + 0.5);
ball1->yPos = ball1->yPos – floor(ball1->dwVelY*dt + 0.5);
ball2->xPos = ball2->xPos – floor(ball2->dwVelX*dt + 0.5);
ball2->yPos = ball2->yPos – floor(ball2->dwVelY*dt + 0.5);
//now calulate the distance between centers (this should be very close to the sum of their radii)
dx = ball2->xPos – ball1->xPos;
dy = ball2->yPos – ball1->yPos;
distance = sqrt(dx*dx+dy*dy);
// Unit vector in the direction of the collision
double ax = dx/distance, ay=dy/distance;
// Projection of the velocities in these axes
double va1 = (ball1->dwVelX*ax + ball1->dwVelY*ay);
double vb1 = (-ball1->dwVelX*ay + ball1->dwVelY*ax);
double va2 = (ball2->dwVelX*ax + ball2->dwVelY*ay);
double vb2 = (-ball2->dwVelX*ay + ball2->dwVelY*ax);
// New velocities in these axes (after collision): ed<=1, for elastic collision ed=1
double vaP1 = va1 + (1+ed)*(va2-va1)/(1 + mass1/mass2);
double vaP2 = va2 + (1+ed)*(va1-va2)/(1 + mass2/mass1);
// Undo the projections
velX1 = vaP1*ax-vb1*ay; velY1 = vaP1*ay+vb1*ax;// new vx,vy for ball 1 after collision
velX2 = vaP2*ax-vb2*ay; velY2 = vaP2*ay+vb2*ax;// new vx,vy for ball 2 after collision
//printf("CENTERS AFTER: X1=%d, Y1=%d X2=%d, Y2=%d\n", xPos1, yPos1, xPos2, yPos2);
//printf("Old Vel: velX1=%d, velY1=%d velX2=%d, velY2=%d\n", dwVelX1, dwVelY1, dwVelX2, dwVelY2);
//new velocities of the balls
ball1->dwVelX = floor(velX1 + 0.5);
ball1->dwVelY = floor(velY1 + 0.5);
ball2->dwVelX = floor(velX2 + 0.5);
ball2->dwVelY = floor(velY2 + 0.5);
//undo the correction done before
ball1->xPos = ball1->xPos + floor(ball1->dwVelX*dt + 0.5);
ball1->yPos = ball1->yPos + floor(ball1->dwVelY*dt + 0.5);
ball2->xPos = ball2->xPos + floor(ball2->dwVelX*dt + 0.5);
ball2->yPos = ball2->yPos + floor(ball2->dwVelY*dt + 0.5);
//printf("New Vel: velX1=%d, velY1=%d velX2=%d, velY2=%d\n", dwVelX1, dwVelY1, dwVelX2, dwVelY2);
}
This function is taken directly from
Professor Fu-Kwun Hwang’s explanation on 2D collisions. Changes made to the velocities and positions in
CalculateNewVelocities() will be reflected back in the caller.
Note: I ran this program quite many times and sometimes it does fail. Maybe something like once in 30 runs. The marbles stick to each other and don’t let go. The problem is in the calculation of ‘dt‘, where it tries to calculate where the balls would have been when they were about to collide (i.e. D = R1 + R2), but since even this is an approximation, it fails sometimes. One fix for this problem is to check if the balls collide after calculating the new position and velocities. If they do then adjust the position values such that new D > R1 + R2. The problem mentioned above happens when after calculating the new positions and velocities for both marbles they still overlap.