今回は前回作った敵のバリエーションを増やしてみましょう。
先ほどのEnemyクラスをAbstractEnemyという抽象クラスに名前変更し、そこから色んな敵に派生させることを考えます。
クラス図的にはこんな感じ。
AbstractEnemyから、SmallEnemy、NormalEnemy、BigEnemyが派生します。
EnemyManagerはAbstractEnemyのポインタを持ちます。
まず実行結果をみてください。
まるっこいのがSmallEnemy、先の章で出てきたのがNormalEnemy、一番でかいのがBigEnemyを示します。
では共通処理はAbstractEnemyクラスが実装し、各々異なる部分を各派生先に実装させるようにします。
AbstractEnemy.h
#pragma once #include "Task.h" class AbstractEnemy : public Task { public: AbstractEnemy(float x, float y); virtual ~AbstractEnemy() = default; void initialize(); bool update() override; protected: virtual void setSize() = 0; float _x, _y;//座標 float _speed;//速さ float _angle;//角度 int _counter;//カウンタ int _width; //幅 int _height;//高さ };
AbstractEnemy.cpp
#include "AbstractEnemy.h" #include <DxLib.h> #include "Image.h" #include "Define.h" AbstractEnemy::AbstractEnemy(float x, float y) : _x(x), _y(y), _speed(0), _angle(0), _counter(0), _width(0), _height(0) { } void AbstractEnemy::initialize() { setSize(); } bool AbstractEnemy::update() { _counter++; _x += cos(_angle)*_speed; _y += sin(_angle)*_speed; return true; }
純粋仮装関数であるdraw()はここでは定義していません。
敵の描画の仕方は派生先に依存するのでそちらで定義します。
更に、敵の大きさ_width、_heightを格納したいですが、抽象クラスではセットできません。
そんな時はvirtual void setSize()=0;を純粋仮装関数として定義し、initialize()から派生先の実装関数をコールします。
派生先にはsetSizeを実装しなければコンパイルエラーになります。
update()内の移動処理はどの敵も共通処理なので、親クラスに書きます。
では派生先である、SmallEnemy、NormalEnemy、BigEnemyの実装をみていきます。
SmallEnemy.h
#pragma once #include "AbstractEnemy.h" class SmallEnemy final : public AbstractEnemy { public: SmallEnemy(float x, float y); ~SmallEnemy() = default; void draw() const override; protected: void setSize() override; };
SmallEnemy.cpp
#include "SmallEnemy.h" #include "Define.h" #include <DxLib.h> #include "Image.h" SmallEnemy::SmallEnemy(float x, float y) : AbstractEnemy(x,y) { _speed = 2.0f; _angle = Define::PI/2; } void SmallEnemy::draw() const { DrawRotaGraphF(_x, _y, 1.0, Define::PI * 2 / 120 * _counter, Image::getIns()->getEnemySmall()[0], TRUE); DrawRotaGraphF(_x, _y, 1.0, Define::PI * 2 / 120 * (_counter + 60), Image::getIns()->getEnemySmall()[1], TRUE); } void SmallEnemy::setSize() { int handle = Image::getIns()->getEnemySmall()[0]; GetGraphSize(handle, &_width, &_height); }
AbstractEnemyには定義していない処理を実装しました。
スピードと角度は独自のものを設定しました。
SmallEnemyは小さな二つの円い画像が回転して表示されるので、そのようにdrawに実装しました。
回転する時によく利用するのがPI*2/hoge*_counterです。
PI*2は360°を意味しますね。hogeの数を小さくするほど高速回転になります。今回120なので2秒で1回転になります。
_counterは1フレームに1ずつ増えますから1秒間に60ずつ増えます。
setSizeでは使用している画像のサイズをセットしました。
次にNormalEnemyです。
NormalEnemy.h
#pragma once #include "AbstractEnemy.h" class NormalEnemy final : public AbstractEnemy { public: NormalEnemy(float x, float y); ~NormalEnemy() = default; void draw() const override; protected: void setSize() override; };
NormalEnemy.cpp
#include "NormalEnemy.h" #include <DxLib.h> #include "Image.h" #include "Define.h" NormalEnemy::NormalEnemy(float x, float y) : AbstractEnemy(x,y) { _speed = 2.0; _angle = Define::PI / 2 - Define::PI / 5; } void NormalEnemy::draw() const { const static int imgID[4] = { 0,1,2,1 }; int add = 3; if (cos(_angle)>0.1) {//右向き add = 6; } if (cos(_angle)<-0.1) {//左向き add = 0; } const int handle = Image::getIns()->getEnemyNormal()[add + imgID[(_counter / 8) % 4]]; DrawRotaGraphF(_x, _y, 1.0, 0.0, handle, TRUE); } void NormalEnemy::setSize() { int handle = Image::getIns()->getEnemyNormal()[0]; GetGraphSize(handle, &_width, &_height); }
SmallEnemyの時と同じくAbstractEnemyに無い部分の実装をしています。
実行結果を見て頂けたら分かる通り、角度は若干下向きより右側にしました。なのでPI/5ほど右方向にしてあります。
drawの中は一件見ても何してるのか分かりにくいですね。
説明していきます。
画像は以下のような画像を使います。
LoadDivGraphでロードすると配列要素としてこの9個に分割される画像は
[0] [1] [2]
[3] [4] [5]
[6] [7] [8]
このように格納されます。
羽がパタパタしている画像は
[0]→[1]→[2]→[1]→ [0]→[1]→[2]→[1]→
の順番でループさせたいです。1フレームに1ずつ増える_counterを使ってこのループを簡単に表現するため、
const static int imgID[4]={0,1,2,1};
を定義しています。この配列要素に左から順にアクセスすることで、0→1→2→1というループが実現できます。
そして、左を向いている時は0,1,2、正面を向いている時は3,4,5、右を向いている時は6,7,8です。
addはその分を加算するものです。
キャラがどっちを向いているかはcosを計算すればすぐわかります。
cosというものは、0°で1、90°で0、180°で-1、270°で0を示します。
※参考
http://www24.atpages.jp/venvenkazuya/math1/trigonometric_ratio6_table1.php
ということでほぼ0の時は正面、
0より少し大きい時は右向き、少し小さい時は左向きだと分かりますので、そのように条件分岐しています。
(_counter / 8) % 4
はどれ位の勢いでパタパタするかを示しています。
%4は配列要素が4つなので、0から3までをループするためにしています。
/8はカウンターの上がる勢いを8分の1に減らしています。もっと早くパタパタさせたい時は8の値を小さくすればよいです。
%4は変えてはいけません。
最後にBitEnemyですが、内容はほぼNormalEnemyと同じです。
BigEnemy.h
#pragma once #include "AbstractEnemy.h" class BigEnemy final : public AbstractEnemy { public: BigEnemy(float x, float y); ~BigEnemy() = default; void draw() const override; protected: void setSize() override; };
BigEnemy.cpp
#include "BigEnemy.h" #include <DxLib.h> #include "Image.h" #include "Define.h" BigEnemy::BigEnemy(float x, float y) : AbstractEnemy(x, y) { _speed = 2.0; _angle = Define::PI / 2 + Define::PI / 5; } void BigEnemy::draw() const { const static int imgID[4] = { 0,1,2,1 }; int add = 3; if (cos(_angle)>0.1) {//右向き add = 6; } if (cos(_angle)<-0.1) {//左向き add = 0; } const int handle = Image::getIns()->getEnemyBig()[add + imgID[(_counter / 8) % 4]]; DrawRotaGraphF(_x, _y, 1.0, 0.0, handle, TRUE); } void BigEnemy::setSize() { int handle = Image::getIns()->getEnemyBig()[0]; GetGraphSize(handle, &_width, &_height); }
NormalEnemyと違う点は角度をちょっと左にしている点、
Image::getIns()->getEnemyBit()になっていて、EnemyBitの画像を取得している点、
setSizeの画像のサイズが異なる点です。
後は同じです。
EnemyManagerではこの3つの敵を出現させています。
EnemyManager.cpp
#include "EnemyManager.h" #include "Define.h" #include "SmallEnemy.h" #include "NormalEnemy.h" #include "BigEnemy.h" using namespace std; EnemyManager::EnemyManager() { _list.emplace_back(make_shared<SmallEnemy >(Define::CENTER_X, 100)); _list.emplace_back(make_shared<NormalEnemy>(Define::CENTER_X-200, 100)); _list.emplace_back(make_shared<BigEnemy> (Define::CENTER_X+200, 100)); for (auto enemy : _list) { enemy->initialize(); } } bool EnemyManager::update() { for (auto enemy : _list) { enemy->update(); } return true; } void EnemyManager::draw() const { for (const auto enemy : _list) { enemy->draw(); } }
リストにプッシュする部分だけ追加したら後は自動的に計算・描画してくれます。