Composite パターンの書き方をざっくり説明
Composite パターンとは
木構造を使用した再帰的なデータ構造でオブジェクトを管理するパターンです。
Composite パターン - Wikipedia
実装方法
class cNode { public: cNode(){} virtual ~cNode() { std::for_each(list_.begin(), list_.end(), [](cNode *node) { delete node; }); } void addChild(cNode *child) { list_.push_back(child); } void update() { doUpdate(); std::for_each(list_.begin(), list_.end(),[=](cNode *node) { node->update(); }); } protected: virtual void doUpdate() { } private: std::list<cNode *> list_; };
このcNodeクラスを継承してオブジェクトを作成します。
これによりオブジェクトは自分に子ノードを設定できるようになります。
実行してみる
#define LOG(MSG) std::cout << MSG << std::endl class cScene : public cNode { public: cScene() {} ~cScene() { LOG("cScene delete"); } virtual void doUpdate()override { LOG("cScene"); } }; class cLabel : public cNode { public: cLabel() {} ~cLabel() { LOG("cLabel delete"); } virtual void doUpdate()override { LOG("cLabel"); } }; class cGraphic : public cNode { public: cGraphic() {} ~cGraphic() { LOG("cGraphic delete"); } virtual void doUpdate()override { LOG("cGraphic"); } }; class cText : public cNode { public: cText() {} ~cText() { LOG("cText delete"); } virtual void doUpdate()override { LOG("cText"); } }; class cPlayer : public cNode { public: cPlayer() {} ~cPlayer() { LOG("cPlayer delete"); } virtual void doUpdate()override { LOG("cPlayer"); } }; int main() { cNode *root = new cNode(); //< 全ての親 auto scene = new cScene(); root->addChild(scene); root->addChild(new cPlayer()); scene->addChild(new cLabel()); scene->addChild(new cGraphic()); scene->addChild(new cText()); root->update(); delete root; return 0; }
実行結果
思ったこと
cocosとか触っているとバリバリ出てくるパターンの一つですねー
すごく使い勝手は良いけど、ノードの登録をミスしてしまったら
オブジェクトの削除タイミングが意図したとおりにならずハマってしまうので注意が必要です。
Strategyパターンの書き方をざっくり説明
実装方法
class cWeapon { public: cWeapon() {} ~cWeapon() {} virtual void Attack() = 0; }; class cGun : public cWeapon { public: cGun() { std::cout << "主人公は銃を装備した" << std::endl; } ~cGun() {} virtual void Attack() { std::cout << "主人公は銃で攻撃!!" << std::endl; } }; class cKnife : public cWeapon { public: cKnife() { std::cout << "主人公はナイフを装備した" << std::endl; } ~cKnife() {} virtual void Attack() { std::cout << "主人公はナイフで攻撃!!" << std::endl; } }; class Player { std::unique_ptr<cWeapon> weapon; public: Player(cWeapon *pWeapon) { HandChange(pWeapon); } ~Player() {} void HandChange(cWeapon *pWeapon){ weapon.reset(pWeapon); } void Attack() { weapon->Attack(); } };
Playerクラスのweaponは銃とナイフの二通りの処理をする必要があります。
そのため銃の挙動を行いたい場合はcGunクラスの処理を、ナイフの処理を行いたい場合はcKnifeクラスの処理を行います。
実行してみる
int main() { std::unique_ptr<Player> player; player.reset(new Player(new cGun)); player->Attack(); player->HandChange(new cKnife); player->Attack(); return 0; }
実行結果
思ったこと
個人的には分岐が多かったり、switch文が乱用されるのを防ぐためによく使用するパターンです
武器の換装やAIの難易度分岐などなど
かなり便利ですが下手に使用するとファイル間の行き来が面倒なのでそこは気を付けないとですねー
TemplateMethodパターンの書き方をざっくり説明
TemplateMethodパターンとは
基底クラスに定型化した処理手順を定義し、処理部分を派生クラスで実装するパターン
Template Method パターン - Wikipedia
実装方法
class cBase { public: cBase() {} ~cBase() {} virtual void Init() { std::cout << "cBase::Init()" << std::endl; } virtual void Update() { std::cout << "cBase::Update()" << std::endl; } virtual void Render() { std::cout << "cBase::Render()" << std::endl; } void Show() { Init(); Update(); Render(); } }; class cTitle : public cBase { public: cTitle() {} ~cTitle() {} virtual void Init() { std::cout << "cTitle::Init()" << std::endl; } virtual void Update() { std::cout << "cTitle::Update()" << std::endl; } virtual void Render() { std::cout << "cTitle::Render()" << std::endl; } }; class cGame : public cBase { public: cGame() {} ~cGame() {} virtual void Init() { std::cout << "cGame::Init()" << std::endl; } virtual void Update() { std::cout << "cGame::Update()" << std::endl; } virtual void Render() { std::cout << "cGame::Render()" << std::endl; } }; class cEnding : public cBase { public: cEnding() {} ~cEnding() {} virtual void Init() { std::cout << "cEnding::Init()" << std::endl; } virtual void Update() { std::cout << "cEnding::Update()" << std::endl; } virtual void Render() { std::cout << "cEnding::Render()" << std::endl; } };
cBaseクラスに定型処理を定義し、cTitle、cGame 、cEnding の各クラスに処理を書いている状態です。
cBaseのshow関数はInit、Update、Render関数を呼び出すようにしています。
実行してみる
int main() { std::unique_ptr<cBase> scene; scene.reset(new cTitle); scene->Show(); scene.reset(new cGame); scene->Show(); scene.reset(new cEnding); scene->Show(); return 0; }
実行結果
思ったこと
ゲーム作ってるとほぼ100%目にするってくらい定石なものです(多分ゲーム以外でも同じだと思う)
シーン管理だったりメニュー画面やダイアログだったり...
やっぱり面倒な定型処理を書かなくて済むのはすごく便利ですよねー
オブジェクト指向を意識してC++でシューティングゲームを作る(6)
前回「オブジェクト指向を意識してC++でシューティングゲームを作る(5) - 怠惰な人間のゲーム制作日記」で敵を出現させるところまでやりました。
今回は実際に敵に弾を当ててみましょう。
まず、当たり判定は円と円の当たり判定で計算します。
ちなみに、円と円の当たり判定のやり方は
(自機の座標(青) - 敵の座標(赤))で敵から自機のベクトル(黄)が出るので
x、y要素を二乗してから足して距離を出し(√でもいいが処理が重たいので非推奨)
そこに自機と敵の半径を足して二乗した(上の計算で√した場合はそのまま)数と比べて足した方が大きければ当たっていると判断できます。
もう少しわかりやすく式を書くとこんな感じ。(学校で最初に習うやり方は多分こっち)
(自機のX座標(青) - 敵のX座標(赤))^2 + (自機のY座標(青) - 敵のY座標(赤))^2 <= (自機と敵のあたり判定の和)^2
とにかく実装していきます。
今から当たり判定関数を作りたいのですが、当たり判定を行う際にそれに必要な情報を揃えないといけません。
ちなみに必要な情報は以下の通りです。
- 自機と敵(又は弾)の座標
- 自機と敵(又は弾)の当たり判定の合計
- 当たった時にどうするかの処理(boolで返すだけでいい場合はなくてもok)
この3つを楽に取得しようとするならば敵、弾、自機全ての親クラス、Objectクラスを呼び出すのが一番楽です。
Object.h
#pragma once class CObject { protected: //DXライブラリで定義されている構造体(中身はfloat型のx,y,z) VECTOR pos; //画像データ格納 int *graphic; //あたり範囲 float range; public: //コンストラクタ(インスタンス生成時に最初に呼ばれる関数) CObject(){}; //デストラクタ(インスタンス削除時に呼ばれる関数) virtual ~CObject(){}; //描画 virtual void Render() = 0; //更新 virtual void Update() = 0; //posのアドレスを渡す関数 VECTOR *GetPos(){ return &pos; } //rangeの値を返す関数 float GetRange(){ return range; } //当たった時の処理(全て処理が違うので仮想関数にする) virtual void HitAction(){}; };
これでGetPos関数で各オブジェクトの座標を、GetRange関数で当たり範囲を、HitActionで当たった時の挙動を扱うことが出来ます。
それでは次に当たり判定の関数を作っていきます。
今回は小規模のゲームなのでObject.cppに書いていきますが、本当はファイルを分けることをお勧めします。
Object.cpp
void ChackHitCircle(CObject *obj1, CObject *obj2) { //やってることはこれ //((obj1.pos.x - obj2.pos.x)*(obj1.pos.x - obj2.pos.x) + (obj1.pos.y - obj2.pos.y)*(obj1.pos.y - obj2.pos.y)) float distance = VSquareSize(VSub(*obj1->GetPos(), *obj2->GetPos())); float range = (obj1->GetRange() + obj2->GetRange())*(obj1->GetRange() + obj2->GetRange()); //HIT if (distance < range) { obj1->HitAction(); obj2->HitAction(); } }
プロトタイプ宣言もしておきます。
Object.h
void ChackHitCircle(CObject *obj1, CObject *obj2);
これで当たり判定関数の完成です。
次にどこで当たり判定をするかですが、この当たり判定関数はObjectクラスを継承したクラスのみしか受け付けてくれません。
つまりManagerクラスでもダメだし、プロトタイプ宣言のみのEnemyクラスもエラーが出ます。
ではどうするか...
BulletManager.cpp
CObject *CBulletManager::GetBullet(int num){ return (CObject*)bullet[num]; }
はい、こうします(笑)
何をやっているかと言うと、ObjectクラスでCEnemy型のポインタをキャストしています。
ちなみにこれに限らず、ポインタ型は、明示的にキャストすることで、他のポインタ型に変換できます(便利ですよね~ww)
これをEnemyManagerにも同じようにします。
EnemyManager.h
#pragma once class CEnemy; //敵の数 #define ENEMY_NUM 100 class CEnemyManager { CEnemy *enemy[ENEMY_NUM]; //敵の画像 int enemyGraphic; public: CEnemyManager(); ~CEnemyManager(); void Update(); void Render(); CObject *GetEnemy(int num){ return (CObject*)enemy[num]; } private: //敵の生成 void Spawn(); };
次に当たり範囲を設定します。
これは各クラスのコンストラクタで定義します。
Enemy.cpp
CEnemy::CEnemy(int *tex, VECTOR &position) { graphic = tex; pos = position; flag = true; hp = 5; range = 10; }
Player.cpp
CPlayer::CPlayer(CManager *pManager) { //CManagerのアドレス格納 manager = pManager; //画像データ格納 graphic = new int; *graphic = LoadGraph("graphic/Player/player.png"); bulletGraphic = LoadGraph("graphic/Bullet/playerBullet.png"); //位置を初期化 pos = VGet(320, 240, 0); range = 10; }
Bullet.cpp
CBullet::CBullet(int *tex, VECTOR &position, VECTOR &vPosition) { graphic = tex; pos = position; vPos = vPosition; flag = true; range = 10; }
次に当たった際の処理を書いていきます
今回は当たったら消す処理をします
Bullet.h
#pragma once //弾の管理 class CBullet :public CObject { //進む方向ベクトル VECTOR vPos; //弾の生存フラグ(true = 生きている) bool flag; public: CBullet(){}; CBullet(int *tex, VECTOR &position, VECTOR &vPosition); ~CBullet(); //描画 void Render(); //更新 void Update(); //弾の生存フラグを取得 bool GetFlag(){ return flag; }; void HitAction(){ flag = false; } };
Enemy.h
#pragma once class CEnemy : public CObject { protected: //敵のHP int hp; //進む方向ベクトル VECTOR vPos; //敵の生存フラグ(true = 生きている) bool flag; public: CEnemy(){}; CEnemy(int *tex, VECTOR &position); ~CEnemy(); //描画 void Render(); //更新 void Update(); //敵の生存フラグを取得 bool GetFlag(){ return flag; }; void HitAction(){ flag = false; } };
と、ここで明らかな間違いを見つけてしまったので修正します
Bullet.cpp
void CBullet::Update() { //最初にフラグを倒す flag = false; pos = VAdd(pos, vPos); //画面内にいるか確認 if (pos.x < GAME_SCREEN_WIDTH && pos.x > 0) { if (pos.y < GAME_SCREEN_HEIGHT && pos.y > 0) { //画面内ならフラグを元に戻す flag = true; } } }
これ間違いなくflagを消しても復活してきますよね、ごめんなさい(笑)
なのでこう修正しました
Bullet.cpp
void CBullet::Update() { pos = VAdd(pos, vPos); //画面内にいるか確認 if (pos.x > GAME_SCREEN_WIDTH || pos.x < 0 || pos.y > GAME_SCREEN_HEIGHT || pos.y < 0) { //画面外なら消す flag = false; } }
んでEnemyの方も同じような感じで作っていたのでそこも修正
Enemy.cpp
void CEnemy::Update() { //最初にフラグを倒す //flag = false; //下に移動 vPos = VGet(0, 1, 0); pos = VAdd(pos, vPos); //画面の縦の長さ+100の数値まで敵が来たら消すようにする if (pos.y < GAME_SCREEN_HEIGHT + 100) { //画面内ならフラグを元に戻す flag = true; } }
Enemy.cpp
void CEnemy::Update() { //下に移動 vPos = VGet(0, 1, 0); pos = VAdd(pos, vPos); //画面の縦の長さ+100の数値まで敵が来たら消すようにする if (pos.y > GAME_SCREEN_HEIGHT + 100) { //画面内ならフラグを元に戻す flag = false; } }
では間違いなく出来ているか検証してみます。
Game.cpp
void CGame::Update() { player->Update(); bulletManager->Update(); enemyManager->Update(); //当たり判定 for (int i = 0; i < BULLET_NUM; i++) { if (bulletManager->GetBullet(i) != NULL) { for (int j = 0; j < BULLET_NUM; j++) { if (enemyManager->GetEnemy(j) != NULL) { ChackHitCircle(bulletManager->GetBullet(i), enemyManager->GetEnemy(j)); } } } } }
弾と敵が消えました。
これで今回は終わりにします。
次は自機の弾と敵の弾の区別の処理を実装していきます
オブジェクト指向を意識してC++でシューティングゲームを作る(5)
前回「オブジェクト指向を意識してC++でシューティングゲームを作る(4) - hainaのゲーム制作日記」で弾を出せる制限をなくしました。
今回は敵を複数描画するところまでやります。
やること
- 全ての敵を管理するクラス制作
- 敵のランダム生成
全ての敵を管理するクラス制作
敵の画像はこれにします。
ではまず「EnemyManager」のhとcppを作成します。
EnemyManagerは前々回くらいで作ったBulletManagerと同じ使い方で、複数ある敵のインスタンスを一括で管理するためのクラスです。
(BulletManagerの詳細はこちら「オブジェクト指向を意識してC++でシューティングゲームを作る(3) - hainaのゲーム制作日記」)
敵のクラスは後で作るのでとりあえず先にヘッダーで前方宣言しておいて敵のインスタンスを格納する変数を定義しておきます。
敵の画像を複数あるインスタンス一つ一つで読み込ませるのは処理の無駄なので、ここで画像を読み込み、その読み込んだ情報のアドレスを後で敵を生成した時にポインタで参照します。
EnemyManager.h
#pragma once class CEnemy; //敵の数 #define ENEMY_NUM 100 class CEnemyManager { CEnemy *enemy[ENEMY_NUM]; //敵の画像 int enemyGraphic; public: CEnemyManager(); ~CEnemyManager(); void Update(); void Render(); };
ここのコンストラクタでもenemyのポインタにNULLを入れます。
ついでにここでNULLでないポインタを更新描画する処理も作っておきます。
EnemyManager.cpp
#include"DxLib.h" #include"Object.h" #include"Enemy.h" #include"EnemyManager.h" CEnemyManager::CEnemyManager() { for (int num = 0; num < ENEMY_NUM; num++) { enemy[num] = NULL; } //敵の画像読み込み enemyGraphic = LoadGraph("graphic/Enemy/enemy.png"); } CEnemyManager::~CEnemyManager() { for (int num = 0; num < ENEMY_NUM; num++) { delete enemy[num]; } } void CEnemyManager::Update() { for (int num = 0; num < ENEMY_NUM; num++) { //NULLでない場合 if (enemy[num] != NULL) { enemy[num]->Update(); //敵が画面外に出た場合 if (enemy[num]->GetFlag() == false) { //削除してからNULLを入れる delete enemy[num]; enemy[num] = NULL; } } } } void CEnemyManager::Render() { for (int num = 0; num < ENEMY_NUM; num++) { //NULLでない場合 if (enemy[num] != NULL) { enemy[num]->Render(); } } }
このままではエラーが出るのでEnemyクラスも作ります。
Enemy.h
#pragma once class CEnemy : public CObject { protected: //敵のHP int hp; //進む方向ベクトル VECTOR vPos; //敵の生存フラグ(true = 生きている) bool flag; public: CEnemy(){}; CEnemy(int *tex, VECTOR &position); ~CEnemy(); //描画 void Render(); //更新 void Update(); //敵の生存フラグを取得 bool GetFlag(){ return flag; }; };
次に敵を動かす部分を作っていきます。
今回は直進してくる敵を作ります。
CEnemy.cpp
#include"DxLib.h" #include"Define.h" #include"Object.h" #include"Enemy.h" CEnemy::CEnemy(int *tex, VECTOR &position) { graphic = tex; pos = position; flag = true; hp = 5; } CEnemy::~CEnemy() { } void CEnemy::Update() { //最初にフラグを倒す flag = false; //下に移動 vPos = VGet(0,1,0); pos = VAdd(pos, vPos); //画面の縦の長さ+100の数値まで敵が来たら消すようにする if (pos.y < GAME_SCREEN_HEIGHT+100) { //画面内ならフラグを元に戻す flag = true; } } void CEnemy::Render() { // 読みこんだグラフィックを回転描画 DrawRotaGraph(pos.x, pos.y, 1.0, 0, *graphic, TRUE); }
敵のランダム生成
次に、敵の生成を行います。
EnemyManagerクラスにSpawn関数を追加します。
CEnemyManager.h
class CEnemyManager { CEnemy *enemy[ENEMY_NUM]; //敵の画像 int enemyGraphic; public: CEnemyManager(); ~CEnemyManager(); void Update(); void Render(); private: //敵の生成 void Spawn(); };
その後Update関数の最初にSpawn関数を呼び出すようにして、Spawn関数でCEnemyクラスの動的確保をします。(ここは弾の生成と同じ)
EnemyManager.cpp
//GAME_SCREEN_WIDTHを使うため #include"Define.h" void CEnemyManager::Update() { //生成 Spawn(); for (int num = 0; num < ENEMY_NUM; num++) { //NULLでない場合 if (enemy[num] != NULL) { enemy[num]->Update(); //弾が画面外に出た場合 if (enemy[num]->GetFlag() == false) { //削除してからNULLを入れる delete enemy[num]; enemy[num] = NULL; } } } } void CEnemyManager::Spawn() { //一定確立で敵が出現 if ((rand() % 100) == 0) { for (int num = 0; num < ENEMY_NUM; num++) { //NULLの場合 if (enemy[num] == NULL) { //敵を生成するX軸を決める int yPos = (rand() % (GAME_SCREEN_WIDTH)); //敵生成 enemy[num] = new CEnemy(&enemyGraphic, VGet(yPos, 0, 0)); //一体生成したら抜けるようにする break; } } } }
では、ここまで書いたコードがしっかり動くか確認してみます。
プレイヤーや弾のクラスと同様に、CGameクラスに前方宣言をして、ポインタを定義します。
CGame.h
class CEnemyManager; class CGame:public CScene { //自機のポインタ変数 CPlayer *player; //弾全体管理しているクラスのポインタ変数 CBulletManager *bulletManager; //敵全体を管理しているクラスのポインタ変数 CEnemyManager *enemyManager; public: CGame(CManager *pManager); ~CGame(); void Update(); void Render(); };
次にcppファイル内で上にEnemyManagerをincludeして、コンストラクタでCEnemyManagerを動的確保、デストラクタで解放、各関数で更新、描画をしていきます。
CGame.cpp
#include"EnemyManager.h" CGame::CGame(CManager *pManager) : CScene(pManager) { //プレイヤーを動的確保 player = new CPlayer(pManager); //CBulletManagerを動的確保 bulletManager = new CBulletManager(); //CEnemyManagerを動的確保 enemyManager = new CEnemyManager(); //CBulletManagerクラスのアドレス取得 player->SetBulletManager(bulletManager); } CGame::~CGame() { //動的確保したものを解放する delete player; delete bulletManager; delete enemyManager; } void CGame::Update() { player->Update(); bulletManager->Update(); enemyManager->Update(); } void CGame::Render() { player->Render(); bulletManager->Render(); enemyManager->Render(); }
ではここで一回実行してみます。
このようにいい感じにバラけて進んで来てくれます。
これで今回は終わりにします。
次は敵と弾の当たり判定の部分をやって行きたいと思います。
オブジェクト指向を意識してC++でシューティングゲームを作る(4)
前回、「オブジェクト指向を意識してC++でシューティングゲームを作る(3)」で弾の発射の部分の所までやりました。
ですが、あのままでは一度に弾を確保できる個数が100発のため、今回は要らなくなった弾を削除する処理をやりたいと思います。
やること
- 前回のコードのバグ修正
- 要らなくなった弾の判定処理
前回のコードのバグ修正
まず最初に私も意図していなかったのですが。右と左又は上と下を同時押しすると左上に移動していくバグを発見したのでここで修正しておきます。
Dxライブラリでベクトルの長さの二乗を取得する関数VSquareSizeを使って長さが0場合は移動の処理をしないようにします。
Player.h
void CPlayer::Update() { //移動する向き VECTOR vPos = VGet(0, 0, 0); //弾の発射を抑制するトリガー static int count = 0; //移動していないときは正規化する必要がないのでこれで移動しているか確認する bool moveFlag = false; if (manager->GetKey()[KEY_INPUT_LEFT] > 0) { vPos.x -= 1.0f; moveFlag = true; } if (manager->GetKey()[KEY_INPUT_RIGHT] > 0) { vPos.x += 1.0f; moveFlag = true; } if (manager->GetKey()[KEY_INPUT_UP] > 0) { vPos.y -= 1.0f; moveFlag = true; } if (manager->GetKey()[KEY_INPUT_DOWN] > 0) { vPos.y += 1.0f; moveFlag = true; } if (moveFlag && VSquareSize(vPos) != 0)//ここを修正 { vPos = VNorm(vPos); vPos = VScale(vPos, PLAYER_SPEED); //現在の座標に移動量を加算する pos = VAdd(pos, vPos); } //弾の発射 if (manager->GetKey()[KEY_INPUT_SPACE] > 0) { count++; } else { count = 0; } if ((count % 10) == 1) { //上に弾を飛ばす bulletManager->Shot(&bulletGraphic, pos, VGet(0, -7, 0)); } }
要らなくなった弾の判定処理
前回の弾の処理で最大100発打つと弾の配列がいっぱいになり、次の弾が出せない仕様にしていましたが、画面外に出ていらなくなった弾を削除することで、次の弾の入れる枠を確保できるようにします。
まず、弾が画面外にあるかどうかの確認のために生存フラグを作ります。
Bullet.h
#pragma once //弾の管理 class CBullet:public CObject { //進む方向ベクトル VECTOR vPos; //弾の生存フラグ(true = 生きている) bool flag; public: CBullet(){}; CBullet(int *tex, VECTOR &position, VECTOR &vPosition); ~CBullet(); //描画 void Render(); //更新 void Update(); //弾の生存フラグを取得 bool GetFlag(){ return flag; }; };
画面外に出た処理をするために画面の大きさをDefine.hを作ってそこで定義します。
Define.hをBullet.cppの上で定義して、
生存フラグはコンストラクタでtrueにし、画面外に出たときにfalseにします。
画面外に出たか確認の処理はUpdate関数の最後に書きます。
Bullet.cpp
CBullet::CBullet(int *tex, VECTOR &position, VECTOR &vPosition) { graphic = tex; pos = position; vPos = vPosition; flag = true; } void CBullet::Update() { //最初にフラグを倒す flag = false; pos = VAdd(pos, vPos); //画面内にいるか確認 if (pos.x < GAME_SCREEN_WIDTH && pos.x > 0) { if (pos.y < GAME_SCREEN_HEIGHT && pos.y > 0) { //画面内ならフラグを元に戻す flag = true; } } }
これで画面外に出た弾はflagがfalseになります。
つまりflagがfalseになっている弾はもう使わない弾という事なので削除してしまいます。
BulletManager.cpp
void CBulletManager::Update() { for (int num = 0; num < BULLET_NUM; num++) { //NULLでない場合 if (bullet[num] != NULL) { bullet[num]->Update(); //弾が画面外に出た場合 if (bullet[num]->GetFlag() == false) { //削除してからNULLを入れる delete bullet[num]; bullet[num] = NULL; } } } }
これで弾を無限に出すことができるようになりました。
この先が少し長くなってしまうため、今回は一旦これで終わりにします。
次回は、敵の処理の部分を作って行きます。
次「オブジェクト指向を意識してC++でシューティングゲームを作る(5) - hainaのゲーム制作日記」