Dotcpp  >  编程教程  >  C/C++游戏类项目  >  弹幕射击游戏C语言版教程带源码

弹幕射击游戏C语言版教程带源码

点击打开在线编译器,边学边练

一、项目介绍

这是一个弹幕射击类小游戏(究极简化版)。方向键控制移动,按住z键射击,按住shift可以缓速移动,击败敌人取得胜利。

游戏C语言实现+easyX图形绘制,视觉交互效果好,趣味性强。

 

编译环境:visual c++ 6.0

第三方库:Easyx2022

 

二、运行截图

游戏操作提示

游戏界面

弹幕游戏2

游戏胜利


三、源码解析

我们先思考游戏的流程。

在程序运行的开始,我们自然需要初始化。接着就是游戏的进行过程,当检测到胜负已分时,给出提示,结束游戏。

 

在游戏过程中,所有的操作都需要得到实时的反馈。只要你输入操作,游戏数据和画面就会即时做出反应。显然我们需要用循环的方式,不断地检测输入,处理游戏数据以及绘制图像。

 

循环的频率是根据游戏刷新频率确定的,每经过一小段时间,循环就会执行一次,检测玩家在这段时间按下了什么按键,然后判断这些按键引起了移动方向的改变,还是子弹的发射,将这些结果写入到游戏数据当中,最后再刷新画面,计算出下一帧界面应该是什么样子。

 

然后是程序中定义的函数,变量及其功能。

void hp_bar();
void show_player();
void show_enemy();
void move_enemy();               //绘制一系列图像
void draw_background();
int generate_line();                                // 若返回 -1,表示生成线条失败
int create_p_b();                                    // 创建自机的子弹
int create_e_b();                                    // 创建敌机的子弹
int destroy_p_b(int index);
int destroy_e_b(int index);                           // 删除一个子弹
#define FRAMERATE 20                                // 画面刷新的周期(ms)
#define FIRERATE 350                                   // 射击间隔时间
#define E_FIRERATE 350                               // 敌人射击间隔
#define BLEED_TIME 150                             // 受伤闪烁时间
#define BACKGROUND 80                            // 绘制背景线条的周期
#define MAX_LINES 75                                 // 最多同屏背景线条数目
#define MAX_PLAYER_BULLETS 40              // 最多同屏自机子弹数目
#define MAX_ENEMY_BULLETS 40              // 最多同屏敌机子弹数目
 
int player_pos[2] = { 30,30 };                                      // 自机位置xy
int enemy_bullet[MAX_ENEMY_BULLETS][2];                         // 敌人的子弹位置
int player_bullet[MAX_PLAYER_BULLETS][2];                   // 自机的子弹位置
int enemy_pos[2] = { 580,240 };                                        // 敌机位置
bool p_b_slots[MAX_PLAYER_BULLETS] = { false };           // 用于判断 player_bullet 的某个位置是否可用
bool e_b_slots[MAX_ENEMY_BULLETS] = { false };
int number_p_b = 0, number_e_b = 0;                             // 记录自机和敌机的子弹数,减少遍历压力
int player_health = 100, enemy_health = 100;
bool isBleeding_p = false, isBleeding_e = false;  // 用于实现命中后的闪烁效果
int background_line[MAX_LINES][3];                                // 背景的线条,三个参数分别是 x、y、长度
bool line_slots[MAX_LINES] = { false };
int number_lines = 0;                                                  // 记录背景线条数目
clock_t begin_time = 0;

 

下面是主函数的源码。由于相对来说较长,所以其中的一部分我会用文字来描述,具体的内容会放在完整源码当中。

 

int main()
{
       initgraph(640, 550, 4);
       srand((unsigned)time(NULL));
       settextcolor(RGB(0, 254, 0));
       settextstyle(30, 0, "微软雅黑");
       outtextxy(50, 200, "方向键移动, Z 攻击, 左 Shift 切换低速模式");
       bool win = false, dead = false;
       clock_t firerate = clock();                                     // 射击控制
       clock_t e_firerate = clock();                                 // 控制敌机的射击
       clock_t runtime = clock();                                    // 用于控制画面刷新频率
       clock_t bleed_p = clock(), bleed_e = clock();       // 用于实现受伤闪烁
       clock_t backgroundline_generate = clock();        // 用于生成背景线条
       Sleep(3000);
       BeginBatchDraw();
       bool leftshift = false;
       begin_time = clock();
       return 0;
}

