介绍
《飞扬的小鸟》(Flappy Bird)是一款广受欢迎且令人上瘾的手机游戏,因其简单却具有挑战性的玩法而大获成功。在本项目中,我们将学习如何使用 C 编程语言实现我们自己版本的《飞扬的小鸟》。
通过完成本项目,你将:
- 学习如何使用
ncurses库进行基于文本的屏幕绘制。 - 掌握 Linux 中的数据结构和系统调用知识。
- 获得在 C 程序中处理键盘事件和实时更新的经验。
👀 预览

🎯 任务
在本项目中,你将学习:
- 如何使用 C 实现基于字符的《飞扬的小鸟》版本。
- 如何处理键盘事件以控制小鸟的移动。
- 如何通过将障碍物从右向左移动来营造向前运动的错觉。
- 如何使用
ncurses库绘制字符界面。
🏆 成果
完成本项目后,你将能够:
- 展示对 C 编程语言的熟练掌握。
- 培养处理键盘事件的技能。
- 在 C 程序中实现实时更新。
- 使用
ncurses库进行基于文本的屏幕绘制。 - 理解 Linux 中的数据结构和系统调用。
基础知识
我们的项目需要一些数据结构的知识,并且涉及到 Linux 中的一些系统调用。
此外,我们还使用了一个名为 ncurses 的基于文本的屏幕绘制库。所以,在编译时,需要添加 -lcurses 选项。
设计思路
为了实现基于字符的《飞扬的小鸟》版本,我们从实现以下三个关键点入手:
- 程序应该能够响应键盘事件。
- 字符界面应该能够实时更新。
- 小鸟应该给人向前飞行的视觉错觉。
针对上述三个问题,我们的解决方案如下:
- 使用 Linux 提供的系统接口来捕获键盘事件。
- 使用 ncurses 库函数来绘制字符界面。
- 为了营造小鸟向前飞行的错觉:
最直接的方法是让小鸟在水平方向上从左向右移动,但这样小鸟在某个时刻会超出右边界。
相反,我们反过来思考:当一个人在向前行驶的汽车中看车外的风景时,风景看起来是向后移动的(运动是相对的)。所以,我们让障碍物从右向左移动,这样既能达到相同的视觉效果,又能避免小鸟超出边界的问题。
定义常量
首先,打开 Xfce 终端并执行以下命令来安装 ncurses 库:
sudo apt update
sudo apt-get install libncurses5-dev
导航到 ~/project 目录并创建项目文件 flappy_bird.c:
cd ~/project
touch flappy_bird.c
接下来,我们需要编写 C 代码。第一步是包含头文件:
## include <curses.h>
## include <stdlib.h>
## include <signal.h>
## include <sys/time.h>
在编写 main() 函数之前,让我们完成一些基本任务。由于我们使用的是终端字符界面,ASCII 字符至关重要。因此,我们需要定义一些常量。
我们将使用 * 表示背景中的柱子,用 O 表示小鸟。代码如下:
## define CHAR_BIRD 'O' // 定义小鸟字符
## define CHAR_STONE '*' // 定义组成柱子的石头
## define CHAR_BLANK ' ' // 定义空白字符
背景中的柱子将使用单链表存储。结构体定义如下:
typedef struct node {
int x, y;
struct node *next;
}node, *Node;
我们还定义一些全局变量:
Node head, tail;
int bird_x, bird_y;
int ticker;
现在我们声明将要创建的函数:
void init(); // 初始化函数,管理游戏的初始化任务
void init_bird(); // 初始化小鸟的位置坐标
void init_draw(); // 初始化背景
void init_head(); // 初始化存储柱子的链表头
void init_wall(); // 初始化存储柱子的链表
void drop(int sig); // 信号接收函数,接收系统信号并将柱子从右向左移动
int set_ticker(int n_msec); // 设置内核的定时器滴答间隔
计时问题
现在让我们来解决如何使背景以固定间隔移动的问题。我们将使用 Linux 系统提供的功能,即信号。
不确定信号是什么?别担心,你可以把它看作是 Linux 内核中的一个定时器,每隔一定时间就会向我们的程序发送一个信号。当接收到信号时,我们的信号处理函数 drop(int sig) 将自动执行。我们只需要在 drop(int sig) 函数中移动柱子。此外,由于信号是由 Linux 内核发送的,所以接收信号不会导致我们的键盘信号接收被阻塞。
现在让我们实现代码,并使用 set_ticker(int n_msec) 函数设置内核的定时器周期:
int set_ticker(int n_msec)
{
struct itimerval timeset;
long n_sec, n_usec;
n_sec = n_msec / 1000;
n_usec = (n_msec % 1000) * 1000L;
timeset.it_interval.tv_sec = n_sec;
timeset.it_interval.tv_usec = n_usec;
timeset.it_value.tv_sec = n_sec;
timeset.it_value.tv_usec = n_usec;
return setitimer(ITIMER_REAL, ×et, NULL);
}
信号处理函数 drop(int sig):
void drop(int sig)
{
int j;
Node tmp, p;
// 清除小鸟原来位置的符号
move(bird_y, bird_x);
addch(CHAR_BLANK);
refresh();
// 更新小鸟的位置并刷新屏幕
bird_y++;
move(bird_y, bird_x);
addch(CHAR_BIRD);
refresh();
// 如果小鸟与柱子碰撞,则结束游戏
if((char)inch() == CHAR_STONE)
{
set_ticker(0);
sleep(1);
endwin();
exit(0);
}
// 检查第一堵墙是否超出边界
p = head->next;
if(p->x < 0)
{
head->next = p->next;
free(p);
tmp = (node *)malloc(sizeof(node));
tmp->x = 99;
tmp->y = rand() % 11 + 5;
tail->next = tmp;
tmp->next = NULL;
tail = tmp;
ticker -= 10; // 加速
set_ticker(ticker);
}
// 绘制新的柱子
for(p = head->next; p->next!= NULL; p->x--, p = p->next)
{
// 用 CHAR_BLANK 替换 CHAR_STONE
for(j = 0; j < p->y; j++)
{
move(j, p->x);
addch(CHAR_BLANK);
refresh();
}
for(j = p->y+5; j <= 23; j++)
{
move(j, p->x);
addch(CHAR_BLANK);
refresh();
}
if(p->x-10 >= 0 && p->x < 80)
{
for(j = 0; j < p->y; j++)
{
move(j, p->x-10);
addch(CHAR_STONE);
refresh();
}
for(j = p->y + 5; j <= 23; j++)
{
move(j, p->x-10);
addch(CHAR_STONE);
refresh();
}
}
}
tail->x--;
}
在信号处理函数中,我们将背景向前移动一列,同时让小鸟下落一行。我们还检查小鸟是否与柱子碰撞。如果碰撞,则游戏结束。
main() 函数
在 main() 函数中,我们首先调用初始化函数 init(),然后进入 while() 循环。该循环主要由三部分组成:
- 检查用户输入:如果按下“w”键或空格键,小鸟将向上移动两行。如果按下“q”键,游戏将退出。如果按下“z”键,游戏将暂停。
- 移动小鸟并重新绘制它。
- 检查小鸟是否撞到管道。
让我们看看代码:
int main()
{
char ch;
init();
while(1)
{
ch = getch(); // 获取键盘输入
if(ch == ' ' || ch == 'w' || ch == 'W') // 如果按下空格键或“w”键
{
// 移动小鸟并重新绘制它
move(bird_y, bird_x);
addch(CHAR_BLANK);
refresh();
bird_y--;
move(bird_y, bird_x);
addch(CHAR_BIRD);
refresh();
// 如果小鸟撞到管道,结束游戏
if((char)inch() == CHAR_STONE)
{
set_ticker(0);
sleep(1);
endwin();
exit(0);
}
}
else if(ch == 'z' || ch == 'Z') // 暂停
{
set_ticker(0);
do
{
ch = getch();
} while(ch!= 'z' && ch!= 'Z');
set_ticker(ticker);
}
else if(ch == 'q' || ch == 'Q') // 退出
{
sleep(1);
endwin();
exit(0);
}
}
return 0;
}
在 main() 函数中,我们首先初始化屏幕,然后在循环中接收键盘输入。如果按下“w”键或空格键,小鸟将向上移动两行。如果按下“q”键,游戏将退出。如果按下“z”键,游戏将暂停。
现在让我们看看 init() 函数:
void init()
{
initscr();
cbreak();
noecho();
curs_set(0);
srand(time(0));
signal(SIGALRM, drop);
init_bird();
init_head();
init_wall();
init_draw();
sleep(1);
ticker = 500;
set_ticker(ticker);
}
init() 函数首先使用 ncurses 提供的函数初始化屏幕。然后它调用几个子函数来执行特定的初始化。请注意,我们安装了一个信号处理函数 drop(),并设置了定时器间隔。
让我们看看每个初始化子函数。
init_bird() 函数初始化小鸟的位置:
void init_bird()
{
bird_x = 5;
bird_y = 15;
move(bird_y, bird_x);
addch(CHAR_BIRD);
refresh();
sleep(1);
}
init_head() 和 init_wall() 函数初始化一个链表来存储管道:
void init_head()
{
Node tmp;
tmp = (node *)malloc(sizeof(node));
tmp->next = NULL;
head = tmp;
tail = head;
}
void init_wall()
{
int i;
Node tmp, p;
p = head;
for(i = 0; i < 5; i++)
{
tmp = (node *)malloc(sizeof(node));
tmp->x = (i + 1) * 19;
tmp->y = rand() % 11 + 5;
p->next = tmp;
tmp->next = NULL;
p = tmp;
}
tail = p;
}
init_draw() 函数初始化屏幕:
void init_draw()
{
Node p;
int i, j;
// 遍历链表
for(p = head->next; p->next!= NULL; p = p->next)
{
// 绘制管道
for(i = p->x; i > p->x-10; i--)
{
for(j = 0; j < p->y; j++)
{
move(j, i);
addch(CHAR_STONE);
}
for(j = p->y+5; j <= 23; j++)
{
move(j, i);
addch(CHAR_STONE);
}
}
refresh();
sleep(1);
}
}
至此,我们的《飞扬的小鸟》游戏就完成了。
编译与运行
执行 gcc 命令进行编译:
cd ~/project
gcc -o flappy_bird flappy_bird.c -lcurses
./flappy_bird

总结
在这个项目中,我们使用 C 编程语言实现了一个基于文本的《飞扬的小鸟》游戏。同学们可以基于本课程进一步完善该游戏,比如给管道添加颜色,或者让管道宽度随机变化。



