Code of Poem
Unknown programmer's programming note.

ブロック崩しを作る (4)

あけましておめでとうございます!

前回ブロックが右上に進むところまでやりました。今回こそ衝突判定をやります。

やることは以下のとおりです。

順番にやっていきたいと思います。

ボールが壁に当たったら跳ね返るようにする

やらなければいけないことを二つに分けて考えます。

先に当たったか判定するところをやります。

矩形の交差判定

現時点では壁もブロックも四角形として表現しています。なので必要なのは四角形同士の交差判定です。平行に並んでいる四角形同士の判定は割と簡単な方です。簡単どころかSDLには矩形の交差を判定するAPI関数が含まれています。リンクを貼っておきます。

SDL_HasIntersection
2つの矩形が交差していればSDL_TRUE、交差していなければSDL_FALSEを返す。
SDL_IntersectRect
2つの矩形の交差している部分の矩形を取得する。

今はボールが当たったかどうか知りたいだけなのでSDL_HasIntersection関数を使えば良さそうです。ですが、一応どのように矩形同士の判定を行うかを一応書いておきます。

素直なやり方は2つ考えられます。そのまま交差しているかを判定する方法と、逆に考えて、交差していないのでなければ交差していると判定する方法です。

一つ目の方法をコードにすると、

bool has_intersection1(const SDL_Rect& a, const SDL_Rect& b)
{
    int a_left   = a.x;
    int a_top    = a.y;
    int a_right  = a.x + a.w - 1;
    int a_bottom = a.y + a.h - 1;
    int b_left   = b.x;
    int b_top    = b.y;
    int b_right  = b.x + b.w;
    int b_bttom  = b.y + b.h;

    return (a_right >= b_left && a_left <= b_right) &&
           (a_bottom >= b_top && a_top <= b_bttom);
}

こんな感じになります。二つ目の方は、

bool has_intersection2(const SDL_Rect& a, const SDL_Rect& b)
{
    int a_left   = a.x;
    int a_top    = a.y;
    int a_right  = a.x + a.w - 1;
    int a_bottom = a.y + a.h - 1;
    int b_left   = b.x;
    int b_top    = b.y;
    int b_right  = b.x + b.w;
    int b_bttom  = b.y + b.h;

    if (a_right < b_left) {
        return false;
    }
    if (a_left > b_right) {
        return false;
    }
    if (a_bottom < b_top) {
        return false;
    }
    if (a_top > b_bttom) {
        return false;
    }
    return true;
}

といった感じです。やや冗長に書きました。中間の変数をなくして、2つ目の方はifで分岐せずに論理演算の結果をそのまま返せば1行で済みます。

大差ないですが、どちらかというと2つ目のほうが理解しやすいのではないかと思います。

rightをx + w - 1、bottomをy + h - 1と、1引いているのは、開始位置xとyそれ自体に1つカウントが必要だからです。

SDLのSDL_HasIntersection関数のソースを見ると、もう少しひねったやり方をしています。理由はちょっと分かりません。何にせよ、せっかく用意されているのでこれから先矩形同士の交差判定はこのSDL_HasIntersection関数を使うことにします。

この関数で一つ注意しておかないといけないのが、2つの矩形がちょうど接するときは交差していると判定されないことです。例えば、

SDL_Rect a{10, 10, 10, 10};
SDL_Rect b{20, 10, 10, 10};
bool r = SDL_HasIntersection(&a, &b);
SDL_Log("%d", r);

とすると0と出力されます。これはSDL_FALSEであって、SDL_FALSEは出力すると0と表示されます。もう一つ重なってると判定される例として、

SDL_Rect a{10, 10, 11, 10};
SDL_Rect b{20, 10, 10, 10};
bool r = SDL_HasIntersection(&a, &b);
SDL_Log("%d", r);

とすると、1と出力されます。これはSDL_TRUEであって、SDL_TRUEは出力すると1と表示されます。たった1単位の違いですがこの違いは重要です。この1ドットの違いを忘れてしまうとのちのち結果が意図したものにならない可能性が高いです。

SDL_HasIntersection関数をゲームに組み込む

作りやすくするために、ブロックのことは一旦忘れておきます。まず壁との判定をやってしまいます。

壁との交差判定を行う場所としては2箇所候補があります。両方ともupdate関数の中ですが、ボールを処理しているときか、壁を処理しているときかのどちらにするかです。

