C++でスネークゲームを作ってみました

今回はC++とSDL2を使ってスネークゲームを作ってみました。

ソースコードはこちらです：https://github.com/soueleven/SnackGame
開発環境はXcodeです。

このゲームはスネークゲームのロジックだけ、UI、音声、エンディングは一切ありません。

結果の画面はこのようになります。
![](https://i.imgur.com/PAVITbk.png)


## ゲームのルール
こちらは[ウィキペディア](https://ja.wikipedia.org/wiki/ヘビゲーム)に参考しましょう：
> プレイヤーはヘビ（1キャラ分のブロックが連なり、細長い形状をしているもの）を操作し、その頭が画面の周囲あるいは自身の身体にぶつからないようにしながら、ランダムで出現するエサを回収する。エサを回収するたびにヘビの身体は1キャラずつ長くなっていく。ヘビは静止することができず、常に動き回っているため、エサを回収すればするほど自身の身体を回避することが難しくなる。

まとめと言うと：

* ヘビは常に動く。
* キーボードを操作し、ヘビの動き方向を変える。
* 餌をランダムで出てくる。
* 餌を回収すればするほど、ヘビの身体が長くなっていく。
* ヘビが身体または壁をぶつかった時、ゲームオーバになる。

## クイックスタート
ここで、ゲームの始めを作りたいと思います。
結果としては、画面を出てきて、ボタンを押すと閉じるという簡単な実装です。

SDL2のインストールについて、他の記事に参考してください。
今回使ったのはSDL2だけで、他のSDLライブラリはインストールしなくでも大丈夫です。

まずは、GameのヘッダファイルでGameクラスを作ります。
この配置は[ゲームプログラミングC++](https://www.amazon.co.jp/ゲームプログラミング-C-プログラミング/s?rh=n%3A3229700051%2Cp_n_feature_seven_browse-bin%3A3232384051)この本を参考しました。

```C++
// <Game.hpp>
class Game {
public:
    Game();
    bool Initialize();
    void RunLoop();
    void Shutdown();
private:
    void ProcessInput();
    void UpdateGame();
    void GenerateOutput();

    SDL_Window* window;
    SDL_Renderer* renderer;

    bool isRunning;
    Uint32 ticksCount;
};
```

publicの方法は、ゲーム初期化、ループ、終了の処理です。
privateの方法は、インプット、アップデート、画面のアウトプットの処理です。
isRunningはこのゲームの実行状態を格納する変数です。
ticksCountはデルタタイムを処理するための変数です。

では、実装の方にいきましょう。

最初は、**Initialize**初期化です。失敗する場合、falseを返し、成功する場合、trueを返します。

```C++
// <Game.cpp>
const int HEIGHT = 480;
const int WIDTH = 640;

Game::Game()
:window(nullptr)
,renderer(nullptr)
,isRunning(true)
,ticksCount(0)
{}

bool Game::Initialize() {
    if (SDL_Init(SDL_INIT_VIDEO) != 0) {
        SDL_Log("Unable to initialize SDL: %s", SDL_GetError());
        return false;
    }

    window = SDL_CreateWindow(
        "",
        SDL_WINDOWPOS_UNDEFINED,
        SDL_WINDOWPOS_UNDEFINED,
        WIDTH,
        HEIGHT,
        SDL_WINDOW_OPENGL
    );

    if (window == NULL) {
        SDL_Log("Failed to create windows: %s", SDL_GetError());
        return false;
    }

    renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED|SDL_RENDERER_PRESENTVSYNC);
    if (renderer == NULL) {
        SDL_Log("Failed to create renderer: %s", SDL_GetError());
        return false;
    }

    return true;
}
```

**RunLoop**は、インプット、更新、アウトプットを繰り返すだけです。

```C++
// <Game.cpp>
void Game::RunLoop() {
    while (isRunning) {
        ProcessInput();
        UpdateGame();
        GenerateOutput();
    }
}
```

**Shutdown**は、ゲーム終了の処理です。

```C++
// <Game.cpp>
void Game::Shutdown() {
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
}
```

**ProcessInput**は、インプットの処理です。
今のは、システム終了の処理とEscボタンを押す時の処理です。

```C++
// <Game.cpp>
void Game::ProcessInput() {
    SDL_Event event;
    while(SDL_PollEvent(&event)) {
        switch(event.type) {
            case SDL_QUIT:
                isRunning = false;
                break;
            default:
                break;
        }
    }

    const Uint8* state = SDL_GetKeyboardState(NULL);

    if (state[SDL_SCANCODE_ESCAPE]) {
        isRunning = false;
    }
}
```

**UpdateGame**は、この後に説明します。

```C++
// <Game.cpp>
void Game::UpdateGame() {
}
```

**GenerateOutput**は、今は背景を生成するだです。

```C++
// <Game.cpp>
void Game::GenerateOutput() {
    // Generate Background
    SDL_SetRenderDrawColor(renderer, 0, 128, 0, 255);
    SDL_RenderClear(renderer);
    
    SDL_RenderPresent(renderer);
}
```

**main**

```C++
// <main.cpp>
int main() {
    Game game;
    bool isSuccess = game.Initialize();

    if (isSuccess) {
        game.RunLoop();
    } else {
        std::cout << "Faild\n";
    }

    game.Shutdown();

    return 0;
}
```

ここまで実行すると、緑の画面を出てきて、Escボタンを押すと、閉じると思います。

本番はここからと思います。

## スネークについて
スネークの格納方法は考えてみましょう。

ヘビは毎回餌を食べるたびに、身長が１つ長くなっていくので、ここはArrayを格納すると、毎回は長さを変更せざる得ない。
なので、Arrayは一番いい選択ではないと思います。

ここで、Linked Listを使うのはどうでしょう。
ヘビの動きを考え見てください。毎回動きの時、体を格納した位置を前を進む、その前は前回の前の位置です。

言葉を説明が難しいですが、画像を見てください。

![](https://i.imgur.com/sJLuEXg.png)

なので、スネークの動きは簡単です。ただの後尾を消し、先頭を追加するだけです。

では、餌を食べる時、どうなりますか。
![](https://i.imgur.com/kkt1Qq1.png)

餌を食べる時、ただ先頭を追加するだけです。

と言うわけで、ヘビの動きが分かりましたね。

* 移動すると、後尾を消し、先頭を追加
* 餌を食べると、先頭を追加

なのでLinked Listの実装は、ただ**先頭追加**と**後尾削除**この２つの方法だけですね。

今回のテーマはLinked Listではないので、詳しいLinked Listの説明は他の参考をしましょう。

* [Linked List Data Structure](https://www.geeksforgeeks.org/data-structures/linked-list/#singlyLinkedList)
* [How to Use Linked List in C++](https://www.bitdegree.org/learn/linked-list-c-plus-plus)
* [C++ 連結リスト](https://qiita.com/yokoto/items/3bbe3754cf378d54f55c)
## スネークの実装
Snackのヘッダファイルを作ります。

最初は、ヘビの幅長さと位置の構造を定義します。

```C++
// <Snack.hpp>
const int POINT_WH = 10; // ヘビの幅と長さ

struct vector2 {
    float x;
    float y;
};
```

SnackのNodeを定義します。格納するのは、位置情報と次のNodeのポインタ情報です。

```C++
// <Snack.hpp>
class Snack; // SnackクラスをSnackNodeのfriendとして使いたいので、先に宣言します。

class SnackNode {
private:
    vector2 pos;
    SnackNode *next;
public:
    SnackNode(vector2 p): pos(p), next(0){};

    vector2 GetPos() {return pos;};
    SnackNode* GetNext() {return next;};

    friend class Snack;
};
```

Snackクラスを定義します。

```C++
// <Snack.hpp>
class Snack {
private:
    SnackNode *first;
public:
    Snack(): first(0){};
    void InsertHead(vector2 pos);
    void DeleteLast();
    int SearchNode(vector2 pos);
    int SearchBodyNode(vector2 pos);

    SnackNode* GetFirst() {return first;};
};
```
それで、Snackを実装します。

最初は**InsertHead**の部分、これは、頭に新しい位置を追加することです。
新しいSnackNodeを定義して、newNodeのnextを元のfirstを指し、firstは新しいSnackNodeを指すことです。

```C++
// <Snack.cpp>
void Snack::InsertHead(vector2 pos) {
    SnackNode *newNode = new SnackNode(pos);
    newNode->next = first;
    first = newNode;
}
```

次は**DeleteLast**の部分、これは、後尾を削除し、元の後から2番目のNodeを最後尾にすることです。
後ろから２番目のNodeを探し、secondLast（後ろから２番目のNode）の次のNode（最後尾）のポインタをリリースし、nextをNULLにすることです。

```C++
// <Snack.cpp>
void Snack::DeleteLast() {
    if (first == 0) {
        return;
    }

    SnackNode *secondLast = first;
    while (secondLast->next->next != 0) {
        secondLast = secondLast->next;
    }

    delete (secondLast->next);
    secondLast->next = NULL;
}
```

**SearchNode**は、ある位置とヘビのNodeの位置を被るところを探すの実装です。
ifの判断式を注目しましょう。これは位置だけではなく、この点とヘビの点をぶつかっているかとうかを判断する式です。
重なっている場合、この位置のインデックスを返し、重なっていない場合、-1を返します。

```C++
// <Snack.cpp>
int Snack::SearchNode(vector2 pos) {
    if (first == 0) {
        return -1;
    }

    SnackNode *current = first;

    int i = 0;
    while (current->next != 0) {
        if ((current->pos.x + POINT_WH > pos.x && pos.x > current->pos.x - POINT_WH) &&
            (current->pos.y + POINT_WH > pos.y && pos.y > current->pos.y - POINT_WH)) {
            return i;
        }
        current = current->next;
        i++;
    }

    return -1;
}
```

**SearchBodyNode**は、SearchNodeと同じです、違うところは探すの時は頭を除く

```C++
// <Snack.cpp>
int Snack::SearchBodyNode(vector2 pos) {
    if (first == 0) {
        return -1;
    }

    SnackNode *current = first->next;
    int i = 0;
    while (current->next != 0) {
        if ((current->pos.x + POINT_WH > pos.x && pos.x > current->pos.x - POINT_WH) &&
            (current->pos.y + POINT_WH > pos.y && pos.y > current->pos.y - POINT_WH)) {
            return i;
        }
        current = current->next;
        i++;
    }

    return -1;
}
```

## ゲームの初期化
ここまで、ヘビの実装も用意しましたので。
ゲーム画面で、ヘビと餌を追加したいと思います。

最初作ったGameのヘッダファイルでSnackをインクルードします
Gameのクラスに**ヘビ**と**餌**と**ヘビの移動方向**を追加します。

```C++
// <Game.hpp>
class Game {
public:
    Game();
    bool Initialize();
    void RunLoop();
    void Shutdown();
private:
    void ProcessInput();
    void UpdateGame();
    void GenerateOutput();

    SDL_Window* window;
    SDL_Renderer* renderer;

    bool isRunning;
    Uint32 ticksCount;

    vector2 feed; // 餌
    int direction; // 移動方向
    Snack snack; // ヘビ
};
```

Gameで以下の常数を追加します。

```C++
// <Game.cpp>
const int DIRECTION_UP = 0; // 上の方向
const int DIRECTION_DOWN = 1; // 下の方向
const int DIRECTION_RIGHT = 2; // 右の方向
const int DIRECTION_LEFT = 3; // 左の方向
```

初期化の関数**Initialize**で、ヘビ初期位置と方向、餌の位置を決めたいと思います。

餌もランダムで生成したいので、C++のsrand関数を使います。
ここで餌とヘビの位置が被った時の問題も考えてみましょう。
被った時はもう一度生成すれば良いです。
この**RandomFeedPosition**関数は、餌を生成し、ヘビのSearchNode関数を使い、被るかとかを判断します。被った場合(-1ではない)、もう一度生成します。

```C++
// <Game.cpp>
void RandomFeedPosition(Snack snack, vector2 *feed) {
    feed->x = (rand() % (WIDTH - POINT_WH) / 10 + 1) * 10;
    feed->y = (rand() % (HEIGHT - POINT_WH) / 10 + 1) * 10;
    while (snack.SearchNode(*feed) > -1) {
        feed->x = (rand() % (WIDTH - POINT_WH) / 10 + 1) * 10;
        feed->y = (rand() % (HEIGHT - POINT_WH) / 10 + 1) * 10;
    }
}
```

```C++
// <Game.cpp>
bool Game::Initialize() {
    ...
    
    direction = DIRECTION_RIGHT;
    
    snack = Snack();
    snack.InsertHead(vector2{WIDTH/2-POINT_WH*4, HEIGHT/2});
    snack.InsertHead(vector2{WIDTH/2-POINT_WH*3, HEIGHT/2});
    snack.InsertHead(vector2{WIDTH/2-POINT_WH*2, HEIGHT/2});
    snack.InsertHead(vector2{WIDTH/2-POINT_WH*1, HEIGHT/2});

    srand((unsigned int)time(NULL));
    RandomFeedPosition(snack, &feed);
}
```

## 画面表示
初期化で生成したヘビと餌は、今の段階で、実行しても、何も反応しないと思います。

**GenerateOutput**で、ヘビと餌の表示を実装します。

```C++
// <Game.cpp>
void Game::GenerateOutput() {
    // Generate Background
    SDL_SetRenderDrawColor(renderer, 0, 128, 0, 255);
    SDL_RenderClear(renderer);

    // Generate Snack
    SDL_SetRenderDrawColor(renderer, 192, 192, 192, 192);
    SnackNode *current = snack.GetFirst();
    while (current != 0) {
        SDL_Rect node{
            static_cast<int>(current->GetPos().x),
            static_cast<int>(current->GetPos().y),
            POINT_WH, // Width
            POINT_WH // Height
        };
        SDL_RenderFillRect(renderer, &node);
        current = current->GetNext();
    }

    // Generate Feed
    SDL_SetRenderDrawColor(renderer, 255, 50, 0, 255);
    SDL_Rect appleNode{
        static_cast<int>(feed.x),
        static_cast<int>(feed.y),
        POINT_WH, // Width
        POINT_WH // Height
    };
    SDL_RenderFillRect(renderer, &appleNode);

    SDL_RenderPresent(renderer);
}
```

## スネークの移動
ここまで、ようやくヘビと餌も出てきました。
次は、このスネークを動きましょう。

まずは、**ProcessInput**でのキーボード処理です。
ここはただのキーボードの上下左右の処理だけですが、幾つの点を注目しましょう。

* 上に移動する時、急に下に変換できないこと。逆も同じです。
* 右に移動する時、急に左に変換できないこと。逆も同じです。

なので、この２つの点について、if判断を追加しました。

```C++
// <Game.cpp>
void Game::ProcessInput() {
    ...

    if (state[SDL_SCANCODE_UP] && (direction != DIRECTION_DOWN)) {
        direction = DIRECTION_UP;
    }
    
    if (state[SDL_SCANCODE_DOWN] && (direction != DIRECTION_UP)) {
        direction = DIRECTION_DOWN;
    }
    
    if (state[SDL_SCANCODE_RIGHT] && (direction != DIRECTION_LEFT)) {
        direction = DIRECTION_RIGHT;
    }
    
    if (state[SDL_SCANCODE_LEFT] && (direction != DIRECTION_RIGHT)) {
        direction = DIRECTION_LEFT;
    }
}
```

## ゲームの更新
しかし、ただいまキーボードの処理が終わりましたが、ヘビは全く動かない、これはただキーボードの処理するだけで、ヘビを反応しないと、静止状態のままですね。

ここから、ゲーム更新の実装をしたいと思います。
まずは、ゲームをどのくらいの更新頻度を行うか。ここで、0.1秒にしましょう。

```C++
// <Game.cpp>
float delay = 0;

void Game::UpdateGame() {
    float deltaTime = SDL_GetTicks() - ticksCount;
    ticksCount = SDL_GetTicks();

    delay+=deltaTime;

    // snack move for per 0.1s
    if (delay >= 100) {
    }
}
```

更新の内容は、

* 方向に従って進む
* ヘビと体の当たり判定
* ヘビと壁の当たり判定
* ヘビと餌の当たり判定

この４つでいいです。

**方向に従って進む**は、具体的に、頭を新しい位置を追加したり、尻尾を捨てったりすることです。

```C++
// <Game.cpp>
void Game::UpdateGame() {
    ...

    // snack move for per 0.1s
    if (delay >= 100) {
        switch (direction) {
            case DIRECTION_UP:
                snack.InsertHead(vector2{snack.GetFirst()->GetPos().x, snack.GetFirst()->GetPos().y-POINT_WH});
                break;
            case DIRECTION_DOWN:
                snack.InsertHead(vector2{snack.GetFirst()->GetPos().x, snack.GetFirst()->GetPos().y+POINT_WH});
                break;
            case DIRECTION_RIGHT:
                snack.InsertHead(vector2{snack.GetFirst()->GetPos().x+POINT_WH, snack.GetFirst()->GetPos().y});
                break;
            case DIRECTION_LEFT:
                snack.InsertHead(vector2{snack.GetFirst()->GetPos().x-POINT_WH, snack.GetFirst()->GetPos().y});
                break;
            default:
                break;
        }
        snack.DeleteLast();
        delay = 0;
    }
}
```

**ヘビと体の当たり判定**は、実際はヘビの頭と体の当たりです。

```C++
// <Game.cpp>
void Game::UpdateGame() {
    ...

    // snack move for per 0.1s
    if (delay >= 100) {
        ...
        if (snack.SearchBodyNode(snack.GetFirst()->GetPos()) > 0) {
            isRunning = false;
        }
    }
}
```

**ヘビと壁の当たり判定**

```C++
// <Game.cpp>
void Game::UpdateGame() {
    ...

    // snack move for per 0.1s
    if (delay >= 100) {
        ...
        if ((snack.GetFirst()->GetPos().x < 0) ||
            (snack.GetFirst()->GetPos().x > (WIDTH - POINT_WH)) ||
            (snack.GetFirst()->GetPos().y < 0 ||
             snack.GetFirst()->GetPos().y > (HEIGHT - POINT_WH))) {
            isRunning = false;
        }
    }
}
```

**ヘビと餌の当たり判定**、これもSearchNodeを使って判断します。当たった場合は頭を追加する

```C++
// <Game.cpp>
void Game::UpdateGame() {
    ...

    // snack move for per 0.1s
    if (delay >= 100) {
        ...
        if (snack.SearchNode(feed) > -1) {
            snack.InsertHead(feed);
            RandomFeedPosition(snack, &feed);
        }
    }
}
```

以上、これで終わりです。