【Rockman2 hack】拡張性・保守性を考える

DALL·E-2024-04-28-18.12.08-A-realistic-image-depicting-a-snake-and-a-crocodile Rockman Reanimated
DALL·E-2024-04-28-18.12.08-A-realistic-image-depicting-a-snake-and-a-crocodile

Windowsにファミコン版ロックマンを移植して、更にハックロム的作成をしていきましょうというプロジェクトです。

ここ数日は仕事が忙しく、同人ロックマン開発がちょっと停滞していますが、コードのリファクタリングを中心に行っておりました。

進捗ですが、前回とほぼ同じ内容の動画を一応アップしておきます。

つまらない内容ですがよろしければご覧ください(^ω^;

今回は、コードの内容について気になった点をいくつか掘り起こしてみたいと思います。


リスクの保有について

以前私のTwitterにもちょくちょく上げておりましたが、ロックマンが床に1ドットめり込んで入ってしまう問題が発生していました。

これは既存バグでした。
ですが実際にはこの現象は限定的に発生するようで、縦方向にスクロールするシーンでのみ発生するようです。
原因は、下に移動する際に小数点を整数に置き換えてBGタイル(背景タイル)の描画座標を決定しているため実際の想定される座標に対して数が切り捨てられる事で起きていたのです。

具体的に言うと、
ロックマンの座標がY軸120(画面の中央線、全体の解像度が縦240のため)で、例えばはしごでも落下でもした時に上下に移動した時、座標値が119.875になった途端BGタイルの描画座標は119で描いてしまい、ロックマンの位置は120想定でBGタイルの位置が119となり位置がずれてしまう要因となっていたのです。
これはオブジェクトの座標計算が浮動小数点数計算をする仕様で、BGタイルは整数換算する仕様という設計バグがあったからです。

上方向なら120.00→120.125で120で問題ないですが、120.00→119.875になった時は119になってしまうと1.0以上移動する事になってしまうため、意図しない位置にロックマンが入ってしまうという。

よく分からない話をしているかもしれません。ごめんなさい🙇‍♂️

そしてこれを取り除くには、既存の画像描画処理に加え、画像データの受渡し用クラスの変更が必要であることが分かりました。

しかし現状のクラス・メソッドロジックでこの仕様変更は規模が大きく単にhotfixで修正する問題ではなく、下手をするとfeatureブランチを分岐して対応するレベルだと感じた私は、このリスクを保有する決断をしました。

問題なのは、現状のクラス設計において画面描画のメソッドや処理実装については、既に機能要件としてコミットしているものであり、これについて改変を行う事で既存の構築されたプログラムに不具合がもたらされる可能性がある事です。

作成したものを改変できないという状態は、拡張性に欠けている可能性があります。
例えば、メソッドが一つの機能のみを有しているという「単一責任の原則」ルールに反している場合は、AとB2つの機能を持っているPというメソッドがあり、Aの機能を持ったQというメソッドを作成しようとした時に、Pで既にAの機能を使っているため、Qで再びAの機能を含めるか、Aの機能をメソッド化する必要が出てきます。
この時点でメンテナンス性が低下し、再利用性(すなわち拡張性)が失われる事になります。

しかしこの同じ処理を一つにまとめる共通化には良しあしがあり、Aのクラスの中に同じ処理Pが存在する場合は、処理Pメソッドを作成する事で処理を単一化できますが、AのクラスとBのクラスで共通する処理は、まとめる事はできないのです(クラスAからクラスBの処理Pにアクセスするとオブジェクト指向の三原則の「隠ぺい」ができなくなるため)。

そういう場合は処理Pを含んだCのクラスを作ればいいじゃないとなるかもしれません。
ですが、それはシンプルにしておけよ(KISS)という有名な原則に基づいて共通化する趣旨から返って逸脱してしまうのです。
ただ、それもしてはいけないとは言い切れないのです。

結局どうすればいいの?という話になるんですが、はっきり言いましょう。

プログラミングにセオリーを求めてはダメです。

状況によって、(必要に応じて)冗長にするべきです。
状況によって、単一化にするべきです。
それが答えです。

話がどんどん脱線していってますがリスクを保有するという事と言うよりかは、「負担」と考えずに必要なものと捉える事が重要だと考えます。

カプセル化と共有パラメータについて

ゲームを開発していると「キャラクターオブジェクト」、「BG(背景)」、「音源」など素材ファイル毎データを管理するクラスを設計していくようになります。

class SpriteLoader : public Loader, public IClays {
public:
    SpriteLoader(std::wstring filepath);
    ~SpriteLoader();
    bool unzip(int allNum, int xNum, int yNum, int xSize, int ySize) override;
    bool use(int16_t axisX, int16_t axisY, size_t number, bool transparent) const override;
    bool changePalette(uint32_t paletteNo, uint16_t red, uint16_t green, uint16_t blue, uint16_t alpha = 0) override;

private:
    int32_t _softImage;  // 画像データのイメージデータを格納
    std::vector<int> _graphicHandler;  // グラフィックハンドラ
    int _allnum;  // グラフィックハンドラのインデックス番号
    int _xnum;  // グラフィックの分割列数
    int _ynum;  // グラフィックの分割行数
    int _xsize;  // グラフィックの幅(分割後の横幅)
    int _ysize;  // グラフィックの丈(分割後の縦長)
};

上記はキャラクターオブジェクトの素材データをメモリロードして描画、パレット変更などの機能を有したスプライトマネージャークラスですが、このクラスは描画処理だけで例えば描画する座標などは管理していません。

それはオブジェクト自体の描画座標については、ゲームプレイヤーの入力情報「だけ」で決まるものではないからです。
ロックマンは壁の中も突き進めるわけではありませんし、画面スクロールするエリアは中央軸座標に配置されるように座標が調整されます。

この場合は、BG背景の情報や他の敵キャラオブジェクトによってプレイヤーオブジェクトの座標が決定するため、SpriteLoaderクラスに座標メンバを置いてクラスに依存するべきではありません。

ではSpriteLoaderや他のクラスとデータを共有するには例えば依存性の注入(Dependency Injection、DI)を行うのが良いかと思います。

私はオブジェクトクラス間でデータを共有するためのデータをパラメータクラスとして定義し、パラメータを持ったそれぞれのオブジェクトクラスを順番に実行していくようにしました。

この設計を採用して理由についてですが、パラメータの管理の「見える化」を意識した作りにした感じです。

拘りがあったわけではないですが、
対抗策の依存性の注入と比べて、共有するデータを必要最低限にして割と分かりやすくメンバやメソッドを自由に作成できるので、設計変更に強い構造となっています。

正直DIも同じ特徴なんですが、自作クラスなら構造を変える事もできる。
例えば必要最低限のパラメータ引数を他のクラスに引き渡す場合は、インターフェースを拡張定義してそれを使います。
そしてパラメータ本体自身は単独インスタンスなので、データがバラバラになることはありません。

class FrameworkConnector {
public:
    FrameworkConnector() : _mapAttrIndex(std::make_unique<MapAttributeIndexer>()) {}
    ~FrameworkConnector() = default;
    
    C16Button getPressKey() const               { return _pressKey; }
    int64_t getPressKey(JPBTN key) const        { return _pressKey.button[static_cast<int64_t>(key)]; }
    void setPressKey(C16Button value)           { _pressKey = value; }
    
    C16Button getReleasedKey() const            { return _releasedKey; }
    int64_t getReleasedKey(JPBTN key) const     { return _releasedKey.button[static_cast<int64_t>(key)]; }
    void setReleasedKey(C16Button value)        { _releasedKey = value; }
    
    uint16_t getPageNo() const                  { return _pageNo; }
    void setPageNo(uint16_t value)              { _pageNo = value; }
    
    PointF getBasePoint() const                 { return _basePoint; }
    void setBasePoint(PointF value)             { _basePoint = value; }

    DiffPointF getDifference(std::wstring name) const;
    void setDifference(std::wstring name, DiffPointF value);
    void setDifference(std::wstring name, DiffEval type, double value);

    DiffPointF getOriginallyDifference(std::wstring name) const;
    void mirroringDifference() { _differenceMirror = _difference; }

    std::unique_ptr<MapAttributeIndexer>& getMapAttributeIndexer() { return _mapAttrIndex; }

    void addVariantMap(std::wstring key, int32_t value) { _vaMap.insert(std::make_pair(key, value)); }
    std::optional<int32_t> popVariantMap(std::wstring key);

private:
    C16Button _pressKey = {};
    C16Button _releasedKey = {};
    uint16_t _pageNo = 0x00;
    PointF _basePoint = {};
    std::map<std::wstring, DiffPointF> _difference;
    std::map<std::wstring, DiffPointF> _differenceMirror;
    std::unique_ptr<MapAttributeIndexer> _mapAttrIndex;
    std::map<std::wstring, int32_t> _vaMap;
};

色々なデータがありますが、これがクラス間で共有するデータの一覧です。

Visual Studioで見たメモリレイアウト。328バイトあります。

結構な量ですね(^_^;)

これを色んなクラスで使いまわすのは相当なメモリ消費をしそうですがこれは必要最低限だから、そう考えるとゲームは割とリソースを喰うシステムなんだなと感じさせられます。

特にキー入力情報はどのクラスでもインプットとして使用されますからね。常に引っ付いていきます😛
このゲームはファミコン仕様のはずなのですが、なぜか16ボタン式🎮です。そしてWindows PC専用なので、キーボード押下情報、必要なボタン数の押下と解放フレーム数を保持するデータ変数を持つため、これだけで既に256バイト常に消費します。

どうするのがベストプラクティスなのかは、実際にある程度形になってメモリー稼働率などをモニタリングしてみないと分からないです。
ですが今できるデータ消費のエコノミーは可能な限りしておくべきです。

問題は結構作った後にメモリ消費量が増えてどうしようもなくなった場合ですよね。
クラスを多く作ったあとにパフォーマンステストをしても構造がガチガチになってるとどうにもならない事が多いです。
そのためにSOLID原則とかを遵守していくんですけどね( ^ω^)

親が初めにする仕事は一番難しい

プログラミングは一般的に難しい単語と記号をいっぱい並べてシステムを作成するものと思われていそうですが、実は半分が

名づけ

だったりします。

このヘッダー名はちょっと逸脱してますが、最近私の身近でも傍系親族が増えた事があり名づけがあったようです。

すごいですよね。人の名前を決める事ってとても難しい事だと思います。
人間の名前って人生の半分以上を左右するイメージに繋がるわけですし、それを親は決めないといけないんですね。

知っていてもそれをいざするとなるとなかなか決められないですよ。

昔から「将来子供ができたらこの名前にする」と考えていればすんなり決まる事なのかもしれませんが。

そう考えると自分の名前も誰かが決めて今こうしてあるんだな、と思うとなんかちょっと不思議な感じがします。

HNは好きなんです😅
「悠(ゆう)」って名前は無難で気に入ってたりします。
悠久ゆうきゅうの悠、ちょっと壮大な感じが…?女性でも使われる名前ですけどね。
もちろん本名ではないですよ(*′ω′*)


ITの世界でも日々何らかの用語が生み出され続けているわけですが、プログラミングにおいても変数や関数、クラス、オブジェクト、プロジェクトなんか名前を付けないといけない要素がたくさん出てきます。

なかでも定数名を決める時が異様に多く、私自身すごく困っています😖💦

/// <summary>
///  Related processing class between the main character and BG attributes
/// </summary>
class PlayerSpectacle : public interfaces::IModelSupply, public IChangeAttribute {
public:
    explicit PlayerSpectacle(std::wstring name, PointF launchPoint, eStatus status, int8_t direction = structure::DirectArrows::DIR_R);
    ~PlayerSpectacle() {}

    void setAttributionField(std::vector<uint8_t>, TileAttributesContainer) override;
    const std::wstring getLabel() const override { return _spectacleScalar.name; }
    const TextureNumber getTileNumber() const override { return _spectacleScalar.textureNumber; }
    std::unique_ptr<PointF> getBasePoint() const override { return std::make_unique<PointF>(_collision.getCoordinate()); };
    std::unique_ptr<PointF> getCenterPoint() const override;
    void initializeCollision();
    SpriteCollision& getCollision() override { return _collision; }
    int8_t getDirection() override { return _spectacleScalar.direction; }
    void changeDirection(const PointF) override;
    void reverseDirection() override;
    void appendingInCoordinateDifference(std::wstring, FrameworkConnector&) override;
    uint8_t getBehindBG(size_t index) const override;
    uint8_t getBottomBG(size_t index) const override;
    bool registerHeightLineCollisionPoint(const DirectionEval directionMark, const DiffPointF diffPoint, const int8_t positionX) override;
    bool registerWidthLineCollisionPoint(const DirectionEval directionMark, const DiffPointF diffPoint, const int8_t positionY) override;
    // opr = To specify exact match: 0, When specifying partial match: 1.
    bool checkDefinitiveTiles(const uint32_t opr, const eTiles tileType, const std::array<uint8_t, 3> collisionPointSet) override;
    const double getObjectSpeedX() const override { return _spectacleScalar.speedX; }
    const double getObjectSpeedY() const override { return _spectacleScalar.speedY; }

protected:
    SpectacleScalar _spectacleScalar;
    SpriteCollision _collision;

    void assigningForXAttribute(std::wstring, FrameworkConnector&);
    void assigningForYAttribute(std::wstring, FrameworkConnector&);
    bool updateBehindBG(FrameworkConnector&);
    bool updateBottomBG(FrameworkConnector&);

private:
    double parityCheckOfMoveDiffPoints(FrameworkConnector& parameter) const;
    double getTheSpecialFulcrumOfBottom(const DirectionEval directionMark) const;
};

これはプロジェクト内の実際のコードですが、一部掲載してみました。

こうみるとC++の予約語ってほとんどないんですね。

bool、double、override、constなどのキーワードは代替できないのであるのは分かるんですが、データ型にしてもDirectionEval型、PointF型、eStatus型とか独自のクラスを作ってそれをデータ型のように使ってます。

「DirectionEval」なんてのは列挙型ってのが分かりますね。こういう名づけをしたい。


観点というか、ポイントなのは後から見てもそれがどういう意味を成しているのかが直感的にわかる名前を付けているかという点。

例えば「たまねぎ」は丸みを帯びたネギ亜科の野菜ですが、「玉のようなねぎ」というイメージ通りの名前ですよね。
なんの違和感もありません。

たまねぎの名前が「じゃがいも」だったら明らかに変でしょうしね~( ˘•_•˘ ).。oஇ

やっぱり名前って大事なんですよね。

子供の名前も親が付けるって話ですが、それもそれで子供の命運を半分親が決めているって事ですから、親も大変ですね。

私には無理です。そんな責任取れないです。
他人の命を軽々しく決めるものではない、というか私にはそんな権限ないと。
そう思ってる辺り今の立場なのもうなづけるんですが😅

また話が外れてきちゃってますが、名前付けるのができないのはそういった私のポリシーが影響しているのかもしれません。

最近はAIに頼めば候補を出してくれる便利な世の中にはなりましたけどね~(*´ω`*)


さて、引き続き自作ロックマンの開発を進めていくわけですが、

次はロックバスターの実装です。

頑張っていきましょう✨٩(′ω′)و✨

コメント

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