void update(vector<Brick>& bricks)
{
    ...
    for (auto& brick : bricks) {
        if (brick.tag == "paddle") {
            brick.rect.x += move_scale * dx;
        } else if (brick.tag == "ball") {
            ...
            // 交差判定を行う場所 候補1
        } else if (brick.tag == "brick") {
        } else if (brick.tag == "wall") {
            // 交差判定を行う場所 候補2
        }
    }
}

候補1のボールが判定を行う方を選択した場合、次のようにかけます。

void update(vector<Brick>& bricks)
{
    ...
    for (auto& brick : bricks) {
        if (brick.tag == "paddle") {
            ...
        } else if (brick.tag == "ball") {
            ...
            for (const auto& other : bricks) {
                if (other.tag == "wall") {
                    bool b = SDL_HasIntersection(&brick.rect, &other.rect);
                    if (b) {
                        SDL_Log("HIT!");
                    }
                }
            }
        } else if (brick.tag == "brick") {
        } else if (brick.tag == "wall") {
        }
    }
}

候補2の壁が判定を行う方を選択した場合、次のようにかけます。

void update(vector<Brick>& bricks)
{
    ...
    for (auto& brick : bricks) {
        if (brick.tag == "paddle") {
            ...
        } else if (brick.tag == "ball") {
            ...
        } else if (brick.tag == "brick") {
        } else if (brick.tag == "wall") {
            for (const auto& other : bricks) {
                if (other.tag == "ball") {
                    bool intersected = SDL_HasIntersection(&brick.rect, &other.rect);
                    if (intersected) {
                        SDL_Log("HIT!");
                    }
                }
            }
        }
    }
}

どちらもループが入れ子になっていて見苦しいです。あえてどちらから選ぶとしたらどちらが良いのでしょうか?少し先を見越して判断してみます。この後パドルとブロックとの判定も追加しなければなりません。注目するべきは内側のforループのところです。ボールの方で処理する場合は、全部のブロックとパドルと壁を処理するために入れ子になったループは必須です。壁の方で判定する場合は、必ずしもforループは必要ありません。なぜなら、壁のところのforループは単にボールを探しているだけだからです。もしボールがすぐに見つかるなら壁にとって全く無関心であるブロックやパドルの分までループする必要はありません。ブロックとパドルにとっても同様で、各々のブロックとパドルが関心を持つのはボールと当たったかどうかだけであり、わざわざループで全てを見て回る必要はありません。

簡潔にいうと、候補1を選んだ場合は追加のforループが必要、候補2を選んだ場合は追加のforループは必要でない、ということです。なので候補2の壁側で判定する方を選択します。

update関数が煩雑になってしまうのを避けるため、衝突に関する処理は別の関数で行うようにしておきます。

void resolve_collision(Brick& self, vector<Brick>& bricks)
{
    Brick ball = {};
    for (const auto& b : bricks) {
        if (b.tag == "ball") {
            ball = b;
            break;
        }
    }

    if (ball.tag != "ball") {
        // ball not found
        return;
    }

    if (SDL_HasIntersection(&self.rect, &ball.rect)) {
        SDL_Log("HIT");
    }
}

void update(vector<Brick>& bricks)
{
    ...
    for (auto& brick : bricks) {
        if (brick.tag == "paddle") {
            ...
        } else if (brick.tag == "ball") {
            ...
        } else if (brick.tag == "brick") {
        } else if (brick.tag == "wall") {
            resolve_collision(brick, bricks);
        }
    }
}

resolve_collisionという名前の関数を新たに作りました。今はログを出力しているだけです。後で衝突の処理も書くことになるのでこのような名前にしました。1つ目の引数は衝突の処理を行う主体、言い換えると自分、なのでselfという名前にしてみました。パーフェクトな名前とは思いません。少し違和感あります。2つ目の引数は、ブロック全てを渡しています。必要なのはボールだけなので本来全てのブロックは必要なく、resolve_collision(Brick& self, Brick& ball)とした方が対称性があって優れていると思えます。しかし、呼び出し側がボールを見つけて渡すという形にすると、今のプログラムの構造では呼び出し側のコードに少し無理が来ます。なので、インターフェイスの完全性と効率性を犠牲にして衝突処理を行う側で毎回ボールを探しに行くようにします(この言い回しはすごく大げさだとは思います)。

