ブロック崩しを作る (5)
前回から1ヶ月程間が空いてしまいました。
次に何をするか、今2つの選択肢があります。
- ブック崩しを作り込んでいく。
- 新しいゲームを作り始める。
前回で一応ゲームの形にはなりました。ここまでは大した苦労はありませんでした。問題は今のままでは全く面白くないことです。ここまでは言ってみればまだほんの序の口で、サウンドやエフェクトを加えたり、ルールを改良していくことで遊べるゲームにまで作り込んでいくところからが本番と言えます。とりあえずゲームっぽく見えるものから、プレイして面白いと思えるゲームを完成させるまでの道のりは長いですが、一つのゲームを徹底的に作り込んでいくことで得られる経験と知識は、上辺だけを触って次々と乗り換えていくよりもきっと大きいです。
もともとブロック崩しを作り始めた目的は、ゲームを題材としてC++の練習をすることでした。このままブロック崩しを作り込んでいくことでもさらにC++の訓練にならないわけではないです。しかし、プログラムが大きくなっていくと、扱う問題が複雑になりそれに伴いコードも大きく複雑になっていき、それに従い目に見える進捗も緩やかになっていきます。そうなると厄介なのはモチベーションを維持するような努力が必要になることです。
残念ながら、現時点ではあまりブロック崩しを作り込んで完成に持っていきたいという欲求はあまりありません。まっさらな環境で新しいゲームを作り直したいという気持ちのほうが強いです。ただ、もう少しだけ作っておきたい部分もあります。
- フレームレート制御
- ボールを四角ではなく円にする
- コードの見直し
フレームレートの制御というのは、ゲームのFPS (Frame Per Second)を管理することです。多くのゲームは大体1秒あたり60フレームとか30フレームとか更新するように作られています。今の段階でどうなっているかと言うと、単に1フレームごとに16ミリ秒の遅延を入れて、60FPSっぽくさせています。16ミリ秒というのは1/60秒を元にしています。
while (running)
{
...
update(...);
render(...);
SDL_Delay(16); // 16ミリ秒休む
}
手抜きもいいところで、1フレームにかかる処理が常に一定でもなければ(そのようなことはありそうにないです)、毎フレーム違った時間で更新されることになるでしょう。updateやrenderにかかった時間を計測して、1フレーム16ミリ秒になるか、全体として1秒間に60フレーム回るようにするか調整を行うべきです。しかし、幸か不幸か、今のブロック崩しでは目に見えて表示がおかしくなったり画面がちらついたりすることもなく、スムーズに動いているように見えます。モチベーションの問題で、うまくいっているように見えることに手を入れるのはあまり乗り気になれません。なので次のゲームに持ち越すことにします。
ボールを四角でなく円にしたいのは、SDLに円を描画するAPIが用意されていないため、自力で円を描画するアルゴリズムを書かなければならず、なかなかおもしろそうな課題だと思えるからです。別の理由としては円を描画することくらいできるようになっておきたいというのもあります。今どきのPCのプラットフォームで自前でベタに円を描くコードを書くことはないかもしれません。しかし、円を書くというごく基礎的なこともできないのはかっこ悪いし、グラフィックスのプログラミングを学習する上で支障が出る恐れもあります。円だけに限らず、三角形を描くとか曲線を描くとか正十二角形を描くとかも考えると2Dグラフィックスにも終わりがありません。それらはまた必要になったとき覚えればいいとして、今、円を描くのはちょうど必要になったところなのでやっておきたいところです。
コードの見直しについては、見落としてなければ記憶しておいたことが2点あります。
- ボール、パドル、ブロック、壁を一つの構造体で表現しているため、無理がきていること。例えばブロックと壁は移動しないので速さなどを持つ必要がないのに速さを持ってしまっている。
- 衝突判定で、ボールが全てのオブジェクトを格納しているvectorの最初の方(2番目)にあるとを仮定してボールを検索しているところ。初期化の順番によって衝突判定の効率が落ちてしまうのを直していおきたい。
とりわけ1つ目の構造体の方を直しておきたいところです。そのためにはプログラムの構成を大きく変える必要があります。単にコードが良くないから直したいという以上に、C++のクラスというものを導入しておきたいという理由があります。
クラスを導入する
今、直したいのはボール、パドル、ブロック、壁を一つの構造体で表現しているところです。それらは次のようになっていました。
struct Brick {
string tag;
SDL_Rect rect;
SDL_Color color;
Vec2 move_dir;
double move_scale;
bool dead;
};
移動の速さや方向が必要になるのはボールとパドルで、壁やブロックには必要ないので明らかに無駄です。無理やり一つの構造体で表現しようとしているので無理がきています。
それぞれを別々の構造体として表現できないでしょうか。つまり、次のように書くことは出来ないでしょうか。
struct Paddle { ... };
struct Ball { ... };
struct Brick { ... };
struct Wall { ... };
悪くないのですが、大きな問題があります。それぞれの構造体の関係性がまったく記述されていないので、コード上で何らかの類似性を持ったものであるということを表現できないことです。何のことかと言うとstd::vectorの要素の型として使えないということです。
std::vector<ここは何?> bricks;
std::vectorの要素の型はPaddleかBallかBrickかWallであると表現する何かを記述する必要があります。共用体やstd::variantと使うことも可能ですが今回はあまり適切ではないので止めておきます。
別の方法として、もっと広く使われている方法で、上記の構造体の型に階層関係を持ち込む手段を取ることにします。階層関係とはどういうことかというと、イメージとしては次のようなものです。
これはGodotという個人的にお気に入りのゲームエンジンのスクリーンショットです。シーンは一つのルートノードを持ち、ゲームにおける全てのものはその子ノードとして存在することになります。各ノードにはさまざまな型になることができます。C++の型とは関係ありません。Godotの型と、今扱おうとしているC++の型であるPaddleやBallなどの型は別物で、Godotのノードの型はあらかじめ用意されているSpriteとかAnimationPlayerとか根本的な性質を持った型で、それらのノードを組み合わせてより具体的なゲームの要素を構成するものを組み立てるようになっています。きっちりこの通りに作り込むつもりは全くないのですがイメージとしてはこのように表現できればよい感じになるのではないかと思います。
型に階層関係を持ち込む
パドルとボールとブロックと壁から抽出できる階層的な関係というのはあるでしょうか?あるいは、共通の部分はあるでしょうか?
先に注意しておきたいのは、階層という概念について、上記のGodotで表現されるゲームのノードの階層と今やろうとしているC++の型の階層は別物だということです。ノードの階層関係はデータ構造ともいえるもので、より大きな枠組みで考えられるものです。C++の型の階層は言語に備わっている機能を直接利用して、型そのものに階層関係を持ち込むことでC++言語が関連を認識できるようにするものです。今話しているのはC++の型の階層のことです。
無理やり関係性をもたせるために、ゲーム内に存在する何かを表すものとしてノードとして表すことにします。かなり乱暴なのですが、ゲーム内に存在するすべてのものをノードという型の階層から派生するものとしてにしまいます。C++では型の派生というものを直接表現できます。
struct Node { ... };
struct Paddle : Node { ... };
struct Ball : Node { ... };
struct Brick : Node { ... };
struct Wall : Node { ... };
このようにすることで、Paddle、Ball、Brick、WallはそれぞれNodeの派生クラスとなります。派生クラスの親であるクラスは基底クラス(Base class)と呼ばれます。この仕組みは継承と呼ばれます。
このNodeクラスを使うとstd::vector<Brick>としていたところは、
std::vector<Node*> nodes;
とすることができるようになります。nodesにはNodeの派生クラスのオブジェクトのポインタを保持させることが出来ます。
オブジェクトそのものを保持させてはいけない点は重要です。オブジェクトそのものを保持させると、派生クラスに追加された基底クラスには含まれないフィールド(メンバ変数と呼ばれます)が削ぎ落とされてしまうスライシングという現象が起こります。
std::vector<Node*>をセットアップするコードは次のように書けるでしょう。
std::vector<Node*> nodes;
auto paddle = new Paddle;
// ...setup paddle
auto ball = new Ball;
// ...setup ball
auto brick1 = new Brick;
auto brick2 = new Brick;
auto brick3 = new Brick;
// ...more bricks and setup bricks
auto top_wall = new Wall;
auto left_wall = new Wall;
auto right_wall = new Wall;
// ...setup walls
nodes.push_back(paddle);
nodes.push_back(ball);
nodes.push_back(brick1);
nodes.push_back(brick2);
nodes.push_back(brick3);
nodes.push_back(top_wall);
nodes.push_back(left_wall);
nodes.push_back(right_wall);
そしてupdate関数とrender関数を次のようにしてみます。
void update_node(Node* node);
void render_node(SDL_Renderer* renderer, Node* node);
void update(vector<Node*>& nodes)
{
for (auto n : nodes) {
update_node(n);
}
}
void render(SDL_Renderer* renderer, const vector<Node*>& nodes)
{
for (const auto n : nodes) {
render_node(renderer, n);
}
}
update_node関数は渡されたノードの本当の型、パドルなのかボールなのかブロックなのか壁なのかを判断して適切に処理を振り分ける必要があります。まず以前のようにtagというフィールドを調べてできないかを検討してみます。
void update_node(Node* node)
{
if (node->tag == "paddle") {
// paddleの処理...
} else if (node->tag == "ball") {
// ballの処理...
} else if (node->tag == "brick") {
// ブロックの処理...
} else if (node->tag == "wall") {
// 壁の処理...
}
}
なんとかなりそうな気もしますが厄介な点があります。それは、update_node関数の引数であるNodeのポインタを通しては、PaddleやBallなど派生クラス固有のフィールド(メンバ変数)にアクセスできないということです。どういうことかと言うと、今Nodeの階層が次のようになっていたとします。
struct Node {
string tag;
SDL_Rect rect;
SDL_Color color;
};
struct Paddle : Node {
Vec2 move_dir;
double move_scale;
const Uint8* key_state;
};
struct Ball : Node {
Vec2 move_dir;
double move_scale;
};
struct Brick : Node {
bool dead;
};
struct Wall : Node {};
そして、ポインタを通してアクセスするのを実験してみます。
Node* ball = new Ball;
ball->tag = "ball";
ball->move_scale = 3.0;
これをコンパイルするとエラーになります。
scratch.cpp:100:11: error: ‘struct Node’ has no member named ‘move_scale’
ball->move_scale = 3.0;
^~~~~~~~~~
実際に指しているオブジェクトの型がBallであっても、Nodeのポインタからはそのオブジェクトの型がBallであるとコンパイル時には判断できないため、Nodeにはmove_scaleのようなメンバ変数はないとエラーになってしまいます。いくつかの動的な型付けの仕組みを採用している言語ではこのような制限はなく、実行時にそのメンバにアクセスできればそれはそれでOKだと解決されます。
この制限は厳しすぎるようにも思えますが、存在しないメンバにアクセスしてしまうありがちなというプログラミングの間違いをコンパイル時にエラーとして検出ができるというメリットともとれます。
しかし、今は何とかしてNodeのポインタを通して派生クラスのメンバ変数にアクセスしたいところです。一つの手段としてキャストしてしまうことが考えられるかもしれません。キャストとはあるオブジェクトの型を無理やり別の型のように解釈するようにすることです。
void use_node_as_ball(Node* node)
{
if (node->tag == "ball") {
Ball* ball = static_cast<Ball*>(node);
ball->move_scale = 3.0;
}
}
キャストにはいくつか種類があります。特にオブジェクトの階層関係にあるかどうかを判断してキャストを行うdynamic_castというのがあって、より適しているかもしれませんが、諸事上により使えません。というのはdymanic_castを使うためには変換元になるオブジェクトがポリモーフィックでなければならないからです。
上のコードはコンパイルは通り、ボールの速さに3.0という値が設定されます。ボールを参照するところを全てキャストを行うようにすれば一応動作するものにはなるでしょう。しかし、NodeからBallを参照する場所全てについてキャストをするというのは、書くまでもなく見苦しいコードになり、プログラミングの間違いも入り込みやすくなりそうです。したがって別の手段を取ることにします。
メンバ関数を使う
update_node関数(とrender_node関数)が使いにくいのは、引数であるNodeのポインタについて、実際にそのポインタが指すオブジェクトの型がNodeに関連するものであるというところまでしか知らないからです。Node及びその派生クラスとupdate_node関数は無関係に定義されたものであり、引数がNodeのポインタである、という以外に何の情報も共有していないのでお互いに何も知らないでいることはごく自然なことです。
もし、Node及びその派生クラスとupdate_node関数とをもっと強い関係で結びつけることができれば、update_node関数の中で今実行しているのはBallの文脈だ、とかブロックの文脈だ、とか判断できるようになるかもしれません。C++では構造体に関数をもたせることが出来ます。関数を持つ構造体はもう構造体という呼び名は適切ではなく、クラスと呼ばれます。クラスに結び付けられた関数はメンバ関数と呼ばれます。
構造体としていたNodeに、メンバ関数を持たせたクラスとして書き換える試みとしてまず次のようにしてみます。
class Node {
public:
Node(const string& tag, const SDL_Rect& rect, const SDL_Color& color);
void update();
void render(SDL_Renderer* renderer);
string tag() const;
SDL_Rect rect() const;
SDL_Color color() const;
private:
string tag_;
SDL_Rect rect_;
SDL_Color color_;
};
class Paddle : public Node {};
class Ball : public Node {};
class Brick : public Node {};
上のクラスを使うとupdate関数とrender関数は次のように書けます。
void update(vector<Node*>& nodes)
{
for (auto n : nodes) {
n->update();
}
}
void render(SDL_Renderer* renderer, const vector<Node*>& nodes)
{
for (const auto n : nodes) {
n->render(renderer);
}
}
structというキーワードの代わりにclassを使いました。この違いは決定的な違いというわけではありません。structもやはりクラスを表現するのに使うことができ、classを使った場合との違いは、メンバのアクセス制限がデフォルトでpublicになること、継承のアクセス制限がデフォルトでpublicになることのみです。必ずこの場合はstructを使い、ある場合はclassを使わなければいかないという場面はありません。よくある使い分けの方針としては、メンバ変数を集めただけの単純な型の場合はstruct、それ以外はclassを使うというものです。
アクセス指定子
アクセス指定子はpublicとprivateの他にもう一つprotectedというのがあります。メンバに対する指定と、継承に対する指定で意味はだいたい同じなのですが、継承に対する指定は若干分かりにくいです。
メンバに対するアクセス指定子による効果は次のようになっています。
- public
- どこからでもアクセスできる。
- private
- そのクラス定義の中からのみアクセスできる。主にメンバ関数の中からのみアクセスできる。
- protected
- そのクラス定義の中からと、派生クラスからアクセスできる。
継承に対するアクセス指定の意味は次のとおりです。
- public
- 派生クラスは、基底クラスのpublicメンバをpublicとして継承する。
- private
- 派生クラスは、基底クラスのすべてのメンバをprivateとして継承する。基底クラスのポインタや参照を通して派生クラスにアクセスすることはできなくなる。そもそも基底クラスのポインタや参照に派生クラスを代入することもできなくなる。
- protected
- 派生クラスは、基底クラスのpublicメンバをprotectedとして継承する。基底クラスのポインタや参照を通して派生クラスにアクセスすることはできなくなる。そもそも基底クラスのポインタや参照に派生クラスを代入することもできなくなる。
もしボールがprivateやprotectedで派生した場合、Nodeのポインタに代入できなくなります。
class Ball : private Node { ... };
Node* n = new Ball;
これは次のようなエラーになります。
scratch.cpp:10:22: error: ‘Node’ is an inaccessible base of ‘Ball’
Node* ball = new Ball;
^~~~
privateやprotectedの継承のアクセス指定はかなり分かりづらいところです。当面private継承とprotected継承は使わないので今はこれ以上は掘り下げないことにします。
public継承はもっとも直感的なものです。基底クラスでprivateであるメンバは派生クラスでもprivateで、protecedはprotectedのままで、publicはpublicのままです。
コンストラクタ
クラス名と同じ名前のメンバ関数は特別な関数で、コンストラクタと呼ばれます。コンストラクタには戻り値がないため、書かなくてOKです。というより書くことは出来ません。通常の関数のように好きなだけ引数をとることができます。
コンストラクタの本体の定義は普通の関数とは変わった書き方をします。
Node::Node(const string& tag, const SDL_Rect& rect, const SDL_Color& color)
: tag_{tag}, rect_{rect} , color_{color}
{}
Node::の部分は、コンストラクタに限らず、メンバ関数の定義をクラスボディの外で定義するときに必要になります。それよりも後ろの関数のシグネチャの「:」に続く部分がコンストラクタの定義の特徴です。これらはメンバの初期化指定子と呼ばれ、ここでできるだけメンバ変数の初期化を行うようにします。絶対というわけではありません。関数の本体のところで初期化することも可能です。
Node::Node(const string& tag, const SDL_Rect& rect, const SDL_Color& color)
{
tag_ = tag;
rect_ = rect;
color_ = color;
}
しかし、本体で定義するとメンバ変数の型によっては、ここではstringがそうなのですが、無駄に2回初期化と代入がされることになることになり、無意味に非効率的になるので、なるべく初期化指定子のところで初期化するのが良いとされています。
仮想関数
Nodeとその階層関係を書いたのですが、問題は解決したのでしょうか?問題とは、Nodeへのポインタを通してでは、派生クラスでその派生クラスが追加したメンバ変数にアクセすることが出来ないということでした。Nodeクラスにupdateメンバ関数とrenderメンバ関数を追加しました。もしこの中で派生クラスがその派生クラス自身の追加のメンバ変数にアクセスすることができれば問題は解決ということになります。
Ballの定義を変更して、Node::update関数を定義してみます。
class Ball : public Node {
public:
Ball(const string& tag, const SDL_Rect& rect, const SDL_Color& color);
private:
Vec2 move_dir_;
double move_scale_;
};
Ball::Ball(const string& tag, const SDL_Rect& rect, const SDL_Color& color)
: Node{tag, rect, color}
{}
void Node::update()
{
if (this->tag_ == "ball") {
this->move_scale_ = 3.0; // ダメ!
}
}
しかし、やはりNode::update関数の中ではBallのメンバ変数を使うことは出来ません。コンパイルするとエラーになります。
scratch.cpp:35:15: error: ‘class Node’ has no member named ‘move_scale_’
this->move_scale_ = 3.0; // ダメ!
^~~~~~~~~~~
thisというのはthisポインタと呼ばれ、メンバ関数の中で使えます。thisはそのメンバ関数を呼び出したオブジェクト自身を指していて、ここではNodeのポインタになります。つまり型はやはりNodeのポインタであるので、BallのポインタではないのでBallのメンバ変数のことは何も知らないのです。
thisポインタは省略することが出来ます。実際ほとんどの場合thisは省略されます。明示的にthis->xのように書くのはどうしても必要なときだけになると思います。上の場合、
void Node::update()
{
if (tag_ == "ball") {
// move_scale_ = 3.0; // ダメ!
}
}
と書いても同じことです。もし、Ballクラスにupdateメンバ関数を追加したらどうなるでしょう?
class Ball {
// ...
void update();
// ...
};
void Ball::update()
{
move_scale_ = 3.0;
}
これはコンパイルをパスします。ですが、もう一つ問題なのは、Nodeのポインタを介してこのupdateメンバ関数を呼び出したときに、適切にこの中でBall::update関数が実行されるのかどうかということです。つまり以下のように書くことができるかどうかということです。
void Ball::update()
{
move_scale_ = 3.0;
cout << "Ball::update()\n";
}
Node* node = new Ball{"ball", {0, 0, 0, 0}, {0, 0, 0, 0}};
node->update(); // Ball::update()が実行されるか?
残念ながらBall::update関数は実行されません。Nodeのポインタを介してアクセスして呼ばれるのはNode::update関数になってしまいます。それに名前の隠蔽という微妙な問題を持ち込んでしまうので、派生クラスで基底クラスと同じ名前の関数を定義するのは避けたほうが良いです。
考え方は悪くないように思えるのですが、期待したとおりの結果を得るにはもう一つC++の言語の支援が必要になります。それは仮想関数と呼ばれる仕組みです。
仮想関数を使えばnode->update()としたとき、Node型のポインタであるnodeが実際に指している派生クラスの型によって、その派生クラスで仮想関数が再定義されていればその関数を呼び出してくれるようになります。具体的には派生クラスで再定義されることを許可するメンバ関数の宣言にvirtualキーワードを付けます。そして、派生クラスで基底クラスのvirtualと宣言されたメンバ関数と全く同じシグネチャの関数を宣言します。
class Node {
public:
...
virtual void update();
virtual void render(SDL_Renderer* renderer);
...
};
class Ball {
public:
...
void update() override;
void render(SDL_Renderer* renderer) override;
...
};
Node* node = new Ball{"ball", {0, 0, 0, 0}, {0, 0, 0, 0}};
node->update(); // Ball::update()が実行される!
この仕組みはオーバーライドと呼ばれます。派生クラスの方のoverrideキーワードの指定は必須ではありません。しかし、基底クラスでvirtualと宣言されていないメンバ関数にoverrideを付けるとエラーになるため、誤ってオーバーライドするつもりで名前を隠蔽してしまうというプログラミングの間違いを防げるので、付けるようにしたほうがいいと思います。
仮想デストラクタ
仮想関数を使えるようになったのでもう終わりに近いのですが、あと一つ絶対知っておかないといけないことがあります。クラスのオブジェクトを初期化するコンストラクタがありますが、その反対のオブジェクトが破棄される時に呼び出されるデストラクタというものが存在します。デストラクタの存在はおまけ程度というようなものではなく、C++ではコンストラクタと同じくらい言語の根幹をなす重要な機能です。デストラクタでの主な仕事は、そのクラスオブジェクトが所有しているリソースの解放です。リソースとは例えばnewしたオブジェクトや、メモリ、何かのハンドルなどです。
今作っているブロック崩しでは、Nodeとその派生クラスはstringをメンバ変数に持っていますが、stringはポインタではなくオブジェクトそのものを保有しているので、Nodeが破棄されるときstringも自動で破棄されます。SDL_RectやSDL_Colorは単純な構造体(POD; Plain Old Data)で各フィールドにはポインタは含まれないので特別な処理は必要ありません。したがって、Nodeにはデストラクタに追加の処理は不要となります。
しかしデストラクタを書かなくていいということにはなりません。Nodeのポインタによって管理されるパドル、ボール、ブロック、壁は、newを使って生成されるためおそらくプログラムのどこかでdeleteされることになります。Nodeのポインタを通してdeleteされるので、おそらく次のようなコードになるでしょう。
Node* node = new Ball{"ball", {0, 0, 0, 0}, {0, 0, 0, 0}};
// どこか別の場所で
delete node;
こうしたとき、updateメンバ関数をvirutalにしなかったときと同じ問題が起こります。つまり、Ballのデストラクタは実行されずNodeのデストラクタのみが実行されます。というだけではなく、このように基底クラスのポインタを介して派生クラスのデストラクタが呼ばれるとき、そのデストラクタがvirtualでないときは何起こるのか分かりません。未定義の動作(undefined behavior)となっています。
Nodeクラスのデストラクタをvirtualとすることで、まるで魔法のようにうまく動作します。試しに実験コードを書いてみます
#include <iostream>
using std::cout;
class Node {
public:
Node() { cout << "Node()\n"; }
virtual ~Node() { cout << "~Node()\n"; }
};
class Ball : public Node {
public:
Ball() { cout << "Ball()\n"; }
~Ball() { cout << "~Ball()\n"; }
};
int main()
{
Node* node = new Ball;
delete node;
}
デストラクタの宣言はクラス名の前に「~」をつけた~Node()のように書きます。コンストラクタはシグネチャが異なれば一つのクラスに複数書くことが出来ますが、デストラクタは一つしか書けません。引数はありません。
実行すると次のように出力されます。
Node()
Ball()
~Ball()
~Node()
出力結果を見ると、コンストラクタは基底クラスから呼ばれ、デストラクタはその逆順に派生クラスから呼ばれるのではないかと想定されます。これはまさに望んでいたものです。
~Ball()にoverrideキーワードを付けることも出来ます。そうすると基底クラスであるNodeでデストラクタがvirtualでなかったときエラーに出来ます。しかし、仮想デストラクタはメンバ関数のオーバーライドとはかなり動作が異なります。普通の、メンバ関数のオーバーライドであれば、オーバーライドされた基底クラスのメンバ関数までは呼ばれないはずです。しかし、virtualなデストラクタの場合は、まず派生クラスのデストラクタが呼ばれて、それに続けて基底クラスのデストラクタも呼ばれるようになっています。デストラクタにoverrideをつけるのがよいのかどうかは微妙なところではないかと思います。
コードを書き直す
ようやく必要な機能が出揃ったので、このクラスと仮想関数の仕組みを使って今までのコードを書き直してみます。
#include <SDL2/SDL.h>
#include <vector>
#include <string>
#include <algorithm>
using std::vector;
using std::string;
using std::find_if;
using std::remove_if;
using std::copy_if;
const int ScreenWidth = 400;
const int ScreenHeight = 400;
class Node;
void resolve_collision(Node* node, vector<Node*>& nodes);
bool overlaps_x(const SDL_Rect& a, const SDL_Rect& b);
bool overlaps_y(const SDL_Rect& a, const SDL_Rect& b);
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};
}
class Node {
public:
enum State { Active, Dead };
Node(const string& tag, const SDL_Rect& rect, const SDL_Color& color);
virtual ~Node();
virtual void update(vector<Node*>& nodes);
virtual void render(SDL_Renderer* renderer);
virtual void on_collision_enter(Node* other);
string tag() const { return tag_; }
SDL_Rect rect() const { return rect_; }
SDL_Color color() const { return color_; }
State state() const { return state_; }
void set_rect(const SDL_Rect& rect) { rect_ = rect; }
void set_state(State state) { state_ = state; }
private:
string tag_;
SDL_Rect rect_;
SDL_Color color_;
State state_;
};
Node::Node(const string& tag, const SDL_Rect& rect, const SDL_Color& color)
: tag_{tag}, rect_{rect}, color_{color}, state_{State::Active}
{}
Node::~Node() {}
void Node::update(vector<Node*>& nodes) {}
void Node::render(SDL_Renderer* renderer)
{
SDL_SetRenderDrawColor(renderer, color_.r, color_.g, color_.b, color_.a);
SDL_RenderFillRect(renderer, &rect_);
}
void Node::on_collision_enter(Node* other) {}
class Paddle : public Node {
public:
Paddle(const string& tag, const SDL_Rect& rect, const SDL_Color& color);
~Paddle();
void update(vector<Node*>& nodes) override;
void set_move_scale(double move_scale) { move_scale_ = move_scale; }
private:
double move_scale_;
const Uint8* key_state_;
};
Paddle::Paddle(const string& tag, const SDL_Rect& rect, const SDL_Color& color)
: Node{tag, rect, color}
, move_scale_{0.0}
, key_state_{SDL_GetKeyboardState(nullptr)}
{}
Paddle::~Paddle() {}
void Paddle::update(vector<Node*>& nodes)
{
double dx = 0.0;
if (key_state_[SDL_SCANCODE_LEFT]) {
dx -= 1.0;
}
if (key_state_[SDL_SCANCODE_RIGHT]) {
dx += 1.0;
}
SDL_Rect r = rect();
r.x += move_scale_ * dx;
set_rect(r);
resolve_collision(this, nodes);
}
class Ball : public Node {
public:
Ball(const string& tag, const SDL_Rect& rect, const SDL_Color& color);
~Ball();
void update(vector<Node*>& nodes) override;
void on_collision_enter(Node* other) override;
Vec2 move_dir() const { return move_dir_; }
void set_move_dir(const Vec2& move_dir) { move_dir_ = normalized(move_dir); }
void set_move_scale(double move_scale) { move_scale_ = move_scale; }
private:
Vec2 move_dir_;
double move_scale_;
};
Ball::Ball(const string& tag, const SDL_Rect& rect, const SDL_Color& color)
: Node{tag, rect, color}
, move_dir_{0.0}
, move_scale_{0.0}
{}
Ball::~Ball() {}
void Ball::update(vector<Node*>& nodes)
{
Vec2 move = scalar_multiplied(move_dir_, move_scale_);
SDL_Rect r = rect();
r.x = round(rect().x + move.x);
r.y = round(rect().y + move.y);
set_rect(r);
}
void Ball::on_collision_enter(Node* other)
{
SDL_Rect prev;
Vec2 move = scalar_multiplied(move_dir_, move_scale_);
prev.x = round(rect().x - move.x);
prev.y = round(rect().y - move.y);
prev.w = rect().w;
prev.h = rect().h;
if (!overlaps_x(prev, other->rect())) {
move_dir_.x *= -1;
}
if (!overlaps_y(prev, other->rect())) {
move_dir_.y *= -1;
}
}
class Brick : public Node {
public:
Brick(const string& tag, const SDL_Rect& rect, const SDL_Color& color);
~Brick();
void update(vector<Node*>& nodes) override;
void on_collision_enter(Node* other) override;
};
Brick::Brick(const string& tag, const SDL_Rect& rect, const SDL_Color& color)
: Node{tag, rect, color}
{}
Brick::~Brick() {}
void Brick::update(vector<Node*>& nodes)
{
resolve_collision(this, nodes);
}
void Brick::on_collision_enter(Node* other)
{
set_state(Node::State::Dead);
}
class Wall : public Node {
public:
Wall(const string& tag, const SDL_Rect& rect, const SDL_Color& color);
~Wall();
void update(vector<Node*>& nodes) override;
};
Wall::Wall(const string& tag, const SDL_Rect& rect, const SDL_Color& color)
: Node{tag, rect, color}
{}
Wall::~Wall() {}
void Wall::update(vector<Node*>& nodes)
{
resolve_collision(this, nodes);
}
bool is_ball(const Node* node) { return node->tag() == "ball"; }
bool is_dead(const Node* node) { return node->state() == Node::State::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(Node* a, Node* b)
{
a->on_collision_enter(b);
b->on_collision_enter(a);
}
void resolve_collision(Node* self, vector<Node*>& nodes)
{
auto ball = find_if(nodes.begin(), nodes.end(), is_ball);
if (ball == nodes.end()) {
return; // not found
}
SDL_Rect rect1 = self->rect();
SDL_Rect rect2 = (*ball)->rect();
if (SDL_HasIntersection(&rect1, &rect2)) {
collision_entered(self, *ball);
}
}
void kill_dead_nodes(vector<Node*>& nodes)
{
vector<Node*> dead_nodes;
copy_if(nodes.begin(), nodes.end(), back_inserter(dead_nodes), is_dead);
auto p = remove_if(nodes.begin(), nodes.end(), is_dead);
nodes.erase(p, nodes.end());
for (auto d : dead_nodes) {
delete d;
}
}
void update(vector<Node*>& nodes)
{
for (auto n : nodes) {
n->update(nodes);
}
kill_dead_nodes(nodes);
}
void render(SDL_Renderer* renderer, vector<Node*>& nodes)
{
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
for (auto n : nodes) {
n->render(renderer);
}
SDL_RenderPresent(renderer);
}
vector<Node*> setup_nodes()
{
vector<Node*> nodes;
// temporary vars
int w, h, x, y;
SDL_Rect rect;
SDL_Color color;
// setup paddle
w = 50;
h = 10;
x = ScreenWidth / 2 - w / 2; // horizontal center of screen
y = ScreenHeight - 2 * h; // 1 unit above from bottom of screen
rect = { x, y, w, h };
color = { 222, 222, 255, 255 };
auto paddle = new Paddle{"paddle", rect, color};
paddle->set_move_scale(3.0);
nodes.push_back(paddle);
// setup ball
w = 1.2 * paddle->rect().h;
h = w; // square
x = paddle->rect().x + (paddle->rect().w / 2) - (w / 2); // center of paddle
y = paddle->rect().y - h; // upon paddle
rect = { x, y, w, h };
color = { 255, 128, 200, 255 };
auto ball = new Ball{"ball", rect, color};
ball->set_move_dir(Vec2{1.0, -1.0});
ball->set_move_scale(3.0);
nodes.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) {
rect.x = x * (brick_width + space) + margin;
rect.y = y * (brick_height + space) + margin;
rect.w = brick_width;
rect.h = brick_height;
color.r = (columns - x) * (255 / columns);
color.g = (y + 1) * (255 / rows);
color.b = (x + 1) * (255 / columns);
color.a = 255;
auto brick = new Brick{"brick", rect, color};
nodes.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;
color = {99, 38, 255, 255};
rect = {v_wall_width, 0, h_wall_width, h_wall_height};
auto top_wall = new Wall{"wall", rect, color};
rect = {0, 0, v_wall_width, v_wall_height};
auto left_wall = new Wall{"wall", rect, color};
rect = {ScreenWidth - v_wall_width, 0, v_wall_width, v_wall_height};
auto right_wall = new Wall{"wall", rect, color};
nodes.push_back(left_wall);
nodes.push_back(right_wall);
nodes.push_back(top_wall);
return nodes;
}
void main_loop(SDL_Renderer* renderer, vector<Node*>& nodes)
{
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(nodes);
}
render(renderer, nodes);
SDL_Delay(16);
}
}
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 5", 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<Node*> nodes = setup_nodes();
main_loop(renderer, nodes);
for (auto n : nodes) {
delete n;
}
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
}
だいぶ長くなってしまいました。これだけ長くなるとファイルを複数に分割した方がよいかもしれません。クラス一つにつきファイル一つという流派もあるくらいです。
衝突判定は素直に書くことが出来ず、ややトリッキーな、あまり直感的ではない作りになっています。
ちょっと気がかりな、気に入らないところは、最基底(一番上)のクラスであるNodeにStateという型のメンバ変数をもたせているところです。ボールが当たったブロックを消すために、ブロックのみにボールが当たったという目印としてdeadという状態をもたせて判定させたかったのですが、Brickクラスにis_deadのような関数を持たせると、Nodeのポインタを通してis_deadを呼び出すことができなくなってしまいました。具体的には、以下のように書くことができなくなってしまいました。
bool is_dead(const Node* node) { return node->is_dead(); }
auto p = remove_if(nodes.begin(), nodes.end(), is_dead);
簡単に解決する方法が見つかりませんでした。やむなく、NodeクラスにDead状態かどうかを判定する機能をもたせることにしました。ブロック以外使わないのでできれば直したいところですが、ゲームの状態を管理するための基盤となるクラスを作り込む必要がありそうです。クラスを導入するという目的はすでに達成したのでここまでにしておきます。
目的は達成しましたが、このバージョンでは最初の方で貼り付けたGodotのスクリーンショットのノードのような階層構造になっていません。vector<Node*>によるフラットな構造になっています。もともとGodotのような階層構造を目標にしていたのでチャレンジしてみることにします。これはおまけとして別ページ貼り付けておきます。
これにて一旦終了
円を描くのをやってないですが、また別の機会にやることにして、SDLを使ったブロック崩しは一旦これにて終了しておきます。