以上是初始化内容。Initgraph()用来初始化绘图区域,settextcolor用来改变字体颜色,outtextxy用于在指定位置输出文字。BeginBatchDraw这个函数用于开始批量绘图。执行后,任何绘图操作都将暂时不输出到绘图窗口上,直到执行 FlushBatchDraw 或 EndBatchDraw 才将之前的绘图输出,这样可以防止画面不同步输出。这几个函数都来自easyx头文件。

初始化中还用clock()函数进行计时。Clock()返回值为clock_t类型,获取进程使用的cpu时间单元总数。等到某个时间点再次调用该函数,就能得出从现在到那时,究竟过了多长时间。

   while (true)
       {
 
              if (clock() - runtime >= FRAMERATE)//只有当距离处理上一帧过去了一定时间,才会开始下一次处理。
              {
                     runtime = clock();
                     cleardevice();//使用当前背景色清空绘图设备。
                     draw_background();//在本文件中定义,绘制背景
                     hp_bar();// 画血条
                     show_player();;//在本文件中定义,绘制玩家
                     show_enemy();;//在本文件中定义,绘制敌人
 
                     int n_p_b = 1, n_e_b = 1;                              // 计数,遍历子弹,刷新位置
                     int p_b_toprocess = number_p_b, e_b_toprocess = number_e_b;       // 需要处理的子弹数
这里number_p_b和number_e_b分别是自机和敌机的子弹数目。为了保证游戏运行正常,可以给双方的同屏弹幕数各设定一个上限,如果当前的子弹超过了上限,则后续的子弹不生成。这一步的意思是让这两个变量继承上一帧的子弹数目。
                     for (int i = 0; i < MAX_PLAYER_BULLETS && (n_p_b <= p_b_toprocess || n_e_b <= e_b_toprocess); ++i)//对每个子弹进行处理,超出限制不处理
                     {
                            if (n_p_b <= p_b_toprocess)                         // 如果子弹已经处理完就不处理了
                            {
                                   if (p_b_slots[i] == true)
                                   {
                                          ++n_p_b;
                                          player_bullet[i][0] += 3;//自机的子弹横向移动三个单位长度
                                          setfillcolor(RGB(150, 180, 210));
                                          if (player_bullet[i][0] >= 635)
                                          {
                                                 destroy_p_b(i);    // 到达了屏幕最右端,销毁子弹
                                          }
 
                                          // 碰撞检测,两个矩形
                                          if ((player_bullet[i][0] + 5 >= enemy_pos[0] - 20 && player_bullet[i][0] - 5 <= enemy_pos[0] + 20) && (player_bullet[i][1] - 5 < enemy_pos[1] + 40 && player_bullet[i][1] + 5 > enemy_pos[1] - 40))
                                                 // 击中敌人
                                          {
                                                 destroy_p_b(i);
                                                 enemy_health -= 8;
                                                 isBleeding_e = true;//被命中后会闪烁
                                                 bleed_e = clock();
                                          }
 
                                          fillrectangle(player_bullet[i][0] - 5, player_bullet[i][1] - 5, player_bullet[i][0] + 5, player_bullet[i][1] + 5);            // 画子弹
                                   }
 
                            }
 
                            if (n_e_b <= e_b_toprocess)...// 敌人的子弹,处理方式和自机类似。
                           
 
                     if (win || dead)
                            break;
                     FlushBatchDraw();
                     move_enemy();
                     if (player_health <= 0)
                            dead = true;
                     if (enemy_health <= 0)
                     {
                            win = true;
                     }
//检验胜利或失败
                     if (GetAsyncKeyState(VK_LSHIFT) & 0x8000)      // 按住 Shift 减速
                     {
                            leftshift = true;
                     }
                     else
                     {
                            leftshift = false;
                     }
 
                     if (GetAsyncKeyState(VK_UP) & 0x8000)
                            // 玩家移动
                     {
                            if (player_pos[1] >= 28)
                                   if (leftshift)
                                          player_pos[1] -= 2;                          // y 的正方向是向下的
                                   else
                                          player_pos[1] -= 5;
                     }
//其它三个方向的移动同理
                     if (clock() - firerate >= FIRERATE && GetAsyncKeyState('Z') & 0x8000)
                            // 玩家开火
                     {
                            firerate = clock();
                            create_p_b();
                     }
 
                     if (clock() - e_firerate >= E_FIRERATE)//敌人间隔固定时间开火
                     {
                            e_firerate = clock();
                            create_e_b();
                     }
 
 
                     if (clock() - bleed_p >= BLEED_TIME)           // 受伤时间结束后关闭受伤闪烁效果
                     {
                            isBleeding_p = false;
                     }
 
                     if (clock() - bleed_e >= BLEED_TIME)           // 受伤时间结束后关闭受伤闪烁效果
                     {
                            isBleeding_e = false;
                     }
 
                     if (clock() - backgroundline_generate >= BACKGROUND)
                     {
                            backgroundline_generate = clock();
                            generate_line();//间隔一段时间绘制背景线条
                     }
              }
       }
       if (win)
       {
              settextcolor(RGB(0, 254, 0));
              settextstyle(35, 0, "黑体");
              outtextxy(150, 200, "你打败了boss!你赢了!!");
       }
       else
       {
              settextcolor(RGB(254, 0, 0));
              settextstyle(35, 0, "黑体");
              outtextxy(140, 200, "你被boss打败了!");
       }//处理胜利或者失败
       FlushBatchDraw();//这个函数用于执行未完成的绘制任务。
       Sleep(5000);
       EndBatchDraw();//这个函数用于结束批量绘制,并执行未完成的绘制任务。
       return 0;
}

 