resolve_collision関数の中で結局forループ使ってるのでは、と思われるかもしれません。全くそのとおりです。しかし、ゲームのパドル、ボール、ブロック、壁をを初期化する時に、ボールはパドルの次に(つまり2番目に)初期化しました。したがってブロック全体を管理するstd::vector<Brick>のオブジェクト、ここでは引数bricks、の常に2番目の位置にあるので実質ループは最初の2回しか実行されないと仮定しています。 break文は即座にそのbreak文を取り囲んでいる一番近いループから抜け出すものです。もし、ボールが見つかったら保存用のオブジェクトに記録してループから抜け出しています。本当ならボールが常に2番目にいるなどという仮定のもとでプログラムを書くべきではありません。もし、初期化を行っているsetup_bricks関数を書き換えて、ボールを最後の位置に持ってくるようにしたら、極めて非効率なコードになります。得られるものはほんのわずかのコードの簡潔さだけです。なのであとで直すべきポイントとして記憶しておいて先に進みます。

std::find_if

ボールを見つけるところはforループで回して順番にタグがボールかどうか調べるというものです。難しい手続きではないので順番に読んでいけば分かりますが、一瞬で判別できるというものではありません。コメントに書いておけば分かりやすいでしょう。

// ボールを見つける
Brick ball = {};
for (const auto& b : bricks) {
    if (b.tag == "ball") {
        ball = b;
        break;
    }
}

もっといい方法があります。STLにはアルゴリズムというものがあります。algorithmヘッダの中に含まれていて、その中にfindやfind_ifといったものが存在しています。今やりたい、vectorの中からボールを見つけるという目的にはfind_ifアルゴリズムがピッタリマッチしています。first_if関数と呼ばずfind_ifアルゴリズムと呼んだのはSTLのアルゴリズムが普通の関数ではなく、関数テンプレートだからです。

find_ifアルゴリズムは3つの引数を取ります。正確さを無視して言うと、1つ目は検索を開始する位置で、2つ目は検索を終了する位置で、3つ目は検索の条件のための関数のようなものです。コードを見た方が理解しやすいと思うので、試しに使ってみます。

#include <algorithm>

bool is_ball(const Brick& b)
{
    return b.tag == "ball";
}

void resolve_collision(Brick& self, vector<Brick>& bricks)
{
    auto ball = find_if(bricks.begin(), bricks.end(), is_ball);
    if (ball == bricks.end()) {
        return; // not found
    }

    if (SDL_HasIntersection(&self.rect, &ball->rect)) {
        SDL_Log("HIT");
    }
}

ポイントを箇条書きで書いておきます。

イテレータは反復子と書かれることも多いです。std::vectorの場合はポインタによく似た使い方をするのですが、ポインタと全く同じとみなすことは出来ません。std::vector<Brick>のイテレーターの型はstd::vector<Brick>::iteratorになります。

std::find_ifアルゴリズムを使ったバージョンは前のforループで探すバージョンと比べて読みやすくなったどうかを考えてみます。forループそのものはfind_ifの呼び出し1行に置き換わっています。しかし、代わりにis_ballというブロックがボールかどうかを判定するための関数を別に記述しなければいけなくなりました。そのため関係するコードの行数トータルはほとんど変わっていません。むしろ、述語をresolve_collisionの外で別途定義しなければいけなくて、手間は増えたようにも感じられます(もし望めばラムダ式を使うことで関数を外部に定義せずに済ますことも出来ます)。しかし、今find_ifアルゴリズムを使うことの最大のメリットは該当する処理がfind_ifという名前で書かれているため、読み手にここはコンテナからボールを探しているのだな、とはっきりと伝わることです。かっこよくいうと、コードがドキュメント性をもつことになります。単にC++のコメントで // ボールを探すと書いても続くコードが本当に正しくボールを探しているのかはコードを実際に読んでみないと分かりません。小さなことに思えますが、STLアルゴリズムの使用に限らず、こういう小さなことの積み重ねがのちのちコードの読みやすさに影響してくることが多いです。

アルゴリズムとイテレータについてはまた別の機会に書きたいと思います。上に書いただけでは簡潔すぎて理解の助けにならないだろうことは承知しています。しかし書き始めると長くなりそうでまたほとんど進まず終わってしまいそうなので…

ボールが跳ね返るようにする

矩形の交差判定によってボールと壁がぶつかったかどうかを判定できるようになりました。今度はぶつかっていたら跳ね返るという処理を作っていきます。最初にはっきりと決めておきたいのが「跳ね返るとはどういうことか」ということです。現実にボールを壁に向かって投げることを考えてみます。前と左と右の壁に囲まれた場所でボールを投げたとします。

