ホームへ戻る

18章. 様々な敵の動きをスマートに実現する方法

 先の章で紹介した方法だと、敵の動きはクラスごとに1種類ずつしか作れませんでした。
しかしBigEnemyにしてもNormalEnemyにしても同じ位を使う敵でも動き方はさまざまです。
かと言ってif文を大量に並べて条件分岐処理をするのはスマートではありません。
ではどうするかというと、AbstractEnemyクラスにmovePatternIDというメンバ変数を持たせ、
そのIDに応じた関数をコールし、その関数内で別々の動きを処理させます。
movePatternIDに応じた関数のコールには関数ポインタを格納した配列を使うのが便利です。

movePatternIDに0が入っていたらmovePattern00()の関数をコールし、
movePatternIDに1が入っていたらmovePattern01()の関数をコールし、
movePatternIDに2が入っていたらmovePattern02()の関数をコールし・・という処理を実現したいとします。

そんな時は

関数ポインタ movePattern[] = { &movePattern00, &movePattern01, &movePattern02};

このように定義した配列に対して

movePattern[movePatternID]();

こんな形でコールすると実現できます。
上の要領で実装したコードを見てみましょう。
敵クラスの移動を処理するクラスを以下のようにEnemyMoverとします。


EnemyMover.h


#pragma once

#include <vector>
#include <memory>

class AbstractEnemy;

class EnemyMover 
{

public:
    EnemyMover();
    virtual ~EnemyMover() = default;
    void move(AbstractEnemy* enemy);

private:
    typedef void(EnemyMover::*FUNC)(AbstractEnemy* enemy);
    std::vector<FUNC> _movePattern;
    void setFunction();

    void movePattern00(AbstractEnemy *enemy);
    void movePattern01(AbstractEnemy *enemy);
    void movePattern02(AbstractEnemy *enemy);
    void movePattern03(AbstractEnemy *enemy);
    void movePattern04(AbstractEnemy *enemy);

};

EnemyMover.cpp


#include "EnemyMover.h"
#include "Macro.h"
#include "Define.h"
#include "AbstractEnemy.h"

using namespace std;

EnemyMover::EnemyMover()
{
    setFunction();
}

void EnemyMover::update(AbstractEnemy* enemy)
{
    const unsigned int id = enemy->getMovePatternID();
    if (_movePattern.size() <= id) {
        ERR("moveIDが不正です");
    }
    (this->*_movePattern[id])(enemy);  //idに応じた移動処理を行う
    enemy->setX(enemy->getX() + cos(enemy->getAngle()) * enemy->getSpeed());//x移動
    enemy->setY(enemy->getY() + sin(enemy->getAngle()) * enemy->getSpeed());//y移動
}

/*
@brief 下に下がって止まり、また下がる
*/
void EnemyMover::movePattern00(AbstractEnemy* enemy)
{
    const int cnt  = enemy->getCounter();
    const int wait = 180;
    if (0 == cnt) {
        enemy->setAngle(Define::PI / 2);
        enemy->setSpeed(3);
    }
    if (60 < cnt && cnt <= 90) {
        enemy->setSpeed(enemy->getSpeed() - 0.1f);
    }
    if (90 + wait < cnt && cnt <= 90 + wait + 30) {
        enemy->setSpeed(enemy->getSpeed() + 0.1f);
    }
}

/*!
@brief 左下へ移動する
*/
void EnemyMover::movePattern01(AbstractEnemy* enemy)
{
    int cnt = enemy->getCounter();
    if (0 == cnt) {
        enemy->setAngle(Define::PI * 3 / 4);
        enemy->setSpeed(4);
    }
}

/*!
@brief 右下へ移動する
*/
void EnemyMover::movePattern02(AbstractEnemy* enemy)
{
    int cnt = enemy->getCounter();
    if (0 == cnt) {
        enemy->setAngle(Define::PI * 1 / 4);
        enemy->setSpeed(4);
    }
}

void EnemyMover::movePattern03(AbstractEnemy* enemy)
{
}

void EnemyMover::movePattern04(AbstractEnemy* enemy)
{
}

/*!
@brief 関数ポインタをリストにセット
*/
void EnemyMover::setFunction()
{
    _movePattern.push_back(&EnemyMover::movePattern00);
    _movePattern.push_back(&EnemyMover::movePattern01);
    _movePattern.push_back(&EnemyMover::movePattern02);
    _movePattern.push_back(&EnemyMover::movePattern03);
    _movePattern.push_back(&EnemyMover::movePattern04);
}

vector _movePattern;
が関数ポインタのベクターです。
setFunction()をコンストラクタからコールし、vectorにセットしています。
後はupdate関数内で指定されたidをコールすればセットした順のidがコールされます。
例えばenemy->getMovePatternID()で取れて来る値が0だったとしたらmovePattern00()が呼ばれます。

