ホームへ戻る

16章. 様々な敵を実装する

 今回は前回作った敵のバリエーションを増やしてみましょう。
先ほどの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();
    }
}

リストにプッシュする部分だけ追加したら後は自動的に計算・描画してくれます。

→分からないことがあれば掲示板で質問して下さい


HPトップへ 質問掲示板へ

- Remical Soft -