上から見下ろして左右がxで前後がyとなるとみなして、上のパターンから次のようになるのではないかと推測します。

さらに、左右の壁に当たったか、上の壁に当たったかを判断するために次のことを考えます。

試しにこれで作ってみます。

void collision_entered(Brick& defender, Brick& attacker)
{
    if (is_ball(attacker)) {
        SDL_Rect prev_ball; // 1フレーム前のボールの矩形
        1フレーム前のボールの位置を計算してセット...

        if (1フレーム前のボールと壁の水平方向に関して、重なっていなければ...) {
            // ボールは左右のどちらからか当たった
            ボールの方向ベクトルのx成分を反転...
        }

        if (1フレーム前のボールと壁の垂直方向に関して、重なっていなければ...) {
            // ボールは上下のどちらかに当たった
            ボールの方向ベクトルのy成分を反転...
        }
    }
}

void resolve_collision(Brick& self, vector<Brick>& bricks)
{
    ...
    if (SDL_HasIntersection(&self.rect, &ball->rect)) {
        collision_entered(self, *ball);
    }
}

衝突した時に実行される関数という意味でconllision_enteredという名前にしました。引数はdefenderとattackerという奇妙な名前にしてあります。attackerがぶつかってきた方で、defenderがぶつかられた方です。通常ぶつかってきたのかぶつかられたかを区別できないし、その必要もないかもしれませんが、ボールと壁の場合ぶつかってきのはボールでぶつかられたのは壁なのは明らかなので、一応区別してあります。

壁に関しては問題はないのですが、実はボールが上下左右どちらから当たってきたのかという判定はこれでは不完全です。なぜなら、1フレーム前で現フレームで当たっているブロックより、左にありかつ下にある、というケースも存在するからです。

Bricks 4 Collision Wrong

もう一つ対応できないケースがあります。ボールがすごい速さで動いていると、1フレームで対象の矩形よりも大きく移動して突き抜けてしまうケースです。これらのケースにも対応するには、現フレームから前フレームの位置へ直線を引いて、その直線と交差の判定をするなどする必要があります。しかし、今のゲームではボールの速度は十分遅いので突き抜ける可能性はかなり低いです。起こりそうもないことに対処してコードを複雑にするよりも、コードを書かかずにシンプルに保つことを選択します。ぶつかってきた方向の問題については起こりうる可能性がありますが、実際に起きてから対処することにします。

この方針で実際のコードを書いてみます。

void collision_entered(Brick& defender, Brick& attacker)
{
    if (is_ball(attacker)) {
        SDL_Rect prev_ball_rect;
        Vec2 move = scalar_multiplied(attacker.move_dir, attacker.move_scale);
        prev_ball_rect.x = round(attacker.rect.x - move.x);
        prev_ball_rect.y = round(attacker.rect.y - move.y);
        prev_ball_rect.w = attacker.rect.w;
        prev_ball_rect.h = attacker.rect.h;

        if (!overlaps_x(prev_ball_rect, defender.rect)) {
            attacker.move_dir.x *= -1;
        }

        if (!overlaps_y(prev_ball_rect, defender.rect)) {
            attacker.move_dir.y *= -1;
        }
    }
}

overlaps_x関数は、2つの矩形の縦方向は無視して横方向が交差しているときtureを返します。overlaps_y関数は、2つの矩形の横方向は無視して縦方向が交差しているときtureを返します。コードは以下のとおりです。

int left(const SDL_Rect& rect)   { return rect.x; }
int top(const SDL_Rect& rect)    { return rect.y; }
int right(const SDL_Rect& rect)  { return rect.x + rect.w - 1; }
int bottom(const SDL_Rect& rect) { return rect.y + rect.h - 1; }

bool overlaps_x(const SDL_Rect& a, const SDL_Rect& b)
{
    return !(left(a) > right(b) || right(a) < left(b));
}

bool overlaps_y(const SDL_Rect& a, const SDL_Rect& b)
{
    return !(top(a) > bottom(b) || bottom(a) < top(b));
}

left関数、top関数、right関数、bottom関数はそれぞれ矩形の左、上、右、下を返す補助関数です。

一応壁の跳ね返りはこれで出来たはずです。

パドルに当たったら跳ね返るようにする

パドルもやってしまいます。

