C 言語を使った Flappy Bird の作成

CCIntermediate
今すぐ練習

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

Flappy Bird は、シンプルでありながらチャレンジングなゲームプレイで大きな人気を博した人気のあるアドディクティブなモバイルゲームです。このプロジェクトでは、C 言語を使って独自のバージョンの Flappy Bird を実装する方法を学びます。

このプロジェクトを追うことで、次のことができます。

  • テキストベースの画面描画に ncurses ライブラリを使用する方法を学ぶ。
  • Linux におけるデータ構造とシステムコールの知識を獲得する。
  • C プログラムでキーボードイベントとリアルタイム更新を処理する経験を得る。

👀 プレビュー

Flappy Bird Preview

🎯 タスク

このプロジェクトでは、次のことを学びます。

  • C を使って文字ベースのバージョンの Flappy Bird を実装する方法。
  • キーボードイベントを処理して鳥の動きを制御する方法。
  • 障害物を右から左に移動させることで前進の錯覚を作り出す方法。
  • ncurses ライブラリを使って文字インターフェイスを描画する方法。

🏆 成果

このプロジェクトを完了すると、次のことができるようになります。

  • C 言語の熟練度を示す。
  • キーボードイベントを処理するスキルを開発する。
  • C プログラムでリアルタイム更新を実装する。
  • テキストベースの画面描画に ncurses ライブラリを利用する。
  • Linux におけるデータ構造とシステムコールを理解する。

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL c(("C")) -.-> c/UserInteractionGroup(["User Interaction"]) c(("C")) -.-> c/BasicsGroup(["Basics"]) c(("C")) -.-> c/ControlFlowGroup(["Control Flow"]) c(("C")) -.-> c/CompoundTypesGroup(["Compound Types"]) c(("C")) -.-> c/FunctionsGroup(["Functions"]) 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 言語を使った Flappy Bird の作成"}} c/constants -.-> lab-298823{{"C 言語を使った Flappy Bird の作成"}} c/if_else -.-> lab-298823{{"C 言語を使った Flappy Bird の作成"}} c/while_loop -.-> lab-298823{{"C 言語を使った Flappy Bird の作成"}} c/structures -.-> lab-298823{{"C 言語を使った Flappy Bird の作成"}} c/function_declaration -.-> lab-298823{{"C 言語を使った Flappy Bird の作成"}} c/function_parameters -.-> lab-298823{{"C 言語を使った Flappy Bird の作成"}} c/user_input -.-> lab-298823{{"C 言語を使った Flappy Bird の作成"}} c/output -.-> lab-298823{{"C 言語を使った Flappy Bird の作成"}} end

基本知識

私たちのプロジェクトには、データ構造の知識が必要であり、Linux におけるいくつかのシステムコールが関係します。

また、ncurses と呼ばれるテキストベースの画面描画ライブラリも使用します。したがって、コンパイル時に -lcurses オプションを追加する必要があります。

設計思想

文字ベースのバージョンの Flappy Bird を実装するには、次の 3 つの要点の実装から始めます。

  1. プログラムはキーボードイベントに応答できるようにする。
  2. 文字インターフェイスはリアルタイムで更新できるようにする。
  3. 鳥が前向きに飛んでいる錯覚を与えるようにする。

上記の 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_STONE を CHAR_BLANK に置き換え
        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--;
}

シグナルハンドラ関数の中で、背景を 1 列分前方に移動させると同時に、鳥を 1 行分落下させます。また、鳥が柱と衝突したかどうかをチェックします。衝突した場合はゲームオーバーです。

✨ 解答を確認して練習

main() 関数

main() 関数では、まず初期化関数 init() を呼び出し、その後 while() ループに入ります。このループは主に 3 つの部分で構成されています。

  1. ユーザーの入力をチェックする:「w」キーまたはスペースバーが押された場合、鳥は 2 行上に移動します。「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」キーまたはスペースバーが押された場合、鳥は 2 行上に移動します。「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);
    }
}

これで、私たちの Flappy Bird ゲームは完成です。

✨ 解答を確認して練習

コンパイルと実行

gcc コマンドを実行してコンパイルします。

cd ~/project
gcc -o flappy_bird flappy_bird.c -lcurses
./flappy_bird
Flappy Bird コードのコンパイル
✨ 解答を確認して練習

まとめ

このプロジェクトでは、C 言語を使ってテキストベースの Flappy Bird ゲームを実装しました。学生はこのコースを基にゲームをさらに向上させることができます。たとえば、パイプに色を追加したり、パイプの幅をランダムに変更したりします。