小球在窗体范围内运动,撞到除底边外的另外三个边缘就反弹一次。如果,小球撞到底边,那么游戏结束。玩家可以通过控制在底边上的一个挡板,让小球撞击到挡板上而反弹,阻止小球撞到底边。
1. 运动的小球
创建一个800 * 600的窗体。坐标系原点在窗体中心,x轴正方向向右,y轴正方向向上。背景色设置为RGB(164, 225, 202),最后调用cleardevice函数,使用背景色清空整个窗体。
#include <easyx.h>
#include <stdio.h>
int main()
{
initgraph(800, 600);
// 坐标系原点在窗体中心,X轴正方向向右,Y轴正方向向上
setorigin(400, 300);
setaspectratio(1, -1);
// 设置背景色
setbkcolor(RGB(164, 225, 202));
// 使用背景色清空窗体
cleardevice();
getchar();
closegraph();
return 0;
}
让小球初始情况下出现在屏幕中间的位置,也就是圆心坐标为(0, 0) 。半径设置为40。
int x = 0, y = 0;
int r = 40;
solidcircle(x, y, r);
?现在,让小球向正右上方运动。
由于小球向正右上方运动,小球的速度方向与X轴正方向夹角为45°。那么,小球的向右上方的速度可以分解为X方向、Y方向上两个速度大小一样、速度方向垂直的速度分量。
速度v 在X方向上的分量为vx。 速度v 在y方向上的分量为vy。
设两分量速度均为5像素/帧,根据勾股定理可知小球的速度为:
小球运动速度为7像素/帧,设定每帧之间暂停40毫秒,若不计算绘图时间,每秒将有25帧。每帧绘制完成,并暂停40毫秒后,将小球当前位置,加上在vx 、vy 两方向上的速度,使得小球位置变化,再绘制下一帧。这样,小球就可以向右上方运动了。
// 圆心坐标
int x = 0, y = 0;
// 速度分量
int vx = 5, vy = 5;
// 小球半径
int r = 40;
while (1)
{
// 清空画面并绘制小球
cleardevice();
solidcircle(x, y, r);
// 每帧40毫秒
Sleep(40);
// 圆心坐标移动
x += vx;
y += vy;
}
可以看到,小球向右上方运动一段时间后,很快就离开了窗体的范围。
2. 碰到边缘反弹
为了把小球的运动范围限制在窗体范围内,我们需要让小球在碰到窗体边缘后反弹。
当小球的速度方向为右上方且碰到顶边时,其速度分量vx 保持不变,而速度分量vy 需要反向,即vy 变为-vy 。变化后,小球从向右上方运动变为向右下方运动。
那么怎样判断小球是否撞击到顶边呢?
可以判断圆心的y 坐标是否等于300。但是,若圆心的y 坐标从1开始,每次移动5像素。那么,圆心的y 坐标在移动时,将越过300,不会等于300。因此,我们使用区间来判断将更加通用。
当圆心y 坐标撞击或越过顶边时,即y >= 300 时。我们将速度分量vy 反向,即可实现撞到或越过顶边反弹的效果。
while (1)
{
cleardevice();
solidcircle(x, y, r);
Sleep(40);
// 撞击或越过顶边后,速度分量vy反向。
if (y >= 300)
{
vy = -vy;
}
x += vx;
y += vy;
}
现在,小球碰撞到或越过顶边可以反弹了。但是,左右两边并没有限制,小球运动到顶边反弹后,从右边离开了窗体。
如法炮制,将当小球撞击或越过左右两边时,会使得速度分量vx 反向。
?当圆心x 坐标撞击或越过左、右两边时,即x >= 400 || x <= -400 时。我们将速度分量vx 反向,即可实现撞到或越过左右两边反弹的效果。
while (1)
{
cleardevice();
solidcircle(x, y, r);
Sleep(40);
if (y >= 300)
{
vy = -vy;
}
// 撞击或越过左右两边后,速度分量vx反向。
if (x <= -400 || x >= 400)
{
vx = -vx;
}
x += vx;
y += vy;
}
小球从窗体中心开始向右上方开始运动,撞击到顶边后,变为向右下方运动。再次撞击到右边后,变为向左下方运动。最后,从底边离开窗体。
虽然,游戏中底边上需要有一个由玩家控制的挡板来阻止小球撞击到底边。不过,目前我们可以让小球撞击或越过底边也反弹。
当圆心坐标y 坐标撞击或越底边时,即y <= -300 时。我们将速度分量vy 反向,即可实现撞到或越过底边反弹的效果。
while (1)
{
cleardevice();
solidcircle(x, y, r);
Sleep(40);
if (y >= 300)
{
vy = -vy;
}
if (x <= -400 || x >= 400)
{
vx = -vx;
}
// 撞击或越过底边后,速度分量vy反向。
if (y <= -300)
{
vy = -vy;
}
x += vx;
y += vy;
}
现在,小球已经可以在撞击任何一边后反弹,始终在窗体范围内运动了。
3. 圆周撞击或越过边界反弹
仔细观察小球在窗体范围内的反弹,可以发现小球只有当圆心到达或越过边界时,才会进行反弹。
为了有更加真实的效果,我们需要让小球的圆周碰撞或越过边界时,就发生反弹。将边界范围根据小球的半径进行调整。
碰撞或越过条件改为如下:
-
顶边:y >= 300 - r -
底边:y <= -300 + r -
左边:x <= -400 + r -
右边:x >= 400 - r
while (1)
{
cleardevice();
solidcircle(x, y, r);
Sleep(40);
if (y >= 300 - r)
{
vy = -vy;
}
if (x <= -400 + r || x >= 400 - r)
{
vx = -vx;
}
if (y <= -300 + r)
{
vy = -vy;
}
x += vx;
y += vy;
}
4. 绘制挡板
现在可以在底边上加上挡板了。挡板使用白色的填充矩形表示,宽度设置为300,高度设置为20。
挡板初始位置在底部上,与左右两边间距一致。即挡板的左上角坐标为(-150, -280) ,右下角坐标为(150, -300) 。
// 挡板左边、顶边、右边、底边坐标
int barLeft, barTop, barRight, barBottom;
barLeft = -150;
barRight = 150;
barTop = -280;
barBottom = -300;
接着,在每一帧画面中绘制挡板。
while (1)
{
cleardevice();
solidcircle(x, y, r);
// 绘制挡板
solidrectangle(barLeft, barTop, barRight, barBottom);
Sleep(40);
if (y >= 300 - r)
{
vy = -vy;
}
if (x <= -400 + r || x >= 400 - r)
{
vx = -vx;
}
if (y <= -300 + r)
{
vy = -vy;
}
x += vx;
y += vy;
}
5. 移动挡板
为了让游戏拥有交互性,我们需要通过键盘a 与d 控制挡板的左右移动。
按下a 键后,挡板向左边移动20像素。barLeft 与barRight 减去20。 按下d 键后,挡板向右边移动20像素。barLeft 与barRight 加上20。
char c = _getch();
if (c == 'a')
{
barLeft -= 20;
barRight -= 20;
}
else if (c == 'd')
{
barLeft += 20;
barRight += 20;
}
另外,要注意挡板不能移动出窗体范围。
-
挡板左侧坐标不小于-400 -
挡板右侧坐标不大于400
char c = _getch();
if (c == 'a')
{
if (barLeft > -400)
{
barLeft -= 20;
barRight -= 20;
}
}
else if (c == 'd')
{
if (barRight < 400)
{
barLeft += 20;
barRight += 20;
}
}
由于_getch 是阻塞函数,它会暂停程序的执行,直到获取到键盘输入为止。而小球的移动不能因为没有键盘输入而暂停。而表达式_kbhit() != 0 可以判断当前是否有键盘按下,若没有键盘输入时,不调用_getch 函数,小球可以继续移动。若有键盘按下,才调用_getch 函数获取具体的输入,更改挡板的位置。
别忘了使用_getch 和_kbhit 函数需要包含头文件#include <conio.h>
while (1)
{
cleardevice();
solidcircle(x, y, r);
solidrectangle(barLeft, barTop, barRight, barBottom);
Sleep(40);
if (y >= 300 - r)
{
vy = -vy;
}
if (x <= -400 + r || x >= 400 - r)
{
vx = -vx;
}
if (y <= -300 + r)
{
vy = -vy;
}
x += vx;
y += vy;
// 控制挡板移动
if (_kbhit() != 0)
{
char c = _getch();
if (c == 'a')
{
if (barLeft > -400)
{
barLeft -= 20;
barRight -= 20;
}
}
else if (c == 'd')
{
if (barRight < 400)
{
barLeft += 20;
barRight += 20;
}
}
}
}
6. 小球碰到挡板反弹
目前已经在底边上添加了挡板,并且可以通过键盘a 和d 控制挡板的移动。接下来,可以把小球碰到或越过底边反弹,改为碰到或越过挡板反弹。
若小球圆周如果在以下灰色区域内,则反弹。
注意,小球是以圆心绘制的。所以,我们把灰色区域上移一个半径。
把撞击到或越过底边的代码改为与撞击到或越过挡板的代码。
while (1)
{
cleardevice();
solidcircle(x, y, r);
solidrectangle(barLeft, barTop, barRight, barBottom);
Sleep(40);
if (y >= 300 - r)
{
vy = -vy;
}
if (x <= -400 + r || x >= 400 - r)
{
vx = -vx;
}
// 撞击或越过挡板后,速度分量vy反向。
if (barLeft <= x && x <= barRight && y <= barTop + r)
{
vy = -vy;
}
x += vx;
y += vy;
if (_kbhit() != 0)
{
char c = _getch();
if (c == 'a')
{
if (barLeft > -400)
{
barLeft -= 20;
barRight -= 20;
}
}
else if (c == 'd')
{
if (barRight < 400)
{
barLeft += 20;
barRight += 20;
}
}
}
}
7. 从挡板旁漏下去
若小球没有被挡板挡住而从底边掉了下去,应当结束游戏,复位各种变量,重新开始。
只要圆心y 坐标小于-300 ,我们就认为小球已经掉下去了。
// 游戏结束
if (y <= - 300)
{
// 复位圆心坐标
x = 0;
y = 0;
// 复位速度分量
vx = 5;
vy = 5;
// 复位挡板
barLeft = -150;
barRight = 150;
barTop = -280;
barBottom = -300;
}
8. 随机初始条件
每次重新开始游戏时,圆心坐标都在(0, 0) 处,速度方向都向右上方似乎有点无趣。不如,我们把游戏初始条件做成随机的吧。
游戏初始条件将随机为:
-
圆心坐标为矩形区域内的随机点 -
速度方向随机为左上、左下、右上、右下其中一个
矩形区域为:
可以使用头文件#include <stdlib.h> 中的随机数rand 函数,生成区间内的随机数。
x = rand() % (400 + 1) - 200;
y = rand() % (300 + 1) - 150;
至于速度方向,先让vx 与vy 重置为5,再让随机数的奇偶性来决定其方向。
-
若随机出一个奇数,速度方向为正。 -
若随机出一个偶数,速度方向为负。 // 游戏结束
if (y <= - 300)
{
// 随机圆心坐标
x = rand() % (400 + 1) - 200;
y = rand() % (300 + 1) - 150;
// 随机速度分量
vx = 5;
vy = 5;
// 奇数为正,偶数为负
if (rand() % 2 == 0)
{
vy = -vy;
}
if (rand() % 2 == 0)
{
vx = -vx;
}
// 复位挡板
barLeft = -150;
barRight = 150;
barTop = -280;
barBottom = -300;
} 现在游戏结束后,重新开始游戏时,游戏的初始条件都可以随机产生了。不如,让第一次运行游戏也有随机的初始条件,把这些代码也在最开头执行一遍。 // 1.重复代码开始
x = rand() % (400 + 1) - 200;
y = rand() % (300 + 1) - 150;
vx = 5;
vy = 5;
if (rand() % 2 == 0)
{
vy = -vy;
}
if (rand() % 2 == 0)
{
vx = -vx;
}
barLeft = -150;
barRight = 150;
barTop = -280;
barBottom = -300;
// 1.重复代码结束
while (1)
{
cleardevice();
solidcircle(x, y, r);
solidrectangle(barLeft, barTop, barRight, barBottom);
Sleep(40);
if (y >= 300 - r)
{
vy = -vy;
}
if (x <= -400 + r || x >= 400 - r)
{
vx = -vx;
}
if (barLeft <= x && x <= barRight && y <= barTop + r)
{
vy = -vy;
}
x += vx;
y += vy;
if (_kbhit() != 0)
{
char c = _getch();
if (c == 'a')
{
if (barLeft > -400)
{
barLeft -= 20;
barRight -= 20;
}
}
else if (c == 'd')
{
if (barRight < 400)
{
barLeft += 20;
barRight += 20;
}
}
}
// 游戏结束
if (y <= -300)
{
// 2.重复代码开始
x = rand() % (400 + 1) - 200;
y = rand() % (300 + 1) - 150;
vx = 5;
vy = 5;
if (rand() % 2 == 0)
{
vy = -vy;
}
if (rand() % 2 == 0)
{
vx = -vx;
}
barLeft = -150;
barRight = 150;
barTop = -280;
barBottom = -300;
// 2.重复代码结束
}
} 这样做虽然能达到第一次开始游戏和重新开始游戏都有随机初始条件的效果。但是,这两段产生随机初始条件的代码是重复的。我们可以把这些代码封装成reset 函数,由于这里涉及的变量较多,可以把所有游戏数据封装为一个结构GameData 。 typedef struct {
// 圆心坐标
int x, y;
// 速度分量
int vx, vy;
// 小球半径
int r;
// 挡板左边、顶边、右边、底边坐标
int barLeft, barTop, barRight, barBottom;
}GameData; reset 函数的参数为GameData 的指针,它可以将传入的结构成员赋值为随机值。 void reset(GameData *gdata)
{
gdata->x = rand() % (400 + 1) - 200;
gdata->y = rand() % (300 + 1) - 150;
gdata->vx = 5;
gdata->vy = 5;
if (rand() % 2 == 0)
{
gdata->vy = -gdata->vy;
}
if (rand() % 2 == 0)
{
gdata->vx = -gdata->vx;
}
gdata->r = 40;
gdata->barLeft = -150;
gdata->barRight = 150;
gdata->barTop = -280;
gdata->barBottom = -300;
} 在游戏第一次开始和重新开始时,都调用函数gdata ,产生随机的游戏初始条件。由于将数据组合到了结构内,所以,原代码中直接使用变量应改为使用结构访问其成员。 由于计算机生成的随机数是伪随机数,默认情况下,每次都会使用同一个种子生成随机数。为了让随机数种子不可预测,可以使用头文件#include <time.h> 中的time 函数获取当前时间,将当前时间作为随机数种子。 // 当前时间作为随机数种子
srand(time(NULL));
// 游戏数据
GameData gdata;
// 初始化游戏数据
reset(&gdata);
while (1)
{
cleardevice();
solidcircle(gdata.x, gdata.y, gdata.r);
solidrectangle(gdata.barLeft, gdata.barTop, gdata.barRight, gdata.barBottom);
Sleep(40);
if (gdata.y >= 300 - gdata.r)
{
gdata.vy = -gdata.vy;
}
if (gdata.x <= -400 + gdata.r || gdata.x >= 400 - gdata.r)
{
gdata.vx = -gdata.vx;
}
if (gdata.barLeft <= gdata.x && gdata.x <= gdata.barRight && gdata.y <= gdata.barTop + gdata.r)
{
gdata.vy = -gdata.vy;
}
gdata.x += gdata.vx;
gdata.y += gdata.vy;
if (_kbhit() != 0)
{
char c = _getch();
if (c == 'a')
{
if (gdata.barLeft > -400)
{
gdata.barLeft -= 20;
gdata.barRight -= 20;
}
}
else if (c == 'd')
{
if (gdata.barRight < 400)
{
gdata.barLeft += 20;
gdata.barRight += 20;
}
}
}
if (gdata.y <= -300)
{
// 初始化游戏数据
reset(&gdata);
}
} 9. 完整代码 #include <easyx.h>
#include <stdio.h>
#include <conio.h>
#include <time.h>
typedef struct {
// 圆心坐标
int x, y;
// 速度分量
int vx, vy;
// 小球半径
int r;
// 挡板左边、顶边、右边、底边坐标
int barLeft, barTop, barRight, barBottom;
}GameData;
void reset(GameData *gdata)
{
gdata->x = rand() % (400 + 1) - 200;
gdata->y = rand() % (300 + 1) - 150;
gdata->vx = 5;
gdata->vy = 5;
if (rand() % 2 == 0)
{
gdata->vy = -gdata->vy;
}
if (rand() % 2 == 0)
{
gdata->vx = -gdata->vx;
}
gdata->r = 40;
gdata->barLeft = -150;
gdata->barRight = 150;
gdata->barTop = -280;
gdata->barBottom = -300;
}
int main()
{
initgraph(800, 600);
// 坐标系原点在窗体中心,X轴正方向向右,Y轴正方向向上
setorigin(400, 300);
setaspectratio(1, -1);
// 设置背景色
setbkcolor(RGB(164, 225, 202));
// 使用背景色清空窗体
cleardevice();
// 当前时间作为随机数种子
srand((unsigned int)time(NULL));
// 游戏数据
GameData gdata;
// 初始化游戏数据
reset(&gdata);
while (1)
{
cleardevice();
// 绘制小球
solidcircle(gdata.x, gdata.y, gdata.r);
// 绘制挡板
solidrectangle(gdata.barLeft, gdata.barTop, gdata.barRight, gdata.barBottom);
// 每帧之间休眠40ms
Sleep(40);
// 撞击或越过顶边反弹
if (gdata.y >= 300 - gdata.r)
{
gdata.vy = -gdata.vy;
}
// 撞击或越过左、右边反弹
if (gdata.x <= -400 + gdata.r || gdata.x >= 400 - gdata.r)
{
gdata.vx = -gdata.vx;
}
// 撞击或越过挡板后反弹
if (gdata.barLeft <= gdata.x && gdata.x <= gdata.barRight && gdata.y <= gdata.barTop + gdata.r)
{
gdata.vy = -gdata.vy;
}
// 小球位置变化
gdata.x += gdata.vx;
gdata.y += gdata.vy;
// 控制挡板移动
if (_kbhit() != 0)
{
char c = _getch();
if (c == 'a')
{
if (gdata.barLeft > -400)
{
gdata.barLeft -= 20;
gdata.barRight -= 20;
}
}
else if (c == 'd')
{
if (gdata.barRight < 400)
{
gdata.barLeft += 20;
gdata.barRight += 20;
}
}
}
if (gdata.y <= -300)
{
// 重置游戏初始数据
reset(&gdata);
}
}
closegraph();
return 0;
}
|