void update(vector<Brick>& bricks)
{
    ...
    for (auto& brick : bricks) {
        if (brick.tag == "paddle") {
            brick.rect.x += move_scale * dx;
            resolve_collision(brick, bricks);
        } else if (brick.tag == "ball") {
            Vec2 move = scalar_multiplied(brick.move_dir, brick.move_scale);
            brick.rect.x = round(brick.rect.x + move.x);
            brick.rect.y = round(brick.rect.y + move.y);
        } else if (brick.tag == "brick") {
        } else if (brick.tag == "wall") {
            resolve_collision(brick, bricks);
        }
    }
}

update関数にresolve_collision関数の呼び出しを加えただけです。一度実行してみます。

ブロックは一旦消しておきました。うまく動いているようです。

ボールがブロックに当たったら跳ね返るようにする

パドルと同じようにupdate関数にresolve_collisiion関数を仕込むだけの簡単なお仕事です。

void update(vector<Brick>& bricks)
{
    ...
    for (auto& brick : bricks) {
        if (brick.tag == "paddle") {
          ...
        } else if (brick.tag == "ball") {
          ...
        } else if (brick.tag == "brick") {
            resolve_collision(brick, bricks);
        } else if (brick.tag == "wall") {
            ...
        }
    }
}

ここはこれで終わりです。

ボールがブロックに当たったらブロックが消えるようにする

ボールがブロックにぶつかったと分かるのはcollision_enter関数の中です。その中から何とかしてそのブロックは削除する必要がある、という情報を伝達しなければいけません。例えば次のように書けないか検討してみます。

void collision_entered(Brick& defender, Brick& attacker)
{
    if (is_ball(attacker)) {
        ...
    }

    if (defender.tag == "brick") {
        defender.dead = true;
    }
}

collision_enter関数に入ってきたということは衝突が起こっているので、もしぶつかられた方がブロックならそのブロックを消えるようにすればよいです。消えるべきブロックという情報はブロック自体に持たせてしまうのが簡単です。しかし、上のようにするためにはブロックに新たなフィールドを設けなければいけません。ブロックしか使わない情報を他のBrickであるパドルやボールや壁も保持することになりますのでできれば避けたいところではあります。しかし、ボールしか使わない方向ベクトルと移動の速さをBrickに持たせたのと同じように、ここでも使わない情報は利用側が無視すればよいという雑な考えを取り入れることにします。理想とは程遠いですが、実害は使用メモリが増えるということ程度に抑えられます。

Brick構造体にbool型のdeadフィールドを追加します。

struct Brick {
    string tag;
    SDL_Rect rect;
    SDL_Color color;
    Vec2 move_dir;
    double move_scale;
    bool dead;
};

update関数でdeadがセットされているブロックをゲーム全体のブロックを管理しているstd::vectorから取り除くようにします。

void update(vector<Brick>& bricks)
{
    ...
    for (auto& brick : bricks) {
        ...
    }

    kill_dead_bricks(bricks);
}

ブロックを取り除く実際の仕事をするのはkill_dead_bricks関数です。

bool is_dead(const Brick& b)
{
    return b.dead;
}
    
void kill_dead_bricks(vector<Brick>& bricks)
{
    auto p = remove_if(bricks.begin(), bricks.end(), is_dead);
    bricks.erase(p, bricks.end());
}

std::remove_ifもSTLアルゴリズムでalgorithmヘッダの中で定義されています。std::remove_ifと合わせて使うstd::vectorからの要素の削除には奇妙な癖があります。

std::remove_ifアルゴリズムはその名前とは裏腹に、実際には削除しないということです。述語であるis_dead関数がtrueを返さ「ない」要素をstd::vectorの前の方に集めていきます。返り値としてイテレータを返すのですが、そのイテレータは、述語がマッチしなかった要素、つまり削除すべきでは「ない」要素を前に集めたあとの状態の最後の要素の次の位置を指しています。

結果、std::remove_ifアルゴリズムが返したイテレータから、std::vector.end()が返すイテレータの位置までが不要な要素ということになります。このとき、この範囲に必ず本当に削除したい要素、つまりis_deadがtrueを返した要素が入っているわけではありませんので、削除したい要素が後ろに並んでいると当てにして何か処理を行うことは出来ません。実質std::remove_ifを適用した後にできることはstd::vector::eraseでコンテナから取り除くことくらいしかないのではないかと思います。

