使用C语言构建《飞扬的小鸟》

CCBeginner
立即练习

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

《飞扬的小鸟》(Flappy Bird)是一款广受欢迎且令人上瘾的手机游戏,因其简单却具有挑战性的玩法而大获成功。在本项目中,我们将学习如何使用C编程语言实现我们自己版本的《飞扬的小鸟》。

通过完成本项目,你将:

  • 学习如何使用 ncurses 库进行基于文本的屏幕绘制。
  • 掌握Linux中的数据结构和系统调用知识。
  • 获得在C程序中处理键盘事件和实时更新的经验。

👀 预览

《飞扬的小鸟》预览

🎯 任务

在本项目中,你将学习:

  • 如何使用C实现基于字符的《飞扬的小鸟》版本。
  • 如何处理键盘事件以控制小鸟的移动。
  • 如何通过将障碍物从右向左移动来营造向前运动的错觉。
  • 如何使用 ncurses 库绘制字符界面。

🏆 成果

完成本项目后,你将能够:

  • 展示对C编程语言的熟练掌握。
  • 培养处理键盘事件的技能。
  • 在C程序中实现实时更新。
  • 使用 ncurses 库进行基于文本的屏幕绘制。
  • 理解Linux中的数据结构和系统调用。

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL c(("`C`")) -.-> c/BasicsGroup(["`Basics`"]) c(("`C`")) -.-> c/ControlFlowGroup(["`Control Flow`"]) c(("`C`")) -.-> c/CompoundTypesGroup(["`Compound Types`"]) c(("`C`")) -.-> c/FunctionsGroup(["`Functions`"]) c(("`C`")) -.-> c/UserInteractionGroup(["`User Interaction`"]) c/BasicsGroup -.-> c/variables("`Variables`") c/BasicsGroup -.-> c/constants("`Constants`") c/ControlFlowGroup -.-> c/if_else("`If...Else`") c/ControlFlowGroup -.-> c/while_loop("`While Loop`") c/CompoundTypesGroup -.-> c/structures("`Structures`") c/FunctionsGroup -.-> c/function_declaration("`Function Declaration`") c/FunctionsGroup -.-> c/function_parameters("`Function Parameters`") c/UserInteractionGroup -.-> c/user_input("`User Input`") c/UserInteractionGroup -.-> c/output("`Output`") subgraph Lab Skills c/variables -.-> lab-298823{{"`使用C语言构建《飞扬的小鸟》`"}} c/constants -.-> lab-298823{{"`使用C语言构建《飞扬的小鸟》`"}} c/if_else -.-> lab-298823{{"`使用C语言构建《飞扬的小鸟》`"}} c/while_loop -.-> lab-298823{{"`使用C语言构建《飞扬的小鸟》`"}} c/structures -.-> lab-298823{{"`使用C语言构建《飞扬的小鸟》`"}} c/function_declaration -.-> lab-298823{{"`使用C语言构建《飞扬的小鸟》`"}} c/function_parameters -.-> lab-298823{{"`使用C语言构建《飞扬的小鸟》`"}} c/user_input -.-> lab-298823{{"`使用C语言构建《飞扬的小鸟》`"}} c/output -.-> lab-298823{{"`使用C语言构建《飞扬的小鸟》`"}} end

基础知识

我们的项目需要一些数据结构的知识,并且涉及到Linux中的一些系统调用。

此外,我们还使用了一个名为 ncurses 的基于文本的屏幕绘制库。所以,在编译时,需要添加 -lcurses 选项。

设计思路

为了实现基于字符的《飞扬的小鸟》版本,我们从实现以下三个关键点入手:

  1. 程序应该能够响应键盘事件。
  2. 字符界面应该能够实时更新。
  3. 小鸟应该给人向前飞行的视觉错觉。

针对上述三个问题,我们的解决方案如下:

  1. 使用Linux提供的系统接口来捕获键盘事件。
  2. 使用ncurses库函数来绘制字符界面。
  3. 为了营造小鸟向前飞行的错觉:

最直接的方法是让小鸟在水平方向上从左向右移动,但这样小鸟在某个时刻会超出右边界。

相反,我们反过来思考:当一个人在向前行驶的汽车中看车外的风景时,风景看起来是向后移动的(运动是相对的)。所以,我们让障碍物从右向左移动,这样既能达到相同的视觉效果,又能避免小鸟超出边界的问题。

定义常量

首先,打开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, &timeset, 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() 循环。该循环主要由三部分组成:

  1. 检查用户输入:如果按下 “w” 键或空格键,小鸟将向上移动两行。如果按下 “q” 键,游戏将退出。如果按下 “z” 键,游戏将暂停。
  2. 移动小鸟并重新绘制它。
  3. 检查小鸟是否撞到管道。

让我们看看代码:

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编程语言实现了一个基于文本的《飞扬的小鸟》游戏。同学们可以基于本课程进一步完善该游戏,比如给管道添加颜色,或者让管道宽度随机变化。

您可能感兴趣的其他 C 教程