之后是各个函数的实现原理。

void hp_bar();
void show_player();
void show_enemy();
void move_enemy();              
void draw_background();

首先是五个绘制图像的函数。它们的逻辑结构都是线性的,只需要依次调用函数即可。

用到的函数有:

setlinecolor用于设置当前设备画线颜色。

line用于画直线。

setfillcolor用于设置当前设备填充颜色。

rectangle用于画无填充的矩形。

fillrectangle用于画有边框的填充矩形。

以及之前提到的绘制文字的函数等。

 

敌机的移动:

void move_enemy()
{
       static bool angle_v;           // 控制敌机的竖直移动方向,true 为向上,到边缘就换向
       static bool angle_h;          // 控制敌机的水平移动方向,true 为向左,到边缘就换向
       static clock_t interval;       // 定时随机换向
 
       if (clock() - interval >= 2000)
       {
              interval = clock();
              if (rand() % 2)                    // 一半的概率换向
                     angle_v = !angle_v;
              if (rand() % 2)
                     angle_h = !angle_h;
       }
       if (angle_v == true)                  //敌机移动
enemy_pos[1] -= 3;
       else
              enemy_pos[1] += 3;
       if (angle_h == true)
              enemy_pos[0] -= 3;
       else
              enemy_pos[0] += 3;
 
       if (enemy_pos[1] >= 440)  // 到了地图边缘就调头
              angle_v = true;
       else if (enemy_pos[1] <= 40)
              angle_v = false;
       if (enemy_pos[0] >= 580)
              angle_h = true;
       else if (enemy_pos[0] <= 380)
              angle_h = false;
 
}

 

创建玩家子弹(敌人同理)

int create_p_b()
{
       if (number_p_b > MAX_PLAYER_BULLETS)                // 空间不够
              return -1;
       for (int i = 0; i < MAX_PLAYER_BULLETS; ++i)      // 搜索 slots,寻找空位
       {
              if (p_b_slots[i] == false)
              {
                     p_b_slots[i] = true;
                     player_bullet[i][0] = player_pos[0] + 45;
                     player_bullet[i][1] = player_pos[1];       // 创建子弹
                     ++number_p_b;
                     break;
              }
       }
       return 0;
}

 

销毁玩家子弹(敌人同理)

int destroy_p_b(int index)
{
       if (index > MAX_PLAYER_BULLETS - 1)//如果子弹数目溢出
              return -2;
       if (p_b_slots[index] == false)//如果子弹已经被销毁
              return -1;
       p_b_slots[index] = false;
       --number_p_b;
       return 0;
}


四、完整源码


C语言弹幕射击游戏完整源码


本文固定URL:https://www.dotcpp.com/course/1245

C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:

一点编程也不会写的:零基础C语言学练课程

解决困扰你多年的C语言疑难杂症特性的C语言进阶课程

从零到写出一个爬虫的Python编程课程

只会语法写不出代码?手把手带你写100个编程真题的编程百练课程

信息学奥赛或C++选手的 必学C++课程

蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程

手把手讲解近五年真题的蓝桥杯辅导课程

趣味项目教程
第一章 C/C++游戏类项目
第二章 C/C++工具及其他类项目
第三章 Python趣味项目
Dotcpp在线编译      (登录可减少运行等待时间)