もう一つ注意したおきたいのは、update関数の全ブロックを走査しているforループの中でstd::vector::eraseを使って要素を取り除いてはいけないということです。eraseで要素を消去すると、それ以前にそのvectorから取得したイテレータや参照は無効になってしまいます。範囲for文での動作もまともになると期待しないほうがいいでしょう。

全部まとめる

ここで今までのコードを全部まとめたものを載せておきます。

#include <SDL2/SDL.h>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

const int ScreenWidth = 400;
const int ScreenHeight = 400;

struct Vec2 {
    double x;
    double y;
};

Vec2 normalized(const Vec2& v)
{
    double length = sqrt(v.x * v.x + v.y * v.y);
    return Vec2{v.x / length, v.y / length};
}

Vec2 scalar_multiplied(const Vec2 &v, double s)
{
    return Vec2{v.x * s, v.y * s};
}

struct Brick {
    string tag;
    SDL_Rect rect;
    SDL_Color color;
    Vec2 move_dir;
    double move_scale;
    bool dead;
};

bool is_ball(const Brick& b) { return b.tag == "ball"; }
bool is_dead(const Brick& b) { return b.dead; }

int left(const SDL_Rect& rect)   { return rect.x; }
int top(const SDL_Rect& rect)    { return rect.y; }
int right(const SDL_Rect& rect)  { return rect.x + rect.w - 1; }
int bottom(const SDL_Rect& rect) { return rect.y + rect.h - 1; }

bool overlaps_x(const SDL_Rect& a, const SDL_Rect& b)
{
    return !(left(a) > right(b) || right(a) < left(b));
}

bool overlaps_y(const SDL_Rect& a, const SDL_Rect& b)
{
    return !(top(a) > bottom(b) || bottom(a) < top(b));
}

void collision_entered(Brick& defender, Brick& attacker)
{
    if (is_ball(attacker)) {
        SDL_Rect prev_ball;
        Vec2 move = scalar_multiplied(attacker.move_dir, attacker.move_scale);
        prev_ball.x = round(attacker.rect.x - move.x);
        prev_ball.y = round(attacker.rect.y - move.y);
        prev_ball.w = attacker.rect.w;
        prev_ball.h = attacker.rect.h;

        if (!overlaps_x(prev_ball, defender.rect)) {
            attacker.move_dir.x *= -1;
        }

        if (!overlaps_y(prev_ball, defender.rect)) {
            attacker.move_dir.y *= -1;
        }
    }

    if (defender.tag == "brick") {
        defender.dead = true;
    }
}

void resolve_collision(Brick& self, vector<Brick>& bricks)
{
    auto ball = find_if(bricks.begin(), bricks.end(), is_ball);
    if (ball == bricks.end()) {
        return; // not found
    }

    if (SDL_HasIntersection(&self.rect, &ball->rect)) {
        collision_entered(self, *ball);
    }
}

void kill_dead_bricks(vector<Brick>& bricks)
{
    auto p = remove_if(bricks.begin(), bricks.end(), is_dead);
    bricks.erase(p, bricks.end());
}

void update(vector<Brick>& bricks)
{
    int dx = 0;
    auto key_state = SDL_GetKeyboardState(nullptr);
    if (key_state[SDL_SCANCODE_LEFT]) {
        dx -= 1;
    }
    if (key_state[SDL_SCANCODE_RIGHT]) {
        dx += 1;
    }

    for (auto& brick : bricks) {
        if (brick.tag == "paddle") {
            brick.rect.x += brick.move_scale * dx;
            resolve_collision(brick, bricks);
        } else if (brick.tag == "ball") {
            Vec2 move = scalar_multiplied(brick.move_dir, brick.move_scale);
            brick.rect.x = round(brick.rect.x + move.x);
            brick.rect.y = round(brick.rect.y + move.y);
        } else if (brick.tag == "brick") {
            resolve_collision(brick, bricks);
        } else if (brick.tag == "wall") {
            resolve_collision(brick, bricks);
        }
    }

    kill_dead_bricks(bricks);
}

void render(SDL_Renderer* renderer, const vector<Brick>& bricks)
{
    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
    SDL_RenderClear(renderer);

    for (auto& brick : bricks) {
        SDL_SetRenderDrawColor(renderer, brick.color.r, brick.color.g,
                               brick.color.b, brick.color.a);
        SDL_RenderFillRect(renderer, &brick.rect);
    }

    SDL_RenderPresent(renderer);
}