movePattern00()内の説明をします。
getCounter()で取れて来る値はAbstractEnemyのメンバ変数int _counter;で、1フレームに1ずつ増える値です。すなわち1秒間に60増えます。
cntが0とはつまり敵が登録された直後、出現時という事を意味します。
この時に角度と速さをセットします。

if (0 == cnt) {
  enemy->setAngle(Define::PI / 2);
  enemy->setSpeed(3);
}

はPI/2つまり下方向に角度をセットし、速度に3[pixel/frame]をセットします。
次に

if (60 < cnt && cnt <= 90) {
   enemy->setSpeed(enemy->getSpeed() - 0.1f);
}

そして、以下のタイミングで再び動き出します。

if (90 + wait < cnt && cnt <= 90 + wait + 30) {
  enemy->setSpeed(enemy->getSpeed() + 0.1f);
}

このようにすることで、自在に敵の移動を処理できます。
AngleとSpeedさえセットすれば
sin,cos計算で移動計算してくれるので、十分ですが、Speedに0をセットして直接xとyを設定しても良いです。

EnemyMoverはAbstractEnemyクラスが持ちます。


AbstractEnemy.h


#pragma once

#include "Task.h"
#include "EnemyMover.h"

class EnemyMover;

class AbstractEnemy : public Task
{
public:
    AbstractEnemy(float x, float y);
    virtual ~AbstractEnemy() = default;
    void initialize();
    bool update() override;

    float getX() const  { return _x; }
    void  setX(float x) { _x = x; }
    float getY() const  { return _y; }
    void  setY(float y) { _y = y; }
    int   getCounter() const    { return _counter; }
    void  setSpeed(float speed) { _speed = speed; }
    float getSpeed() const      { return _speed; }
    void  setAngle(float angle) { _angle = angle; }
    float getAngle() const      { return _angle; }
    int   getMovePatternID() const { return _moveID;  }

protected:
    virtual void setSize() = 0;
    bool isInside() const;

    EnemyMover _mover;

    float _x, _y;//座標
    float _speed;//速さ
    float _angle;//角度

    int _counter;//カウンタ
    int _width; //幅
    int _height;//高さ

    int _movePatternID;//移動パターン
};

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),
    _moveID(0)
{
}

void AbstractEnemy::initialize()
{
    setSize();
}

bool AbstractEnemy::update()
{
    _mover.move(this);
    _counter++;
    return isInside();
}

/*!
@brief 現在の位置が画面内か?
*/
bool AbstractEnemy::isInside() const
{
    if (_counter < 60) {//最初の1秒は判定しない
        return true;
    }
    if (_x < -_width/2 || Define::OUT_W + _width/2 < _x || _y < -_height/2 || Define::OUT_H + _height/2 < _y) {
        return false;
    }
    return true;
}

敵クラスのメンバ変数にセットゲットしたかったので、今回setterとgetterをいくつか追加しました。
省略しますが、_moveIDは
BitEnemyのコンストラクタで、_moveID = 0;
NormalEnemyのコンストラクタで、_moveID = 1;
SmallEnemyのコンストラクタで、_moveID = 2;
をセットしています。

これに伴ってEnemyManagerのコンストラクタでインスタンスを生成する部分の引数を変更しています。


EnemyManager.cpp


#include "EnemyManager.h"
#include "Define.h"
#include "SmallEnemy.h"
#include "NormalEnemy.h"
#include "BigEnemy.h"
#include <DxLib.h>

using namespace std;

EnemyManager::EnemyManager()
{
    _list.emplace_back(make_shared<BigEnemy>   (Define::CENTER_X - 100.f, -80.f));
    _list.emplace_back(make_shared<BigEnemy>   (Define::CENTER_X + 100.f, -80.f));
    _list.emplace_back(make_shared<NormalEnemy>(Define::CENTER_X + 400.f, -80.f));
    _list.emplace_back(make_shared<NormalEnemy>(Define::CENTER_X + 300.f, -80.f));
    _list.emplace_back(make_shared<SmallEnemy> (Define::CENTER_X - 400.f, -80.f));
    _list.emplace_back(make_shared<SmallEnemy> (Define::CENTER_X - 300.f, -80.f));

    for (auto enemy : _list) {
        enemy->initialize();
    }
}

bool EnemyManager::update()
{
    for (auto it = _list.begin(); it != _list.end();) {
        if ((*it)->update() == false) {
            it = _list.erase(it);
        }
        else {
            it++;
        }
    }
    return true;
}

void EnemyManager::draw() const
{
    DrawFormatString(0, 20, GetColor(255, 255, 255), "敵の数 = %d", _list.size());
    for (const auto enemy : _list) {
        enemy->draw();
    }
}


このように実装することでmovePatternIDに好きな値を入れるだけで好きな動きを実現することが出来るようになりました。
次はこのようなパラメータをスクリプトで指定できるようにしてみましょう。

実行結果

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


HPトップへ 質問掲示板へ

- Remical Soft -