C语言实现 Flappy Bird 游戏
在本文中,我将带你一步步实现一个基于C语言的Flappy Bird游戏,并结合代码示例详细讲解实现过程中的关键逻辑、设计思路及其背后的原因。同时,我也会分享在实现过程中遇到的一些问题和解决方案。
引言
Flappy Bird 是一款因其简单但极具挑战性而广受欢迎的小游戏。本文的目标是使用C语言和ncurses
库在Linux系统的终端中实现一个简化版的Flappy Bird游戏。在这个游戏中,鸟由字符O
表示,障碍物由字符*
组成。玩家需要通过键盘控制鸟的高度,避免撞到不断从右向左移动的障碍物。
项目环境
在开始编码之前,我们需要配置开发环境并安装所需的库:
- 操作系统:Linux(本文使用Ubuntu为例)
- 编辑器:GVim 或者其他你喜欢的文本编辑器
- 库:
ncurses
,用于在终端中绘制字符界面。
安装ncurses
库:
sudo apt-get install libncurses5-dev
项目环境与准备工作
在开始编码之前,我们需要配置开发环境并安装所需的库。以下是具体步骤:
-
打开Xfce终端:首先在你的Linux系统中打开Xfce终端。
-
安装
ncurses
库:sudo apt-get install libncurses5-dev
-
进入项目目录:接下来,进入你想要存放项目的目录,比如
Code
目录。cd Code
-
创建项目文件:在
Code
目录下创建一个名为my_flappy_bird.c
的C语言文件。touch my_flappy_bird.c
-
使用GVim编辑器编写代码:打开GVim编辑器开始编写代码。
vim my_flappy_bird.c
代码实现与逻辑解析
1. 初始化与设置
首先,我们需要导入相关的头文件,并定义一些常量和全局变量。
#include <curses.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/time.h>
// 定义鸟、石头和空格的符号
#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;
逻辑解析:
ncurses
库的引入使得我们可以在终端中处理字符界面。- 定义的字符符号用于在屏幕上表示鸟和障碍物。
- 使用链表结构体存储柱子的位置是因为链表可以动态调整元素数量,更灵活地管理障碍物的出现和移动。
设计理由:
- 选择链表而非数组是因为柱子的数量不固定,如果使用数组,不仅难以管理,而且会增加不必要的空间开销和复杂度。
2. 核心函数设计
在主函数main
之前,我们需要编写几个初始化和信号处理函数。
2.1 初始化函数
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);
}
void init_bird() {
bird_x = 5;
bird_y = 15;
move(bird_y, bird_x);
addch(CHAR_BIRD);
refresh();
sleep(1);
}
逻辑解析:
init()
函数负责整个游戏的初始化,包括屏幕设置、鸟的位置初始化、链表(用于存储障碍物)的初始化等。init_bird()
函数专注于鸟的初始化位置,并在屏幕上显示。
设计理由:
- 将初始化分为多个函数,使得代码更具可读性和可维护性。如果所有初始化工作都在
main
函数中进行,会导致代码过于冗长和复杂。
2.2 信号处理与定时器设置
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);
}
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) {
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--;
}
逻辑解析:
set_ticker()
函数设置定时器,使得游戏每隔一定时间自动更新一次屏幕。drop()
函数在每次定时器触发时运行,用于更新障碍物和鸟的位置,并检测是否发生碰撞。
设计理由:
- 使用定时器而非循环的方式更新屏幕是为了避免因循环占用大量CPU资源导致系统性能下降。
- 在
drop()
函数中,通过动态生成新的障碍物并删除超出边界的障碍物,使得游戏能够持续进行。
3. 主循环与用户输入
int main() {
char ch;
init();
while(1) {
ch = getch();
if(ch == ' ' || ch == 'w' || ch == '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;
}
逻辑解析:
- 主循环通过不断读取用户输入来控制游戏的进行。
getch()
函数用于捕获键盘输入,玩家按下空格或w
键时,鸟会向上飞;按下z
键暂停游戏,q
键退出游戏。
设计理由:
- 将用户输入和游戏逻辑放在主循环中,使得代码逻辑清晰明了,并且便于后续扩展(如添加更多控制逻辑)。
结果展示:
暴露的问题
主要组件
-
Linux环境的设置
- Linux命令的不熟悉:在开始项目之前,我需要熟悉基本的Linux命令,如
cd
、mkdir
、vim
等。这些命令对于在文件系统中导航、创建目录以及编辑文件至关重要。刚开始时,我对这些命令的语法和用法感到困惑,但通过实践和查阅文档,我逐渐掌握了它们。
- Linux命令的不熟悉:在开始项目之前,我需要熟悉基本的Linux命令,如
-
使用
ncurses
库ncurses
库函数的学习:ncurses
是一个功能强大的库,它提供了在终端中创建文本用户界面的函数。然而,由于我对ncurses
不熟悉,因此在理解其提供的各种函数时遇到了一些困难,例如initscr()
、cbreak()
、noecho()
和refresh()
等函数。它们分别用于初始化屏幕、处理输入、关闭回显以及刷新显示。通过查阅资料和不断实践,我逐渐掌握了这些函数的用法。
-
信号处理与游戏逻辑的理解
-
信号处理与游戏逻辑:信号处理是C编程中的一个高级主题,它是我在本项目中遇到的最大挑战之一。
setitimer
函数用于设置定时器,定时器会定期向程序发送信号。drop
函数是信号处理程序,用于更新游戏状态,比如移动鸟和障碍物。编写这些函数需要对Linux中的信号机制有深入的理解,这花费了我不少时间。 -
指针与链表的理解:另一个主要挑战是理解指针,尤其是在处理链表时。在游戏中,障碍物是用链表表示的,
p->next
语法用于遍历链表节点。起初,我对这种语法感到困惑,不确定它的作用。在学习了链表的工作原理并通过实验后,我逐渐理解了如何使用指针来操作链表节点。
-
-
使用
sleep
函数- 调整睡眠时间:
sleep
函数用于在游戏中引入延迟,从而产生平滑的动画效果。找到合适的睡眠时间是一项挑战:时间过短,游戏会运行得太快;时间过长,游戏则变得难以操作。经过多次尝试,我最终找到了一个既具有挑战性又不过于苛刻的时间设置。
- 调整睡眠时间:
结论
通过C语言和ncurses
库实现的Flappy Bird游戏展示了如何在终端中创建一个简单但具有挑战性的游戏。在这个过程中,我遇到了许多挑战,包括对Linux命令的不熟悉、ncurses
库的使用、信号处理与游戏逻辑的编写,以及对指针和链表的理解。这些问题的解决不仅帮助我深入理解了C语言,也让我在Linux环境下的编程能力有了显著提高。
这个项目的成功实施证明了即使在字符界面中,我们也可以用C语言创造出有趣的游戏。如果你也对C语言和Linux系统感兴趣,强烈推荐你尝试实现这个项目,它将为你带来巨大的成就感和学习体验。
希望这篇博客能帮助你更好地理解如何使用C语言实现Flappy Bird游戏。如果你有任何疑问或需要进一步的帮助,请随时与我联系!