vector<Brick> setup_bricks()
{
    vector<Brick> bricks;

    // setup paddle
    Brick paddle;
    paddle.tag = "paddle";
    paddle.rect.w = 50;
    paddle.rect.h = 10;
    paddle.rect.x = ScreenWidth / 2 - paddle.rect.w / 2;
    paddle.rect.y = ScreenHeight - 2 * paddle.rect.h;
    paddle.color = {222, 222, 255, 255};
    paddle.move_dir = {};
    paddle.move_scale = 3.0;
    paddle.dead = false;
    bricks.push_back(paddle);

    // setup ball
    Brick ball;
    ball.tag = "ball";
    ball.rect.w = paddle.rect.h * 1.2;
    ball.rect.h = ball.rect.w; // square
    ball.rect.x = paddle.rect.x + (paddle.rect.w / 2) - (ball.rect.w / 2);
    ball.rect.y = paddle.rect.y - ball.rect.h;
    ball.color = {255, 128, 200, 255};
    ball.move_dir = normalized(Vec2{1.0, -1.0});
    ball.move_scale = 3.0;
    ball.dead = false;
    bricks.push_back(ball);

    // setup bricks
    const int space = 4;
    const int margin = 50;
    const int columns = 10;
    const int rows = 15;
    const int brick_width =
        (ScreenWidth - (2 * margin) - ((columns - 1) * space)) / columns;
    const int brick_height = brick_width * 0.3;
    for (int y = 0; y < rows; ++y) {
        for (int x = 0; x < columns; ++x) {
            Brick brick;
            brick.tag = "brick";
            brick.rect = {x * (brick_width + space) + margin,
                          y * (brick_height + space) + margin,
                          brick_width, brick_height};
            Uint8 r = (columns - x) * (255 / columns);
            Uint8 g = (y + 1) * (255 / rows);
            Uint8 b = (x + 1) * (255 / columns);
            brick.color = {r, g, b, 255};
            brick.move_dir = {};
            brick.move_scale = 0;
            brick.dead = false;
            bricks.push_back(brick);
        }
    }

    // setup walls
    const int v_wall_width = 10;
    const int v_wall_height = ScreenHeight;
    const int h_wall_width = ScreenWidth - 2 * v_wall_width;
    const int h_wall_height = v_wall_width;
    const SDL_Color wall_color = {99, 38, 255, 255};
    Brick left_wall = {
        "wall",
        {0, 0, v_wall_width, v_wall_height},
        wall_color, {}, 0, false
    };
    Brick right_wall = {
        "wall",
        {ScreenWidth - v_wall_width, 0, v_wall_width, v_wall_height},
        wall_color, {}, 0, false
    };
    Brick top_wall = {
        "wall",
        {v_wall_width, 0, h_wall_width, h_wall_height},
        wall_color, {}, 0, false
    };
    bricks.push_back(left_wall);
    bricks.push_back(right_wall);
    bricks.push_back(top_wall);

    return bricks;
}

int main()
{
    if (SDL_Init(SDL_INIT_VIDEO) != 0) {
        SDL_Log("SDL_Init() error: %s", SDL_GetError());
        exit(1);
    }

    SDL_Window* window =
        SDL_CreateWindow("Bricks SDL Version 4", 0, 0, ScreenWidth, ScreenHeight, 0);
    if (window == nullptr) {
        SDL_Log("SDL_CreateWindow() error: %s", SDL_GetError());
        exit(1);
    }

    SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
    if (renderer == nullptr) {
        SDL_Log("SDL_CreateRenderer() error: %s", SDL_GetError());
        exit(1);
    }

    vector<Brick> bricks = setup_bricks();

    bool running = true;
    bool paused = true;
    while (running) {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = false;
            } else if (event.type == SDL_KEYDOWN) {
                switch (event.key.keysym.sym) {
                case SDLK_ESCAPE:
                case SDLK_RETURN:
                    running = false;
                    break;
                case SDLK_SPACE:
                    paused = !paused;
                    break;
                default:
                    break;
                }
            }
        }

        if (!paused) {
            update(bricks);
        }
        render(renderer, bricks);

        SDL_Delay(16);
    }

    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
}

実行してみます。

割とゲームっぽくなってきました。

次回は

まだまだ改良の余地はありますが、正直ブロック崩しに飽きてきました…しばらくブロック崩しから離れたいところです。何か別の題材を扱うかもしれませんしもう一回くらいブロック崩しを扱うかもしれません。