C++OpenSiv3D入門講座 Vol. 02 ファイル分け・コンポジション
実際にゲームを作る時、ファイル分けをすることが必要になる。 今回はクラスの内容を.hと.cppに分ける。
ファイル分け
以下のように、.hと.cppにクラスの定義を分けて書くことが出来る。今後は基本的に.hと.cppにクラスの定義を分けて書くこと。(ただし、解答例ではファイル数が多くなるのを避けるためすべてMain.cppに書いている場合がある) .cppには、名前空間を指定するため、クラス名::関数名と書く必要がある。以下は、コンソールでのプレイヤー例。
#include <iostream> class Player{ public: int x, y; Player(int _x, int _y); void update(); void draw(); void showXY(); };
#include "Player.h" Player::Player(int _x, int _y) { x = _x; y = _y; } void Player::update(){ std::cout << "Playerのupdateが呼ばれました。" << std::endl; } void Player::draw(){ std::cout << "Playerのdrawが呼ばれました。" << std::endl; } void Player::showXY(){ std::cout << "PlayerのshowXYが呼ばれました。 x:" << x << " y:" << y << std::endl; }
#include <iostream> #include "Player.h" int main(){ Player player{ 20, 30 }; player.update(); player.draw(); player.showXY(); return 0; }
Tips:定義への移動
複数のファイルに分けると関数やクラスの定義を見に行くのに時間がかかるが、おおよそのIDE(Visual StudioやXcode)では、関数・クラスにカーソルを合わせて右クリックを押して出るダイアログ内の「定義に移動」等の項目を押すことで定義に移動できる。それぞれショートカットも用意されており、例えばVisual Studioであれば関数・クラスにカーソルを合わせてF12を押すことで定義に移動できる。他にも様々な機能やショートカットがあるので、調べてみよう。
コンストラクタの書き方 その2(メンバイニシャライザ)
コンストラクタの数値の初期化は以下のようにも書くことができる。自分で定義したクラスをなど初期化する場合、こう書いたほうが、インスタンスのコピーが発生しなくなるため処理が早くなる。以後はこの書き方、メンバイニシャライザを使って初期化を行う。
#include <iostream> class Vector2{ public: int x; int y; Vector2(int _x, int _y): x{ _x }, y{ _y } { } }; int main(){ Vector2 obj{ 1, 2 }; std::cout << obj.x << " " << obj.y << std::endl; return 0; }
コンポジション
クラスをクラスのメンバにすることが出来る。 以下の例では、Vector2クラスをまず定義し、PlayerクラスでVector2クラスのメンバを持っている。
#include <iostream> class Vector2{ public: int x, y; Vector2(int _x, int _y) : x{ _x }, y{ _y } { } }; class Player{ public: Vector2 pos; int hp; Player(int _x, int _y, int _hp) : pos{ _x, _y }, hp{ _hp } { } }; int main(){ Player player{ 100, 200, 64 }; std::cout << "x:" << player.pos.x << ", y:" << player.pos.y << std::endl; std::cout << "hp:" << player.hp << std::endl; return 0; }
コンポジションをしている場合、メンバイニシャライザでメンバ変数のコンストラクタを呼ぶ。 上記の例では、Vector2インスタンスのコンストラクタをメンバイニシャライザで呼んでいる。
クラスをコンポジションした場合、クラスに含まれているクラスのコンストラクタが先に呼ばれる。デストラクタは逆の順で呼ばれる。HogeやPiyoは日本では、「特に意味のない名前」を表す。
#include <iostream> class Hoge{ public: int x; Hoge(int _x): x{ _x } { std::cout << "Hogeのコンストラクタが呼ばれました" << std::endl; } ~Hoge(){ std::cout << "Hogeのデストラクタが呼ばれました" << std::endl; } }; class Piyo{ public: Hoge Hoge_; int y; Piyo(int _x, int _y): Hoge_{ _x }, y{ _y } { std::cout << "Piyoのコンストラクタが呼ばれました" << std::endl; } ~Piyo(){ std::cout << "Piyoのデストラクタが呼ばれました" << std::endl; } }; int main(){ Piyo piyo{ 100, 200 }; std::cout << piyo.Hoge_.x << " " << piyo.y << std::endl; return 0; }
インクルード
#include
を使うと自分が定義したヘッダーをインクルードできる。インクルードすると、その部分にヘッダーの中身のコードが展開される。
pragma onceによるインクルードガード
Visual StudioやXcodeなどのコンパイラでは、#pragma once
を使うと一度読み込まれたヘッダーは読み込まれないようになる。複数のファイルから読み込まれるヘッダーの一番上につけることで、多重定義を防ぐことが出来る。
以下の例では、PlayerクラスとEnemyクラスで共通で使うVector2クラスを作り、それをどちらのファイルからも読み込むコンソールプログラムの例である。よって、Vector2クラスの#pragma onceを外すと、コンパイルエラーが起きる。
#pragma once #include <iostream> class Vector2{ public: int x, y; Vector2(int _x, int _y) : x{ _x }, y{ _y } { } };
#include <iostream> #include "Vector2.h" class Enemy{ public: Vector2 pos; Enemy(int x, int y) : pos{ x, y } { } };
#include <iostream> #include "Vector2.h" class Player{ public: Vector2 pos; Player(int x, int y) : pos{ x, y } { } };
#include <iostream> #include "Player.h" #include "Enemy.h" int main(){ Player player{ 100, 200 }; Enemy enemy{ 300, 400 }; std::cout << player.pos.x << ", " << player.pos.y << std::endl; std::cout << enemy.pos.x << ", " << enemy.pos.y << std::endl; return 0; }
実際にゲームを作るときは、ヘッダファイルにはとりあえず#pragma once
をつけよう。#pragma once
は特に害はない。
コンパイラによっては#pragma once
が対応していない場合がある。そのような場合にはこちらの方法でインクルードガードを作成しよう。
まとめ
- クラスの定義は.hと.cppに分けて書く
- IDEの「定義に移動」等の機能を活用しよう
- クラスのメンバ変数の初期化はメンバイニシャライザで行う
- コンポジションをしている場合、メンバイニシャライザでメンバ変数のコンストラクタを呼ぶ
- ヘッダファイルにはとりあえず
#pragma once
をつける
演習問題(OpenSiv3D)
前回の演習問題で作ったプログラムのPlayerクラス, Enemyクラスのコンストラクタの初期化を今回教えた形式、メンバイニシャライザで書き直せ。
前回Siv3Dで作ったプロジェクト上で、Player.h, Player.cpp, Enemy.h, Enemy.cppを作り、PlayerクラスとEnemyクラスをファイル分けせよ。
OpenSiv3Dには2次元座標を表す、Vec2クラスがある。これを用いてPlayerクラス、Enemyクラスを書き換えよ。同時にdraw関数内の、Circleクラスの初期化もVec2クラスで行うよう書き換えよ。
ヒント
Vec2クラスを用いた円描写は以下のようになる。
Circle{ 座標(Vec2), 半径(double) }.draw(Color{ r, g, b }):