DxLibとC++でゲームを作りたい

ゲームづくりのための覚え書き

ファイル分けってどうすんの?

ファイル分けについて

ゲームを作る上で...というかプログラミングをする上でまず考えなければいけないのがこのファイル分けについて。

というのも、一つのファイルに長々と書いていくと、見づらいし分かりづらいし下までスクロールするのめんどくさい。

 

技術的な面でも色々とあるけどそれは省略。

 

じゃあどうすんねん?って話。

 

私がゲームを作る時にやっているファイル分けの考え方と分けたファイルの中身をどう使うのかを書いていきます。

 

  • クラスごとに分ける
    まずはこれ。クラスごとに分けます。具体的には、cppとhファイルは基本一セット。そして一つのセットに付き基本的にクラスは一つ記述します。

    理由としては、ファイルの名前とクラスの名前を同じにすることでincludeする際に考えなくて良い点や、includeした時に必要ない部分まで展開せずに済む点、機能やデータごとに細かく分けることで実装後の変更の際にイジる必要がある部分を最小限に済ませるため、などがあります。
  • 継承元の純粋仮想関数を持つクラスは.hのみでも良い
    少し難易度が上がり、先の話ですが、継承して実装することが前提のクラスを設計する場合、純粋仮想関数を定義することがあります。
    この場合はcppファイルに書くことがないためhファイルのみでも良いです。

  • 各機能ごとにManagerクラスを作る
    ゲームを作っていると様々なオブジェクトやインスタンスを生成することになります。その際にデータと機能の部分を分離しておかないと管理が大変になるだけでなく、どこで何を保持しているのか一々考えながら書かなければならず面倒です。
    そのため、各機能、オブジェクトごとに管理クラスを作ります。
    名前は〇〇Managerに統一し、フォルダを作ってぶちこみます。
    (例)シューティングゲームの場合
    ・Bullet (敵の弾と自分の弾の区別も必要)
    ・Enemy
    ・Item
    ・MasterData
    ・Scene
    ・Game全体
    などに対してそれぞれManagerを作ります。最後の全体のManagerは特に必須で、GameManagerクラスのUpdate,Draw関数を毎フレーム実行することでゲーム全体が動くように作っていくのが分かりやすいと思います。
    Managerには以下の要素を実装します。
    ・Create〇〇関数
     弾なら弾、敵なら敵を作り出す関数です。全てのオブジェクトは出来るだけManagerのこの関数を通して生成したいです。プログラムが短くなるだけでなく、拡張しやすくなります。
    ・std::vector もしくはstd::list
     Create〇〇関数で生成したオブジェクトを入れておく配列です。もちろんprivate領域に置くこと推奨で、外側から勝手にオブジェクトを増やしたり減らしたり出来ないようにします。間違いやバグの抑制になる他、範囲外エラーなどの抑止にもなるため、stdライブラリの要素は積極的に使っていきましょう。
    ※どっちを使ったらええねんって人へ
     何も考えずに便利なのはvecterです。添字番号も使えるしfor文との相性も抜群で使いやすい。ただし、要素を配列から外した時に勝ってに詰めてくれません。
    頻繁に中身が消えたり入れ替わったりする場合はlistを使った方が良いです。イテレータ使うのが面倒ですが。
    Get〇〇関数、GetList関数
     privateに置いたオブジェクトを外部で使いたいとき、参照したいときにこの関数を通して渡します。まぁ、どこの参考文献でもだいたいやってるし詳しくはそちらで。ワンポイントアドバイスとしては、Listを渡すときはコピーが重いので必ず参照渡しにすることに気をつけたほうが良いかも。

    ※GameManagerの超人化、神化防止について
     良くなってしまいがちな現象です。GameManagerはどこでもincludeするし便利だからこいつに全部の要素実装しちまえばいいや...
    の結果がGameManagerの超人化、神化です。
    結果としてくっそ使いづらくなるし一ファイル一クラスにした意味が無くなります。
    GameManagerはあくまでもゲーム全体を管理するクラスです。
    なので、Player個人についてとか、一つ一つの弾についてとか知ったこっちゃありません。知ってたらまずいです。
    機能として実装するのは、各Managerを生成する機能や入れ物のポインタ変数、ポインタ変数の取得関数とUpdate,Draw関数です。
    あとはまぁ、開発序盤であれば、汎用関数を置いておくのもありです。いずれ汎用関数をまとめたクラスに置くべきですが、DxLibの関数を使いやすくした自作関数とか、画像ロードの多重化を防ぐunorderdmapを使った関数とか・・・

  • あんまりグダグダ書いていても分かりづらいので実際のコードの一例を乗せます

    -------GameManager.h------

    //シングルトンで作られたゲームマネージャクラス
    #include<string>
    #include<unordered_map>

    //クラスがあるよ宣言 cppで.hをincludeすること
    class BulletManager;

    class GameManager {

    protected:
      //コンストラクタ隠蔽
        GameManager();
        ~GameManager();
    public:
        //インスタンスがなければ生成、あれば返す関数
        static GameManager* Instance(); 
     
     //ゲームの更新関数系SceneManagerのUpdateとdrawを回す

     void Update(const float Deltatime);
        void Draw(const float Deltatime);

        //GameManagerの初期化
        void InitGameManager();
     //deltatime取得
     const float GetDeltatime();

     //画像を読み込んでmapに入れる関数
        //すでにあるghならそれを返す
        const int LoadGraphEx(std::string Gh);

     //BulletManager取得関数
     inline BulletManager* GetBulletManager(){
       return bManager;
       }
    private:

     //一度読み込んだghを保存するmap
        std::unordered_map<std::string, int> ghmap;

     //フレーム秒
     float deltatime=0;

     //シングルトンインスタンス
        static GameManager* instance;

        //BulletManagerインスタンス
        BulletManager* bManager = nullptr;

     
    他にも汎用関数などを実装しているときもあるけど大体こんな感じ
    基本的にゲーム全体に影響を及ぼす内容や各ポインタの受け渡し、各マネージャクラスの所持を担当します。

  • で、ファイル分けした後でどうやって使うの?
    ファイル分けをした後で問題になるのが、どうやって使うんそれ?って事。
    別ファイルにあるんだし勝手に関数名とか書いてもエラーになります。
    .hファイルをincludeしてもだめです。いくつか方法はあります。

    1.グローバル変数でクラス変数を宣言してexternで取得
    最初は多分ここから入る人が多いと思います。
    WINMAIN関数がある場所かGameManagerのグローバル領域にクラス型変数を宣言し、その中身を使いたい場所でexternでアクセスする。問題はグローバル変数であるということ。できれば卒業したいところです。

    2.ポインタ変数を用意し、newする
    使いたい場所でポインタ変数を作り、インスタンスを生成します。これの問題点は、一つ作ったインスタンスを共有する際に逐一Get関数を作る必要が有ること、必ず最後にdeleteをする必要が有ること、ポインタを誰が持つのかを考える必要があることなどです。

    3.スマートポインタで変数を用意し、共有する。
    私は大体これを使います。std::shared_ptr<Player>myPlayer=nullptrとして宣言し、myPlayer=playerManager->GetPlayer()として取得します。
    スマートポインタは共有している場所が全て無くなれば自動的にdeleteされるはずなのでdeleteが必要ありません。

    その他にも、シングルトンならばどこでも GetInstance関数で取得すれば良いから便利だなーとかあります。特にManager系はこれにすることが多いです。

他にも色々分けるときの基準とかフォルダ整理とかincludeのパスについてとかあるけどそれはまた今度。

取り合えず一発目の記事としてはこのへんで終わっておきます。
もっと具体的なコードを乗せる記事を増やしたほうが良いかなーとも思うので、ぼちぼち書くと思います。

ではまた今度。