【C++】Jumpin’ Rock Flash

DALL·E-2024-02-27-23.21.16-Revise-the-illustration C/C++
DALL·E-2024-02-27-23.21.16-Revise-the-illustration

こんにちは!( `・∀・)ノ

結構間が空いてしまいましたが、FC版ロックマンの挙動を完コピするコードを必死に書いてます。

疲れた😅

まだ完全に全ての動作が実装されたわけではないのですが、一応初期動作部分は検証も兼ねて順調に進んでいってます。

2016年頃も同じ工程をプログラミングしてましたが、今と比べてかなりユニークなスパゲティコードを8ヶ月くらいで作り上げてました。
今は2ヶ月でそれ以上のプログラムコードをオブジェクト指向設計で作る事ができるようになりました。

自分で言うのは変ですが、私すごい。
よく頑張ってるよ😭

あの頃のコードと見比べて、自分が成長した姿を客観視しかなり驚いてます。


現況

プロジェクトが始まったのが去年の暮れからなので、2ヶ月が経過しました。

今実装できている機能としては、

  • BGタイルの表示アルゴリズム
    • 16×16のタイルをバイナリファイルの情報を元に並べる機能
  • スプライトデータの表示アルゴリズム
    • スプライトオブジェクトを画面に描画する機能
  • BGコリジョンポイント(接触判定)
    • 壁属性と空間属性を判断してスプライトデータの移動を制御する
  • 画面スクロールアルゴリズム
    • 操作キャラクターに対する線対称(点対称)の画面のスクロール
  • ロックマンの基本動作
    • 地上移動、ジャンプ、落下のアクションアルゴリズム

が現時点で実装済みになります。

ロックマンの基本動作は梯子の昇り降り、武器発射に加えて、スライディングを追加実装する予定です。

ロックマンの基本動作 (1)

今回のコーディング総括・気づいたところ、C++20の機能

コンポジション(合成)

クラスの設計を行っていると必ず機能の分割問題に直面します。

クラスは単一責任を持たせた上でデータを管理する事が求められており、このルールが守られない多目的クラスがあると、忽ちそこへ多くのクラスが吸い寄せられていきます。

オブジェクト指向における「単一責任の原則」はSOLID原則の一つです。

くどいようですが、このSOLID原則は当ブログで私が酸っぱく連発する単語ですのでご容赦ください😇

クラスに複数の責任を持たせないようにする事で、再利用性や保守性が向上し、コンポジション(合成)などの組み込みが容易になります。
以下の記事がとても分かりやすい説明をされています😊

単一責任原則で無責任な多目的クラスを爆殺する – Qiita

ところでコンポジションという方法ですが、これは具体的に言うと継承などのオブジェクト指向特有の機能を使うのではなく、クラスのメンバに他クラスの静的インスタンス、参照を持たせましょうという思想です。

これはどういう時に使われるかと言うと、多重継承などを必要とするクラスなどで複雑な継承を回避する目的で利用されます。

class IDriven {
public:
    virtual ~IDriven() = default;
    virtual void run() = 0;
    virtual void brake() = 0;
}

class SpeedMeter {
public:
     SpeedMeter() {}
     ~SpeedMeter() {}
     void calcSpeed(int acceleration) { ... }
     void show() { ... }
}

class RedCar : public IDriven {
public:
    explicit RedCar(int a, int b) { ... }
    ~RedCar() { ... }
    void run() override { /* 走行する処理 */ }
    void brake() override { /* 走行をやめる処理 */ }
    bool turnArrow(eDirection arrow, int turnNum) { /* 曲がる処理 */ }
    // ... 他に多くの処理

private:
    SpeedMeter _speedMeter;    // ←コンポジションを利用
}

上記の場合だと、RedCarクラスの中であたかもSpeedMeterクラスを継承したかのようにSpeedMeterクラスのshowメソッドなどを使う事ができます。

更にRedCarを別のクラスにコンポジションで持たせた場合においても、

class RedCar : public IDriven {
public:
    SpeedMeter getSpeedMeter() { return _speedMeter; }
    // ... 省略
}

class Fomula1 {
public:
    Fomula1() {}
    ~Fomula1() {}
    void running() { _redCar.getSpeedMeter(); ... }

private:
    RedCar _redCar;
}

間接的にアクセッサを使用して簡単に取得できます。

一見アクセッサ(プロパティ)の実装が都度必要になる面倒なように見えますが、これによって可読性が損なわれないメリットがあります。

プログラミングは名づけと構造化の設計の繰り返しなので、如何にして後世に扱いやすいものを残すかに尽きます。

ソースコードなんて今は分かっていても、明日にはもう半分くらい分からなくなってます。
これ、真面目な話です(・ω・)


三項演算子

三項演算子についてですが、結構昔から可読性が悪くなるという理由からコーディング規約で使用禁止としているケースが見受けられるようです。

void PlayerSpectacle::changeDirection(const PointF diff) {
    using namespace structure;
    _direction = 0 < diff.X ? DirectArrows::DIR_R : 0 > diff.X ? DirectArrows::DIR_L : _direction;
}

今回のプロジェクトでも割と使っているのですが、可読性が悪いと感じる事はあっても読めない事はないと思います。

私が単純に慣れてしまったせいもあるのでしょうが。

上記の場合は、三項演算子の入れ子をしています。
流石にこれは可読性が悪くなっているように思いますね。

しかし特段、影響の少ない部分では上記でもありな気もしますし、上記の場合はコードが実質一文だけで、メソッド名で可読性の良し悪しを吸収しようとしているので、状況によるという事ですかね。


ラムダ式(クロージャ)

ラムダ式という仕組みがC++でも使えます。

これはオブジェクト指向設計において非常に強力なプログラミングテクニックです。

これは無名関数と呼ばれるスコープの制限された領域における使い回しができるローカル処理です。

スカラ(Scala)などの関数型プログラミングではメイン盾として使われる技術ですね。

実際にプロジェクトの中で使用している箇所がありますので、メソッド掲載します。

void BackgroundScrolling::doNavigate(raw::PointF diff, int32_t clientRoomNo, raw::PointF& basePoint, double& dependencyX, double& dependencyY) {
    _objectAdjustPoint = PointF();
    _clientRoomNo = clientRoomNo;

    // ラムダ構文でクロージャを定義
    auto adjustBackground = [&](double diff, CoordinateF& basePointComponent, double& dependency, CoordinateF& adjustPointComponent, const int64_t centerLine, auto isPossibleMoveFunction) {
        // 画面スクロールができるかどうかを検証します。
        if (!tryMoveBackground(diff, basePointComponent, dependency, centerLine, isPossibleMoveFunction)) {
            if (auto d = dependency + diff; ((0 > diff && d < centerLine) || (0 < diff && d > centerLine)) && basePointComponent) {
                dependency += CoordinateF(diff) - basePointComponent;
                adjustPointComponent = CoordinateF(diff) - basePointComponent;
                basePointComponent = 0;
            }
            else {
                dependency += diff;
                adjustPointComponent = diff;
            }
        }
        else {
            if ((0.0 < diff && centerLine > dependency) || (0.0 > diff && centerLine < dependency)) {
                adjustPointComponent = centerLine - dependency;
                dependency += centerLine - dependency;
                basePointComponent -= diff - static_cast<double>(adjustPointComponent);     // Ensure proper type conversion
            }
            else {
                basePointComponent -= diff;
            }
        }
    };

    // クロージャを使用して、X軸方向の画面スクロールを処理します。
    adjustBackground(diff.X, basePoint.X, dependencyX, _objectAdjustPoint.X, H_CENTER_LINE,
        [](double diff, const AddressScraper& scraper, int pageIndex) -> bool {
            return diff > 0 ? scraper.isPossibleGoRight(pageIndex) : scraper.isPossibleGoLeft(pageIndex);
        });

    // クロージャを使用して、Y軸方向の画面スクロールを処理します。
    adjustBackground(diff.Y, basePoint.Y, dependencyY, _objectAdjustPoint.Y, V_CENTER_LINE,
        [](double diff, const AddressScraper& scraper, int pageIndex) -> bool {
            return diff > 0 ? scraper.isPossibleGoUnder(pageIndex) : scraper.isPossibleGoOver(pageIndex);
        });
}

これは画面スクロールをする際に実際にスクロールを命令する距離(raw::PointF diff)を渡して画面スクロール処理を行うためのメソッドです。
色々書いてますが、代入文の箇所は無視してください(^^;)

注目するのは、クロージャを定義してそれを二回利用している点です。

このような状況では、クロージャを使って同じ処理をしている箇所を一箇所に集約する事で、メンテナンス箇所もシンプルに集約されます。(auto adjustBackground = [&](double diff…の箇所です)

それによって行っている処理が読みやすくなります。

しかしこれを使用する場合において注意点は、必ず共通する処理の箇所で利用するという使用条件があります。
あくまで目的はコードを減らす事ではなく、可読性を維持するためです。

上記はauto型のクロージャを定義しています。
これは型推論と呼ばれるC++の機能です。

現状はreturnしておらず実質はvoid型になるのですが、型推論を使用して、コードに戻り値を追加した場合についても、そのままクロージャの実行部分で代入式を書くだけで戻り値の型を指定する必要がない事を示すものです。

では、ここでラムダ式を使わなかったらどうなるのか?

それは次の通りです。


リファクタリングと生成A.I.の親密な関係について

私は普段からよく使っているのですが、OpenAI社のChatGPT(4.0)はここ数年で話題のAIチャットサービスで、このチャットを使うと様々な人間の質問に対して有機質な傾向の回答を得る事ができます。

ChatGPT 4.0 は $20 / month で使用可能な代表的なチャット形式AIサービス、無料利用も可能となっている

この記事のアイキャッチ画像もAIで作成したものを適当にチョイスして掲載しております。


このサービスは色んな事に使えてなかなか面白いのですが、私が個人的に注目しているのは特定のカテゴリに対する技術支援です。

プログラミングで言うと、やはりリファクタリングを代行、または提案してくれる事でしょう。

クリエイティブな作業が目立つコーディングにおいて、不要な関数の削除や、バグの要因解析、はたまたVisual StudioのIntelliSenseのようなコードのリファクタリングもしてくれます!

例えば、上記の画面スクロールの処理は元々以下のようにコーディングしました。

void BackgroundScrolling::doNavigate(raw::PointF diff, int32_t clientRoomNo, raw::PointF& basePoint, double& dependencyX, double& dependencyY) {
    _objectAdjustPoint = raw::PointF();
    _clientRoomNo = clientRoomNo;

    if (diff.X) {    // Shift left and right.
        if (!tryMoveBackgroundHorizonal(diff.X, basePoint, dependencyX)) {  // Fixed screen.
            if (basePoint.X) {
                dependencyX += diff.X - basePoint.X;
                _objectAdjustPoint.X = diff.X - basePoint.X;
                basePoint.X -= basePoint.X;
            }
            else {
                dependencyX += diff.X;
                _objectAdjustPoint.X = diff.X;
            }
        }
        else {  // Slide the background image horizontally.
            if ((0.0 < diff.X && H_CENTER_LINE > dependencyX) || (0.0 > diff.X && H_CENTER_LINE < dependencyX)) {
                _objectAdjustPoint.X = H_CENTER_LINE - dependencyX;
                dependencyX += H_CENTER_LINE - dependencyX;
                basePoint.X -= diff.X - _objectAdjustPoint.X;
            }
            else {
                basePoint.X -= diff.X;
            }
        }
    }
    if (diff.Y) {    // Shift up and down.
        if (!tryMoveBackgroundVertical(diff.Y, basePoint, dependencyY)) {  // Fixed screen.
            if (basePoint.Y) {
                dependencyY += diff.Y - basePoint.Y;
                _objectAdjustPoint.Y = diff.Y - basePoint.Y;
                basePoint.Y -= basePoint.Y;
            }
            else {
                dependencyY += diff.Y;
                _objectAdjustPoint.Y = diff.Y;
            }
        }
        else {  // Slide the background image vertically.
            if ((0.0 < diff.Y && V_CENTER_LINE > dependencyY) || (0.0 > diff.Y && V_CENTER_LINE < dependencyY)) {
                _objectAdjustPoint.Y = V_CENTER_LINE - dependencyY;
                dependencyY += V_CENTER_LINE - dependencyY;
                basePoint.Y -= diff.Y - _objectAdjustPoint.Y;
            }
            else {
                basePoint.Y -= diff.Y;
            }
        }
    }
}

// true:BG Scrolling、false:BG Scroll locked
bool BackgroundScrolling::tryMoveBackgroundHorizonal(double x, raw::PointF& basePoint, double& dependencyX) {
    auto [scraper, pageindex] = createScraperAndGetPageIndex();

    if (0.0 < x) {    // Go to the right.
        auto newline = dependencyX + x;
        if (newline <= H_CENTER_LINE) {  // if left side of screen
            return false;
        }
        else if (0.0 > (basePoint.X - x) && !scraper.isPossibleGoRight(pageindex)) {  // There is no right neighbor, the entire screen is currently displayed (map page base point is 0)
            return false;
        }
    }
    else if (0.0 > x) {   // Go to the left.
        auto newline = dependencyX + x;
        if (newline >= H_CENTER_LINE) {  // if right side of screen
            return false;
        }
        else if (0.0 < (basePoint.X - x) && !scraper.isPossibleGoLeft(pageindex)) {   // There is no left neighbor, the entire screen is currently displayed (map page base point is 0)
            return false;
        }
    }
    return true;
}

// true:BG Scrolling、false:BG Scroll locked
bool BackgroundScrolling::tryMoveBackgroundVertical(double y, raw::PointF& basePoint, double& dependencyY) {
    auto [scraper, pageindex] = createScraperAndGetPageIndex();

    if (0.0 < y) {    // Proceed down.
        auto newline = dependencyY + y;
        if (newline <= V_CENTER_LINE) {  // if upper of screen
            return false;
        }
        else if (0.0 > (basePoint.Y - y) && !scraper.isPossibleGoUnder(pageindex)) {  // There is no lower adjacency, the entire screen is currently displayed (map page base point is 0)
            return false;
        }
    }
    else if (0.0 > y) {   // Proceed up.
        auto newline = dependencyY + y;
        if (newline >= V_CENTER_LINE) {  // if lower of screen
            return false;
        }
        else if (0.0 < (basePoint.Y - y) && !scraper.isPossibleGoOver(pageindex)) {   // There is no upper neighbor, the entire screen is currently displayed (map page base point is 0)
            return false;
        }
    }
    return true;
}

結構長いですね。

よく見ると、同じようなコードを同じように定義して書いてあります。

これをChatGPTに丸投げしてみました。

そこで得た回答が以下です。

void BackgroundScrolling::doNavigate(raw::PointF diff, int32_t clientRoomNo, raw::PointF& basePoint, double& dependencyX, double& dependencyY) {
    using namespace raw;
    _objectAdjustPoint = PointF();
    _clientRoomNo = clientRoomNo;

    auto adjustBackground = [&](double diff, CoordinateF& basePointComponent, double& dependency, CoordinateF& adjustPointComponent, const int64_t centerLine, auto isPossibleMoveFunction) {
        if (!tryMoveBackground(diff, basePointComponent, dependency, centerLine, isPossibleMoveFunction)) { // Fixed screen
            if (basePointComponent) {
                dependency += CoordinateF(diff) - basePointComponent;
                adjustPointComponent = CoordinateF(diff) - basePointComponent;
                basePointComponent = 0;     // Reset basePointComponent instead of subtracting itself
            }
            else {
                dependency += diff;
                adjustPointComponent = diff;
            }
        }
        else { // Slide the background image
            if ((0.0 < diff && centerLine > dependency) || (0.0 > diff && centerLine < dependency)) {
                adjustPointComponent = centerLine - dependency;
                dependency += centerLine - dependency;
                basePointComponent -= diff - static_cast<double>(adjustPointComponent);     // Ensure proper type conversion
            }
            else {
                basePointComponent -= diff;
            }
        }
        };

    // Adjust horizontally
    adjustBackground(diff.X, basePoint.X, dependencyX, _objectAdjustPoint.X, H_CENTER_LINE,
        [](double diff, const AddressScraper& scraper, int pageIndex) -> bool {
            return diff > 0 ? scraper.isPossibleGoRight(pageIndex) : scraper.isPossibleGoLeft(pageIndex);
        });

    // Adjust vertically
    adjustBackground(diff.Y, basePoint.Y, dependencyY, _objectAdjustPoint.Y, V_CENTER_LINE,
        [](double diff, const AddressScraper& scraper, int pageIndex) -> bool {
            return diff > 0 ? scraper.isPossibleGoUnder(pageIndex) : scraper.isPossibleGoOver(pageIndex);
        });
}

// true:BG Scrolling、false:BG Scroll locked
bool BackgroundScrolling::tryMoveBackground(double diff, raw::CoordinateF& basePoint, double& dependency, const int64_t centerLine, std::function<bool(double, const AddressScraper&, int)> isPossibleMove) {
    auto [scraper, pageindex] = createScraperAndGetPageIndex();
    auto newline = dependency + diff;

    if (0.0 < diff) {    // Proceed in positive direction.
        if (newline <= centerLine) {  // if before center of screen
            return false;
        }
        else if (0.0 > (basePoint - diff) && !isPossibleMove(diff, scraper, pageindex)) {
            return false;
        }
    }
    else if (0.0 > diff) {   // Proceed in negative direction.
        if (newline >= centerLine) {  // if after center of screen
            return false;
        }
        else if (0.0 < (basePoint - diff) && !isPossibleMove(diff, scraper, pageindex)) {
            return false;
        }
    }
    return true;
}

メソッドが一つ無くなってますが、機能はなくなってません。
行数を60%に削減し、冗長だった同じ文はあっという間に解消されました。

isPossibleMoveFunctionは引数ですが、この型はautoになっています。
これはC++20で導入された非型関数テンプレートと呼ばれるものです。

このコードを書くより、今このエントリーを書いているほうが長く掛かっているくらいです(笑)😂

実はC#.NETのマップエディタもこの強力なサポートを得て、アプリケーションとして構築しています。

そしてこのリファクタリング技術は全てAIが独自に生成した知識である点には驚かざるを得ません。

人類はとんでもない技術を得てしまいました(謎


アクションアルゴリズムのリファクタリング

そして極めつけのリファクタリングが以下になります。

まずは元のコードです。

void MainPlayer::process(components::FrameworkConnector& parameter) {
    operateActionForStatus(parameter);
}

void MainPlayer::operateActionForStatus(components::FrameworkConnector& parameter) {
    using namespace input;
    using namespace structure;
    const size_t DR = 0;    // NOTE : Refer to the graphic tileset to find the location of the left and right sprites.
    const size_t DL = 100;  // NOTE : Refer to the graphic tileset to find the location of the left and right sprites.
    auto leftKey = parameter.getPressKey(JPBTN::LEFT);
    auto rightKey = parameter.getPressKey(JPBTN::RIGHT);
    auto xMoveSign = 0 < leftKey ? -1 : 0 < rightKey ? 1 : 0;
    changeDirection(PointF(xMoveSign, 0.0, 0.0));
    auto relations = _direction == DirectArrows::DIR_R ? DR : DL;
    if (eStatus::UNCONTROLLABLE == _status) {
        // ???
    }
    else if (eStatus::DAMAGED == _status) {
        // ダメージアニメーション?
    }
    else if (eStatus::LADDERING == _status) {
        if (0 < parameter.getPressKey(JPBTN::UP) || 0 < parameter.getPressKey(JPBTN::DOWN)) {
            // はしごアクション?
        }
    }
    // TODO : チェック判定が多すぎます。可読性に悪影響
    // HACK : speedY = 0x00.40P0がいたる箇所に書かれているが、この値は重力指数です。重力に対する速度は「v=u+at」の運動方程式で割り出される事が推奨されます
    else if (eStatus::STANDING == _status || eStatus::LAUNCH_RUN == _status || eStatus::RUNNING == _status || eStatus::BRAKE_RUN == _status || eStatus::LANDING == _status || eStatus::SLIDING == _status) {
        _speedY = 0x00.40P0;
        // Begin a running.
        if (leftKey || rightKey) {
            if (eStatus::RUNNING == _status) _textureNumber = _floor.running(_status, xMoveSign, _speedX, _textureNumber) + relations;
            else _textureNumber = _floor.beginRunning(_status, _direction, _speedX) + relations;
        }
        // Continuous running.
        else if ((!leftKey && !rightKey)) {
            if (eStatus::LAUNCH_RUN == _status || eStatus::RUNNING == _status || eStatus::BRAKE_RUN == _status) _textureNumber = _floor.endRunning(_status, _direction, _speedX, _textureNumber) + relations;
            // After landing from the air...
            else if (eStatus::LANDING == _status) {
                _floor.brakes(_status, _textureNumber, relations);
                _speedX = 0x00P0;
            }
            else _speedX = 0x00P0;
        }
        parameter.setDifference(_name, DiffEval::X, _speedX);
        parameter.setDifference(_name, DiffEval::Y, _speedY);
        commitAttributes(_name, parameter);

        // Check if there is a floor and fall down?
        inputForAttributesPoint(DiffPointF(0.0, _speedY, 0.0));
        if (!_collision.hasTouchTheFloor(parameter.getPageNo(), parameter.getBasePoint(), DirectionEval::DOWN, _speedY)) {
            _air.checkTheSpaceAbove(parameter, _speedY);        // Plateau stops rising.
            _air.falling(_status, parameter.getPressKey(JPBTN::A), xMoveSign, _speedX, _speedY);
            _textureNumber = 40 + relations;
        }
        else if (1 == parameter.getPressKey(JPBTN::A)) {      // This is on the floor.
            if (_air.checkTheSpaceAbove(parameter, _speedY)) {
                _air.jumping(_status, xMoveSign, _speedX, _speedY);
                _textureNumber = 40 + relations;
            }
        }

        if (eStatus::STANDING == _status) _textureNumber = _floor.waiting() + relations;
    }
    else if (eStatus::HOVERING == _status) {
        auto onFloor = false;
        _speedX = xMoveSign ? xMoveSign * 0x01.50P0 : 0x00P0;
        auto xDir = 0.0 <= _speedX ? DirectionEval::RIGHT : DirectionEval::LEFT;
        parameter.setDifference(_name, DiffEval::X, _speedX);
        parameter.setDifference(_name, DiffEval::Y, _speedY);
        commitAttributes(_name, parameter);

        if (0.0 > _speedY) {
            inputForAttributesPoint(DiffPointF(0.0, _speedY, 0.0));
            if (_collision.hasTouchTheFloor(parameter.getPageNo(), parameter.getBasePoint(), DirectionEval::UP, _speedY)) _speedY = 0x00P0;
        }
        else if (0.0 < _speedY) {
            inputForAttributesPoint(DiffPointF(0.0, _speedY, 0.0));
            onFloor = _collision.hasTouchTheFloor(parameter.getPageNo(), parameter.getBasePoint(), DirectionEval::DOWN, _speedY);
        }
        else {
            inputForAttributesPoint(DiffPointF(0.0, 0x01.00P0, 0.0));
            onFloor = _collision.hasTouchTheFloor(parameter.getPageNo(), parameter.getBasePoint(), DirectionEval::DOWN, 0x01.00P0);
        }
        if (!onFloor) {
            _air.falling(_status, parameter.getPressKey(JPBTN::A), xMoveSign, _speedX, _speedY);
            _textureNumber = 40 + relations;
        }
        else {      // This is on the floor.
            if (1 == parameter.getPressKey(JPBTN::A)) {
                if (_air.checkTheSpaceAbove(parameter, _speedY)) {
                    _air.jumping(_status, xMoveSign, _speedX, _speedY);
                    _textureNumber = 40 + relations;
                }
            }
            else {
                _textureNumber = _floor.landing(_status, _speedX) + relations;
                parameter.setDifference(_name, DiffEval::Y, parameter.getDifference(_name).Y + 0x00.01P0);
                _speedY = 0x00.40P0;
            }
        }
    }
}

長いですね。

しかしこれでも短くなったというのが正直な感想です。

それはこのプロジェクト以前に作成していた元コードが存在し、それはなんと上記のコード行数の倍規模にまで登るものでした。

bool PlayerObject::Sub_PlaneMove(void) {

    // TODO:全体的に最適化したい。(現状は移植性を高めるために解析結果をそのまま反映して実装している)
    // This code installs the analysis of NES ROM(Rockman2) data as it is. It's not natural for source code standards. 

    // [歩行、ジャンプ] 前提条件として床の上にいる状態 かつ 操作可能状態である事
    if (isControllable && onFloor) {
        if (Sub_WalkInterruptCheck() && (Get_HoldKeyC16Value(C16_RIGHT) || Get_HoldKeyC16Value(C16_LEFT))) {
            // 歩行フラグを参照する。歩行処理の継続なのか、歩行処理を開始するのか
            if (isWalk) {
                // ロックマンの歩行ステータスを細かく監視し、それぞれに応じた処理を実行する。
                if (4 == status) {
                    /* 歩行開始後の処理 */
                    if (!Sub_BeginWalkScripts()) return false;
                }
                else if (5 == status) {
                    /* 歩行中の継続処理 */
                    if (!Sub_WalkerScripts()) return false;
                }
            }
            else {
                /* 歩行開始の処理 */
                if (!Sub_BeginWalkScripts()) return false;
            }
        }
        else {
            // 歩行を行っていた場合 (status == 4, 5, 7, 8)
            if (4 <= status && 8 >= status && 6 != status) {
                /* 歩行終了時の処理 (クリープ) */
                if (!Sub_EndWalkScripts()) return false;
            }
            // ■ 歩行処理をしていないデフォルトケースはここを通過する。
            else {
                if (3 == status) speedX = (float)0x00.00P0;  // 横移動速度の設定
            }
        }

        /*
         * スライディングをするためには、地上で立ち状態の時に十字キーの下とジャンプキーを同時に押下する事でアクションを開始する。
         */
        if (Get_HoldKeyC16Value(C16_DOWN) && 1 == Get_HoldKeyC16Value(C16_BATSU)) {
            
            // スライディング実行可能判定
            attribute.head.enable = true;
            if (1 == Sub_RelocateAttributesPointWithObject(ATTRIBUTE_OF_SLIDE_POINTS, (0.00f * EXPANDED_SCALE), AND, 0, 0, 0)) {
                /* スライディング処理 */
                if (!Sub_SliderScripts()) return false;
            }
            attribute.head.enable = false;
        }
        else {
            // スライディングフラグを参照する。スライディング継続なのか、スライディングしないのか
            if (isSlider) {
                // スライディング実行可能判定
                if (1 == Sub_RelocateAttributesPointWithObject(ATTRIBUTE_OF_FRONT_POINTS, (speedX * EXPANDED_SCALE), AND, 0, 0, 0)) {
                    if (11 == status) {
                        attribute.ceil.enable = true;
                        if (1 == Sub_RelocateAttributesPointWithObject(ATTRIBUTE_OF_CEIL_POINTS, (-2.00f * EXPANDED_SCALE), OR, 1, 1, 1)) {
                            /* スライディング処理 */
                            if (!Sub_SliderScripts()) return false;
                        }
                        else {
                            /* スライディング中断処理 */
                            if (!Sub_EndSliderScripts()) return false;
                        }
                        attribute.ceil.enable = false;
                    }
                    else if (!Sub_SliderScripts()) return false;
                }
                else {
                    attribute.ceil.enable = true;
                    if (1 == Sub_RelocateAttributesPointWithObject(ATTRIBUTE_OF_CEIL_POINTS, (-2.00f * EXPANDED_SCALE), OR, 1, 1, 1)) {
                        /* スライディング処理 */
                        if (!Sub_SliderScripts()) return false;
                    }
                    else {
                        /* スライディング中断処理 */
                        if (!Sub_EndSliderScripts()) return false;
                    }
                    attribute.ceil.enable = false;
                }
            }
            else {

            }
        }

        // ジャンプしていない場合重力荷重
        speedY = (float)-0x00.40P0;     // 縦移動速度の設定

        movX = (speedX * EXPANDED_SCALE);
        movY = (speedY * EXPANDED_SCALE);

        // 当たり判定の再評価をする
        if (!Sub_RelocateAttributesPointWithObject()) return false;

        // 着地判定
        if (1 == Sub_RelocateAttributesPointWithObject(ATTRIBUTE_OF_FLOOR_POINTS, (0.00f * EXPANDED_SCALE), AND, 0, 0, 0)) onFloor = false;
        else onFloor = true;

        // 宙に出ている場合は重力処理
        if (!onFloor) {
            if (!Sub_FallingScripts()) return false;
        }
        else {
            /*
             * 飛び上がるためには、地上にいる状態時にジャンプキーを押下する事でアクションを開始し、押下を継続する事でアクションを継続する。
             */
             // {×}キーの押下によりジャンプ(処理)を開始する
            if (Sub_JumpInterruptCheck() && (!Get_HoldKeyC16Value(C16_DOWN)) && 1 == Get_HoldKeyC16Value(C16_BATSU)) {
                /* ジャンプ開始の処理 */
                if (!Sub_JumpScripts()) return false;
            }
        }
        // 待機処理
        if (!Sub_FreeStandingScripts()) return false;
    }
    else if (isControllable && !(onFloor)) {
        // 宙のX軸移動量
        if (Get_HoldKeyC16Value(C16_RIGHT) || Get_HoldKeyC16Value(C16_LEFT)) {
            speedX = (float)0x01.50P0;
        }
        else speedX = 0x00.00P0;

        movX = (speedX * EXPANDED_SCALE);
        movY = (speedY * EXPANDED_SCALE);

        // 当たり判定の再評価をする
        if (!Sub_RelocateAttributesPointWithObject()) return false;

        
        // 縦方向移動速度がプラスの場合は天井判定
        if ((float)0x00.00P0 < movY) {
            if (1 == Sub_RelocateAttributesPointWithObject(ATTRIBUTE_OF_CEIL_POINTS, (0.01f * EXPANDED_SCALE), OR, 1, 1, 1)) {
                isJump = false;
                speedY = 0x00.00P0;     // 縦移動速度の設定
            }
        }
        // 縦方向移動速度がプラスではない場合は着地判定
        else {
            if (1 == Sub_RelocateAttributesPointWithObject(ATTRIBUTE_OF_FLOOR_POINTS, (0.00f * EXPANDED_SCALE), AND, 0, 0, 0)) onFloor = false;
            else onFloor = true;
        }

        if (!onFloor) {
            if (Get_HoldKeyC16Value(C16_BATSU)) {
                // 継続してジャンプを行っている場合
                if (isJump) {
                    /* ジャンプ継続の処理 */
                    if (!Sub_JumpScripts()) return false;
                }
                else {
                    /* 落下の処理 */
                    if (!Sub_FallingScripts()) return false;
                }
            }
            else {
                /* 落下の処理 */
                if (!Sub_FallingScripts()) return false;
            }
        }
        else {
            /*
             * 飛び上がるためには、地上にいる状態時にジャンプキーを押下する事でアクションを開始し、押下を継続する事でアクションを継続する。
             */
             // {×}キーの押下によりジャンプ(処理)を開始する
            if (1 == Get_HoldKeyC16Value(C16_BATSU)) {
                /* ジャンプ開始の処理 */
                if (!Sub_JumpScripts()) return false;
            }
            // 着地判定で着地している場合は着地処理
            else if (!Sub_LandingScripts()) return false;
        }
    }
    return true;
}

一番上にコメントで「移植性を高めるために解析結果をそのまま反映している」と書かれていますが、簡潔に言うとこのプロジェクトは、ファミコンのロムをPCに吸い上げたデータを元にアルゴリズムのメモリ解析を実施して作成している背景があります。

ぶっちゃけそれはしちゃいけないのですが、プロジェクトの方針上そういう事をしていますヾノ’∀’o)ホントハダメ
(まあちょっとその話はまた今度。。。笑)

話が脱線したのですが、以下リファクタリング結果です。

class IChangeAttribute {
public:
    virtual void inputForAttributesPoint(const raw::DiffPointF) = 0;
    virtual void commitAttributes(std::wstring, components::FrameworkConnector&) = 0;
    virtual SpriteCollision getCollision() const = 0;
    virtual int8_t getDirection() = 0;
    virtual void changeDirection(const PointF) = 0;
    virtual ~IChangeAttribute() = default;
};
class IPlayerBehavior {
public:
    virtual bool execute(IChangeAttribute& player, components::FrameworkConnector& parameter) = 0;
    virtual ~IPlayerBehavior() = default;
};
class FloorBehavior : public IPlayerBehavior {
public:
    explicit FloorBehavior(std::wstring name, eStatus& status, size_t& texture, double& x, double& y, std::array<int16_t, 2>& counter)
        : _name(name), _status(status), _textureNumber(texture), _speedX(x), _speedY(y), _counter(counter) {}
    ~FloorBehavior() {}

protected:
    std::wstring _name;
    eStatus& _status;
    size_t& _textureNumber;
    double& _speedX;
    double& _speedY;

    size_t beginRunning();
    size_t running(const size_t relations);
    size_t endRunning(const size_t relations);
    size_t brakeRunning(const size_t relations);
    size_t waiting();
    void falling(bool isJump);
    void jumping();
};
class LandingBehavior : public FloorBehavior {
public:
    explicit LandingBehavior(std::wstring name, eStatus& status, size_t& texture, double& x, double& y, std::array<int16_t, 2>& counter)
        : FloorBehavior(name, status, texture, x, y, counter) {}
    ~LandingBehavior() {}
    bool execute(IChangeAttribute& player, components::FrameworkConnector& parameter) override;
};


class BrakeRunBehavior : public FloorBehavior {
public:
    explicit BrakeRunBehavior(std::wstring name, eStatus& status, size_t& texture, double& x, double& y, std::array<int16_t, 2>& counter)
        : FloorBehavior(name, status, texture, x, y, counter) {}
    ~BrakeRunBehavior() {}
    bool execute(IChangeAttribute& player, components::FrameworkConnector& parameter) override;
};


class RunningBehavior : public FloorBehavior {
public:
    explicit RunningBehavior(std::wstring name, eStatus& status, size_t& texture, double& x, double& y, std::array<int16_t, 2>& counter)
        : FloorBehavior(name, status, texture, x, y, counter) {
    }
    ~RunningBehavior() {}
    bool execute(IChangeAttribute& player, components::FrameworkConnector& parameter) override;
};


class LaunchRunBehavior : public FloorBehavior {
public:
    explicit LaunchRunBehavior(std::wstring name, eStatus& status, size_t& texture, double& x, double& y, std::array<int16_t, 2>& counter)
        : FloorBehavior(name, status, texture, x, y, counter) {
    }
    ~LaunchRunBehavior() {}
    bool execute(IChangeAttribute& player, components::FrameworkConnector& parameter) override;
};


class StandingBehavior : public FloorBehavior {
public:
    explicit StandingBehavior(std::wstring name, eStatus& status, size_t& texture, double& x, double& y, std::array<int16_t, 2>& counter)
        : FloorBehavior(name, status, texture, x, y, counter) {
    }
    ~StandingBehavior() {}
    bool execute(IChangeAttribute& player, components::FrameworkConnector& parameter) override;
};
class MainPlayer : public PlayerSpectacle {
public:
    explicit MainPlayer(std::wstring name, eStatus status, raw::PointF launchPoint);
    ~MainPlayer() {}
    bool process(components::FrameworkConnector&) override;

private:
    std::wstring _name;
    size_t _textureNumber;
    std::map<eStatus, std::unique_ptr<IPlayerBehavior>> _actionMap;

    bool onTheFloor() const;
};

MainPlayer::MainPlayer(std::wstring name, eStatus status, raw::PointF launchPoint) : _name(name), PlayerSpectacle(launchPoint, status, structure::DirectArrows::DIR_R) {
    _textureNumber = 0;
    _actionMap[eStatus::BRAKE_RUN]  = std::make_unique<BrakeRunBehavior>(name, _status, _textureNumber, _speedX, _speedY, _generalCounter);
    _actionMap[eStatus::LANDING]    = std::make_unique<LandingBehavior>(name, _status, _textureNumber, _speedX, _speedY, _generalCounter);
    _actionMap[eStatus::LAUNCH_RUN] = std::make_unique<LaunchRunBehavior>(name, _status, _textureNumber, _speedX, _speedY, _generalCounter);
    _actionMap[eStatus::RUNNING]    = std::make_unique<RunningBehavior>(name, _status, _textureNumber, _speedX, _speedY, _generalCounter);
    _actionMap[eStatus::STANDING]   = std::make_unique<StandingBehavior>(name, _status, _textureNumber, _speedX, _speedY, _generalCounter);
}

bool MainPlayer::process(components::FrameworkConnector& parameter) {
    if (_actionMap.find(_status) != _actionMap.end()) {
        if (!_actionMap[_status]->execute(*this, parameter)) return false;
    }
    return true;
}

ちょっと長くなってしまいましたが、上記のように operateActionForStatus メソッドの中身は全て _actionMap[_status] の mapコンテナに集約され、プレイヤーのステータス(着地、スタンド、浮遊など)に応じて IPlayerBehaviorインターフェースを継承した各アクションアルゴリズムの実行メソッド(execute)で処理されます。

これにより、ジャンプ処理はジャンプ用のクラスを参照すればすぐに処理まで辿り着けますし、カプセル化されているため、他のアクションと干渉する事もほとんどありません。

これはステートパターンというGoFのデザインパターンを使用しています。

これの派生パターンでストラテジパターンというものもあり、それを使っても同じ要領でmapコンテナに格納して管理できます。

そして、C++ではメモリアクセスを頻繁に行うが故の代償でメモリリークが背中合わせに危険因子として存在していますが、スマートポインタを惜しみなく使ってアクションの破棄もバッチリです。

上記の設計を駆使して色々と実装してきましたが、C++はC#.NETと比べても少し難解で言語仕様が複雑な気がしますが、意味が分かればかなり構造化された再利用性の高いライブラリなども作れると思います。

近年C++の人気が再燃しているのを聞きましたが、敢えて選ぶのではなく時代が必要とするニーズに答えられる数少ない言語なのかもしれません😄


8方向スクロールについて

画面スクロール、ロックマンの移動動作を実装したので、以前から気になっていたFCロックマンでは未実装だった8way scroll(8方向スクロール)をやってみました。

今のところステージなどは作ってないのでどういった形で採用するかは未定ですが、折角なのでどこかで使ってみようと思います。

ファミコンのロックマンという感じじゃないところがなんとも新鮮というか、違和感というか・・・
賛否あるかもしれません。

ちなみにまだサウンド関連は何も実装されてません。

次の課題

とりあえずロックマンの基本アルゴリズムは先に一通り実装していく予定にしているので、次の記事では梯子のアクションとスライディング、ロックバスター(標準武器)のいずれかの紹介になるかと思います。

ちょっとブログが放置気味になりつつあるので、何か別のネタのエントリーを挟もうかと思いますが、基本的にはしばらくこの記事の開発後記が中心になりそうです。

一応後続の開発アジェンダを掲載しておきますね。

  1. はしごの昇り降りアクションの実装
  2. スライディングアクションの実装
  3. ロックマンの武器の実装
  4. 効果音の実装
  5. ロックマンの色チェンジアルゴリズムの実装
  6. 固定画面スクロールの実装
  7. 自動画面スクロールの実装
  8. 敵キャラクターの実装
  9. バイタリティー概念の導入
  10. READY~登場アニメーションの実装

特殊武器とか、BGMとかはまだ先になるかと思います。

上記だけで2ヶ月くらい掛かるんじゃないですかね。
サウンドドライバの実装がまだインフラ開発になるので、そこが時間掛かるかな、と。

まあちまちまやります(^。^)y-.。o○

コメント

タイトルとURLをコピーしました