[C++]シューティングゲーム 1


シューティングではなくアクションゲームで基礎を練習するなら ↓
https://yonezawashin.github.io/yonezawashin.github.io-2020c/practice.html

数学のベクトルの基礎を練習するなら ↓
https://yonezawashin.github.io/yonezawashin.github.io-2020c/vector.html

ポリゴンと3D描画とライティングの基礎を練習するなら(3Dエディタの作成) ↓
https://yonezawashin.github.io/yonezawashin.github.io-2020c/polygon3d/index.html

複数分割画面で複数プレイヤで遊ぶ3Dのタイルボックスゲームを制作するなら ↓
https://yonezawashin.github.io/yonezawashin.github.io-2020c/tile3d/index.html


  1. シューティングC++で目指すところ(意義)
  2. プロジェクトの作成
  3. 画像ファイルの追加
  4. Imageクラスの作成
  5. Gameクラスの作成
  6. 自機の作成
    - Playerクラスの作成
  7. 自機弾の作成
    - 循環参照・インクルードの罠
    - シングルトン型:唯一物体タイプの追加
    - 仲介マネージャーGameManagerの追加
    - 【※メモリから弾を削除】自機弾PlayerBulletの削除処理
    - 共通削除機能を【テンプレートとして定義】
    - 【実験】メモリ浪費弾の実験
    - 自機弾の角度を変えられるようにする
    - vectorとlistの性能考察
  8. ベクトル構造体でX,Y,ZをVector3型でまとめて管理する
    - ベクトルの基本
    - ベクトルの計算
  9. 敵の作成
    - 共通のベースとなるGameObjectを作成する
    - Zako0クラスの作成

  10. テキスト2もこのファイル内にまとめた。

    [C++]シューティングゲーム 2

    このHTMLのWEBテキストはBlueGriffonという無料のソフトで書いた
    https://forest.watch.impress.co.jp/library/software/bluegriffon/
    githubで自分のサイトを無料公開できるので自作サイトにも挑戦しては


シューティング[C++]で目指すところ(意義)

シューティングの制作をC++で行うことで、以下を作業を通じて【体得しよう】

最終的には、C++で制作したゲームの完成を目標にしましょう。



なお、C++は下手にお金出して本を買うくらいなら以下のサイトが教科書代わりになりそうです。
https://atcoder.jp/contests/apg4b
https://zenn.dev/reputeless/books/standard-cpp-for-competitive-programming/viewer/string
https://ezoeryou.github.io/cpp-intro/

プロジェクトの作成

まずはDXライブラリ公式のC++のライブラリをダウンロード

以下のDXライブラリの公式サイトからC++のDXライブラリをダウンロードしてください。
https://dxlib.xsrv.jp/dxdload.html
ダウンロードできたらzipファイルを【C:ドライブ直下に展開】してください



ダウンロードできたらzipファイルを【C:ドライブ直下に展開】



プロジェクト作成

Visual Studio 2019を起動してください。

起動したら、新しいプロジェクトの作成をクリック

C++ 空のプロジェクトを選択して次へ

今回のプロジェクト名は【1】「自分の作るゲーム名」か【2】毎回C++のプロジェクトを作るのが面倒な人は今回作るプロジェクトをフォルダごと量産するとして「DXLibGameBase」(ゲームの基礎ベース、コピペで量産用プロジェクト)にしましょう。

プロジェクトが作成されたら、まず【main.cpp】作りましょう。


少なくとも一つ【~.cpp】作らないと出てこない次にやる設定↓で【出てこない項目】があります(【C/C++全般】が出てこない!)


次にプロジェクトのややこしい設定をしっかりやる

プロジェクトの【設定を開く前に】最低限一つだけでもcppファイルがプロジェクトにないと【出てこない設定項目がある】ので新しい項目を追加でC++ソースファイル(.cpp)のファイルを一つ作成しておく。



表示モードを切り替えてみて、main.cppができたかを確認する。


一つだけcppファイルを作成した後、プロジェクトを右クリックしてプロパティをクリック


【設定】マルチバイト文字セットを使用


設定画面の【構成プロパティ】→【詳細】の【文字セット】をマルチバイト文字セットを使用に変更


【設定】C/C++→全般→追加のインクルードディレクトリにDXライブラリのフォルダ位置(PATH)を追加


設定画面の【C/C++全般】→【全般】の【追加のインクルードディレクトリ】にDXライブラリのフォルダ位置(PATH)を設定


フォルダのパス(位置)文字列は以下のgifアニメのようにコピーして貼り付け


【設定】リンカ→全般→追加のライブラリディレクトリにDXライブラリのフォルダ位置(PATH)を追加


設定画面の【リンカー】→【全般】の【追加のライブラリディレクトリ】にDXライブラリのフォルダ位置(PATH)を設定


フォルダのパス(位置)文字列は以下のgifアニメのようにコピーしたものを貼り付け



【設定】リンカ→システム→サブシステムをウィンドウに(コンソールのままだと黒画面の文字ベースシステムになる)


設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定
この設定をするとプログラムの開始位置(エントリポイントという)がWinMainというところから始まるようになる。(Consoleの場合はMain)



以上で設定項目は終わり。OKをおして設定を反映しよう


設定を反映させたらサンプルプログラムをmain.cppにコピペして動作を確認

以下の【ウィンドウを出すだけの最小限サンプル】を動かして設定を確認しましょう

main.cpp【ウィンドウを出すだけの最小限サンプル】を編集します:

#include "DxLib.h"

// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  // 画面モードの設定
  SetGraphMode( 960 , 540 , 32 ) ; // 画面サイズ960×540のカラービット数32ビットで起動
  SetWindowSize(960, 540);// ウィンドウサイズ960×540(こことSetGraphModeのサイズが異なると画像がゆがむ)
  ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
  SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
  SetMainWindowText("ゲームのウィンドウ名を変えるときはここ");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも
  //↑ここまでの設定は↓下のDXライブラリ初期化より先にやらないとDxLib_Init()中は画面がフルスクリーンになって終わってからウィンドウサイズが変更になり見苦しい

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // この位置にウィンドウの中に画像を描く処理などを書く
  // DXライブラリ公式の色々なサンプルを試してみよう!
  // https://dxlib.xsrv.jp/dxfunc.html
  
  
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}

解像度の定義

【C++で】画面解像度を定義するクラスを作ってみましょう。ソリューションエクスプローラーのプロジェクトを右クリックし、追加>ヘッダファイル(.h)
Screen.hという名前でヘッダファイルを生成し、そのファイルにクラスを記述します。

Screen.hを編集します:

#ifndef _SCREEN_H
#define _SCREEN_H
// #ifndefは「_SCREEN_H」がifもしもn(not) def(定義)されていないなら#endifに挟まれたコードまでを有効化する(だから2度目以降は無効になる)
// 二重定義を防止するために#から始まる【プリプロセッサ】を使っている
// 【プリプロセッサ】はビルドする段階でコード自体に【前処理】として影響を与える
// たとえば
// ・ファイルの読み込み (including) →#includeがこれ
// ・マクロの展開(シンボルを、あらかじめ定義された規則に従って置換する)
// ・コンパイル条件によるソースコードの部分的選択→【#ifndef ~ #endifに囲まれた部分】は一度(#defineで)定義されると無効化
// ・コメントの削除 【 // 】のコメントの削除も【プリプロセス=前段階で行われる】

// 画面解像度
class Screen
{
public: //publicはC#と違いpublic:以下にまとめて書かれるスタイル
  static const int Width = 960; // 幅
  static const int Height = 540; // 高さ C#と違いstaticクラスではなく個々の【すべての変数にstaticをつけて】staticなクラスとする
};// C#と違ってクラスの定義も;セミコロンで終わる
#endif




main.cppを編集してScreen.hを読み込ませます:

#include "DxLib.h"
#include "Screen.h"
// ↑#includeで別ファイルのヘッダファイル.hをこのコード内に読み込む

// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  // 画面モードの設定
  SetGraphMode( Screen::Width, Screen::Height, 32 ) ; // 画面サイズWidth×Heightのカラービット数32ビットで起動
  SetWindowSize(Screen::Width, Screen::Height);// ウィンドウサイズWidth×Height(こことSetGraphModeのサイズが異なると画像がゆがむ)
  ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
  SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
  SetMainWindowText("ゲームのウィンドウ名を変えるときはここ");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }


  // (中略).....................................

  // ソフトの終了
  return 0;
}

画像ファイルの追加

次は画像ファイルをプロジェクトに追加します。フォルダは【ウィンドウズ上のエクスプローラで作成】します。








GIMPをダウンロードして画像をつくってみましょう。
https://forest.watch.impress.co.jp/library/software/gimp/

ダウンロードしたら起動して【ファイル】→【新しい画像】


キャンバスのサイズはとりあえず64×64でつくってみます(自由につくってもよいがサイズをおぼえておいて、あとで自分のプログラムで読み込むときに合わせないといけないよ)


透明のレイヤーをつくって背景が透けるようにしておく


消しゴムで消して透明にしておく


新しいレイヤをつくって、そこのレイヤに自分の画像をえがいていく





マウスで長押しして楕円選択をえらんで円形にえがくエリアをえらぶ



ぬりつぶしの色を決めて、えらんでいるエリアを円形にぬりつぶす



テキストのボタンをおして円のうちがわに自機という文字をうちこむ



画像を保存するフォルダをつくっておく



つくったフォルダにエクスポートする





自分でboss1.png、boss2.png、boss3.pngを180×180など別のサイズでつくってみたり、
webでゲーム素材、フリーなどのワードで検索して、素材をさがしてみよう(著作権や利用規約をよく読んで注意しないとお金とられて財布が空になっちゃうので注意!!)
https://pipoya.net/sozai/
https://qiita.com/TD12734/items/5dc8068732f94452efe2


えがいた画像をべつのフォルダに保存した場合は、下のアニメようにウィンドウズでImageフォルダーにコピーして貼り付けしてください。





試しに表示してみる

main.cppにプログラムを追加し、画像が正しく表示できるかテストします。

#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
#include "Screen.h"
// #includeで別ファイルのヘッダファイル.hをこのコード内に読み込む

// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  // 画面モードの設定
  SetGraphMode(Screen::Width, Screen::Height, 32 ) ; // 画面サイズWidth×Heightのカラービット数32ビットで起動
  SetWindowSize(Screen::Width, Screen::Height);// ウィンドウサイズWidth×Height(こことSetGraphModeのサイズが異なると画像がゆがむ)
  ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
  SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
  SetMainWindowText("ゲームのウィンドウ名を変えるときはここ");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }


  //表示しているスクリーンの後ろで隠れて次に描く画像を先に描くモード
  // これとペアでScreenFlip();でつぎのページと入れ替えでちらつきを防ぐ
  SetDrawScreen(DX_SCREEN_BACK);

  // ゲームのwhileループを開始する前の初期化処理
  int bossImage = -1;
  bossImage = LoadGraph("Image/boss1.png");
  assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  int x = 100; // Xの初期位置
  int vx = 10;

  ScreenFlip();

  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {// ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに

    // 1フレームごとの更新処理
    if (x < 0)
    {
      vx = 10; //速度の変更
    }
    else if (x > Screen::Width)
    {
      vx = -10;
    }
    x += vx; // X位置の更新

    // 描画処理(位置x, y, 拡大率, 回転, 画像ID, TRUEなら透過有効)
    DrawRotaGraphF(x, 200, 1.0f, 0, bossImage, TRUE);

    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }

  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}


Imageクラスの作成

Imageクラスの作成

Imageクラスを作成しましょう。Imageクラスの役割は次の2つです。


ソリューションエクスプローラーからプロジェクトを右クリックし、追加>ヘッダファイル
「Image」という名前でヘッダファイルを作り、クラスを定義します。

Image.hの内容を次のようにします:

#ifndef IMAGE_H_
#define IMAGE_H_

#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用

class Image
{
public:
  Image() {}; // 初期化コンストラクタ:定義と{}空の処理
  ~Image() {}; // 破棄する処理デストラクタ:定義と{}空の処理
  static void Load();

  static int bossImage; //ボス画像のハンドラ(読込画像番号)

private:

};
#endif


次に対となるcppファイルを作成します。
ソリューションエクスプローラーからプロジェクトを右クリックし、追加>cppファイル
「Image.cpp」という名前でcppファイルを作り、クラスを実装します。

Image.cppの内容を次のようにします:

#include "Image.h"

int Image::bossImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね

void Image::Load()
{
  bossImage = LoadGraph("Image/boss1.png");
  assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
}



試しに【Image.hを使って】表示してみる

main.cppのプログラムを変更し、画像が正しく表示できるかテストします。

#include "DxLib.h"
#include "Screen.h"
#include "Image.h" // 画像読み込みクラス


// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  // 画面モードの設定
  SetGraphMode(Screen::Width, Screen::Height, 32 ) ; // 画面サイズWidth×Heightのカラービット数32ビットで起動
  SetWindowSize(Screen::Width, Screen::Height);// ウィンドウサイズWidth×Height(こことSetGraphModeのサイズが異なると画像がゆがむ)
  ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
  SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
  SetMainWindowText("ゲームのウィンドウ名を変えるときはここ");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }

  //表示しているスクリーンの後ろで隠れて次に描く画像を先に描くモード
  // これとペアでScreenFlip();でつぎのページと入れ替えでちらつきを防ぐ
  SetDrawScreen(DX_SCREEN_BACK);

  // ゲームのwhileループを開始する前の初期化処理
  Image::Load();
  int bossImage = -1;
  bossImage = LoadGraph("Image/boss1.png");
  assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  int x = 100; // Xの初期位置
  int vx = 10;

  ScreenFlip();

  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {// ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに

    // 1フレームごとの更新処理
    if (x < 0)
    {
      vx = 10; //速度の変更
    }
    else if (x > Screen::Width)
    {
      vx = -10;
    }
    x += vx; // X位置の更新

    // 描画処理(位置x, y, 拡大率, 回転, 画像ID, TRUEなら透過有効)
    DrawRotaGraphF(x, 200, 1.0f, 0, Image::bossImage, TRUE);

    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }

  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}


Gameクラスの作成

Gameクラスの作成

Gameクラスを作成しましょう。Gameクラスの重要な役割は次の3つ。


ソリューションエクスプローラーからプロジェクトを[右クリック]し、[追加] → [新しい項目] → Game.hというファイル名を入力
「Game.h」という名前でヘッダファイルを作り、クラスを定義します。

Game.hの内容を次のようにします:

#ifndef GAME_H_
#define GAME_H_

#include "Image.h"
#include "Screen.h"

class Game
{
public :
  /*        ↓{ }を.hに書くことでcppに分けて書くはずの処理ぶぶんを.hに書いてもよい */
  Game() {  }; // 初期化コンストラクタ
  ~Game() {  }; // 破棄処理デストラクタ
  void Init(); // Init処理(定義だけ)
  void Update(); // 更新処理(定義だけ)
  void Draw();// 描画処理(定義だけ)

  int x = 0; //staticじゃないintはここで=0で初期化できる
  int vx = 0;

};
#endif


次に対となるcppファイルを作成します。
ソリューションエクスプローラーからプロジェクトを[右クリック]し、[追加] → [新しい項目] → Game.cppというファイル名を入力
「Game.cpp」という名前でcppファイルを作り、クラスの処理を実装します。

Game.cppの内容を次のようにします:

#include "Game.h"

void Game::Init()
{// Init処理
  Image::Load(); //画像の読込み
  x = 100; // Xの初期位置
  vx = 10; // ボスの初期速度
}

void Game::Update()
{// 更新処理

  if (x < 0)
  {
    vx = 10; //速度の変更
  }
  else if (x > Screen::Width)
  {
    vx = -10; //画面端で移動方向反転
  }
  x += vx; // ボス画像のX位置の更新

}

void Game::Draw()
{// 描画処理
  DrawRotaGraphF(x, 200, 1.0f, 0, Image::bossImage, TRUE);

}

【Gameクラスを使って】表示してみる

main.cppのプログラムを変更し、画像が正しくGameクラスで表示できるかテストします。

#include "DxLib.h"
#include "Screen.h"
#include "Image.h" // 画像読み込みクラス
#include "Game.h" 


// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  // 画面モードの設定
  SetGraphMode(Screen::Width, Screen::Height, 32 ) ; // 画面サイズWidth×Heightのカラービット数32ビットで起動
  SetWindowSize(Screen::Width, Screen::Height);// ウィンドウサイズWidth×Height(こことSetGraphModeのサイズが異なると画像がゆがむ)
  ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
  SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
  SetMainWindowText("ゲームのウィンドウ名を変えるときはここ");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }

  //表示しているスクリーンの後ろで隠れて次に描く画像を先に描くモード
  // これとペアでScreenFlip();でつぎのページと入れ替えでちらつきを防ぐ
  SetDrawScreen(DX_SCREEN_BACK);

  // ゲームのwhileループを開始する前の初期化処理
  Image::Load();
  int x = 100; // Xの初期位置
  int vx = 10;

  // Init初期化処理
  Game game; //Gameの定義(定義と同時に初期化コンストラクタが実行される)
  game.Init(); // gameのInit準備


  ScreenFlip();

  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {// ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに

    // Update更新処理
    game.Update(); // gameの更新処理
    
    // Draw描画処理
    game.Draw();
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }

  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}


自機の作成

プレイヤを操作するには便利クラスInputクラスが必須!

Inputクラスを作成しましょう。

ソリューションエクスプローラーからプロジェクトを[右クリック]し、[追加] → [新しい項目] → Game.hというファイル名を入力
「Input.h」という名前でヘッダファイルを作り、クラスを定義します。

Input.hを新規作成して以下を実装

#ifndef INPUT_H_
#define INPUT_H_

#include "DxLib.h"


// 入力クラス
class Input
{
public:
  static int prevState; // 1フレーム前の状態
  static int currentState; // 現在の状態

  // 初期化。最初に1回だけ呼んでください。
  static void Init()
  {
    prevState = 0;
    currentState = 0;
  }

  // 最新の入力状況に更新する処理。
  // 毎フレームの最初に(ゲームの処理より先に)呼んでください。
  static void Update()
  {
    prevState = currentState;

    currentState =  GetJoypadInputState(DX_INPUT_KEY_PAD1);
  }

  // ボタンが押されているか?
  static bool GetButton(int buttonId)
  {
    // 今ボタンが押されているかどうかを返却
    return (currentState & buttonId) != 0;
  }

  // ボタンが押された瞬間か?
  static bool GetButtonDown(int buttonId)
  {
    // 今は押されていて、かつ1フレーム前は押されていない場合はtrueを返却
    return ((currentState & buttonId) & ~(prevState & buttonId)) != 0;
  }

  // ボタンが離された瞬間か?
  static bool GetButtonUp(int buttonId)
  {
    // 1フレーム前は押されていて、かつ今は押されていない場合はtrueを返却
    return ((prevState & buttonId) & ~(currentState & buttonId)) != 0;
  }
}; //←【注意】クラス定義の終わりにはコロンが必要だよ!

#endif  //ここでエラー出た人は↑【注意】の;コロン忘れ


次に対となるcppファイルを作成します。
ソリューションエクスプローラーからプロジェクトを[右クリック]し、[追加] → [新しい項目] → Input.cppというファイル名を入力
「Input.cpp」という名前でcppファイルを作ります(たった2行の初期化static int変数の初期化のために必須)。

Input.cppを新規作成して、その内容を次のようにします:

#include "Input.h"

int Input::prevState { 0 }; // 1フレーム前の状態
int Input::currentState { 0 }; // 現在の状態
// ↑static intのときはこの行書かないとシンボリックエラーになる件(このためだけにcpp書かなきゃならん)


.cppを作成して.h側のstatic変数を書き連ねていないと↓下のような外部シンボルの未解決エラーが出るので、今後は下記エラーが出たときはcpp忘れを疑ってください



Playerクラスを作成する前に、忘れずにプレイヤ画像はDXのプロジェクトのImageフォルダにコピー貼り付けしましょう。
Imageフォルダにコピーしたらプロジェクトで

次は画像ファイルをプロジェクトに追加します。画像は【プロジェクトを右クリックで「追加」→「既存の項目」で画像を追加】します。


Image.hの内容を次のようにします:

#ifndef IMAGE_H_
#define IMAGE_H_

#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用

class Image
{
public:
  Image() {}; // 初期化コンストラクタ:定義と{}空の処理
  ~Image() {}; // 破棄する処理デストラクタ:定義と{}空の処理
  static void Load();

  static int bossImage; //ボス画像のハンドラ(読込画像番号)
  static int player; //プレイヤ画像のハンドラ

private:

};
#endif


次に対となるcppファイルも変更します。
Image.cppの内容を次のようにします:

#include "Image.h"

int Image::bossImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{-1};

void Image::Load()
{
  bossImage = LoadGraph("Image/boss1.png");
  assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  player= LoadGraph("Image/player.png");
  assert(player!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

}



Playerクラスの作成

Playerクラスを作成しましょう。
Playerクラスをつくるだけで安心すると【落とし穴】があります。Game.cppでInput.Update();を忘れるとプレイヤが動きません。

Update()やDraw()やInput.Update()を忘れないように注意して進めましょう。

ソリューションエクスプローラーからプロジェクトを[右クリック]し、[追加] → [新しい項目] → Player.hというファイル名を入力
「Player.h」という名前でヘッダファイルを作り、クラスを定義します。

Player.hの内容を次のようにします:

#ifndef PLAYER_H_
#define PLAYER_H_

#include "DxLib.h"
#include "Input.h"
#include "Image.h"

class Player
{
public:
  const float MoveSpeed = 6; // 移動速度

  float x; // x座標
  float y; // y座標

  // コンストラクタ
  // x : 初期位置x
  // y : 初期位置y
  Player(float x, float y)
  {
    this->x = x;
    this->y = y;
  }

  // 更新処理
  void Update()
  {
    float vx = 0; // x方向移動速度
    float vy = 0; // y方向移動速度

    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      vx = -MoveSpeed; // 左
    }
    else if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      vx = MoveSpeed; // 右
    }
    if (Input::GetButton(PAD_INPUT_UP))
    {
      vy = -MoveSpeed; // 上
    }
    else if (Input::GetButton(PAD_INPUT_DOWN))
    {
      vy = MoveSpeed; // 下
    }

    // 実際に位置を動かす
    x += vx;
    y += vy;
  }

  // 描画処理
  void Draw()
  {
    DrawRotaGraphF(x, y, 1.0f, 0, Image::player, TRUE);
  }
};

#endif


Game.hにPlayerを追加します:

#ifndef GAME_H_
#define GAME_H_

#include "Image.h"
#include "Screen.h"
#include "Player.h"

class Game
{
public :
  /*        ↓{ }を.hに書くことでcppに分けて書くはずの処理ぶぶんを.hに書いてもよい */
  Game() {  }; // 初期化コンストラクタ
  ~Game() {  }; // 破棄処理デストラクタ
  void Init(); // Init処理(定義だけ)
  void Update(); // 更新処理(定義だけ)
  void Draw();// 描画処理(定義だけ)

  int x = 0; //staticじゃないintはここで=0で初期化できる
  int vx = 0;

  Player player{ 100, Screen::Height / 2 }; // 自機の初期化
};
#endif

次にGame.cppファイルにUpdateとDraw処理を追加してプレイヤの動作を確認しましょう。

Game.cppの内容を次のようにします:

#include "Game.h"

void Game::Init()
{// Init処理
  Image::Load(); //画像の読込み
  x = 100; // Xの初期位置
  vx = 10; // ボスの初期速度
}

void Game::Update()
{// 更新処理

  Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

  if (x < 0)
  {
    vx = 10; //速度の変更
  }
  else if (x > Screen::Width)
  {
    vx = -10; //画面端で移動方向反転
  }
  x += vx; // ボス画像のX位置の更新

  player.Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
}

void Game::Draw()
{// 描画処理
  DrawRotaGraphF(x, 200, 1.0f, 0, Image::bossImage, TRUE);

  player.Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
}

さて、無事にプレイヤをキー入力(↑↓←→)で動かせるようになりましたか???。



√2の定義

【C++でも】斜め方向の移動速度を√2で割って補正するために、自分用数学定義クラスを作ってみましょう。ソリューションエクスプローラーのプロジェクトを右クリックし、追加>ヘッダファイル(.h)
MyMath.hという名前でヘッダファイルを生成し、そのファイルにクラスを記述します。

MyMath.hを新規作成してその内容を編集します:

#ifndef _MYMATH_H
#define _MYMATH_H
// 数学関連クラス
class MyMath
{
public:
  // C++標準では、静的static定数は【整数型】または【列挙型】のみを.hのクラス内で初期化できます。
  // これが、intやenumの初期化はエラーにならないのに他のstatic floatなどがエラーになる理由です。

  // ルート2(変数名の定義だけ)
  static const float Sqrt2; // = 1.41421356237f;(.hではfloat小数型の初期化はできない int型はできるのに)

}; // 【注意】セミコロン抜けで【宣言が必要ですエラー】

#endif // 【宣言が必要ですエラーは上のセミコロン抜け】




MyMath.cppを新規作成してその内容を編集して変数名にSqrt2 = √2 = 1.41421356237f を 割り当てます。

#include "MyMath.h"

const float MyMath::Sqrt2 = 1.41421356237f;//(floatは有効桁は実質7桁まで正確だがそれ以降は環境によって誤差出る)



定義だけじゃなく、実際にプレイヤを動かしているPlayer.hも編集しなきゃ意味がないよ!お忘れなく!!
Player.hの内容を次のようにします:

#ifndef PLAYER_H_
#define PLAYER_H_

#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"

class Player
{
public:
 const float MoveSpeed = 6; // 移動速度

 float x; // x座標
 float y; // y座標

  // コンストラクタ
  // x : 初期位置x
  // y : 初期位置y
 Player(float x, float y)
  {
    this->x = x;
    this->y = y;
  }

  // 更新処理
 void Update()
  {
    float vx = 0; // x方向移動速度
    float vy = 0; // y方向移動速度

    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      vx = -MoveSpeed; // 左
    }
    else if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      vx = MoveSpeed; // 右
    }
    if (Input::GetButton(PAD_INPUT_UP))
    {
      vy = -MoveSpeed; // 上
    }
    else if (Input::GetButton(PAD_INPUT_DOWN))
    {
      vy = MoveSpeed; // 下
    }

    // 斜め移動も同じ速度になるように調整
    if (vx != 0 && vy != 0)
    {
      vx /= MyMath::Sqrt2;
      vy /= MyMath::Sqrt2;
    }

    // 実際に位置を動かす
    x += vx;
    y += vy;
  }

  // 描画処理
 void Draw()
  {
    DrawRotaGraphF(x, y, 1, 0, Image::player, TRUE);
  }
};

#endif




自機弾PlayerBulletクラスの作成

PlayerBulletクラスを新規作成しましょう。


ソリューションエクスプローラーからプロジェクトを[右クリック]し、[追加] → [新しい項目] → PlayerBullet.hというファイル名を入力
「PlayerBullet.h」という名前でヘッダファイルを作り、クラスを定義します。

PlayerBullet.hの内容を次のようにします:

#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_

#include "DxLib.h"
#include "Image.h"

// 自機弾クラス
class PlayerBullet
{
public:
  const float Speed = 25; // 移動速度

  float x; // x座標
  float y; // y座標

  // コンストラクタ
  PlayerBullet(float x, float y)
  {
    this->x = x;
    this->y = y;
  }

  // 更新処理
  void Update()
  {
    // 移動
    x += Speed;
  }

  // 描画処理
  void Draw()
  {
    DrawRotaGraphF(x, y, 1, 0, Image::playerBullet,TRUE);
  }
};


#endif


Image.hに自機弾PlayerBullet画像定義を追加します:

#ifndef IMAGE_H_
#define IMAGE_H_

#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用

class Image
{
public:
  Image() {}; // 初期化コンストラクタ:定義と{}空の処理
  ~Image() {}; // 破棄する処理デストラクタ:定義と{}空の処理
  static void Load();

  static int bossImage; //ボス画像のハンドラ(読込画像番号)
  static int player; //プレイヤ画像のハンドラ
  static int playerBullet; //プレイヤの弾画像のハンドラ

private:

};
#endif


次に対となるcppファイルも変更します。
Image.cppの内容を次のようにします:

#include "Image.h"

int Image::bossImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{-1};
int Image::playerBullet{-1};

void Image::Load()
{
  bossImage = LoadGraph("Image/boss1.png");
  assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  player= LoadGraph("Image/player.png");
  assert(player!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  playerBullet = LoadGraph("Image/player_bullet.png");
  assert(playerBullet!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

}




Game.hにPlayerBulletを追加します:

#ifndef GAME_H_
#define GAME_H_

#include <vector>// 配列std::vectorを使う
#include <memory>//スマートなポインタを使うのに必要(shared_ptrなど)


#include "Image.h"
#include "Screen.h"
#include "Player.h"


//前方宣言でPlayerBulletという【ポインタの型】があることだけを宣言すれば#includeせずにstd::vectorの中身にPlayerBulletのリンクだけなら入れられる
class PlayerBullet;


class Game
{
public :
  Game() {}; // 初期化コンストラクタ
  ~Game() {}; // 破棄処理デストラクタ
  void Init(); // Init処理(定義だけ)
  void Update(); // 更新処理(定義だけ)
  void Draw();// 描画処理(定義だけ)

  int x = 0;
  int vx = 0;

  Player player{ 100, Screen::Height / 2 }; // 自機の初期化
  std::shared_ptr<Player> player{ std::make_shared<Player>(100, Screen::Height / 2, this) }; // 自機の初期化
  //↑プレイヤを回し読みポインタ型に。std::make_shared<Player>でメモリ上に実体を生成
  std::vector<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト

  //可変長配列vector ↑【shared_ptr:共有ポインタ】

  // ★【shared_ptr:共有ポインタ】とは【データの回し読み(読まれなくなったら廃棄)】
  // 「例えば、兄弟で週刊少年ジャンプを【回し読みするときは】
  // おかんに【全員読み終わるまではメモリから捨てられないように】する
  // 【読むつもりの人のカウンタ.use_count()】がゼロになると【おかんに捨てられる(リンク切れはメモリから廃棄する)】」

};

#endif

次にGame.cppファイルにUpdateとDrawに自機弾更新と描画の処理を追加しましょう。

Game.cppの内容を次のようにします:

#include "Game.h"

#include "PlayerBullet.h" // ★Update(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理

  Image::Load(); //画像の読込み
  x = 100; // Xの初期位置
  vx = 10;
};

void Game::Update()
{// 更新処理

  Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

  if (x < 0)
  {
    vx = 10; //速度の変更
  }
  else if (x > Screen::Width)
  {
    vx = -10;
  }
  x += vx; // X位置の更新

  player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
  //ポインタ↑になると.から->でのアクセスになる

  // 自機弾の更新処理
  // for文で全自機弾をループで回してUpdateで更新
  for (const auto& b : playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Update();
  }

  
  
  //【放置厳禁!!!このままここに弾の削除処理書かないとメモリがどんどん減ってくよ(気づかないうちにどんどん使用メモリが増える..怖)】
  
  
};

void Game::Draw()
{// 描画処理
  DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);

  player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
  //ポインタ↑になると.から->でのアクセスになる

  // 自機弾の描画処理
  // for文で全自機弾をループで回してUpdateで更新
  for (const auto& b : playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Draw();
  }

};




Player.hにPlayerBulletへの参照を得るためGameへのポインタ参照を追加します:

#ifndef PLAYER_H_
#define PLAYER_H_

#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"

class Game; //←【エラー地獄に注意!】ここにGameクラスがあるものとして書くことで【循環相互インクルード問題】を回避

//↑Gameクラス内にPlayer変数を持っているからPlayerの定義が終わらないとGameの定義が完了しない(未定義エラー)
//【なのに】Playerクラス内にGame変数を持っているからPlayerの定義も終わらない!!(未定義エラー)
//【つまりPlayerとGameが互いに定義が終わるのを待つ】【循環相互インクルードどうぞどうぞ状態】になる
//【解決するために】とりあえずclass Game;とだけここに書いてGameは定義完了済と見せかけているわけです!
//[エラー:Playerは未定義です]とか[エラー:Gameは未定義です]とか定義してるはずのものが未定義の場合は大体これ!!


class Player
{
public:
  const float MoveSpeed = 6; // 移動速度

  float x; // x座標
  float y; // y座標

  Game* game { nullptr }; // Gameのインスタンスの参照
  //↑ * ポインタ型です!プレイヤの【内部に】ゲームの情報を【メモリ実体として持つ】と
  //そのプレイヤを親のゲームがもつと【延々とループで終わりなきメモリ爆発ループ】になるから
  //メモリに対しての参照=【レコードの針をGameに指して】【Gameの住所アドレスだけをプレイヤ内部に持つ】


  // コンストラクタ
  // x : 初期位置x
  // y : 初期位置y
  Player(float x, float y, Game* game)
  {
    this->x = x;
    this->y = y;
    this->game = game;
  }

  // 更新処理
  void Update();//【★←Player.cppを新規作成してそちらに処理を書き移す】

  // 描画処理
  void Draw();//【★←Player.cppを新規作成してそちらに処理を書き移す】

};

#endif


↑Player.hにあった上記処理部分を↓Player.cppを新規作成して書き移します

Player.cppにPlayerBulletの発射処理を追加します:

#include "Player.h"

#include "Game.h" // ★game->playerBulletsなどClass Game;でカバーできない【定義の内部のplayerBulletsにアクセスするのでインクルード必要】
#include "PlayerBullet.h" // ★弾を生成して【使う処理があるのでインクルード必要】




// 更新処理【書き移す際にはPlayer::という形で::所属が必要になります】
void Player::Update()
{
  float vx = 0; // x方向移動速度
  float vy = 0; // y方向移動速度

  if (Input::GetButton(PAD_INPUT_LEFT))
  {
    vx = -MoveSpeed; // 左
  }
  else if (Input::GetButton(PAD_INPUT_RIGHT))
  {
    vx = MoveSpeed; // 右
  }
  if (Input::GetButton(PAD_INPUT_UP))
  {
    vy = -MoveSpeed; // 上
  }
  else if (Input::GetButton(PAD_INPUT_DOWN))
  {
    vy = MoveSpeed; // 下
  }

  // 斜め移動も同じ速度になるように調整
  if (vx != 0 && vy != 0)
  {
    vx /= MyMath::Sqrt2;
    vy /= MyMath::Sqrt2;
  }

  // 実際に位置を動かす
  x += vx;
  y += vy;

  // ボタン押下で自機弾を発射
  if (Input::GetButtonDown(PAD_INPUT_1))
  {
    // emplace_backで配列に追加。make_shared↓でプレイヤ弾を新しく生成
    game->playerBullets.emplace_back(std::make_shared<PlayerBullet>(x, y));
  }

}

// 描画処理
void Player::Draw()
{
  DrawRotaGraphF(x, y, 1, 0, Image::player, TRUE);
}




【確認してみましょう】ここまでで一旦弾が打てるかチェックしてみましょう!【メモリは削除されないバージョンなので要注意!!】





循環参照・インクルードの罠

【さてここで大量のエラーで詰んだ人も多いのでは】おそらくは【Player.hに弾を弾を撃つ処理を書いたのでは】

それにしても【なぜヘッダh.ファイルとcppファイルに書く違いだけで大量のエラー】が出てしまうのでしょう?



原因である【循環参照・循環インクルード】について説明します。
実はインクルードしたPlayer.hやGame.hは【⇒ビルド】した際に【Player.hの中身が#include "Player.h"へとコピペ】されるのです
そして問題は相互に【Player.hで#include "Game.h"】⇔【Game.hで#include "Player.h"】すると
互いへの【コピペがループする】ため【延々とGameかPlayerの定義が完成せず】
結果、邪悪なことに【全然関係ないPlayerBulletが未定義などと、とばっちりエラーが起こる】のです


さてこの【循環参照・循環インクルード】への対策をどうするかが問題です。

【1】 ヘッダ.hでのインクルードを避け.cppファイルでインクルードするのが基本的には有効です(ヘッダ.h同士が相互にコピペしあわないから)
【2】 しかし根本的には【★相互に参照しあう構造の設計自体すっきりさせたい】です。

今はPlayer⇔Gameの1対1の関係でまだ把握可能ですが、ザコや敵の弾などが他に増えてくると
恐怖の三角関係や四角関係など【どこかで一周回ると無限インクルード発生】が起こってきます。
まだ【1対1の循環なら見抜ける】けど【★三角ループや四角ループは発見が難しすぎ】です。

そう、相互に行き来する【★矢印を一つへ集中させる】のです。
こういったパターンを【★仲介者(Meditator:メディエーター)パターン】あるいは【★窓口(Facade:ファサード)パターン】と呼びます。
このパターンは【循環参照問題以外にも色んな利点】があり多用されます。
一点に集中させれば【メモリの状況もここに集中します】し
ここを経由すれば【どこからでもどこへでも連絡でき指令が出せます】
そしていちいち初期化の時に【参照の連絡先の交換が必要なくなります】

ついでにもう一つ【★唯一物(シングルトン)パターン】も取り入れておきます。

こういった先人の生み出したパターンを【デザインパターン】といいます。

有用なデザインパターンは23ほどあり、状況に合わせてうまく取り入れることが上級者への道で
変な構造で作っていると起こる面倒な時間のロスが減り【残業時間が減ります】
案外【ブラックな現場は構造のデザインそのものが悪かった】ことも多いです(なんかいちいちやりづらい環境)
こういう【構造自体に疑問を持てる人が開発のリーダーシップを取れる】と心地よく制作が進むので
時間とスキルに余裕ができたら将来のリーダーに向けて【デザイン構造パターン】を勉強するとよいかもしれません。



さて、ではまずSingleton.hを追加しましょう。
コード中身は難しい文法が多いですが、
基本の要点は

【1】「代入」「コピー」「実体ポインタへの直接アクセス」を★private:として【外部から禁止】
【2】「実体へは★GetInstance()関数経由でしかアクセスできない」よう窓口を絞り
【3】「テンプレ化」★template<class T>して何でも唯一化できるようになっています


シングルトン型:唯一物体タイプの追加

難しいコードなので導入するだけでも良いですが、
C++の「代入」「コピー」「テンプレ化」は基礎として大事なので頑張って6割ほどは理解できるようになりたいですね。
2,3年後上級者となって読み返して9割ほど理解できるようになるよう成長してゆきましょう。

Singleton.h

#ifndef SINGLETON_H_
#define SINGLETON_H_

// テンプレートT型シングルトンSingleton<~> ~になんでも指定して唯一物にできる
// ★Singleton<T>型は【必ず唯一で複製不可】(使い方:どこからでもアクセスし変数を共有するクラス向き)
// ★ゲーム内の【どこからアクセスしても統一性が保たれる】(唯一だから)「クラス名と実体が一致」
template<class T>
class Singleton
{
public:
  // GetInstanceを通して【しか】T型クラスをつくれないうえに、
  // ★作ったものは【かならず唯一で複製不可】
  // それが【シングルトン型】!
  // ゲーム中で唯一で複製不可にしたい【ゲームを一律管理するクラス】などに最適!
  // https://teratail.com/questions/17416
  // http://rudora7.blog81.fc2.com/blog-entry-393.html
  // ②↓同じ関数の定義はプログラム全体で1つだけしか許されません。(static関数名は被らない)
  static inline T& GetInstance()
  {  //①↓関数の中で定義されたstatic変数は、関数の定義毎に1つ領域が確保されます。
    static T instance; //メモリ上にstatic変数確保(静かに常に待機するinstance)
    return instance; //①=(常駐)②=(唯一) ①と②両方満たすinstanceは【メモリ上に唯一】
  }
  //★【{}内のローカルstaticの特性:ラベル(上の例ではinstance)がリセットされずに常駐】https://monozukuri-c.com/langc-static-memory/
  //つまり、ゲームマネージャなどを★リセットされず常駐状態にできるわけです

protected:
  Singleton() {} // 外部でのインスタンス作成は禁止(protected:内部公開、外部禁止)
  virtual ~Singleton() {} // 仮想デストラクタ(シングルトンを【継承するためには忘れちゃダメ】)

private:  //代入やコピーをprivateにして外部クラスからアクセスできないように【縛る】
  void operator=(const Singleton& obj) {} // 代入演算子禁止
  Singleton(const Singleton& obj) {} // コピーコンストラクタ禁止(コピーできない【唯一】)
  static T* instance; // private:だから外部アクセスできない【GetInstance()経由しかアクセスできないように縛る】
};

// いつもcppで書いたstatic変数初期化も★【Tテンプレートクラスだからすべて.hヘッダにまとめて書いた】
template< class T >
T* Singleton< T >::instance = 0; //cppでの定義を.h内に書いた(static変数はclass内で初期化できないから)

#endif


仲介マネージャーGameManagerの追加

続いて仲介の役割を担当するGameManager.hを作成します。

#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_

#include <memory>
#include <vector>

#include "Singleton.h"

class Player; // クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class PlayerBullet;

class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
  friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可

  std::shared_ptr<Player> player{ nullptr }; // 自機の初期化

  std::vector<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
  // 複数リスト↑vector ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる

protected:
  GameManager() {}; // 外部からのインスタンス作成は禁止
  virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};

#endif

Game.hからGameManager.hに移行した処理を消して代わりにGameMangerへの連絡先gmを確保します。

#ifndef GAME_H_
#define GAME_H_

#include <vector>//配列 std::vectorを使う
#include <memory>//スマートなポインタを使うのに必要(shared_ptrなど)

#include "Image.h"
#include "Screen.h"
#include "Player.h"

//前方宣言でPlayerBulletという【ポインタの型】があることだけを宣言すれば#includeせずにstd::vectorの中身にPlayerBulletのリンクだけなら入れられる
class PlayerBullet;


#include "GameManager.h"

class Game
{
public :
  Game() {}; // 初期化コンストラクタ
  ~Game() {}; // 破棄処理デストラクタ

  //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
  GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る


  void Init(); // Init処理(定義だけ)
  void Update(); // 更新処理(定義だけ)
  void Draw();// 描画処理(定義だけ)

  int x = 0;
  int vx = 0;

  std::shared_ptr<Player> player{ std::make_shared<Player>(100, Screen::Height / 2, this) }; // 自機の初期化
  //↑プレイヤを回し読みポインタ型に。std::make_shared<Player>でメモリ上に実体を生成
  std::vector<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト


};

#endif

次にGame.cppファイルのPlayerやPlayerBulletのアクセスをGamaManagerのgm経由に書き換えましょう。

Game.cppの内容を次のように変更します:

#include "Game.h"

#include "Player.h" // ★初期化やUpdate(),Draw()など【実際の処理の関数を呼出すのでインクルード必要】
#include "PlayerBullet.h" // ★Update(),Draw()など【実際の処理の関数を呼出すのでインクルード必要】

void Game::Init()
{// Init処理

  Image::Load(); //画像の読込み
  x = 100; // Xの初期位置
  vx = 10;

  gm.player = std::make_shared<Player>((float)100, (float)(Screen::Height / 2)); // 自機の初期化
};

void Game::Update()
{// 更新処理

  Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

  if (x < 0)
  {
    vx = 10; //速度の変更
  }
  else if (x > Screen::Width)
  {
    vx = -10;
  }
  x += vx; // X位置の更新

  gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
  //ポインタ↑になると.から->でのアクセスになる

  // 自機弾の更新処理
  // C#のforeach文と同じく全自機弾を更新できる
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Update();
  }
  
  
  //【放置厳禁!!!このままここに弾削除処理書かないとメモリがどんどん減ってくよ(気づかないうちにね..怖)】
  
  
};

void Game::Draw()
{// 描画処理
  DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);

  gm.player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
  //ポインタ↑になると.から->でのアクセスになる

  // 自機弾の描画処理
  // C#のforeach文と同じく全自機弾を更新できる
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Draw();
  }
};


Player.hもGameManagerのgm経由でのアクセスにするので#include "Game.h"やGame*のポインタはなくなります。

#ifndef PLAYER_H_
#define PLAYER_H_

#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"

class Game;//←【エラー地獄に注意!】ここにGameクラスがあるものとして書くことで【循環相互インクルード問題】を回避

#include "GameManager.h"

class Player
{
public:
  const float MoveSpeed = 6; // 移動速度

  float x; // x座標
  float y; // y座標

  //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
  GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る


  Game* game { nullptr }; // Gameのインスタンスの参照
  //↑ * ポインタ型です!プレイヤの【内部に】ゲームの情報を【メモリ実体として持つ】と
  //そのプレイヤを親のゲームがもつと【延々とループで終わりなきメモリ爆発ループ】になるから
  //メモリに対しての参照=【レコードの針をGameに指して】【Gameの住所アドレスだけをプレイヤ内部に持つ】


  // コンストラクタ
  // x : 初期位置x
  // y : 初期位置y
  Player(float x, float y, Game* game)
  {
    this->x = x;
    this->y = y;
    this->game = game;
  }

  // 更新処理の関数定義
  void Update();

  // 描画処理の関数定義
  void Draw();

};

#endif


Player.cppもGameManagerのgm経由でのアクセスに書き換えます

#include "Player.h"

#include "Game.h"
#include "PlayerBullet.h" // ★弾を生成して【使う処理があるのでインクルード必要】

// 更新処理【書き移す際にはPlayer::という形で::所属が必要になります】
void Player::Update()
{
  float vx = 0; // x方向移動速度
  float vy = 0; // y方向移動速度

  if (Input::GetButton(PAD_INPUT_LEFT))
  {
    vx = -MoveSpeed; // 左
  }
  else if (Input::GetButton(PAD_INPUT_RIGHT))
  {
    vx = MoveSpeed; // 右
  }
  if (Input::GetButton(PAD_INPUT_UP))
  {
    vy = -MoveSpeed; // 上
  }
  else if (Input::GetButton(PAD_INPUT_DOWN))
  {
    vy = MoveSpeed; // 下
  }

  // 斜め移動も同じ速度になるように調整
  if (vx != 0 && vy != 0)
  {
    vx /= MyMath::Sqrt2;
    vy /= MyMath::Sqrt2;
  }

  // 実際に位置を動かす
  x += vx;
  y += vy;

  // ボタン押下で自機弾を発射
  if (Input::GetButtonDown(PAD_INPUT_1))
  {
    // emplace_backで配列に追加。make_shared↓でプレイヤ弾を新しく生成
    gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(x, y));
  }
}

// 描画処理
void Player::Draw()
{
  DrawRotaGraphF(x, y, 1, 0, Image::player, TRUE);
}

【※メモリから弾を削除】自機弾PlayerBulletの削除処理

PlayerBulletクラスを改造しましょう。

PlayerBullet.hの内容を次のように【改造】します:

#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_

#include "DxLib.h"
#include "Image.h"
#include "Screen.h"

// 自機弾クラス
class PlayerBullet
{
public:
  const float Speed = 25; // 移動速度
  const int VisibleRadius = 16; // 見た目の半径
  
  // ↓【使用要注意!(テスト用!!)】メモリ消費弾(4MB)撃つたびに4MB消費(下の行のコメント外すと)
  //【使用注意】int memMUDA[1000000]; // 4バイト×1000000= 4MBの無駄メモリ

  float x; // x座標
  float y; // y座標
  bool isDead = false; // 死亡フラグ

  // コンストラクタ
  PlayerBullet(float x, float y)
  {
    this->x = x;
    this->y = y;
  }

  // 更新処理
  void Update()
  {
    // 移動
    x += Speed;
    
    // 画面外に出たら、死亡フラグを立てる
    if (x - VisibleRadius > Screen::Width)
    {
      isDead = true;
    }
  }

  // 描画処理
  void Draw()
  {
    DrawRotaGraphF(x, y, 1, 0, Image::playerBullet,TRUE);
  }
};


#endif


次にGame.cppファイルに自機弾PlayerBulletの【削除処理】を追加しましょう。

Game.cppの内容を次のようにします:

#include "Game.h"
#include "Player.h" // ★初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // ★Update(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理

  Image::Load(); //画像の読込み
  x = 100; // Xの初期位置
  vx = 10;

  gm.player = std::make_shared<Player>((float)100, (float)Screen::Height / 2); // 自機の初期化
};

void Game::Update()
{// 更新処理

  Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

  if (x < 0)
  {
    vx = 10; //速度の変更
  }
  else if (x > Screen::Width)
  {
    vx = -10;
  }
  x += vx; // X位置の更新

  gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】

  // 自機弾の更新処理
  // for文で全自機弾をループで回してUpdateで更新する
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Update();
  }
  
  
  //return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
  
  // 自機弾のリストから死んでるものを除去する
  // 非常に複雑な式に見えるがこれでisDeadのものを削除
  // https://stackoverflow.com/questions/42723205/remove-if-from-a-stdvector-of-shared-pointers-with-a-member-function
  gm.playerBullets.erase(
    std::remove_if(gm.playerBullets.begin(), gm.playerBullets.end(),
      [](std::shared_ptr<PlayerBullet> &ptr) {
        //int usingCount = -1;//テスト用コード(以下のコメントアウト部分は省略可)
        //if (ptr->isDead) usingCount = ptr.use_count();//おかんのコレ捨てるよカウンタ
        //弾を捨てるまでの残り↑カウント(1だとisDeadがtrueなら0になり削除へ
        return ptr->isDead;
      }),
    gm.playerBullets.end()
  );

};

void Game::Draw()
{// 描画処理
  DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);

  gm.player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】

  // 自機弾の描画処理
  // for文で全自機弾をループで回してUpdateで更新する
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Draw();
  }
};


さて、削除部分の処理が非常に複雑ですね。C#と比較してみましょう。
【C#】
playerBullets.RemoveAll(pb => pb.isDead);
【C++】

gm.playerBullets.erase(
  std::remove_if(gm.playerBullets.begin(), gm.playerBullets.end(),
    [](std::shared_ptr<PlayerBullet> &ptr) {
      //int usingCount = -1;//テスト用コード(以下のコメントアウト部分は省略可)
      //if (ptr->isDead) usingCount = ptr.use_count();//おかんのコレ捨てるよカウンタ
      //弾を捨てるまでの残り↑カウント(1だとisDeadがtrueなら0になり削除へ
      return ptr->isDead;
    }),
  gm.playerBullets.end()
);


テスト用のコードを消して読みやすくしてみましょう。
【C++】

gm.playerBullets.erase(
  std::remove_if(gm.playerBullets.begin(), gm.playerBullets.end(),
    [](std::shared_ptr<PlayerBullet> &ptr) {
      return ptr->isDead;
    }),
  gm.playerBullets.end()
);


これでもまだ複雑ですが、なぜ複雑に見えるか?
【実は二つの処理が同時に行われている】ためです。

【1】erase処理 : 指定した配列の要素を削除する処理です。
基本構文は
  v.erase(【削除開始位置】, 【終了位置】);
で、プレイヤ弾の削除処理では

gm.playerBullets.erase(
  ……
    ……
      ……
    ……,
  gm.playerBullets.end()
);

よく見ると実は
gm.playerBullets.erase(【複雑な何か】, gm.playerBullets.end() );
となっているだけで実は【複雑な何か】 ~ 【end()終了位置】までの削除処理を行っているだけだとわかります。
こういう風にカンマ,や(かっこ)で区切られた構造を読み解く力が身に付けば複雑なコードでも

【分解ステップを踏んでゆけば読み解けるようになります】

そして次の「複雑な何か↓」も分解して読み解いてみましょう。
【2】remove_if処理 : ある条件を①満たすものを【配列の後ろ】に、②満たさないものを【前に】集め①の始まりの位置を返す
基本構文はvを配列とすると
  std::remove_if(v.begin(), v.end(), 条件);//配列v開始位置から終了位置まで「条件」に従い分離並べ替えし条件を満たす後ろに並べ替えられたものの分離された先頭位置を返す
つまりremove_ifは配列の後ろに条件を満たすものを分離する処理の担当で削除すべきものの先頭の位置をeraseに渡す担当だとわかります。

要するに【1】と【2】で協力して「削除」と「分離並べ替え」を【分担していただけ】だったわけです。

そしてその条件式が

[](std::shared_ptr<PlayerBullet> &ptr) { return ptr->isDead; }

というスタイルでisDeadがtrueかfalseかreturnするという【ちょっとクセの強い条件式の書き方】で書かれていたということです。

ここは【慣れてゆくしかないです】何度も見て【見慣れる】ことです。

しかし、なぜ【わざわざeraseとremove_ifを組合わせる必要があるのか】
それは【erase単体だけだと削除処理が遅い】からです。
vectorはメモリ上にデータが【隙間なく詰まってます】ゆえに【通常性能は速いです】
ただ引き換えに【あいだのデータを1個消す】だけで【隙間を埋める座席移動が発生】します。
100万のデータが座ってる中、中央付近のデータを1個消すだけで【50万回ぶん一つずつ隙間を埋める座席移動が発生】します。
これがeraseだけ異様に遅い理由です。
逆に映画館の座席で通路付近に座ってる人を思い浮かべてください。
映画が終わると通路付近の座席の人はすぐに立って横に出れば最初に通路への出られますよね。
ゆえに配列の【後ろから】一つ削除する【pop_back処理は速い】です。
したがって【後ろに消したいものを先に集めてしまえばよいのです】
これが【remove_if】処理です。remove_ifは条件を指定して【削除候補を後ろに並べ替えます】
ゆえに【2】remove_ifで先に後ろに並べ替えて【1】eraseでまとめて削除の【1】【2】のハイブリッドが必要なのです。

ただ、毎回この複雑なコードを書くと【コードが見にくくなる】ので【テンプレート化】に挑戦してみましょう。

共通削除機能を【テンプレートとして定義】

GameManager.hに共通の削除機能として【テンプレート機能】として追加してみましょう。

#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_

#include <memory>
#include <vector>

#include "Singleton.h"

class Player; // クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class PlayerBullet;

class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
  friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可

  std::shared_ptr<Player> player{ nullptr }; // 自機の初期化

  std::vector<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
  // 複数リスト↑vector ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる



  // ★削除処理を共通テンプレート関数にする
  // [共通テンプレート関数]https://programming-place.net/ppp/contents/cpp/language/009.html#function_template
  template <typename T, class T_if>
  void EraseRemoveIf(std::vector<T> &v, T_if if_condition)
  {   //            特定のタイプT↑  ↑配列v   ↑条件式if_condition
    v.erase(
      std::remove_if(v.begin(), v.end(),if_condition),
      v.end() //  ↓remove_ifの位置
    );//例.[生][生][死][死][死]← v.end()の位置
  };


protected:
  GameManager() {}; // 外部からのインスタンス作成は禁止
  virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};

#endif



そしてGame.cppの削除処理をシンプルに書き直します

#include "Game.h"
#include "Player.h" // ★初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // ★Update(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理

  Image::Load(); //画像の読込み
  x = 100; // Xの初期位置
  vx = 10;

  gm.player = std::make_shared<Player>((float)100, (float)Screen::Height / 2); // 自機の初期化
};

void Game::Update()
{// 更新処理

  Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

  if (x < 0)
  {
    vx = 10; //速度の変更
  }
  else if (x > Screen::Width)
  {
    vx = -10;
  }
  x += vx; // X位置の更新

  gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】

  // 自機弾の更新処理
  // for文で全自機弾をループで回してUpdateで更新する
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Update();
  }
  
  
  //return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
  
  // 自機弾の削除処理
  gm.EraseRemoveIf(gm.playerBullets,
    [](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

  
  // 自機弾のリストから死んでるものを除去する
  // 非常に複雑な式に見えるがこれでisDeadのものを削除
  // https://stackoverflow.com/questions/42723205/remove-if-from-a-stdvector-of-shared-pointers-with-a-member-function
  gm.playerBullets.erase(
    std::remove_if(gm.playerBullets.begin(), gm.playerBullets.end(),
      [](std::shared_ptr<PlayerBullet> &ptr) {
        //int usingCount = -1;//テスト用コード(以下のコメントアウト部分は省略可)
        //if (ptr->isDead) usingCount = ptr.use_count();//おかんのコレ捨てるよカウンタ
        //弾を捨てるまでの残り↑カウント(1だとisDeadがtrueなら0になり削除へ
        return ptr->isDead;
      }),
    gm.playerBullets.end()
  );

};

void Game::Draw()
{// 描画処理
  DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);

  gm.player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】

  // 自機弾の描画処理
  // for文で全自機弾をループで回してUpdateで更新する
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Draw();
  }
};

さて、使う側はすっきり2行にまとまりました。
これなら削除するリストがenemiesやenemyBulletsなど増えてきても書きやすいです。

テンプレート化template <typename T>を習得することは上級者への第一歩です。
C++だけでなく【C#でもテンプレート化ができます】ぜひ練習して習得してゆきましょう。
逆の目線で言うと【実は今まではテンプレート化された使いやすい機能を使わせてもらっていた】のです。意識せずに。
使う側が意識しなくていいほど【使いやすいテンプレート】を自力で作れることが理想形です。トライしてゆきましょう。


【実験】メモリ浪費弾の実験

ここまでで一応弾の削除処理は書けました。しかし本当に弾が削除されているか実感がありません。
弾1個は現状では【かなり微小なメモリしか】使用しません。
実行して【メモリのグラフを見ても増えているか減っているか見えないほど微小です】
そこで【人為的に弾1個のメモリを大きくする】実験をしてみましょう。
注意が必要です【危険な実験】です(ほったらかすと)。【実験コードは必ず元に戻してください!!】
では実験を始めてみましょう。

PlayerBullet.hの内容を次のように【改造】します:

#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_

#include "DxLib.h"
#include "Image.h"
#include "Screen.h"

// 自機弾クラス
class PlayerBullet
{
public:
  const float Speed = 25; // 移動速度
  const int VisibleRadius = 16; // 見た目の半径
  
  // ↓【使用要注意!(テスト用!!)】メモリ消費弾(4MB)撃つたびに4MB消費(下の行のコメント外すと)
  int memMUDA[1000000]; // 4バイト×1000000= 4MBの無駄メモリ

  float x; // x座標
  float y; // y座標
  bool isDead = false; // 死亡フラグ

  // コンストラクタ
  PlayerBullet(float x, float y)
  {
    this->x = x;
    this->y = y;
  }

  // 更新処理
  void Update()
  {
    // 移動
    x += Speed;
    
    // 画面外に出たら、死亡フラグを立てる
    if (x - VisibleRadius > Screen::Width)
    {
      isDead = true;
    }
  }

  // 描画処理
  void Draw()
  {
    DrawRotaGraphF(x, y, 1, 0, Image::playerBullet,TRUE);
  }
};


#endif


無駄メモリをあえて人為的に定義して弾を撃ってみましょう。
メモリの【グラフがうなぎ上りに上昇しなければ】プレイヤの弾に関してはメモリの削除と開放が成功している証拠です。

試しにGame.cppで削除処理をわすれた場合の実験をしてみましょう。

#include "Game.h"
#include "Player.h" // ★初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // ★Update(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理

  Image::Load(); //画像の読込み
  x = 100; // Xの初期位置
  vx = 10;

  gm.player = std::make_shared<Player>((float)100, (float)Screen::Height / 2); // 自機の初期化
};

void Game::Update()
{// 更新処理

  Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

  if (x < 0)
  {
    vx = 10; //速度の変更
  }
  else if (x > Screen::Width)
  {
    vx = -10;
  }
  x += vx; // X位置の更新

  gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】

  // 自機弾の更新処理
  // for文で全自機弾をループで回してUpdateで更新する
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Update();
  }
  
  
  return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
  
  // 自機弾の削除処理
  //gm.EraseRemoveIf(gm.playerBullets,
  //  [](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

  
};

void Game::Draw()
{// 描画処理
  DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);

  player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】

  // 自機弾の描画処理
  // for文で全自機弾をループで回してUpdateで更新する
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Draw();
  }
};

実験が終わったら実験コードをもとに戻しましょうPlayerBullet.hの内容を次のようにします

#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_

#include "DxLib.h"
#include "Image.h"
#include "Screen.h"

// 自機弾クラス
class PlayerBullet
{
public:
  const float Speed = 25; // 移動速度
  const int VisibleRadius = 16; // 見た目の半径
  
  // ↓【使用要注意!(テスト用!!)】メモリ消費弾(4MB)撃つたびに4MB消費(下の行のコメント外すと)
  //【実験コード】int memMUDA[1000000]; // 4バイト×1000000= 4MBの無駄メモリ

  float x; // x座標
  float y; // y座標
  bool isDead = false; // 死亡フラグ

  // コンストラクタ
  PlayerBullet(float x, float y)
  {
    this->x = x;
    this->y = y;
  }

  // 更新処理
  void Update()
  {
    // 移動
    x += Speed;
    
    // 画面外に出たら、死亡フラグを立てる
    if (x - VisibleRadius > Screen::Width)
    {
      isDead = true;
    }
  }

  // 描画処理
  void Draw()
  {
    DrawRotaGraphF(x, y, 1, 0, Image::playerBullet,TRUE);
  }
};


#endif


Game.cppの削除処理ももとに戻しましょう。

#include "Game.h"
#include "Player.h" // ★初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // ★Update(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理

  Image::Load(); //画像の読込み
  x = 100; // Xの初期位置
  vx = 10;

  gm.player = std::make_shared<Player>((float)100, (float)Screen::Height / 2); // 自機の初期化
};

void Game::Update()
{// 更新処理

  Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

  if (x < 0)
  {
    vx = 10; //速度の変更
  }
  else if (x > Screen::Width)
  {
    vx = -10;
  }
  x += vx; // X位置の更新

  gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】

  // 自機弾の更新処理
  // for文で全自機弾をループで回してUpdateで更新する
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Update();
  }
  
  
  //return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
  
  // 自機弾の削除処理
  gm.EraseRemoveIf(gm.playerBullets,
    [](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

  
};

void Game::Draw()
{// 描画処理
  DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);

  player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】

  // 自機弾の描画処理
  // for文で全自機弾をループで回してUpdateで更新する
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Draw();
  }
};


自機弾の角度を変えられるようにする

数学の基礎のおさらい【cosθとsinθはx,yに対応する】【たて×よこの比率(範囲:min:-1.0~1.0:max)】



PlayerBullet.hに角度をつけられるようにします

#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_

#include <cmath> //sin,cosを使うのに必要

#include "DxLib.h"
#include "Image.h"
#include "Screen.h"

// 自機弾クラス
class PlayerBullet
{
public:
  const float Speed = 25; // 移動速度
  const int VisibleRadius = 16; // 見た目の半径
  
  // ↓【使用要注意!(テスト用!!)】メモリ消費弾(4MB)撃つたびに4MB消費(下の行のコメント外すと)
  //【実験コード】int memMUDA[1000000]; // 4バイト×1000000= 4MBの無駄メモリ

  float x; // x座標
  float y; // y座標
  bool isDead = false; // 死亡フラグ

  float vx; // x方向移動速度
  float vy; // y方向移動速度
  float angle; // 移動角度

  // コンストラクタ
  PlayerBullet(float x, float y, float angle)
  {
    this->x = x;
    this->y = y;

    this->angle = angle;
    vx = (float)std::cos(angle) * Speed;
    vy = (float)std::sin(angle) * Speed;
  }

  // 更新処理
  void Update()
  {
    // 移動
    x += vx;
    y += vy;
    
    // 画面外に出たら、死亡フラグを立てる
    if (x + VisibleRadius < 0 || x - VisibleRadius > Screen::Width ||
      y + VisibleRadius < 0 || y - VisibleRadius > Screen::Height
)
    {
      isDead = true;
    }
  }

  // 描画処理
  void Draw()
  {
    DrawRotaGraphF(x, y, 1, angle, Image::playerBullet,TRUE);
  }
};


#endif


MyMath.hを編集します:

#ifndef _MYMATH_H
#define _MYMATH_H
// 数学関連クラス
class MyMath
{
public:
  static const float Sqrt2;  // = 1.41421356237f;(.hではfloat小数型の初期化はできない int型はできるのに)
  static const float PI; // 円周率 3.14159265359f;
  static const float Deg2Rad; // 度からラジアンに変換する定数 PI / 180f;

}; // 【注意】セミコロン抜けで【宣言が必要ですエラー】

#endif //【宣言が必要ですエラーは上のセミコロン抜け】


MyMath.cppを編集して変数に数値を割り当て。

#include "MyMath.h"

const float MyMath::Sqrt2 = 1.41421356237f;//(floatは有効桁は実質7桁まで正確だがそれ以降は環境によって誤差出る)
const float MyMath::PI = 3.14159265359f; // 円周率
const float MyMath::Deg2Rad = MyMath::PI / 180; // 度からラジアンに変換する定数


Player.cppのプレイヤの弾の生成を改修します。

#include "Player.h"
#include "MyMath.h"

(前略)...................................

  // ボタン押下で自機弾を発射
  if (Input::GetButtonDown(PAD_INPUT_1))
  {
    // C++ではAddがemplace_backに↓ newがmake_shared↓に
    gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(x, y, 0));//右
    gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(x, y, -15 * MyMath::Deg2Rad));//右上
    gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(x, y, +15 * MyMath::Deg2Rad));//右下
  }
}

(以下略)


vectorとlistの性能考察

データのリスト(今回は弾のリスト)をどうデータ管理するかにはstd::vectorとstd::listの2パターンの格納方法が考えられます。

※vectorは大量の弾が増えてきたときにまとまったメモリの領域を割り当てる必要が出てくるので、ランダムアクセスしたいなどの用途がない場合は、
list構造にしておくほうが、メモリの再確保などで遅くなる可能性を回避できるので、listに書き換えてみましょう。

GameManager.hで管理しているPlayerBulletのvector配列をlist構造に変更してみましょう。

#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_

#include <memory>
#include <vector>
#include <list>

#include "Singleton.h"

class Player; // クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class PlayerBullet;

class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
  friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可

  std::shared_ptr<Player> player{ nullptr }; // 自機の初期化

  std::list<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
  // 複数リスト↑list ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる



  // ★削除処理を共通テンプレート関数にする
  // [共通テンプレート関数]https://programming-place.net/ppp/contents/cpp/language/009.html#function_template
  template <typename T, class T_if>
  void EraseRemoveIf(std::list<T> &v, T_if if_condition)
  {   //            特定のタイプT↑  ↑配列v   ↑条件式if_condition
    v.erase(
      std::remove_if(v.begin(), v.end(),if_condition),
      v.end()   
    ); 
  };

protected:
  GameManager() {}; // 外部からのインスタンス作成は禁止
  virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};

#endif


ベクトル構造体でX,Y,ZをVector3型でまとめて管理する

ベクトルの基本

現状ではX,Yは別々の変数として定義していますが、これを数学のベクトルを扱うVector3型を定義してそちらに置き換えましょう(std::vectorとはつづりが同じだけどまったく別のもの)

Vector3.hを作成します(拡張も見越して3DのZ座標も導入しておきます)

#ifndef VECTOR3_H_
#define VECTOR3_H_

#include <array> //vector配列と基本同じだが【有限】の個数である違いがあるxyzなど3つ固定の個数ならstd::vectorよりstd::array型がよい
#include <cmath>
#include <limits> // 無限大などをstd::numeric_limits<float>::infinity()で取得

//★【UnityのVector演算コード】
// https://github.com/Unity-Technologies/UnityCsReference/blob/02d565cf3dd0f6b15069ba976064c75dc2705b08/Runtime/Export/Math/Vector3.cs

// 3変数x,y,zを持つ構造体。3つの数字をまとめて扱うならXYZ座標以外にも使えるよ。x,y,z別々に足したり引いたり面倒でしょう。
struct Vector3
{
// struct型はpublic:がなくてもデフォルトがpublic:  private:を書けばプライベートにもできる
  float x = 0; // x座標
  float y = 0; // y座標
  float z = 0; //【3D】z座標

  // コンストラクタ 初期化 z = 0.fでzのデフォルト値があるので2Dとしても使える
  Vector3(float x = 0.f, float y = 0.f, float z = 0.f)
  {
    this->x = x;
    this->y = y;
    this->z = z;
  }
  //仮想デストラクタ
  virtual ~Vector3()
  {

  }


};

#endif


基本的にはX,Y,Zを上記コードでVector3型で一つの定義にまとめることができます。

次に少し便利にすべく少しトリッキーですが、共用体を使ってx,y,zをstd::array型でxyzとしてまとめて扱えるようにしてみましょう

Vector3.hを書き換えます。

#ifndef VECTOR3_H_
#define VECTOR3_H_

#include <array> //vector配列と基本同じだが【有限】の個数である違いがあるxyzなど3つ固定の個数ならstd::vectorよりstd::array型がよい
#include <cmath>
#include <limits> // 無限大などをstd::numeric_limits<float>::infinity()で取得

//★【UnityのVector演算コード】
// https://github.com/Unity-Technologies/UnityCsReference/blob/02d565cf3dd0f6b15069ba976064c75dc2705b08/Runtime/Export/Math/Vector3.cs

// 3変数x,y,zを持つ構造体。3つの数字をまとめて扱うならXYZ座標以外にも使えるよ。x,y,z別々に足したり引いたり面倒でしょう。
struct Vector3
{
// struct型はpublic:がなくてもデフォルトがpublic:  private:を書けばプライベートにもできる
  //float x = 0; // x座標
  //float y = 0; // y座標
  //float z = 0; // z座標 3つの定義をunionで配列xyzと共用することでxyz[1]=5.5f;とすると yも5.5となる【運命共用体】(メモリ上は同じ位置になる)

  union { // ★共用体unionテクニック https://inemaru.hatenablog.com/entry/2016/03/02/005408
    struct { // [参考ビットサイズ] https://marycore.jp/prog/c-lang/data-type-ranges-and-bit-byte-sizes/
      float x;// x座標
      float y;// y座標
      float z;// z座標
    }; //[匿名共用体とは] https://zenn.dev/block/articles/beda29f11f05afc147ef
    std::array<float, 3> xyz; // float xyz[3];と同じ意味 float 3個ぶんのデータサイズでx,y,z 3個ぶんと一致するので★unionで共用
  }; // unionは異なる複数のものをメモリ上の同一の番地に割り当てられる⇒x,y,z分けて記述するの面倒なとき配列xyz[3]をfor文i=0~3で回せる


  // コンストラクタ 初期化 z = 0.fでzのデフォルト値があるので2Dとしても使える
  Vector3(float x = 0.f, float y = 0.f, float z = 0.f)
  {
    this->xyz = { x,y,z };// たった1行で書ける
  }
  //仮想デストラクタ
  virtual ~Vector3()
  {

  }


};

#endif


ベクトルの計算

次にベクトル同士を足したり引いたり割ったりできるように、ベクトルの計算関数を追加しましょう。

Vector3.hを書き換えます。

#ifndef VECTOR3_H_
#define VECTOR3_H_

#include <array> //vector配列と基本同じだが【有限】の個数である違いがあるxyzなど3つ固定の個数ならstd::vectorよりstd::array型がよい
#include <cmath>
#include <limits> // 無限大などをstd::numeric_limits<float>::infinity()で取得

//★【UnityのVector演算コード】
// https://github.com/Unity-Technologies/UnityCsReference/blob/02d565cf3dd0f6b15069ba976064c75dc2705b08/Runtime/Export/Math/Vector3.cs

// 3変数x,y,zを持つ構造体。3つの数字をまとめて扱うならXYZ座標以外にも使えるよ。x,y,z別々に足したり引いたり面倒でしょう。
struct Vector3
{
// struct型はpublic:がなくてもデフォルトがpublic:  private:を書けばプライベートにもできる
  //float x = 0; // x座標
  //float y = 0; // y座標
  //float z = 0; // z座標 3つの定義をunionで配列xyzと共用することでxyz[1]=5.5f;とすると yも5.5となる【運命共用体】(メモリ上は同じ位置になる)

  union { // ★共用体unionテクニック https://inemaru.hatenablog.com/entry/2016/03/02/005408
    struct { // [参考ビットサイズ] https://marycore.jp/prog/c-lang/data-type-ranges-and-bit-byte-sizes/
      float x;// x座標
      float y;// y座標
      float z;// z座標
    }; //[匿名共用体とは] https://zenn.dev/block/articles/beda29f11f05afc147ef
    std::array<float, 3> xyz; // float xyz[3];と同じ意味 float 3個ぶんのデータサイズでx,y,z 3個ぶんと一致するので★unionで共用
  }; // unionは異なる複数のものをメモリ上の同一の番地に割り当てられる⇒x,y,z分けて記述するの面倒なとき配列xyz[3]をfor文i=0~3で回せる

  // コンストラクタ 初期化 z = 0.fでzのデフォルト値があるので2Dとしても使える
  Vector3(float x = 0.f, float y = 0.f, float z = 0.f)
  {
    this->xyz = { x,y,z };// たった1行で書ける
  }
  //仮想デストラクタ
  virtual ~Vector3()
  {

  }

  // Unityのstatic変数を参考に https://docs.unity3d.com/ja/current/ScriptReference/Vector3.html
  static const Vector3 zero;// = { 0.f, 0.f, 0.f };
  static const Vector3 one; // = { 1.f, 1.f, 1.f };
  static const Vector3 forward;// = { 0.f, 0.f, 1.f };
  static const Vector3 back; // = { 0.f, 0.f, -1.f };
  static const Vector3 up; // = { 0.f, 1.f, 0.f };
  static const Vector3 down;// = { 0.f, -1.f, 0.f };
  static const Vector3 left; // = {-1.f, 0.f, 0.f };
  static const Vector3 right;// = { 1.f, 0.f, 0.f };
  //[負の値の最小値] https://stackoverflow.com/questions/20016600/negative-infinity
  static const Vector3 negativeInfinity;
  //[極大値]https://cpprefjp.github.io/reference/limits/numeric_limits/lowest.html
  static const Vector3 positiveInfinity;


  // 2つのベクトルの内積
  static float dot(const Vector3& leftVec, const Vector3& rightVec)
  {
    return leftVec.x * rightVec.x + leftVec.y * rightVec.y + leftVec.z * rightVec.z; //普通にx同士,y同士,z同士掛けるだけ
  }

  // 2つのベクトルの外積
  static Vector3 cross(const Vector3& leftVec, const Vector3& rightVec)
  {
    return Vector3{
      leftVec.y * rightVec.z - leftVec.z * rightVec.y, // x = 左y * 右z - 左z * 右y
      leftVec.z * rightVec.x - leftVec.x * rightVec.z, // y = 左z * 右x - 左x * 右z
      leftVec.x * rightVec.y - leftVec.y * rightVec.x };// z = 左x * 右y - 左y * 右x //行列独特の特殊な掛け方
  }


  // ベクトルの 2 乗の長さを返します(平方根しないぶん計算速い)(スタティック版)
  static float _sqrMagnitude(const Vector3& _vec)
  {
    return _vec.x * _vec.x + _vec.y * _vec.y + _vec.z * _vec.z; // x*x + y*y + z*z
  }

  // ベクトルの長さ(0から点(x,y,z)までの距離)(スタティック版)
  static float _magnitude(const Vector3& _vec)
  {
    return std::sqrt(_sqrMagnitude(_vec)); // √x*x + y*y + z*z
  }


  // destArrayにデータを保管しておく
  Vector3& copyToArray(std::array<float, 3>& destArray)
  {
    destArray = this->xyz;//配列としてコピー
    return *this;
  }
  // sourceArray配列からVector3データを生成
  Vector3 fromArray(std::array<float, 3>& sourceArray)
  {
    return Vector3{ sourceArray[0], sourceArray[1], sourceArray[2] }; //配列から初期化
  }


  // ベクトルの長さ(0から点(x,y,z)までの距離)
  float magnitude()
  {
    return std::sqrt(x * x + y * y + z * z); // √(x*x + y*y + z*z)
  };

  // ベクトルの 2 乗の長さを返します(平方根しないぶん計算速い)
  float sqrMagnitude()
  {
    return x * x + y * y + z * z; // x*x + y*y + z*z
  };


  // 正規化したベクトルを返す
  Vector3 normalized()
  {
    float mag = magnitude();
    if (mag < 0.00001f) // ほぼ0ベクトルか?
      return *this;
    else
      return Vector3{ x / mag, y / mag, z / mag }; // x / |x|, y / |y|, z / |z|
  }



  /*----- 演算子オーバーロード -----*/

  // 逆ベクトル
  inline Vector3 operator - () const
  {
    return *this*-1;
  }

  // Vectorをそのまま足し合わせる
  inline Vector3 operator + () const
  {
    return *this;
  }

  // Vector同士の足し算 x,y,z個別に足し合わせる
  Vector3 operator + (const Vector3 add_v3) const
  {
    Vector3 v3; //★*thisじゃダメな理由 a = vec1 + vec2のとき vec1の数値が書き変わったらあかんから
    v3.x = this->x + add_v3.x; //[コレはダメ] this->x = this->x + add_v3.x;
    v3.y = this->y + add_v3.y;
    v3.z = this->z + add_v3.z;

    return v3;
  }

  // Vector同士の引き算 x,y,z個別に引き合わせる
  Vector3 operator - (const Vector3 minus_v3) const
  {
    Vector3 v3;

    v3.x = this->x - minus_v3.x;
    v3.y = this->y - minus_v3.y;
    v3.z = this->z - minus_v3.z;

    return v3;
  }

  // Vector同士の掛け算 x,y,z個別に掛け合わせる
  Vector3 operator * (float multiply_num) const
  {
    Vector3 v3;

    v3.x = this->x * multiply_num;
    v3.y = this->y * multiply_num;
    v3.z = this->z * multiply_num;

    return v3;
  }

  // Vector同士の割り算 x,y,z個別に割り合わせる
  // 0.fでは割れないようにしてある
  Vector3 operator / (float divide_num) const
  {
    if (divide_num == 0.0f)
    return *this;

    Vector3 v3;
    v3.x = this->x / divide_num;
    v3.y = this->y / divide_num;
    v3.z = this->z / divide_num;

    return v3;
  }

  Vector3& operator += (const Vector3 add_v3)
  {
    this->x += add_v3.x;
    this->y += add_v3.y;
    this->z += add_v3.z;

    return *this; //*thisを返すことで v1 + v2 + v3見たく数珠繋ぎできる
  }

  Vector3& operator -= (const Vector3 minus_v3)
  {
    this->x -= minus_v3.x;
    this->y -= minus_v3.y;
    this->z -= minus_v3.z;

    return *this;
  }

  Vector3& operator *= (float multiply_num)
  {
    this->x *= multiply_num;
    this->y *= multiply_num;
    this->z *= multiply_num;

    return *this;
  }

  // 0.fでは割れないようにしてある
  Vector3& operator /= (float divide_num)
  {
    if (divide_num == 0.0f)
      return *this;

    this->x /= divide_num;
    this->y /= divide_num;
    this->z /= divide_num;

    return *this;
  }

  // 代入演算子 x,y,zを全部 change_numに変える
  Vector3& operator = (float change_num)
  {
    this->x = change_num;
    this->y = change_num;
    this->z = change_num;

    return *this;
  }

  // 一致演算子 x,y,zを全部一致するか 一つでも違えばfalse
  bool operator == (const Vector3& v3_other)
  {
    if (this->x != v3_other.x) return false;
    if (this->y != v3_other.y) return false;
    if (this->z != v3_other.z) return false;
    return true;
  }

  // 不一致演算子 x,y,zを全部不一致か 一つでも同じならfalse
  bool operator != (const Vector3& v3_other)
  {
    if (this->x == v3_other.x) return false;
    if (this->y == v3_other.y) return false;
    if (this->z == v3_other.z) return false;
    return true;
  }

  /*---- 以下 DXライブラリ向けにVector3とDX版Vectorの変換方法を用意 ---------------*/

// DXlibを#includeしていればDX_LIB_Hも定義済みなので下記関数も定義される VECTORの定義はDXライブラリ版のVector3
#ifdef DX_LIB_H
  // Vector3からDxLibのVECTORへの変換
  VECTOR Vec3ToVec()
  {
    VECTOR Result;
    Result.x = this->x;
    Result.y = this->y;
    Result.z = this->z;
    return Result;
  }

  // 左辺値がDXのVECTOR型のときの=代入対応
  inline operator VECTOR() const
  {
    VECTOR Result;
    Result.x = this->x;
    Result.y = this->y;
    Result.z = this->z;
    return Result;
  }

  // 右辺値がDXのVECTOR型のときの=代入対応
  Vector3 operator=(const VECTOR& other)
  {
    this->x = other.x;
    this->y = other.y;
    this->z = other.z;
    return *this;
  }
#endif


};

#endif



Vector3.cppを新規作成してその内容を次のようにします:

#include "Vector3.h"

// Unityのstatic変数を参考に https://docs.unity3d.com/ja/current/ScriptReference/Vector3.html
Vector3 const Vector3::zero = Vector3(0.f, 0.f, 0.f );
Vector3 const Vector3::one = Vector3(1.f, 1.f, 1.f );
Vector3 const Vector3::forward = Vector3(0.f, 0.f, 1.f );
Vector3 const Vector3::back = Vector3(0.f, 0.f, -1.f );
Vector3 const Vector3::up = Vector3(0.f, 1.f, 0.f );
Vector3 const Vector3::down = Vector3(0.f, -1.f, 0.f );
Vector3 const Vector3::left = Vector3(-1.f, 0.f, 0.f );
Vector3 const Vector3::right = Vector3(1.f, 0.f, 0.f );
//[負の値の最小値] https://stackoverflow.com/questions/20016600/negative-infinity
Vector3 const Vector3::negativeInfinity = Vector3(-std::numeric_limits<float>::infinity(), -std::numeric_limits<float>::infinity(), -std::numeric_limits<float>::infinity() );
//[極大値]https://cpprefjp.github.io/reference/limits/numeric_limits/lowest.html
Vector3 const Vector3::positiveInfinity = Vector3(std::numeric_limits<float>::infinity(), std::numeric_limits<float>::infinity(), std::numeric_limits<float>::infinity() );


プレイヤや弾のクラスのx,yをVector3型に置きかえてみます

PlayerBullet.hのx,yをVector3型におきかえます。

#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_

#include <cmath> //sin,cosを使うのに必要

#include "DxLib.h"
#include "Image.h"
#include "Screen.h"
#include "Vector3.h"

// 自機弾クラス
class PlayerBullet
{
public:

  Vector3 position; // xyz座標
  Vector3 v; // xyz方向移動速度

  const float Speed = 25; // 移動速度
  const int VisibleRadius = 16; // 見た目の半径
  


 float x; // x座標
 float y; // y座標


  bool isDead = false; // 死亡フラグ

  float vx; // x方向移動速度
  float vy; // y方向移動速度

  float angle; // 移動角度

  // コンストラクタ
  PlayerBullet(Vector3 pos, float angle) : position { pos }
  {
    this->x = x;
    this->y = y;


    this->angle = angle;
    this->v = Vector3((float)std::cos(angle) * Speed,(float)std::sin(angle) * Speed, 0);
    vx = (float)std::cos(angle) * Speed;
    vy = (float)std::sin(angle) * Speed;

  }

  // 更新処理
  void Update()
  {
    // 移動
    position += v;
    x += vx;
    y += vy;

    
    // 画面外に出たら、死亡フラグを立てる
    if (position.x + VisibleRadius < 0 || position.x - VisibleRadius > Screen::Width ||
      position.y + VisibleRadius < 0 || position.y - VisibleRadius > Screen::Height
)
    {
      isDead = true;
    }
  }

  // 描画処理
  void Draw()
  {
    DrawRotaGraphF(position.x, position.y, 1, angle, Image::playerBullet,TRUE);
  }
};


#endif


Player.hの内容を次のようにします:

#ifndef PLAYER_H_
#define PLAYER_H_

#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"
#include "Vector3.h"

#include "GameManager.h"

class Player
{
public:

  Vector3 position; // xyz座標

 const float MoveSpeed = 6; // 移動速度

 float x; // x座標
 float y; // y座標


  //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
  GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

  // コンストラクタ
  // pos : 初期位置
 Player(Vector3 pos) : position { pos }
  {
    this->x = x;
    this->y = y;

  }

  virtual ~Player() {}; // 仮想デストラクタ ※無いと【削除された関数を参照しようとしてますエラー】が出るかも

  // 更新処理の関数定義
  void Update();

  // 描画処理の関数定義
  void Draw();

};

#endif

Player.cppもVector3での処理に書き換えます

#include "Player.h"

#include "PlayerBullet.h" // ★弾を生成して【使う処理があるのでインクルード必要】

// 更新処理【書き移す際にはPlayer::という形で::所属が必要になります】
void Player::Update()
{
  float vx = 0; // x方向移動速度
  float vy = 0; // y方向移動速度

  Vector3 v{0,0,0}; // xyz方向移動速度

  if (Input::GetButton(PAD_INPUT_LEFT))
  {
    v.x = -MoveSpeed; // 左
  }
  else if (Input::GetButton(PAD_INPUT_RIGHT))
  {
    v.x = MoveSpeed; // 右
  }
  if (Input::GetButton(PAD_INPUT_UP))
  {
    v.y = -MoveSpeed; // 上
  }
  else if (Input::GetButton(PAD_INPUT_DOWN))
  {
    v.y = MoveSpeed; // 下
  }

  // 斜め移動も同じ速度になるように調整
  if (v.magnitude() > 0)
  {
    v /= MyMath::Sqrt2;
    vx /= MyMath::Sqrt2;
    vy /= MyMath::Sqrt2;

  }

  // 実際に位置を動かす
  position += v;
  x += vx;
  y += vy;


  // ボタン押下で自機弾を発射
  if (Input::GetButtonDown(PAD_INPUT_1))
  {
    // C++ではAddがpush_backに↓ newがmake_shared↓に
    gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, 0));//右
    gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, -15 * MyMath::Deg2Rad));//右上
    gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, +15 * MyMath::Deg2Rad));//右下
  }
}

// 描画処理
void Player::Draw()
{
  DrawRotaGraphF(position.x, position.y, 1, 0, Image::player, TRUE);
}

次にGame.cppファイルのPlayerの生成処理をVector3に対応する形に書き換えましょう。

Game.cppの内容を次のように変更します:

#include "Game.h"

#include "Player.h" // ★初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // ★Update(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理

  Image::Load(); //画像の読込み
  x = 100; // Xの初期位置
  vx = 10;

  gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
};


(中略)...

};

敵の作成【共通のGameObjectを継承する】

Enemyクラスを作成するにあたって共通の基底(ベース)クラスとなるGameObjectを導入する

Enemyクラスを定義してそれを継承する形でZako0をつくることもできますが
その構造で作り進めると循環参照インクルードが起こってしまいがちです。
なぜなら、衝突判定で【PlayerBullet⇔Enemy⇔Playerの相互の矢印の参照ループが生まれやすくなります】
そこで共通の基底(ベース)クラスとなるGameObject、すなわち全てのものが継承する【ベースとなる継承元】を導入します。
これで矢印はGameObjectにまとまりオブジェクト同士の参照をせずに済みます。

共通のベースとなるGameObjectを作成する

GameObject.hを作成します(拡張も見越して3DのZ座標も導入しておきます)。

#ifndef GAMEOBJECT_H_
#define GAMEOBJECT_H_

#include "DxLib.h"
#include "Vector3.h"
#include <string> //文字タグのため
#include <memory> //回し読みポインタの定義のため

// ゲーム上に表示される物体の基底クラス。
// プレイヤーや敵、アイテムなどはこのクラスを継承して作る。
class GameObject
{
public:
  Vector3 position {0,0,0}; // xyz座標
  Vector3 v{0,0,0}; //xyz方向の速度

  bool isDead = false; // 死んだ(削除対象)フラグ

  std::string tag = ""; // Zako0やBossやPlayerなど小カテゴリ
  std::string typeTag = ""; // EnemyやPlayerなど大カテゴリ
  std::string statusTag = ""; // 爆発状態やアイテムゲット状態など自由に使えばよい

  // コンストラクタ
  GameObject(Vector3 pos) : position{ pos }
  {

  }
  //★仮想デストラクタ【忘れるとメモリがヤバいダメ絶対】
  //【注意!】ベースの基底では必ず定義しないとstringなどが浪費され極悪なメモリ被害に発展
  virtual ~GameObject()
  {

  }

  //★純粋仮想関数=0は継承したZakoやPlayerなどの必須機能(継承したら絶対override必須縛り)
  // 更新処理
  virtual void Update() = 0; //純粋仮想関数に(継承したらoverride必須)

  // 描画処理
  virtual void Draw() = 0; //純粋仮想関数に(継承したらoverride必須)

  //★virtual仮想関数は共通カスタマイズ機能(継承してもoverride必須ではないご自由にカスタマイズ)
  // 衝突したときの関数(仮のvirtual関数←純粋仮想関数と違い継承してもoverride必須ではない)
  virtual void OnCollision(std::shared_ptr<GameObject> other)
  {

  }

};

#endif

Enemy.hを作成します。

#ifndef ENEMY_H_
#define ENEMY_H_

#include "GameObject.h"

#include "GameManager.h"

// 敵の基底クラス。
// 全ての敵は、このクラスを継承して作る。
// ★【勉強】abstract型はC++にない!!【純粋仮想関数】を使って作る
class Enemy : public GameObject
{
public:

  //【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
  GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る


  // コンストラクタ
  Enemy(Vector3 pos) : GameObject( pos ) // GameObjectをベースとして初期化処理を継承する
  {
    this->typeTag = "Enemy";
  }

  // 更新処理。派生クラスでオーバーライドして使う
  virtual void Update() = 0;//virtual ~ = 0;で【純粋仮想関数に】これで【C#のabstract型に】

  // 描画処理。派生クラスでオーバーライドして使う
  virtual void Draw() = 0; //virtual ~ = 0;で【純粋仮想関数に】【関数 = 0で未定義状態を表現】

  //【勉強】関数 = 0って何?と思った人は【関数もメモリ上では住所アドレスに過ぎない】ことを知ろう
  // ★関数 = 0はつまり関数の住所ポインタの針が【メモリ上の何かを針で参照してない(住所不定無処理)】
  // つまり【関数が未定義ってこと】=【あいまい部分があるabstractクラスってこと】
  //http://etc2myday.jugem.jp/?eid=204
  //http://wisdom.sakura.ne.jp/programming/c/c54.html
  //逆にいうと【関数を配列】にするっていうトリッキーなこともできる!
  //http://www.ced.is.utsunomiya-u.ac.jp/lecture/2012/prog/p3/kadai3/virtualfunc2.php

  // 自機弾に当たったときの処理
  virtual void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet)
  {
  }
};

#endif

Player.hも共通のベースとなるGameObjectを継承するように書き換えます。

#ifndef PLAYER_H_
#define PLAYER_H_

#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"
#include "Vector3.h"

#include "GameObject.h"

#include "GameManager.h"

class Player : public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:

  //【★注意!】ここに【positionの定義が残ってるとGameObjectと被る】ので
  // 名前の同じ変数が2つ【パラレルワールドに存在してしまう!】
  // Playerからx=13,y=5に変えたつもりでも【ベースのGameObjectはx=0,y=0のままの不思議バグで混乱!】

  Vector3 position; // xyz座標

  const float MoveSpeed = 6; // 移動速度


  //【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
  GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

  // コンストラクタ
  // pos : 初期位置
  Player(Vector3 pos) : GameObject( pos ) //【忘れると】GameObjectの初期化処理が飛ばされて大混乱!
  {
  }

  virtual ~Player() {}; // 仮想デストラクタ

  // 更新処理の関数定義
  void Update();

  // 描画処理の関数定義
  void Draw();

};

#endif

PlayerBullet.hもGameObjectを継承する形に書き換えます。

#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_

#include <cmath> //sin,cosを使うのに必要

#include "DxLib.h"
#include "Image.h"
#include "Screen.h"
#include "Vector3.h"

#include "GameObject.h"

// 自機弾クラス
class PlayerBullet: public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:
  //【★注意!GameObjectと被る変数は全部消す】
  Vector3 position; // xyz座標
  Vector3 v; // xyz方向移動速度


  const float Speed = 25; // 移動速度
  const int VisibleRadius = 16; // 見た目の半径
  

  bool isDead = false; // 死亡フラグ

  float angle; // 移動角度

  // コンストラクタ
  PlayerBullet(Vector3 pos, float angle) : GameObject( pos )
  {
    this->tag = "PlayerBullet";

    this->angle = angle;
    this->v = Vector3((float)std::cos(angle) * Speed, (float)std::sin(angle) * Speed, 0);
  }


  // 更新処理
  void Update()
  {
    (中略)....................
  }

  // 描画処理
  void Draw()
  {
    DrawRotaGraphF(position.x, position.y, 1, angle, Image::playerBullet,TRUE);
  }
};


#endif

Zako0クラスの作成

Zako0.hを作成します。

#ifndef ZAKO_0_H_
#define ZAKO_0_H_

#include "Enemy.h"
#include "Image.h"

// ザコ0クラス
class Zako0 : public Enemy
{
public:
  // コンストラクタ
  Zako0(Vector3 pos)
    : Enemy( pos )
  {
    this->tag = "Zako0";
  }

  // 更新処理
  void Update() override
  {
    position.x -= 1; // とりあえず左へ移動
  }

  // 描画処理
  void Draw() override
  {
    DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako0, TRUE);
  }
};

#endif

Image.hにザコ0,1,2,3と敵弾の画像定義を追加します:

#ifndef IMAGE_H_
#define IMAGE_H_

#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用

class Image
{
public:
  Image() {}; // 初期化コンストラクタ:定義と{}空の処理
  ~Image() {}; // 破棄する処理デストラクタ:定義と{}空の処理
  static void Load();

  static int bossImage; //ボス画像のハンドラ(読込画像番号)
  static int player; //プレイヤ画像のハンドラ
  static int playerBullet; //プレイヤの弾画像のハンドラ
  static int enemyBullet16; //敵弾 画像のハンドラ(読込画像番号)
  static int zako0; //ザコ0 画像のハンドラ(読込画像番号)
  static int zako1; //ザコ1 画像のハンドラ(読込画像番号)
  static int zako2; //ザコ2 画像のハンドラ(読込画像番号)
  static int zako3; //ザコ3 画像のハンドラ(読込画像番号)

private:

};
#endif


次に対となるcppファイルも変更します。
Image.cppの内容を次のようにします:

#include "Image.h"

int Image::bossImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{-1};
int Image::playerBullet{-1};
int Image::enemyBullet16{-1};
int Image::zako0{-1};
int Image::zako1{-1};
int Image::zako2{-1};
int Image::zako3{-1};

void Image::Load()
{
  bossImage = LoadGraph("Image/boss1.png");
  assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  player= LoadGraph("Image/player.png");
  assert(player!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  playerBullet = LoadGraph("Image/player_bullet.png");
  assert(playerBullet!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  enemyBullet16 = LoadGraph("Image/enemy_bullet_16.png");
  assert(enemyBullet16!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる


  zako0 = LoadGraph("Image/zako0.png");
  assert(zako0!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる


  zako1 = LoadGraph("Image/zako1.png");
  assert(zako1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる


  zako2 = LoadGraph("Image/zako2.png");
  assert(zako2!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる


  zako3 = LoadGraph("Image/zako3.png");
  assert(zako3!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

}

敵を管理する仕組みの作成

GameManager.hに敵のリストを追加しましょう。

#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_

#include <memory>
#include <vector>

#include "Singleton.h"

class Player; // クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class PlayerBullet;
class Enemy;

class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
  friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可

  std::shared_ptr<Player> player{ nullptr }; // 自機のポインタ
  std::list<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
  std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
  // 複数リスト↑list ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる


(中略)..............
protected:
  GameManager() {}; // 外部からのインスタンス作成は禁止
  virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};

#endif

Game.cppに敵の生成と更新と描画処理を追加しましょう。

#include "Game.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理

  Image::Load(); //画像の読込み
  x = 100; // Xの初期位置
  vx = 10;

  gm.player = std::make_shared<Player>(Vector3((float)100, (float)Screen::Height / 2)); // 自機の初期化
  // とりあえず適当に敵を生成
  gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)600, (float)100)));
  gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)1000, (float)500)));
};

void Game::Update()
{// 更新処理

  Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

  if (x < 0)
  {
    vx = 10; //速度の変更
  }
  else if (x > Screen::Width)
  {
    vx = -10;
  }
  x += vx; // X位置の更新

  gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】

  // 自機弾の更新処理
  // for文で全自機弾をループで回してUpdateで更新する
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Update();
  }
  
  // 敵の更新処理
  for (const auto& b : gm.enemies)
  {
    b->Update();
  }

  
  //return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
  
  gm.EraseRemoveIf(gm.playerBullets,
    [](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  
};

void Game::Draw()
{// 描画処理
  DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);

  // 敵の描画処理
  for (const auto& b : gm.enemies)
  {
    b->Draw();
  }


  gm.player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】

  // 自機弾の描画処理
  // for文で全自機弾をループで回してUpdateで更新する
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Draw();
  }
};




[C++]シューティングゲーム 2


  1. 敵と自機弾の当たり判定の作成
  2. 敵に耐久力を持たせる(おまけでSinの波運動のコード例も)
  3. 自機と敵の当たり判定
  4. 自機のライフの作成
  5. 敵弾の作成
    - 練習問題(MyRandomで±15度に散乱弾を撃つ敵キャラ)
    - 練習問題(やられると点滅後自爆する敵キャラ)

  6. 次のテキスト3はこちら。


    [C++]シューティングゲーム 3

敵と自機弾の当たり判定の作成

当たり判定の式をMyMathクラスに定義します。

MyMath.hを編集します:

#ifndef _MYMATH_H
#define _MYMATH_H

#include "Vector3.h"

// 数学関連クラス
class MyMath
{
public:
  static const float Sqrt2;  // = 1.41421356237f;(.hではfloat小数型の初期化はできない int型はできるのに)
  static const float PI; // 円周率 3.14159265359f;
  static const float Deg2Rad; // 度からラジアンに変換する定数 PI / 180f;

  /// <summary>
  /// 円と円が重なっているかを調べる
  /// </summary>
  /// <param name="pos1">円1の中心</param>
  /// <param name="radius1">円1の半径</param>
  /// <param name="pos2">円2の中心</param>
  /// <param name="radius2">円2の半径</param>
  /// <returns>重なっていればtrue、重なっていなければfalseを返却する</returns>
  static bool CircleCircleIntersection(
    Vector3 pos1, float radius1,
    Vector3 pos2, float radius2)
  { //[2乗のまま < 比較すると高速] https://www.stmn.tech/entry/2016/06/15/033835
    return ((pos1 - pos2).sqrMagnitude()
      < (radius1 + radius2) * (radius1 + radius2));
  }

}; // 【注意】セミコロン抜けで【宣言が必要ですエラー】

#endif //【宣言が必要ですエラーは上のセミコロン抜け】


Enemy.hを改修します。当たり判定の半径を表す変数と、死亡フラグ、被弾時の処理を追加します。

#ifndef ENEMY_H_
#define ENEMY_H_

#include "GameObject.h"

#include "GameManager.h"

// 敵の基底クラス。
// 全ての敵は、このクラスを継承して作る。
// ★【勉強】abstract型はC++にない!!【純粋仮想関数】を使って作る
class Enemy : public GameObject
{
public:

  //【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
  GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

  float collisionRadius = 32; // 当たり判定の半径
  float life = 1; // 耐久力

  // コンストラクタ
  Enemy(Vector3 pos) : GameObject( pos ) // GameObjectをベースとして初期化処理を継承する
  {
    this->typeTag = "Enemy";
  }

  // 更新処理。派生クラスでオーバーライドして使う
  virtual void Update() = 0;//virtual ~ = 0;で【純粋仮想関数に】これで【C#のabstract型に】

  // 描画処理。派生クラスでオーバーライドして使う
  virtual void Draw() = 0; //virtual ~ = 0;で【純粋仮想関数に】【関数 = 0で未定義状態を表現】

  //【勉強】関数 = 0って何?と思った人は【関数もメモリ上では住所アドレスに過ぎない】ことを知ろう
  // ★関数 = 0はつまり関数の住所ポインタの針が【メモリ上の何かを針で参照してない(住所不定無処理)】
  // つまり【関数が未定義ってこと】=【あいまい部分があるabstractクラスってこと】
  //http://etc2myday.jugem.jp/?eid=204
  //http://wisdom.sakura.ne.jp/programming/c/c54.html
  //逆にいうと【関数を配列】にするっていうトリッキーなこともできる!
  //http://www.ced.is.utsunomiya-u.ac.jp/lecture/2012/prog/p3/kadai3/virtualfunc2.php

  // 自機弾に当たったときの処理 関数の【★引数をGameObject型↓にすることで循環インクルード抑止】
  virtual void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet)
  {
    life -= 1; // ライフを減らす

    // ライフが無くなったら、死亡
    if (life <= 0)
    {
      isDead = true;
    }

  }
};

#endif

PlayerBullet.hを改修します。当たり判定を表す変数を追加します。

#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_

#include <cmath> //sin,cosを使うのに必要

#include "DxLib.h"
#include "Image.h"
#include "Screen.h"

#include "GameObject.h"

// 自機弾クラス
class PlayerBullet: public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:
  const float Speed = 25; // 移動速度
  const int VisibleRadius = 16; // 見た目の半径
  

  float collisionRadius = 16; // 当たり判定の半径
  float angle; // 移動角度


  (中略)....................

  // 更新処理
  void Update()
  {
    (中略)....................
  }

  // 描画処理
  void Draw()
  {
    DrawRotaGraphF(x, y, 1, angle, Image::playerBullet,TRUE);
  }

  // 敵にぶつかった時の処理
  void OnCollisionEnemy(std::shared_ptr<GameObject> other)
  {
    isDead = true;
  }

};

#endif

Game.cppを改修します。自機弾と敵が当たっているか調べる処理を追加します。

#include "Game.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理

  (中略)..................
};

void Game::Update()
{// 更新処理

  Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

  (中略)..................

  gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】

  // 自機弾の更新処理
  // for文で全自機弾をループで回してUpdateで更新する
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Update();
  }
  
  // 敵の更新処理
  for (const auto& b : gm.enemies)
  {
    b->Update();
  }
  
  // 自機弾と敵の衝突判定
  for(const auto& playerBullet : gm.playerBullets)
  {
    // 自機弾が死んでたらスキップする
    if (playerBullet->isDead)
      continue;

    for(const auto& enemy : gm.enemies)
    {
      // 敵が死んでたらスキップする
      if (enemy->isDead)
        continue;

      // 自機弾と敵が重なっているか?
      if (MyMath::CircleCircleIntersection(
        playerBullet->position, playerBullet->collisionRadius,
        enemy->position, enemy->collisionRadius))
      {
        // 重なっていたら、それぞれのぶつかったときの処理を呼び出す
        enemy->OnCollisionPlayerBullet(playerBullet);
        playerBullet->OnCollisionEnemy(enemy);

        // 衝突の結果、自機弾が死んだら、この弾のループはおしまい
        if (playerBullet->isDead)
          break;
      }
    }
  }

  
  //return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
  
  // 自機弾のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.playerBullets,
    [](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

  // 敵のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.enemies,
    [](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

  
};

void Game::Draw()
{// 描画処理
(以下略)............

敵に耐久力を持たせる(おまけでSinの波運動のコード例も)

Zako1.hのlifeを5にすることでZako1を硬くしてみましょう。
ついでにsinの波運動も試してみましょう。

Zako1.hを新規作成します。黄色の部分がZako0との違いです。

#ifndef ZAKO_1_H_
#define ZAKO_1_H_

#include "Enemy.h"

#include <cmath> // Sinの計算に使う
#include "MyMath.h"

#include "Image.h"

// ザコ1クラス
class Zako1 : public Enemy
{
public:
  float sinMove = 0; // Sinの動きをする際の0~360度
  float sinRadius = 70; // Sinの動きをするときの半径
  
  // コンストラクタ
  Zako1(Vector3 pos) : Enemy ( pos )
  {
    this->tag = "Zako1";//オブジェクトの種類判別タグ
    life = 5;
  }

  // 更新処理
  void Update() override
  {
    position.x -= 1; // 左へ移動

    float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
    sinMove += 2.5f; // Sinの波運動の角度を進める
    float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;

    // (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
    position.y += currentSinY - prevSinY; // 上下方向のSinの波運動
  }

  // 描画処理
  void Draw() override
  {
    DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako1, TRUE);
  }
};

#endif

Game.cppにZako1の生成処理を追加します。

#include "Game.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理

  (中略)..................

  // とりあえず適当に敵を生成
  gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)600, (float)100)));
  gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)1000, (float)500)));
  gm.enemies.emplace_back(std::make_shared<Zako1>(Vector3((float)1000, (float)150)));
};

自機と敵の当たり判定

Player.hに当たり判定半径を定義します。

#ifndef PLAYER_H_
#define PLAYER_H_

#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"
#include "Vector3.h"

#include "GameObject.h"

#include "GameManager.h"

class Player : public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:
  const float MoveSpeed = 6; // 移動速度

  float collisionRadius = 32; // 当たり判定半径

  //【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
  GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

  // コンストラクタ
  // pos : 初期位置
  Player(Vector3 pos) : GameObject( pos ) //【忘れると】GameObjectの初期化処理が飛ばされて大混乱!
  {
  }

  virtual ~Player() {}; // 仮想デストラクタ ※無いと【削除された関数を参照しようとしてますエラー】が出るかも
  // 更新処理の関数定義
  void Update();

  // 描画処理の関数定義
  void Draw();

  // 敵にぶつかった時の処理
  void OnCollisionEnemy(std::shared_ptr<GameObject> other);

};

#endif

Player.cppに敵との当たり判定処理を追加します。

#include "Player.h"

#include "PlayerBullet.h" // 弾を生成して【使う処理があるのでインクルード必要】

// 更新処理
void Player::Update()
{
  // 継承したGameObjectにvがあるのでvをfloatで定義し直さないように注意
  v = Vector3(0,0,0); // xyz方向移動速度

  if (Input::GetButton(PAD_INPUT_LEFT))
  {
    v.x = -MoveSpeed; // 左
  }
  else if (Input::GetButton(PAD_INPUT_RIGHT))
  {
    v.x = MoveSpeed; // 右
  }
  if (Input::GetButton(PAD_INPUT_UP))
  {
    v.y = -MoveSpeed; // 上
  }
  else if (Input::GetButton(PAD_INPUT_DOWN))
  {
    v.y = MoveSpeed; // 下
  }

  // 斜め移動も同じ速度になるように調整
  if (v.magnitude() > 0)
  {
    v /= MyMath::Sqrt2;
  }

  // 実際に位置を動かす
  position += v;

  // ボタン押下で自機弾を発射
  if (Input::GetButtonDown(PAD_INPUT_1))
  {
    // C++ではAddがpush_backに↓ newがmake_shared↓に
    gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, 0));//右
    gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, -15 * MyMath::Deg2Rad));//右上
    gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, +15 * MyMath::Deg2Rad));//右下
  } }

// 描画処理
void Player::Draw()
{
  DrawRotaGraphF(position.x, position.y, 1, 0, Image::player, TRUE);
}

// 敵にぶつかった時の処理
void Player::OnCollisionEnemy(std::shared_ptr<GameObject> other)
{
  isDead = true;
}

Game.cppを改修します。自機が死んだとき消す処理と自機と敵の衝突判定を追加します。

#include "Game.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理

  (中略)..................
};

void Game::Update()
{// 更新処理

  Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

  (中略)..................

  if (!gm.player->isDead) // 自機が死んでいなければ
    
gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】

  // 自機弾の更新処理
  // for文で全自機弾をループで回してUpdateで更新する
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Update();
  }
  
  // 敵の更新処理
  for (const auto& b : gm.enemies)
  {
    b->Update();
  }
  
  // 自機弾と敵の衝突判定
  for(const auto& playerBullet : gm.playerBullets)
  {
    // 自機弾が死んでたらスキップする
    if (playerBullet->isDead)
      continue;

    for(const auto& enemy : gm.enemies)
    {
      // 敵が死んでたらスキップする
      if (enemy->isDead)
        continue;

      // 自機弾と敵が重なっているか?
      if (MyMath::CircleCircleIntersection(
        playerBullet->position, playerBullet->collisionRadius,
        enemy->position, enemy->collisionRadius))
      {
        // 重なっていたら、それぞれのぶつかったときの処理を呼び出す
        enemy->OnCollisionPlayerBullet(playerBullet);
        playerBullet->OnCollisionEnemy(enemy);

        // 衝突の結果、自機弾が死んだら、この弾のループはおしまい
        if (playerBullet->isDead)
          break;
      }
    }
  }
  
  // 自機と敵の衝突判定
  for(const auto& enemy : gm.enemies)
  {
    // 自機が死んでたらこれ以上判定しない
    if (gm.player->isDead)
      break;

    // 敵が死んでたらスキップ
    if (enemy->isDead)
      continue;

    // 円同士の衝突判定で調べる
    if (MyMath::CircleCircleIntersection(
      gm.player->position, gm.player->collisionRadius,
      enemy->position, enemy->collisionRadius))
    {
      gm.player->OnCollisionEnemy(enemy);
    }
  }

  
  //return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
  
  // 自機弾のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.playerBullets,
    [](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

  // 敵のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.enemies,
    [](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  
};

void Game::Draw()
{// 描画処理
  DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);

  // 敵の描画処理
  for (const auto& e : gm.enemies)
  {
    e->Draw();
  }

  if (!gm.player->isDead) // 自機が死んでいなければ
    
gm.player->Draw(); // プレイヤの描画【忘れるとプレイヤ表示されない】

  // 自機弾の描画処理
  // C#のforeach文と同じく全自機弾を更新できる
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Draw();
  }
};

自機のライフの作成

Player.hにライフと無敵時間を定義します。

#ifndef PLAYER_H_
#define PLAYER_H_

#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"

#include "GameObject.h"

#include "GameManager.h"

class Player : public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:
  const float MoveSpeed = 6; // 移動速度
  int MutekiJikan = 120; // 無敵時間

  float collisionRadius = 32; // 当たり判定半径

  int life = 3; // ライフ
  int mutekiTimer = 0; // 残り無敵時間。0以下なら無敵じゃないってこと

  //【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
  GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

  // コンストラクタ
  // pos : 初期位置
  Player(Vector3 pos) : GameObject( pos ) //【忘れると】GameObjectの初期化処理が飛ばされて大混乱!
  {
  }

  virtual ~Player() {}; // 仮想デストラクタ ※無いと【削除された関数を参照しようとしてますエラー】が出るかも
  // 更新処理の関数定義
  void Update();

  // 描画処理の関数定義
  void Draw();

  // 敵にぶつかった時の処理
  void OnCollisionEnemy(std::shared_ptr<GameObject> other);

  // ダメージを受ける処理
  void TakeDamage();

};

#endif

Player.cppに敵との当たり判定処理を追加します。

#include "Player.h"

#include "PlayerBullet.h" // 弾を生成して【使う処理があるのでインクルード必要】

// 更新処理
void Player::Update()
{
  // 継承したGameObjectにvがあるのでvをfloatで定義し直さないように修正
  v = Vector3(0,0,0); // xyz方向移動速度

  if (Input::GetButton(PAD_INPUT_LEFT))
  {
    v.x = -MoveSpeed; // 左
  }
  else if (Input::GetButton(PAD_INPUT_RIGHT))
  {
    v.x = MoveSpeed; // 右
  }
  if (Input::GetButton(PAD_INPUT_UP))
  {
    v.y = -MoveSpeed; // 上
  }
  else if (Input::GetButton(PAD_INPUT_DOWN))
  {
    v.y = MoveSpeed; // 下
  }

  // 斜め移動も同じ速度になるように調整
  if (v.magnitude() > 0)
  {
    v /= MyMath::Sqrt2;
  }

  // 実際に位置を動かす
  position += v;

  // ボタン押下で自機弾を発射
  if (Input::GetButtonDown(PAD_INPUT_1))
  {
    // C++ではAddがpush_backに↓ newがmake_shared↓に
    gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, 0));//右
    gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, -15 * MyMath::Deg2Rad));//右上
    gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, +15 * MyMath::Deg2Rad));//右下
  }
  mutekiTimer--; // 無敵タイマーは無条件で毎フレームカウントダウン
}

// 描画処理
void Player::Draw()
{
  // 無敵ではない場合は描画
  // 無敵中は2フレームに1回描画(点滅)
  if (mutekiTimer <= 0 || mutekiTimer % 2 == 0)
    
DrawRotaGraphF(position.x, position.y, 1, 0, Image::player, TRUE);
}

// 敵にぶつかった時の処理
void Player::OnCollisionEnemy(std::shared_ptr<GameObject> other)
{
  // 無敵じゃなければダメージを受ける
  if (mutekiTimer <= 0)
  {
    TakeDamage();
  }

}

// ダメージを受ける処理
void Player::TakeDamage()
{
  life -= 1; // ライフ減少

  if (life <= 0)
  {
    // ライフが無くなったら死亡
    isDead = true;
  }
  else
  {
    // 無敵時間発動
    mutekiTimer = MutekiJikan;
  }
}

敵弾の作成

敵弾の作成(発射間隔を空けて撃つ方法を学ぶ)

EnemyBullet.hを新規作成します。

#ifndef ENEMYBULLET_H_
#define ENEMYBULLET_H_

#include <cmath> //sin,cosを使うのに必要

#include "DxLib.h"
#include "Image.h"
#include "Screen.h"
#include "Vector3.h"

#include "GameObject.h"

// 敵弾クラス
class EnemyBullet: public GameObject
{
public:
  const int VisibleRadius = 8; // 見た目の半径
  

  float angle; // 移動角度(垂直Vertical)
  float angleH=0; // 移動角度(水平Horizontal)


  // コンストラクタ
  EnemyBullet(Vector3 pos, float angle, float speed) : GameObject( pos )
  {
    this->angle = angle;

    // 角度からx方向の移動速度を算出
    v.x = (float)std::cos(angle) * speed;
    // 角度からy方向の移動速度を算出
    v.y = (float)std::sin(angle) * speed;
  }

  // コンストラクタ
  EnemyBullet(Vector3 pos, float angleV, float angleH, float speed) : GameObject( pos )
  {
    this->angle = angleV;
    this->angleH = angleH;

    // [3Dの角度は水平・垂直に分割]https://teratail.com/questions/126004
    float cosAngleV = (float)std::cos(angle);
    // 角度からx方向の移動速度を算出
    v.x = cosAngleV * (float)std::sin(angleH) * speed;
    // 角度からy方向の移動速度を算出
    v.y = (float)std::sin(angle) * speed;
    // 角度からz方向の移動速度を算出
    v.z = cosAngleV * (float)std::cos(angleH) * speed;
  }

  // 更新処理
  void Update()
  {
    // 速度の分だけ移動
    position += v;

    // 画面外に出たら死亡フラグを立てる
    if (position.y + VisibleRadius < 0 || position.y - VisibleRadius > Screen::Height ||
      position.x + VisibleRadius < 0 || position.x - VisibleRadius > Screen::Width)
    {
      isDead = true;
    }
  }

  // 描画処理
  void Draw()
  {
    DrawRotaGraphF(position.x, position.y, 1, angle, Image::enemyBullet16,TRUE);
  }

  // プレイヤにぶつかった時の処理
  virtual void OnCollisionPlayer(std::shared_ptr<GameObject> other)
  {
    isDead = true;
  }
};

#endif

GameManager.hにEnemyBulletを管理するリストの定義を追加します。

#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_

#include <memory>
#include <vector>

#include "Singleton.h"

class Player; // クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class PlayerBullet;
class Enemy;
class EnemyBullet;

class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
  friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可

  std::shared_ptr<Player> player{ nullptr }; // 自機のポインタ
  std::list<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
  std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
  std::list<std::shared_ptr<EnemyBullet>> enemyBullets; // 敵弾のリスト
  // 複数リスト↑list ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる


(中略)..............
protected:
  GameManager() {}; // 外部からのインスタンス作成は禁止
  virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};

#endif

Game.cppにEnemyBulletの更新Updateと削除Remove、描画Drawの処理を追加します。

#include "Game.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "EnemyBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理

  (中略)..................
};

void Game::Update()
{// 更新処理

  Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

  (中略)..................

  if (!gm.player->isDead) // 自機が死んでいなければ
    gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】

  // 自機弾の更新処理
  // for文で全自機弾をループで回してUpdateで更新する
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Update();
  }

  // 敵弾の更新処理
  for (const auto& b : gm.enemyBullets)
  {
    b->Update();
  }


  // 敵の更新処理
  for (const auto& b : gm.enemies)
  {
    b->Update();
  }
  
  // 自機弾と敵の衝突判定
  for(const auto& playerBullet : gm.playerBullets)
  {
    // 自機弾が死んでたらスキップする
    if (playerBullet->isDead)
      continue;

    for(const auto& enemy : gm.enemies)
    {
      // 敵が死んでたらスキップする
      if (enemy->isDead)
        continue;

      // 自機弾と敵が重なっているか?
      if (MyMath::CircleCircleIntersection(
        playerBullet->position, playerBullet->collisionRadius,
        enemy->position, enemy->collisionRadius))
      {
        // 重なっていたら、それぞれのぶつかったときの処理を呼び出す
        enemy->OnCollisionPlayerBullet(playerBullet);
        playerBullet->OnCollisionEnemy(enemy);

        // 衝突の結果、自機弾が死んだら、この弾のループはおしまい
        if (playerBullet->isDead)
          break;
      }
    }
  }
  
  // 自機と敵の衝突判定
  for(const auto& enemy : gm.enemies)
  {
    // 自機が死んでたらこれ以上判定しない
    if (gm.player->isDead)
      continue;

    // 敵が死んでたらスキップ
    if (enemy->isDead)
      continue;

    // 円同士の衝突判定で調べる
    if (MyMath::CircleCircleIntersection(
      gm.player->position, gm.player->collisionRadius,
      enemy->position, enemy->collisionRadius))
    {
      gm.player->OnCollisionEnemy(enemy);
    }
  }
  
  //return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
  
  // 自機弾のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.playerBullets,
    [](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  // 敵のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.enemies,
    [](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  // 敵弾のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.enemyBullets,
    [](std::shared_ptr<EnemyBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

  
};

void Game::Draw()
{// 描画処理
  DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);

  // 敵の描画処理
  for (const auto& e : gm.enemies)
  {
    e->Draw();
  }

  if (!gm.player->isDead) // 自機が死んでいなければ
    gm.player->Draw(); // プレイヤの描画【忘れるとプレイヤ表示されない】

  // 自機弾の描画処理
  // for文で全自機弾をループで回してUpdateで更新する
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Draw();
  }

  // 敵弾の描画処理
  for (const auto& b : gm.enemyBullets)
  {
    b->Draw();
  }

};

Zako0.hに弾を発射間隔を開けながら撃たせます。

#ifndef ZAKO_0_H_
#define ZAKO_0_H_

#include "Enemy.h"
#include "Image.h"
#include "MyMath.h"
#include "EnemyBullet.h" // make_sharedなど【実際の弾を撃ち生成処理を行うのでインクルード必要】

// ザコ0クラス
class Zako0 : public Enemy
{
public:
  int counter = 0;

  // コンストラクタ
  Zako0(Vector3 pos) : Enemy( pos )
  {
    this->tag = "Zako0";
  }

  // 更新処理
  void Update() override
  {
    position.x -= 1; // とりあえず左へ移動

    counter++;

    if (counter % 10 == 0)
    {
      gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, 180 * MyMath::Deg2Rad, 8));
    }

  }

  // 描画処理
  void Draw() override
  {
    DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako0, TRUE);
  }
};

#endif

Zako0.hの弾の発射間隔を開ける方法は別のやり方もあります。

#ifndef ZAKO_0_H_
#define ZAKO_0_H_

#include "Enemy.h"
#include "Image.h"
#include "MyMath.h"
#include "EnemyBullet.h" // make_sharedなど【実際の弾を撃ち生成処理を行うのでインクルード必要】

// ザコ0クラス
class Zako0 : public Enemy
{
public:
  int coolTime = 0; // クールタイム(冷却時間。0になるまで次の弾が撃てない)

  // コンストラクタ
  Zako0(Vector3 pos) : Enemy( pos )
  {
    this->tag = "Zako0";
  }

  // 更新処理
  void Update() override
  {
    position.x -= 1; // とりあえず左へ移動

    coolTime--;

    if (coolTime <= 0)
    {
      gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, 180 * MyMath::Deg2Rad, 8));
      coolTime += 10;
    }

  }

  // 描画処理
  void Draw() override
  {
    DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako0, TRUE);
  }
};

#endif

練習問題(色んな弾の撃ち方を試す)

弾の発射角度の方向を180度 + (-15度~+15度)にすることで散乱弾を実現します。

まず、乱数生成に必要な乱数クラスMyRandom.hを作成します。

#ifndef MYRANDOM_H_
#define MYRANDOM_H_

#include <random>

class MyRandom
{
public:
  // ゲーム中で唯一のインスタンス
  static std::random_device rnd;// 非決定的な乱数生成器
  static std::mt19937 random; // メルセンヌ・ツイスタの32ビット版

  // 初期化(シード指定無し)
  static void Init()
  { // https://cpprefjp.github.io/reference/random/random_device.html
    random.seed(rnd()); // 乱数をシードにすることで毎回ランダムにする
  }

  // 初期化(シードを指定)
  static void Init(int seed)
  {
    random.seed(seed);
  }

  // 指定した範囲の整数の乱数を取得する
  static int Range(int min, int max)
  {
    std::uniform_int_distribution<> randRange(min, max); // [min, max] 範囲の一様乱数
    return randRange(random);
  }

  // 指定した範囲の小数の乱数を取得する
  static float Range(float min, float max)
  {
    std::uniform_real_distribution<> randRange(min, max); // [min<= x <=max] 範囲の一様乱数
    return (float)randRange(random);
  }

  // 指定した確率(%)でtrueになる
  static bool Percent(float probability)
  {
    return Range(0.0f, 1.0f) * 100 <= probability;
  }

  // 指定した範囲で乱数を返却する。
  // 例えば1.5fを指定すると、-1.5~+1.5の範囲の値を返却する。
  static float PlusMinus(float value)
  {
    return Range(-value, value);
  }
};

#endif

staticなクラスなのでMyRandom.cppを作成して変数をcppで定義初期化します。

#include "MyRandom.h"

std::random_device MyRandom::rnd;// 非決定的な乱数生成器
std::mt19937 MyRandom::random; // メルセンヌ・ツイスタの32ビット版 https://qiita.com/angel_p_57/items/971d0208186f81d5d044

Zako2.hを作成して弾の発射方向を-15度~+15度までランダムに散らすようにしてみます。

#ifndef ZAKO_2_H_
#define ZAKO_2_H_

#include "Enemy.h"
#include "Image.h"
#include "Screen.h"
#include <cmath> // Sin Cosの計算に使う
#include "MyMath.h"
#include "MyRandom.h"
#include "EnemyBullet.h" // make_sharedなど【実際の弾を撃ち生成処理を行うのでインクルード必要】

// ザコ2クラス
class Zako2 : public Enemy
{
public:
  int coolTime = 0; // クールタイム(冷却時間。0になるまで次の弾が撃てない)

  // コンストラクタ
  Zako2(Vector3 pos) : Enemy( pos )
  {
    this->tag = "Zako2";
  }

  // 更新処理
  void Update() override
  {
    if(position.x > Screen::Width - 200)
    { // スクリーンの端から200の位置で止まる
      position.x -= 1; // とりあえず左へ移動
    }

    coolTime--;

    if (coolTime <= 0)
    {
      float bulletAngle = MyRandom::Range(-15.0f,15.0f); // ±15度の乱数の角度を取得
      gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, (180 + bulletAngle) * MyMath::Deg2Rad, 8));
      coolTime += 10;
    }
  }

  // 描画処理
  void Draw() override
  {
    DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako2, TRUE);
  }
};

#endif

Game.cppにMyRandom乱数とザコ2の初期化を追加します。

#include "Game.h"
#include "MyRandom.h" 
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "EnemyBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako2.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理
  Image::Load(); //画像の読込み
  MyRandom::Init(); // 乱数シードの初期化

  (中略)..................

  // とりあえず適当に敵を生成
  gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)600, (float)100)));
  gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)1000, (float)500)));
  gm.enemies.emplace_back(std::make_shared<Zako1>(Vector3((float)1000, (float)150)));
  gm.enemies.emplace_back(std::make_shared<Zako2>(Vector3((float)1200, (float)250)));
};

void Game::Update()
{// 更新処理

  (中略)..................

};

void Game::Draw()
{// 描画処理

  (中略)..................
};


どうでしょうか(-15度~+15度)の散乱弾を撃つ敵キャラを作れましたか?

まずはザコ2を試してみましょう。うまくいったら次のザコ3を試してみましょう。


こんどはZako3.hを作成して10ダメージ受けると点滅状態(5秒=5×60=300フレーム)になり、isDeadと同時に弾を全方向に0度~360度に散らすようにしてみます。

勉強になるテクニックとして【共通処理の上書きオーバーライド】や【点滅処理の条件式】や【ド・モルガンの定理】を使っているので上級者も1度は試してみましょう。

#ifndef ZAKO_3_H_
#define ZAKO_3_H_

#include "Enemy.h"
#include "Image.h"
#include "Screen.h"
#include <cmath> // Sin Cosの計算に使う
#include "MyMath.h"
#include "Vector3.h"
#include "MyRandom.h"
#include "EnemyBullet.h" // make_sharedなど【実際の弾を撃ち生成処理を行うのでインクルード必要】

// ザコ3クラス(自爆型機雷タイプの敵キャラ)
class Zako3 : public Enemy
{
public:
  int bombCounter = -1; // やられたときのisDeadまでのカウントダウン
  int BombTime = 300; // 爆発までのカウント時間
  int life = 10; // 敵ライフ

  // コンストラクタ
  Zako3(Vector3 pos) : Enemy( pos )
  {
    this->tag = "Zako3";
  }

  // 更新処理
  void Update() override
  {
    if(position.x > Screen::Width - 350)
    { // スクリーンの端から350の位置で止まる
      position.x -= 1; // とりあえず左へ移動
    }
    else
    { // スクリーン端から350の位置で自爆モードに入る
      life = 0; // 画面中央付近で勝手にlife=0で自爆モードに入る
    }

    bombCounter--; // 爆発カウンタは常にカウントダウン(-1から始まるときは0を通過しないので爆発しない)

    if(life <= 0 && bombCounter < 0)
      bombCounter = BombTime; // 爆発モードに入る(bombCounterがプラス値になり0になると爆発)

    if (bombCounter == 0) // 初期状態は-1からカウントダウン-1,-2,-3..するからここには入らずに済む
    {
      for(int i=0; i < 360; i = i+10) //【勉強】for文は1ずつプラスじゃなくてもよい
      {
        gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, (180 + i) * MyMath::Deg2Rad, 6));
      }
      isDead = true; // 爆発カウンタ==0で初めてisDead状態に
    }
  }

  // 【勉強】共通処理を上書きオーバーライドで上塗りして共通処理を無効化・乗っ取るテクニック
  // 自機弾との衝突処理を★【上書きオーバーライド】して死亡フラグが立たないように
  virtual void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet) override
  {
    life -= 1; // ライフを減らす

    // 死亡フラグはこのザコ3だけ特別に衝突ではONにしない
  }

  // 描画処理
  void Draw() override
  {  
    //【ド・モルガンの定理を学ぼう】||と&&の逆の関係性の法則
    //【勉強】条件【または||】は条件【かつ&&】の(life <= 0 && bombCounter > 0)のちょうど逆条件!
    if(life > 0 || bombCounter <= 0
     || (bombCounter % 8 < 4) ) //[点滅]パラパラ8枚の周期で余り4以下の時だけ描く
      DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako3, TRUE);
  }
};

#endif

Game.cppにザコ3の生成処理を追加します。

#include "Game.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "EnemyBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako2.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako3.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理

  (中略)..................

  // とりあえず適当に敵を生成
  gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)600, (float)100)));
  gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)1000, (float)500)));
  gm.enemies.emplace_back(std::make_shared<Zako1>(Vector3((float)1000, (float)150)));
  gm.enemies.emplace_back(std::make_shared<Zako2>(Vector3((float)1200, (float)250)));
  gm.enemies.emplace_back(std::make_shared<Zako3>(Vector3((float)1300, (float)400)));
};

void Game::Update()
{// 更新処理

  (中略)..................

};

void Game::Draw()
{// 描画処理

  (中略)..................
};


どうでしょうか爆弾っぽい自爆タイプの敵キャラが作れましたか?


[C++]シューティングゲーム 3


  1. 自機と敵弾の当たり判定
  2. 爆発エフェクトの作成(分割画像DivImage型の定義)
    爆発クラスの作成
    管理する仕組みの作成
    爆発を出す処理(Enemyで敵の共通の爆発を生成)
  3. ボスの作成
  4. 【練習問題】ボスの状態ごとの行動パターンのプログラミング
  5. プレイヤのライフをバーで表示させる
  6. アイテムクラスとザコからのドロップ処理の作成
  7. Item0を画面のはしっこで光らせてバウンドさせる処理の作成
  8. Item1クラスでボスへクリティカルヒットしたらバリアーとなる星のアイテムをドロップする処理の作成

  9. 次のテキスト4はこちら。


    [C++]シューティングゲーム 4

自機と敵弾の当たり判定

自機と敵弾がぶつかったら、自機がダメージを受けるようにしましょう。

EnemyBullet.hに当たり判定の大きさを表す変数を追加します。

#ifndef ENEMYBULLET_H_
#define ENEMYBULLET_H_

#include <cmath> //sin,cosを使うのに必要

#include "DxLib.h"
#include "Image.h"
#include "Screen.h"

#include "GameObject.h"

// 敵弾クラス
class EnemyBullet: public GameObject
{
public:
  const int VisibleRadius = 8; // 見た目の半径
  float collisionRadius = 8; // 当たり判定半径

  (以下略)........................

Player.hに敵弾とぶつかったときの処理の定義を追加します。

#ifndef PLAYER_H_
#define PLAYER_H_

#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"

#include "GameObject.h"

#include "GameManager.h"

class Player : public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:

  (中略)...........

  // 敵にぶつかった時の処理
  void OnCollisionEnemy(std::shared_ptr<GameObject> other);

  // 敵弾とぶつかった時の処理
  void OnCollisionEnemyBullet(std::shared_ptr<GameObject> other);


  // ダメージを受ける処理
  void TakeDamage();
};

#endif

Player.cppに敵弾とぶつかったときの処理を追加します。

#include "Player.h"

#include "PlayerBullet.h" // 弾を生成して【使う処理があるのでインクルード必要】

// 更新処理
void Player::Update()
{
  (中略).........
}

// 描画処理
void Player::Draw()
{
  (中略).........
}

// 敵にぶつかった時の処理
void Player::OnCollisionEnemy(std::shared_ptr<GameObject> other)
{
  // 無敵じゃなければダメージを受ける
  if (mutekiTimer <= 0)
  {
    TakeDamage();
  }
}

// 敵弾とぶつかった時の処理
void Player::OnCollisionEnemyBullet(std::shared_ptr<GameObject> other)
{
  // 無敵じゃなければダメージを受ける
  if (mutekiTimer <= 0)
  {
    TakeDamage();
  }
}


// ダメージを受ける処理
void Player::TakeDamage()
{
  life -= 1; // ライフ減少

  if (life <= 0)
  {
    // ライフが無くなったら死亡
    isDead = true;
  }
  else
  {
    // 無敵時間発動
    mutekiTimer = MutekiJikan;
  }
}

Game.cppに当たり判定の処理を追加します。

#include "Game.h"
#include "MyRandom.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "EnemyBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako2.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako3.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理

  (中略)..................
};

void Game::Update()
{// 更新処理

  Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

  (中略)..................

  if (!gm.player->isDead) // 自機が死んでいなければ
    gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】

  // 自機弾の更新処理
  // for文で全自機弾をループで回してUpdateで更新する
  for (const auto& b : gm.playerBullets)
  {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
    b->Update();
  }
  
  // 敵弾の更新処理
  for (const auto& b : gm.enemyBullets)
  {
    b->Update();
  }
  
  // 自機と敵弾の衝突判定
  for (const auto& enemyBullet : gm.enemyBullets)
  {
    // 自機が死んでたらこれ以上判定しない
    if (gm.player->isDead)
      break;

    // 敵弾が死んでたらスキップ
    if (enemyBullet->isDead)
      continue;

    // 円同士の衝突判定で調べる
    if (MyMath::CircleCircleIntersection(
      gm.player->position, gm.player->collisionRadius,
      enemyBullet->position, enemyBullet->collisionRadius))
    {
      gm.player->OnCollisionEnemyBullet(enemyBullet);
    }
  }


  // 敵の更新処理
  for (const auto& e : gm.enemies)
  {
    e->Update();
  }
  
  // 自機弾と敵の衝突判定
  for(const auto& playerBullet : gm.playerBullets)
  {
    // 自機弾が死んでたらスキップする
    if (playerBullet->isDead)
      continue;

    for(const auto& enemy : gm.enemies)
    {
      // 敵が死んでたらスキップする
      if (enemy->isDead)
        continue;

      // 自機弾と敵が重なっているか?
      if (MyMath::CircleCircleIntersection(
        playerBullet->position, playerBullet->collisionRadius,
        enemy->position, enemy->collisionRadius))
      {
        // 重なっていたら、それぞれのぶつかったときの処理を呼び出す
        enemy->OnCollisionPlayerBullet(playerBullet);
        playerBullet->OnCollisionEnemy(enemy);

        // 衝突の結果、自機弾が死んだら、この弾のループはおしまい
        if (playerBullet->isDead)
          break;
      }
    }
  }
  
  // 自機と敵の衝突判定
  for(const auto& enemy : gm.enemies)
  {
    // 自機が死んでたらこれ以上判定しない
    if (gm.player->isDead)
      continue;

    // 敵が死んでたらスキップ
    if (enemy->isDead)
      continue;

    // 円同士の衝突判定で調べる
    if (MyMath::CircleCircleIntersection(
      gm.player->position, gm.player->collisionRadius,
      enemy->position, enemy->collisionRadius))
    {
      gm.player->OnCollisionEnemy(enemy);
    }
  }
  
  //return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
  
  // 自機弾のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.playerBullets,
    [](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

  // 敵のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.enemies,
    [](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  
};

void Game::Draw()
{// 描画処理
  (中略)......
};

爆発エフェクトの作成(分割画像DivImage型の定義)

分割画像DivImage型でデータ内部に分割画像の分割XY数や画像サイズ、画像数も覚えておく

DivImage.hを新規作成して、カスタムしたデータタイプ【DivImage型】を定義して縦2×横8の分割された爆発画像を読込みましょう。

爆発画像は https://twitter.com/jetyam/status/887664013936541696の縦2×横8の64pixの画像を想定

#ifndef DIVIMAGE_H_
#define DIVIMAGE_H_

#include "DxLib.h"
#include <assert.h> // 画像の2のべき乗チェックに使う

// 分割画像を読込むためのデータ構造(画像数や画像ハンドル配列、3Dに使う場合の2のべき乗チェック機能をもつ)
class DivImage
{  // 2Dの分割画像や3Dに分割Div画像を使うと画像全体が使われてしまうのを回避するための情報
public:
  int XNum = 0;
  int YNum = 0;
  int XSize = 0;
  int YSize = 0;
  int* HandleArray = nullptr;
  int AllNum = 0;
  bool is3DTexture = false;

// 初期化 : X方向分割画像数 XNum, Y方向分割画像数 YNum, 画像 横 幅XSizeピクセル, 画像 縦 高さYSizeピクセル
  DivImage(int XNum, int YNum, int XSize, int YSize, bool is3DTexture=false)
  { // 初期化コンストラクタ
    this->XNum = XNum; // X方向分割画像数
    this->YNum = YNum; // Y方向分割画像数
    this->XSize = XSize; // 画像 横 幅XSize(ピクセルドット数)
    this->YSize = YSize; // 画像 縦 高さYSize(ピクセルドット数)
    AllNum = XNum * YNum; // トータルの分割画像数
    this->is3DTexture = is3DTexture; // 3Dテクスチャ分割画像かtrueだと2のべき乗チェックが入る
    // ★div分割画像読込ハンドラ保存用のint配列メモリを確保し-1で初期化
    this->HandleArray = new int[AllNum]; // 配列を確保し-1で初期化
    for (int i = 0; i < AllNum; i++) HandleArray[i] = -1;
    //↑【演習★】初期化後の動的配列をデバッガで見てみよう[ary,256として再評価ボタン]
    // http://visualstudiostudy.blog.fc2.com/blog-entry-17.html
    int* ary = HandleArray;
#ifdef _DEBUG // デバッグ機能(デバッグの時だけ _DEBUG が定義)
    if(is3DTexture==true) ImagePowCheck((*this)); // *(this)でthisポインタの示す変数内容を表す
#endif
  };

  ~DivImage()
  { // デストラクタでメモリを解放
    if (this->HandleArray != nullptr)
      delete[] this->HandleArray;
  };

  //【勉強 []添え字演算子を独自に定義】https://programming.pc-note.net/cpp/operator.html
  // 内部のHandleArrayへ今までの様にImage::explosion[5]とかで簡単アクセスできるように【あいだに噛ます】
  int const& operator [](int index) const
  {
    if(index<0 || AllNum<=index) assert("画像データのアクセス番号がおかしい!" == "");
    return HandleArray[index];
  }
  int& operator [](int index)
  {
    if(index<0 || AllNum<=index) assert("画像データのアクセス番号がおかしい!" == "");
    return HandleArray[index];
  }

#ifdef _DEBUG // デバッグ機能(デバッグの時だけ _DEBUG が定義)
  bool is_pow2(unsigned int x) // 2のべき乗か計算
  { // https://programming-place.net/ppp/contents/c/rev_res/math012.html
    return (x != 0) && (x & (x - 1)) == 0;
  }

  void ImagePowCheck(DivImage& divImage)
  { // ★3Dに使う画像は2のべき乗でなければ受け付けないようにする
    // https://yttm-work.jp/gmpg/gmpg_0031.html
    // https://yappy-t.hatenadiary.org/entry/20100110/1263138881
    if (divImage.XSize > 0 && divImage.YSize > 0
      && is_pow2(divImage.XSize) && is_pow2(divImage.YSize)) return;
    else assert("3Dに使うなら2のべき乗の画像サイズにしなきゃ" == "");
  }
#endif

private: // コピーと代入をプライベートにして禁止する
    // コピーコンストラクタの禁止privateオーバーロード
    DivImage(const DivImage& divImage) {};

    // 代入演算子の禁止privateオーバーロード
    void operator=(const DivImage& divImage) {};
};

#endif

Image.hでカスタムしたデータタイプ【DivImage型】で縦2×横8の分割された爆発画像を読込みましょう。

#ifndef IMAGE_H_
#define IMAGE_H_

#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
#include "DivImage.h" // 分割画像の読込みに使う

class Image
{
public:
  Image() {}; // 初期化コンストラクタ:定義と{}空の処理
  ~Image() {}; // 破棄する処理デストラクタ:定義と{}空の処理
  static void Load();
  static int LoadDivGraph(const TCHAR* FileName, DivImage& divImage);

  static int bossImage; //ボス画像のハンドラ(読込画像番号)
  static int player; //プレイヤ画像のハンドラ
  static int playerBullet; //プレイヤの弾画像のハンドラ
  static int enemyBullet16; //敵弾 画像のハンドラ(読込画像番号)
  static int zako0; //ザコ0 画像のハンドラ(読込画像番号)
  static int zako1; //ザコ1 画像のハンドラ(読込画像番号)
  static int zako2; //ザコ2 画像のハンドラ(読込画像番号)
  static int zako3; //ザコ3 画像のハンドラ(読込画像番号)
  static DivImage explosion; // [分割画像]爆発エフェクト

private:

};
#endif


次に対となるcppファイルも変更します。
Image.cppの内容を次のようにします:

#include "Image.h"

int Image::bossImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{-1};
int Image::playerBullet{-1};
int Image::enemyBullet16{-1};
int Image::zako0{-1};
int Image::zako1{-1};
int Image::zako2{-1};
int Image::zako3{-1};
// ★分割画像は初期化のときに{X方向画像数, Y方向画像数, 画像横幅XSize,画像縦幅YSize}を指定
DivImage Image::explosion{ 8, 2, 64, 64 };
//↑どのような画像を使うかで変わる
// 例えば https://pipoya.net/sozai/assets/effects/effect-event-1/ のイベントエフェクト.zipの
// 1800×180の縦一列のみなら DivImage Image::explosion{ 10, 1, 180, 180 };となる


void Image::Load()
{
  bossImage = LoadGraph("Image/boss1.png");
  assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  player= LoadGraph("Image/player.png");
  assert(player!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  playerBullet = LoadGraph("Image/player_bullet.png");
  assert(playerBullet!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  enemyBullet16 = LoadGraph("Image/enemy_bullet_16.png");
  assert(enemyBullet16!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  zako0 = LoadGraph("Image/zako0.png");
  assert(zako0!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  zako1 = LoadGraph("Image/zako1.png");
  assert(zako1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  zako2 = LoadGraph("Image/zako2.png");
  assert(zako2!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  zako3 = LoadGraph("Image/zako3.png");
  assert(zako3!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  // div分割画像をロード
  Image::LoadDivGraph("Image/explosion.png", explosion);
  for (int i = 0; i < explosion.AllNum; i++)
  { // 画像読込失敗
    if (explosion.HandleArray[i] == -1) assert("爆発分割画像読込失敗" == "");
  }

}

// DXライブラリのLoadDivGraphを使いやすくラッピング
// ★関数に参照渡しを行う
// 関数の引数に参照渡しを行うことでdivImageの中身を書き換えることができる(値渡しでは書換不可)
// https://qiita.com/agate-pris/items/05948b7d33f3e88b8967
// https://qiita.com/RuthTaro/items/f35c3a26779c0ca1a41a
int Image::LoadDivGraph(const TCHAR* FileName, DivImage& divImage)
{
  return DxLib::LoadDivGraph(FileName, divImage.XNum * divImage.YNum,
    divImage.XNum, divImage.YNum, divImage.XSize, divImage.YSize, divImage.HandleArray);
};

爆発クラスの作成

Explosion.hを新規作成して爆発エフェクトを表示するクラスを作ります。

#ifndef EXPLOSION_H_
#define EXPLOSION_H_

#include "Image.h"

#include "GameObject.h"

// 爆発エフェクトクラス
class Explosion : public GameObject
{
public:
  int counter = 0; // 時間を計るための変数
  int imageIndex = 0; // 表示すべき画像の番号

  // コンストラクタ
  Explosion(Vector3 pos) : GameObject( pos )
  {
    this->tag = "Explosion";
  }

  // 更新処理
  void Update() override
  {
    counter++;

    imageIndex = counter / 3; // そのままだと速すぎるので、3で割る

    // 絵は16枚しかない(0~15)ので、
    // 16番目を表示しようとしたら終了。
    if (imageIndex >= 16)
    {
      isDead = true;
    }
  }

  // 描画処理
  void Draw() override
  {
  // 分割して読み込んだ絵のハンドルが各要素に格納されているので、
  // 要素番号を指定してハンドルを取り出して使う。
    DrawRotaGraphF(position.x, position.y, 1, 0, Image::explosion[imageIndex], TRUE);
  }
};

#endif

管理する仕組みの作成

GameManager.hにExplosionを管理するリストの定義を追加します。

#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_

#include <memory>
#include <vector>

#include "Singleton.h"

class Player; // クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class PlayerBullet;
class Enemy;
class EnemyBullet;
class Explosion;

class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
  friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可

  std::shared_ptr<Player> player{ nullptr }; // 自機のポインタ
  std::list<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
  std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
  std::list<std::shared_ptr<EnemyBullet>> enemyBullets; // 敵弾のリスト
  std::list<std::shared_ptr<Explosion>> explosions; // 爆発エフェクトのリスト
  // 複数リスト↑list ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる


(中略)..............
protected:
  GameManager() {}; // 外部からのインスタンス作成は禁止
  virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};

#endif

Game.cppに当たり判定の処理を追加します。

#include "Game.h"

#include "MyRandom.h"

#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "EnemyBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Explosion.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako2.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako3.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理

  (中略)..................
};

void Game::Update()
{// 更新処理

  Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

  (中略)..................

  // 自機と敵の衝突判定
  for(const auto& enemy : gm.enemies)
  {
    // 自機が死んでたらこれ以上判定しない
    if (gm.player->isDead)
      continue;

    // 敵が死んでたらスキップ
    if (enemy->isDead)
      continue;

    // 円同士の衝突判定で調べる
    if (MyMath::CircleCircleIntersection(
      gm.player->position, gm.player->collisionRadius,
      enemy->position, enemy->collisionRadius))
    {
      gm.player->OnCollisionEnemy(enemy);
    }
  }
  
  // 爆発エフェクトの更新処理
  for (const auto& e : gm.explosions)
  {
    e->Update();
  }

  

  //return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
  
  // 自機弾のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.playerBullets,
    [](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  // 敵のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.enemies,
    [](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  // 敵弾のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.enemyBullets,
    [](std::shared_ptr<EnemyBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  //爆発エフェクトのリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.explosions,
    [](std::shared_ptr<Explosion>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

  
};

void Game::Draw()
{// 描画処理
  (中略)......

  // 敵の描画処理
  for (const auto& e : gm.enemies)
  {
    e->Draw();
  }
  
  // 爆発エフェクトの描画処理
  for (const auto& e : gm.explosions)
  {
    e->Draw();
  }

  
  (中略)......

};

爆発を出す処理(Enemyで敵の共通の爆発を生成)

Enemy.hを改修して、敵が死んだときに爆発エフェクトを出すようにしましょう。

#ifndef ENEMY_H_
#define ENEMY_H_

#include "GameObject.h"

#include "GameManager.h"

#include "Explosion.h"

// 敵の基底クラス。
// 全ての敵は、このクラスを継承して作る。
// ★【勉強】abstract型はC++にない!!【純粋仮想関数】を使って作る
class Enemy : public GameObject
{
public:

  //【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
  GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

  float collisionRadius = 32; // 当たり判定の半径
  bool isDead = false; // 死亡フラグ
  float life = 1; // 耐久力

  // コンストラクタ
  Enemy(Vector3 pos) : GameObject( pos ) // GameObjectをベースとして初期化処理を継承する
  {
    this->typeTag = "Enemy";
  }

  // 更新処理。派生クラスでオーバーライドして使う
  virtual void Update() = 0;//virtual ~ = 0;で【純粋仮想関数に】これで【C#のabstract型に】

  // 描画処理。派生クラスでオーバーライドして使う
  virtual void Draw() = 0; //virtual ~ = 0;で【純粋仮想関数に】【関数 = 0で未定義状態を表現】


  // 自機弾に当たったときの処理
  virtual void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet)
  {
    life -= 1; // ライフを減らす

    // ライフが無くなったら、死亡
    if (life <= 0)
    {
      isDead = true;
      // 爆発を出す
      gm.explosions.emplace_back(std::make_shared<Explosion>(Vector3(position.x, position.y)));

    }
  }
};

#endif


ここまでで、実行して爆発が出るか試してみましょう!

ボスの作成

まずはボスの3状態の画像をImageフォルダに用意しておきましょう。

Image.hにboss1.png、boss2.png、boss3.pngの読込み処理を追加します。

#ifndef IMAGE_H_
#define IMAGE_H_

#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
#include "DivImage.h" // 分割画像の読込みに使う

class Image
{
public:
  Image() {}; // 初期化コンストラクタ:定義と{}空の処理
  ~Image() {}; // 破棄する処理デストラクタ:定義と{}空の処理
  static void Load();
  static int LoadDivGraph(const TCHAR* FileName, DivImage& divImage);

  static int bossImage; //ボス画像のハンドラ(読込画像番号)
  static int player; //プレイヤ画像のハンドラ
  static int playerBullet; //プレイヤの弾画像のハンドラ
  static int enemyBullet16; //敵弾 画像のハンドラ(読込画像番号)
  static int zako0; //ザコ0 画像のハンドラ(読込画像番号)
  static int zako1; //ザコ1 画像のハンドラ(読込画像番号)
  static int zako2; //ザコ2 画像のハンドラ(読込画像番号)
  static int zako3; //ザコ3 画像のハンドラ(読込画像番号)
  static int boss1; //ボス通常 画像のハンドラ(読込画像番号)
  static int boss2; //ボス2気絶 画像のハンドラ(読込画像番号)
  static int boss3; //ボス3発狂 画像のハンドラ(読込画像番号)
  static DivImage explosion; // [分割画像]爆発エフェクト

private:

};
#endif


次に対となるcppファイルも変更します。
Image.cppの内容を次のようにします:

#include "Image.h"

int Image::bossImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{-1};
int Image::playerBullet{-1};
int Image::enemyBullet16{-1};
int Image::zako0{-1};
int Image::zako1{-1};
int Image::zako2{-1};
int Image::zako3{-1};
int Image::boss1{-1};
int Image::boss2{-1};
int Image::boss3{-1};
// ★分割画像は初期化のときに{X方向画像数, Y方向画像数, 画像横幅XSize,画像縦幅YSize}を指定
DivImage Image::explosion{ 8, 2, 64, 64 };

void Image::Load()
{
  bossImage = LoadGraph("Image/boss1.png");
  assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  player= LoadGraph("Image/player.png");
  assert(player!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  playerBullet = LoadGraph("Image/player_bullet.png");
  assert(playerBullet!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  enemyBullet16 = LoadGraph("Image/enemy_bullet_16.png");
  assert(enemyBullet16!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  zako0 = LoadGraph("Image/zako0.png");
  assert(zako0!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  zako1 = LoadGraph("Image/zako1.png");
  assert(zako1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  zako2 = LoadGraph("Image/zako2.png");
  assert(zako2!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  zako3 = LoadGraph("Image/zako3.png");
  assert(zako3!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  boss1 = LoadGraph("Image/boss1.png");
  assert(boss1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる


  boss2 = LoadGraph("Image/boss2.png");
  assert(boss2!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる


  boss3 = LoadGraph("Image/boss3.png");
  assert(boss3!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる


  // div分割画像をロード
  Image::LoadDivGraph("Image/explosion.png", explosion);
  for (int i = 0; i < explosion.AllNum; i++)
  { // 画像読込失敗
    if (explosion.HandleArray[i] == -1) assert("爆発分割画像読込失敗" == "");
  }
}

// DXライブラリのLoadDivGraphを使いやすくラッピング
// ★関数に参照渡しを行う
// 関数の引数に参照渡しを行うことでdivImageの中身を書き換えることができる(値渡しでは書換不可)
// https://qiita.com/agate-pris/items/05948b7d33f3e88b8967
// https://qiita.com/RuthTaro/items/f35c3a26779c0ca1a41a
int Image::LoadDivGraph(const TCHAR* FileName, DivImage& divImage)
{
  return DxLib::LoadDivGraph(FileName, divImage.XNum * divImage.YNum,
    divImage.XNum, divImage.YNum, divImage.XSize, divImage.YSize, divImage.HandleArray);
};


Boss.hを新規作成してif else でStateに応じて行動を遷移させてみましょう。

登場」画面右から出てくる
 ↓
通常」通常の状態。通常の攻撃を行う
 ↓
気絶」気絶状態の絵になる
 ↓
発狂」怒った絵になる。激しい攻撃を繰り広げてくる
 ↓
死亡」少し時間をかけて死亡の演出を行う
 ↓
完全に消滅


Boss.hを新規作成します。

#ifndef BOSS_H_
#define BOSS_H_

#include "Enemy.h"

#include "Image.h"

// ボスクラス。Enemyを継承して作る
class Boss : public Enemy
{
public:
    // ボスの状態種別
    enum class State
    {
        Appear, // 登場
        Normal, // 通常時
        Swoon, // 気絶時
        Angry, // 発狂モード
        Dying, // 死亡
    };

    State state = State::Appear; // 現在の状態
    int swoonTime = 120; // 残り気絶時間
    int dyingTime = 180; // 死亡時のアニメーション時間

    // コンストラクタ
    Boss(Vector3 pos) : Enemy( pos )
    {
        this->tag = "Boss"; // オブジェクトの種類判別タグ
        life = 100; // ライフ
        collisionRadius = 70; // 当たり判定半径
    }

    // 更新処理
    void Update() override
    {
        if (state == State::Appear) // 登場状態
        {
            position.x -= 1; // 左へ移動

            if (position.x <= 750) // x座標が750以下になったら
            {
                state = State::Normal; // 通常状態へ移行
            }
        }
        else if (state == State::Normal) // 通常状態
        {
        }
        else if (state == State::Swoon) // 気絶状態
        {
            swoonTime--; // タイマー減少

            if (swoonTime <= 0) // タイマーが0になったら
            {
                state = State::Angry; // 発狂モードへ
            }
        }
        else if (state == State::Angry) // 発狂モード
        {
        }
        else if (state == State::Dying) // 死亡中
        {
            dyingTime--; // タイマー減少

            if (dyingTime <= 0) // タイマーが0になったら
            {
                isDead = true; // 完全に消滅
            }
        }
    }

    // 描画処理
    void Draw() override
    {
        if (state == State::Appear || state == State::Normal) // 登場時と通常時
        {
            DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss1, TRUE); // 普通の画像
        }
        else if (state == State::Swoon) // 気絶時
        {
            DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss2, TRUE); // 気絶時の画像
        }
        else if (state == State::Angry) // 発狂モード
        {
            DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss3, TRUE); // 怒ってる画像
        }
        else if (state == State::Dying) // 死亡中
        {
            DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss2, TRUE); // 気絶時の画像
        }
    }

    // 自機弾に当たったときの処理をoverride(上書き)する
    virtual void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet) override
    {
        // 登場時、気絶時、死亡時は被弾しても何もしない
        if (state == State::Appear || state == State::Swoon || state == State::Dying)
            return;

        life -= 1; // ライフを減らす

        if (life <= 0)
        {
            // ライフが無くなったら、すぐ消滅するのではなく、死亡状態へ移行
            state = State::Dying;
        }
        else if (state == State::Normal && life <= 50)
        {
            // 通常状態でライフが50以下になったら、気絶する
            state = State::Swoon;
        }
    }
};

#endif

Game.cppにボスの生成処理を追加してボスが期待通りに状態遷移することを確認しましょう。

#include "Game.h"

#include "MyRandom.h"

#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"
#include "Zako0.h"
#include "Zako1.h"
#include "Zako2.h"
#include "Zako3.h"
#include "Boss.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理
    Image::Load(); //画像の読込み
    MyRandom::Init(); // 乱数シードの初期化

    (中略).................

    gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
    // とりあえず適当に敵を生成
    gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
}

(以下略).................


【練習問題】ボスの状態ごとの行動パターンのプログラミング

さきほどのボスは登場してもじっとしたままです。
Zako0~Zako3の動きを応用して自分オリジナルのボスの行動パターンを自作してみてください。
例として下記に実装例を置いてはおきますがあくまで参考として、自分で考えて組んでみることが大事です。
ここまでの復習と集大成として、Zako0~Zako3のコードを移植しながら、プログラミングすることが肝要です。

Boss.hの行動パターンのプログラム例(lifeとenumの状態をAngry以外にも増やせばもっと長期戦も演出できる)

#ifndef BOSS_H_
#define BOSS_H_

#include "Enemy.h"

#include "Image.h"

#include <cmath> // Sin Cosの計算に使う
#include "MyMath.h"
#include "MyRandom.h" // 乱数方向弾に使う


// ボスクラス。Enemyを継承して作る
class Boss : public Enemy
{
public:
    // ボスの状態種別
    enum class State
    {
        Appear, // 登場
        Normal, // 通常時
        Swoon, // 気絶時
        Angry, // 発狂モード
        Dying, // 死亡
    };

    State state = State::Appear; // 現在の状態
    int swoonTime = 120; // 残り気絶時間
    int dyingTime = 180; // 死亡時のアニメーション時間

    int coolTime = 0; // クールタイム(冷却時間。0になるまで次の弾が撃てない)
    float sinMove = 0; // Sinの動きをする際の0~360度
    float sinRadius = 120; // Sinの動きをするときの半径


    // コンストラクタ
    Boss(Vector3 pos) : Enemy( pos )
    {
        this->tag = "Boss"; // オブジェクトの種類判別タグ
        life = 100; // ライフ
        collisionRadius = 70; // 当たり判定半径
    }

    // 更新処理
    void Update() override
    {
        if (state == State::Appear) // 登場状態
        {
            position.x -= 1; // 左へ移動

            if (position.x <= 750) // x座標が750以下になったら
            {
                state = State::Normal; // 通常状態へ移行
            }
        }
        else if (state == State::Normal) // 通常状態
        {
            coolTime--;
           
            if (coolTime <= 0)
            {   // ボスの弾はスピード16で2倍の速度で撃ち出す
                gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, 180 * MyMath::Deg2Rad, 16));
                coolTime += 200; // そのかわり発射間隔は長め
            }
           
            float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
            sinMove += 2.5f; // Sinの波運動の角度を進める
            float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
           
            // (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
            position.y += currentSinY - prevSinY; // 上下方向のSinの波運動

        }
        else if (state == State::Swoon) // 気絶状態
        {
            swoonTime--; // タイマー減少

            if (swoonTime <= 0) // タイマーが0になったら
            {
                coolTime = 0; // クールタイムを0にリセットしてから
                state = State::Angry; // 発狂モードへ
            }
        }
        else if (state == State::Angry) // 発狂モード
        {
            coolTime--;
           
            if (coolTime <= 0)
            {
                gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, 180 * MyMath::Deg2Rad, 16));
                coolTime += 20;
            }
           
            float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
            sinMove += 10.5f; // Sinの波運動の角度を進める
            float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
           
            // (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
            position.y += currentSinY - prevSinY; // 上下方向のSinの波運動

        }
        else if (state == State::Dying) // 死亡中
        {
            dyingTime--; // タイマー減少

            if (dyingTime <= 0) // タイマーが0になったら
            {
                isDead = true; // 完全に消滅
            }
        }
    }

    // 描画処理
    void Draw() override
    {
        if (state == State::Appear || state == State::Normal) // 登場時と通常時
        {
            DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss1, TRUE); // 普通の画像
        }
        else if (state == State::Swoon) // 気絶時
        {
            DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss2, TRUE); // 気絶時の画像
        }
        else if (state == State::Angry) // 発狂モード
        {
            DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss3, TRUE); // 怒ってる画像
        }
        else if (state == State::Dying) // 死亡中
        {
            DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss2, TRUE); // 気絶時の画像
        }
    }

    // 自機弾に当たったときの処理をoverride(上書き)する
    virtual void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet) override
    {
        // 登場時、気絶時、死亡時は被弾しても何もしない
        if (state == State::Appear || state == State::Swoon || state == State::Dying)
            return;

        life -= 1; // ライフを減らす

        if (life <= 0)
        {
            // ライフが無くなったら、すぐ消滅するのではなく、死亡状態へ移行
            state = State::Dying;
        }
        else if (state == State::Normal && life <= 50)
        {
            // 通常状態でライフが50以下になったら、気絶する
            state = State::Swoon;
        }
    }
};

#endif


enumを使った状態の遷移はボスだけでなくZakoにも応用できます。
プログラマの腕の見せ所(醍醐味)は、さまざまな敵の行動パターンを組むことにあります。
(これを発展させると、プレイヤの状態に合わせて知的に行動するAIにつながっていきます)


プレイヤのライフをバーで表示させる

Player.hに現在のライフをバーで表示する処理の定義を追加します。

#ifndef PLAYER_H_
#define PLAYER_H_

#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"

#include "GameObject.h"

#include "GameManager.h"

class Player : public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:

  (中略)...........

  int lifeMax = 20; // 最大ライフ
  int life = 3; // ライフ
  int mutekiTimer = 0; // 残り無敵時間。0以下なら無敵じゃないってこと

  (中略)...........

  // 描画処理
  void Draw();//【★←Player.cppを新規作成してそちらに処理を書き移す】

  // ライフをバーで表示する処理
  void DrawLifeBar();


  // 敵にぶつかった時の処理
  void OnCollisionEnemy(std::shared_ptr<GameObject> other);

  // 敵弾とぶつかった時の処理
  void OnCollisionEnemyBullet(std::shared_ptr<GameObject> other);

  // ダメージを受ける処理
  void TakeDamage();
};

#endif

Player.cppに現在のライフをバーで表示する処理の定義を追加します。

#include "Player.h"

  (中略).........

// 更新処理
void Player::Update()
{
  (中略).........
}

// 描画処理
void Player::Draw()
{
  (中略).........
  
  // ライフをバーで表示する処理
  DrawLifeBar();

}

// ライフをバーで表示する処理
void Player::DrawLifeBar()
{
  // ライフをバー表示する際の色設定
  unsigned int frameColor = GetColor(255, 255, 255); // バーの枠の色
  unsigned int backColor = GetColor(0, 0, 0); // バーの背景の色
  unsigned int lineColor = GetColor(0, 0, 0); // バーの区切り目盛りの色
  unsigned int red = GetColor(255, 0, 0), orange = GetColor(255, 100, 0); // ピンチ時の赤、オレンジ
  unsigned int green = GetColor(0, 255, 0), blue = GetColor(0, 255, 255); // 10までは緑、11以上は水色
  int barMax = (lifeMax > 10) ? 10 : lifeMax; // バーの目盛りの数
  int barWidth = 40, barPart = barWidth / barMax, rectWidth = (barPart + 1) * barMax;
  int barHeight = 4; // バーの表示する高さ(4+1+1) 枠は上+1, 下+1, 目盛ぶん↑+1して×10し直すとバーの四角形幅になる
  Vector3 barPos{ position.x - rectWidth / 2, position.y + collisionRadius }; // バーの表示位置
  
  // バーの背景を描画
  DrawBox(barPos.x - 1, barPos.y - 1, barPos.x + 1 + rectWidth, barPos.y + 1 + barHeight, backColor, TRUE);
  // バーの枠を描画
  DrawBox(barPos.x - 1, barPos.y - 1, barPos.x + 1 + rectWidth, barPos.y + 1 + barHeight, frameColor, FALSE);
  auto barColor = (life <= 1) ? red : (life <= 2) ? orange : green; // lifeが 残り1:赤,残り2:橙 10以下:緑
  // バーの中身を10まで描画
  int barNum = (life < 10) ? life : 10;
  for (int i = 0; i < barNum; ++i)
    DrawBox(barPos.x + i * (barPart + 1), barPos.y,
            barPos.x + (i + 1) * (barPart + 1), barPos.y + barHeight, barColor, TRUE);
  // バーの中身(水色)を20まで描画
  int blueNum = (life > 20) ? 10 : (life > 10) ? life - 10 : 0; // lifeが残り10以上のときは水色
  for (int i = 0; i < blueNum; ++i)
    DrawBox(barPos.x + i * (barPart + 1), barPos.y,
            barPos.x + (i + 1) * (barPart + 1), barPos.y + barHeight, blue, TRUE);
  // バーの目盛りを描画
  for (int i = 0; i < 9; ++i)
    DrawLine(barPos.x + (i + 1) * (barPart + 1), barPos.y,
             barPos.x + (i + 1) * (barPart + 1), barPos.y + barHeight, (i + 1 == life) ? barColor : lineColor, TRUE);
}


  (以下略).........


いかがでしょうか、プレイヤの下にライフのバーが表示されるようになりましたか?
現状では、ライフを回復するすべがないので、ライフが10を超えることはないですが、
ライフが10を超えると、10までの緑色のバーの上のレイヤに、0から折り返して水色で20までライフが表示される機能もプログラミングしておきました。

次はザコがライフを回復するアイテムをドロップする処理をプログラミングしてみましょう。

アイテムクラスとザコからのドロップ処理の作成

ザコからプレイヤのライフを回復するアイテムをドロップさせてみましょう。
GIMPで16×16のハートのドット絵画像を自作してheart.pngという名前でpng画像としてDXプロジェクトのImageフォルダにエクスポートしておきましょう。

heart.xcf(GIMPの編集ファイル)←右クリック[名前をつけてリンク先を保存]でダウンロードすればGIMPで編集できる
heart.png(↑GIMPから名前をつけてエクスポートしたpng画像ファイル)←右クリック[名前をつけてリンク先を保存]でダウンロード

Image.hにheart.pngの読込み処理を追加します。

#ifndef IMAGE_H_
#define IMAGE_H_

#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
#include "DivImage.h" // 分割画像の読込みに使う

class Image
{
public:
  Image() {}; // 初期化コンストラクタ:定義と{}空の処理
  ~Image() {}; // 破棄する処理デストラクタ:定義と{}空の処理
  static void Load();
  static int LoadDivGraph(const TCHAR* FileName, DivImage& divImage);

  static int bossImage; //ボス画像のハンドラ(読込画像番号)
  static int player; //プレイヤ画像のハンドラ
  static int playerBullet; //プレイヤの弾画像のハンドラ
  static int enemyBullet16; //敵弾 画像のハンドラ(読込画像番号)
  static int zako0; //ザコ0 画像のハンドラ(読込画像番号)
  static int zako1; //ザコ1 画像のハンドラ(読込画像番号)
  static int zako2; //ザコ2 画像のハンドラ(読込画像番号)
  static int zako3; //ザコ3 画像のハンドラ(読込画像番号)
  static int boss1; //ボス通常 画像のハンドラ(読込画像番号)
  static int boss2; //ボス2気絶 画像のハンドラ(読込画像番号)
  static int boss3; //ボス3発狂 画像のハンドラ(読込画像番号)
  static DivImage explosion; // [分割画像]爆発エフェクト
  static int item0; //アイテム0 画像のハンドラ(読込画像番号)

private:

};
#endif


次に対となるcppファイルも変更します。
Image.cppの内容を次のようにします:

#include "Image.h"

int Image::bossImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{-1};
int Image::playerBullet{-1};
int Image::enemyBullet16{-1};
int Image::zako0{-1};
int Image::zako1{-1};
int Image::zako2{-1};
int Image::zako3{-1};
int Image::boss1{-1};
int Image::boss2{-1};
int Image::boss3{-1};
// ★分割画像は初期化のときに{X方向画像数, Y方向画像数, 画像横幅XSize,画像縦幅YSize}を指定
DivImage Image::explosion{ 8, 2, 64, 64 };
int Image::item0{-1};

void Image::Load()
{
  bossImage = LoadGraph("Image/boss1.png");
  assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  player= LoadGraph("Image/player.png");
  assert(player!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  playerBullet = LoadGraph("Image/player_bullet.png");
  assert(playerBullet!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  enemyBullet16 = LoadGraph("Image/enemy_bullet_16.png");
  assert(enemyBullet16!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  zako0 = LoadGraph("Image/zako0.png");
  assert(zako0!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  zako1 = LoadGraph("Image/zako1.png");
  assert(zako1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  zako2 = LoadGraph("Image/zako2.png");
  assert(zako2!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  zako3 = LoadGraph("Image/zako3.png");
  assert(zako3!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  boss1 = LoadGraph("Image/boss1.png");
  assert(boss1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  boss2 = LoadGraph("Image/boss2.png");
  assert(boss2!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  boss3 = LoadGraph("Image/boss3.png");
  assert(boss3!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  // div分割画像をロード
  Image::LoadDivGraph("Image/explosion.png", explosion);
  for (int i = 0; i < explosion.AllNum; i++)
  { // 画像読込失敗
    if (explosion.HandleArray[i] == -1) assert("爆発分割画像読込失敗" == "");
  }

  item0 = LoadGraph("Image/heart.png");
  assert(item0!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる


}

// DXライブラリのLoadDivGraphを使いやすくラッピング
// ★関数に参照渡しを行う
// 関数の引数に参照渡しを行うことでdivImageの中身を書き換えることができる(値渡しでは書換不可)
// https://qiita.com/agate-pris/items/05948b7d33f3e88b8967
// https://qiita.com/RuthTaro/items/f35c3a26779c0ca1a41a
int Image::LoadDivGraph(const TCHAR* FileName, DivImage& divImage)
{
  return DxLib::LoadDivGraph(FileName, divImage.XNum * divImage.YNum,
    divImage.XNum, divImage.YNum, divImage.XSize, divImage.YSize, divImage.HandleArray);
};

Player.hにライフを回復する処理を追加します。

#ifndef PLAYER_H_
#define PLAYER_H_

#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"

#include "GameObject.h"

#include "GameManager.h"

class Player : public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:

  (中略)...........


  // ダメージを受ける処理
  void TakeDamage();

  // ライフが回復する処理
  void RecoverLife(int amount);

};

#endif

Player.cppにライフを回復する処理を追加します。

#include "Player.h"

  (中略).........

// 更新処理
void Player::Update()
{
  (中略).........
}


  (中略).........

// ダメージを受ける処理
void Player::TakeDamage()
{
  life -= 1; // ライフ減少

  if (life <= 0)
  {
    // ライフが無くなったら死亡
    isDead = true;
  }
  else
  {
    // 無敵時間発動
    mutekiTimer = MutekiJikan;
  }
}

// ライフが回復する処理
void Player::RecoverLife(int amount)
{
  life += amount; // ライフ回復
}


Item.hを新規作成します。

#ifndef ITEM_H_
#define ITEM_H_

#include "GameObject.h"

#include "GameManager.h"

// アイテムのベース基底クラス
class Item : public GameObject
{
public:
    //【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
   
    float collisionRadius; // 当たり判定の半径
   

    // コンストラクタ v:移動速度(方向) radius:当たり判定の半径
    Item(Vector3 pos, Vector3 v, float radius = 8)
        : collisionRadius{ radius }, GameObject( pos ) // GameObjectをベースとして初期化処理を継承する
    {
        this->typeTag = "Item"; // オブジェクトの種類判別タグ
        this->v = v; // GameObjectの速度vを利用する
    }

    // 更新処理。派生クラスでオーバーライドして使う
    virtual void Update() = 0;//virtual ~ = 0;で【純粋仮想関数に】これで【C#のabstract型に】

    // 描画処理。派生クラスでオーバーライドして使う
    virtual void Draw() = 0; //virtual ~ = 0;で【純粋仮想関数に】【関数 = 0で未定義状態を表現】

    // 自機に当たったときの処理 派生クラスでオーバーライドして使う
    virtual void OnCollisionPlayer(std::shared_ptr<GameObject> object)
    {
        // アイテムごとに継承でoverrideして個別処理を書く
    }
};

#endif


このベース基底となるItem.hを継承してItem0やItem1などを作成します。

Item0.hを新規作成します。

#ifndef ITEM_0_H_
#define ITEM_0_H_

#include "Item.h"
#include "Image.h"
#include "Screen.h"

// アイテム0クラス ライフを回復するアイテム
class Item0 : public Item
{
public:
    int amount; // 回復量など

    // コンストラクタ
    Item0(Vector3 pos,
          Vector3 v = Vector3{ -0.5f,0.0f }, // デフォルトは-0.5fで遅めのスクロール量
          int amount = 1)
        : amount{ amount }, Item(pos, v, 8)
    {
        this->tag = "Item0"; // オブジェクトの判別タグ
    }

    // 更新処理
    virtual void Update() override
    {
        this->position += this->v; // vのx,y量ぶん移動
        
        
        // スクリーンの画面外になったらisDeadにして消す
        if (position.x + collisionRadius < 0 || Screen::Width < position.x - collisionRadius
         || position.y + collisionRadius < 0 || Screen::Height < position.y - collisionRadius )
        {
            isDead = true;
        }
    }

    // 描画処理
    virtual void Draw() override
    {
        DrawRotaGraphF(position.x, position.y, 1, 0, Image::item0, TRUE);
    }

    // 自機に当たったときの処理 上書きoverrideする
    virtual void OnCollisionPlayer(std::shared_ptr<GameObject> object) override;
};

#endif



Item0.cppを新規作成します。

#include "Item0.h"

#include "Player.h" // アイテムとプレイヤは相互にインクルードしそうなのでcppで#include

// 自機に当たったときの処理 上書きoverrideする
void Item0::OnCollisionPlayer(std::shared_ptr<GameObject> object)
{
    //[共有ポインタをダウンキャスト(親GameObject → 子Playerに)]
    // https://cpprefjp.github.io/reference/memory/shared_ptr/dynamic_pointer_cast.html
    if (std::shared_ptr<Player> pPlayer = std::dynamic_pointer_cast<Player>(object))
    {
        pPlayer->RecoverLife(amount); // ライフを回復する
    }
    isDead = true; // 取ったら消される
}


さらに、このItem0をザコがやられる直前にドロップさせる処理を追加します。

まずはベースとなるEnemy.hにアイテムをドロップするvirtual仮想関数を追加して継承overrideできるように準備します。

#ifndef ENEMY_H_
#define ENEMY_H_

#include "GameObject.h"

#include "GameManager.h"

// 敵の基底クラス。
// 全ての敵は、このクラスを継承して作る。
// ★【勉強】abstract型はC++にない!!【純粋仮想関数】を使って作る
class Enemy : public GameObject
{
public:

  //【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
  GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

  float collisionRadius = 32; // 当たり判定の半径
  float life = 1; // 耐久力

  // コンストラクタ
  Enemy(Vector3 pos) : GameObject( pos ) // GameObjectをベースとして初期化処理を継承する
  {
    this->typeTag = "Enemy";
  }

  // 更新処理。派生クラスでオーバーライドして使う
  virtual void Update() = 0;//virtual ~ = 0;で【純粋仮想関数に】これで【C#のabstract型に】

  // 描画処理。派生クラスでオーバーライドして使う
  virtual void Draw() = 0; //virtual ~ = 0;で【純粋仮想関数に】【関数 = 0で未定義状態を表現】


  // 自機弾に当たったときの処理 関数の【★引数をGameObject型↓にすることで循環インクルード抑止】
  virtual void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet)
  {
    life -= 1; // ライフを減らす

    // ライフが無くなったら、死亡
    if (life <= 0)
    {
      isDead = true;
    }
    
    DropItem(); // アイテムを落とす処理(落とすかどうかはoverrideされた関数次第)
  }
  
  // アイテムを落とす処理 継承overrideして各ザコごとに違うドロップを実装できる
  virtual void DropItem()
  {
    // 継承overrideして各ザコごとに違う処理を実装する
  }

};

#endif


例として、Zako1.hにアイテム0をドロップさせる処理を追加します。

#ifndef ZAKO_1_H_
#define ZAKO_1_H_

#include "Enemy.h"

#include <cmath> // Sinの計算に使う
#include "MyMath.h"
#include "Image.h"

#include "MyRandom.h" // 乱数でアイテムのドロップを抽選する
#include "Item0.h"


// ザコ1クラス
class Zako1 : public Enemy
{
public:
  float sinMove = 0; // Sinの動きをする際の0~360度
  float sinRadius = 70; // Sinの動きをするときの半径
  
  // コンストラクタ
  Zako1(Vector3 pos) : Enemy ( pos )
  {
    this->tag = "Zako1";//オブジェクトの種類判別タグ
    life = 5;
  }

  // 更新処理
  void Update() override
  {
    position.x -= 1; // 左へ移動

    float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
    sinMove += 2.5f; // Sinの波運動の角度を進める
    float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;

    // (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
    position.y += currentSinY - prevSinY; // 上下方向のSinの波運動
  }

  // 描画処理
  void Draw() override
  {
    DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako1, TRUE);
  }

  // アイテムを落とす処理
  void DropItem() override
  {
    if (isDead) // 消えるまえに
    {
      if (MyRandom::Percent(50.0f)) // 50%の確率でアイテムをドロップ
      {
        gm.items.emplace_back(std::make_shared<Item0>(position));
      }
    }
  }

};

#endif


さて、これでザコ1からアイテムがドロップはしますが、まだプレイヤとアイテムの当たり判定がありません。
プレイヤとアイテムの当たり判定処理を追加しましょう。

GameManager.hにItemを管理するリストの定義を追加します。

#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_

#include <memory>
#include <vector>

#include "Singleton.h"

class Player; // クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class PlayerBullet;
class Enemy;
class EnemyBullet;
class Explosion;
class Item;

class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
  friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可

  std::shared_ptr<Player> player{ nullptr }; // 自機のポインタ
  std::list<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
  std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
  std::list<std::shared_ptr<EnemyBullet>> enemyBullets; // 敵弾のリスト
  std::list<std::shared_ptr<Explosion>> explosions; // 爆発エフェクトのリスト
  std::list<std::shared_ptr<Item>> items; // アイテムのリスト
  // 複数リスト↑list ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる


(中略)..............
protected:
  GameManager() {}; // 外部からのインスタンス作成は禁止
  virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};

#endif

Game.cppにアイテムの当たり判定の処理を追加します。

#include "Game.h"

#include "MyRandom.h"

#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "EnemyBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Explosion.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako2.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako3.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako3.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Item.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理

  (中略)..................
};

void Game::Update()
{// 更新処理

  Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

  (中略)..................

  
  // 爆発エフェクトの更新処理
  for (const auto& e : gm.explosions)
  {
    e->Update();
  }
  
  // アイテムの更新処理
  for (const auto& itm : gm.items)
  {
    itm->Update();
  }

  
  
  // 自機とアイテムの衝突判定
  for (const auto& itm : gm.items)
  {
    // 自機が死んでたらこれ以上判定しない
    if (gm.player->isDead)
      break;
    
    // アイテムが死んでたらスキップ
    if (itm->isDead)
      continue;
    
    // 円同士の衝突判定で調べる
    if (MyMath::CircleCircleIntersection(
        gm.player->position, gm.player->collisionRadius,
        itm->position, itm->collisionRadius) )
    {
      itm->OnCollisionPlayer(gm.player);
    }
  }

  

  //return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
  
  // 自機弾のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.playerBullets,
    [](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  // 敵のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.enemies,
    [](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  // 敵弾のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.enemyBullets,
    [](std::shared_ptr<EnemyBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  //爆発エフェクトのリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.explosions,
    [](std::shared_ptr<Explosion>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  //アイテムのリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.items,
    [](std::shared_ptr<Item>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

  
};

void Game::Draw()
{// 描画処理
  (中略)......

  // 敵弾の描画処理
  for (const auto& b : gm.enemyBullets)
  {
    b->Draw();
  }
  
  // アイテムの描画処理
  for (const auto& itm : gm.items)
  {
    itm->Draw();
  }

}


さて、ザコ1を倒すと回復アイテムを50%の確率でドロップし、プレイヤがそれを取るとライフが1回復するようになりましたか?


Item0を画面のはしっこで光らせてバウンドさせる処理の作成

Item0.hに画面のはしっこの反射処理とisFlashフラグで一瞬光らせる処理を追加します。

#ifndef ITEM_0_H_
#define ITEM_0_H_

#include "Item.h"
#include "Image.h"
#include "Screen.h"

// アイテム0クラス ライフを回復するアイテム
class Item0 : public Item
{
public:
    int amount; // 回復量など
    int bounceNum; // 跳ね返る回数
    bool isFlash; // 画面が一瞬真っ白になる(跳ね返る瞬間)


    // コンストラクタ
    Item0(Vector3 pos,
          Vector3 v = Vector3{ -0.5f,0.0f }, // デフォルトは-0.5fで遅めのスクロール量
          bool isFlash = false, int bounceNum = 0,
          int amount = 1)
        : isFlash{ isFlash }, bounceNum{ bounceNum }, amount{ amount }, Item(pos, v, 8)
    {
        this->tag = "Item0"; // オブジェクトの判別タグ
    }

    // 更新処理
    virtual void Update() override
    {
        this->position += this->v; // vのx,y量ぶん移動
        
        if (bounceNum > 0) // 反射可能回数が 0 以上なら
        {
            bool isBounce = false; // 反射するか
            if (position.x < collisionRadius)
            {
                v.x = -v.x; // x逆方向に反射(マイナスをつけて方向反転)
                position.x = collisionRadius; // めりこんだぶんをスクリーンの境界線に沿わす
                isBounce = true;
            }
            else if (Screen::Width - collisionRadius < position.x)
            {
                v.x = -v.x; // x逆方向に反射(マイナスをつけて方向反転)
                position.x = Screen::Width - collisionRadius; // めりこんだぶんを境界線に沿わす
                isBounce = true;
            }
            
            if (position.y < collisionRadius)
            {
                v.y = -v.y; // y逆方向に反射(マイナスをつけて方向反転)
                position.y = collisionRadius; // めりこんだぶんをスクリーンの境界線に沿わす
                isBounce = true;
            }
            else if (Screen::Height - collisionRadius < position.y)
            {
                v.y = -v.y; // y逆方向に反射(マイナスをつけて方向反転)
                position.y = Screen::Height - collisionRadius; // めりこんだぶんを境界線に沿わす
                isBounce = true;
            }
            
            if (isBounce) // 反射判定がtrueならば
            {
                --bounceNum; // 跳ね返り可能回数を -1 する
                v *= 0.94f; // 反射時のスピード減衰率
                isFlash = true; // 画面を一瞬フラッシュする
            }
        }

        
        
        // スクリーンの画面外になったらisDeadにして消す
        if (position.x + collisionRadius < 0 || Screen::Width < position.x - collisionRadius
         || position.y + collisionRadius < 0 || Screen::Height < position.y - collisionRadius )
        {
            isDead = true;
        }
    }

    // 描画処理
    virtual void Draw() override
    {
        if (isFlash) // 画面を一瞬真っ白にする(跳ね返る瞬間)
        {
            // 画面のスクリーンと同じサイズの大きな白い四角を描く
            DrawBox(0, 0, Screen::Width, Screen::Height, GetColor(200, 200, 200), TRUE);
            // 白画面の上に黒丸を描くとバウンド位置が目立つ(衝撃波っぽい)
            DrawCircle(position.x, position.y, collisionRadius * 5, GetColor(0, 0, 0), TRUE);
        }

       
        DrawRotaGraphF(position.x, position.y, (isFlash)? 2.0f : 1, 0, Image::item0, TRUE);
       
        isFlash = false; // すぐにfalseに戻せば一瞬だけしか光らない
    }

    // 自機に当たったときの処理 上書きoverrideする
    void OnCollisionPlayer(std::shared_ptr<GameObject> object) override;
};

#endif


例として、Zako1.hにアイテム0の初速度と反射する回数と乱数で飛ぶ角度を決めて初期化する処理を追加します。

#ifndef ZAKO_1_H_
#define ZAKO_1_H_

#include "Enemy.h"

#include <cmath> // Sinの計算に使う
#include "MyMath.h"
#include "Image.h"

#include "MyRandom.h" // 乱数でアイテムのドロップと飛ぶ角度を抽選する
#include "Item0.h"

// ザコ1クラス
class Zako1 : public Enemy
{
public:
  float sinMove = 0; // Sinの動きをする際の0~360度
  float sinRadius = 70; // Sinの動きをするときの半径
  
  // コンストラクタ
  Zako1(Vector3 pos) : Enemy ( pos )
  {
    this->tag = "Zako1";//オブジェクトの種類判別タグ
    life = 5;
  }

  // 更新処理
  void Update() override
  {
    position.x -= 1; // 左へ移動

    float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
    sinMove += 2.5f; // Sinの波運動の角度を進める
    float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;

    // (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
    position.y += currentSinY - prevSinY; // 上下方向のSinの波運動
  }

  // 描画処理
  void Draw() override
  {
    DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako1, TRUE);
  }

  // アイテムを落とす処理
  void DropItem() override
  {
    if (isDead) // 消えるまえに
    {
      if (MyRandom::Percent(50.0f)) // 50%の確率でアイテムをドロップ
      {
        float itemSpeed = 25.0f; // アイテムの飛び出すスピード
        float angle = MyRandom::Range(0.0f, 360.0f) * MyMath::Deg2Rad; // 飛び出す角度
        Vector3 v_Item{ std::cos(angle) * itemSpeed, std::sin(angle) * itemSpeed }; // 飛ぶ方向ベクトル
        int bounceNum = 10; // バウンド回数

        gm.items.emplace_back(std::make_shared<Item0>(position, v_Item, true, bounceNum));
      }
    }
  }
};

#endif


いかがでしょう、アイテムが画面のはしっこでバウンドして、一瞬画面がフラッシュするようになりましたか?
現状シューティングの構成要素は「プレイヤ」「敵」「弾」「アイテム」ですが、そのうち自由にアレンジできるのは「敵、弾、アイテム」です。
「敵、弾」が増えたとしてもゲームが難しくなるだけなので工夫したところで、プレイヤが苦戦するだけで面白さのアレンジの限界が来るはずです。
一方、3つのうち「アイテム」だけがプレイヤ側のメリットとなる変化をゲームに起こすことができます。
敵の弾がバウンドしたとしても、画面に弾があふれかえっている状況だと何が何だかわからなくなるでしょう。
なので、効果的にバウンドさせて見せるなら「アイテム」のほうがよいかな、と思いました。
また、アイテムは取れなくてもそれはそれでゲームは進むので、ゆったり単調に動くよりは、高速で動くほうが、ゲームにメリハリがつくかと思います。

ただのバウンドではありますが、モンストもただのバウンドをひたすら発展させてヒットに向かったわけなので「シンプルなものこそあなどることなかれ」ですね。

Item1クラスでボスへクリティカルヒットしたらバリアーとなる星のアイテムをドロップする処理の作成

ボスから「プレイヤの周りをまわって、敵の弾からプレイヤを守る星バリアのアイテム」をドロップさせてみましょう。
GIMPで16×16のスターのドット絵画像を自作してstar.pngという名前でpng画像としてDXプロジェクトのImageフォルダにエクスポートしておきましょう。

star.xcf(GIMPの編集ファイル)←右クリック[名前をつけてリンク先を保存]でダウンロードすればGIMPで編集できる
star.png(↑GIMPから名前をつけてエクスポートしたpng画像ファイル)←右クリック[名前をつけてリンク先を保存]でダウンロード

Image.hにstar.pngの読込み処理を追加します。

#ifndef IMAGE_H_
#define IMAGE_H_

#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
#include "DivImage.h" // 分割画像の読込みに使う

class Image
{
public:
  Image() {}; // 初期化コンストラクタ:定義と{}空の処理
  ~Image() {}; // 破棄する処理デストラクタ:定義と{}空の処理
  static void Load();
  static int LoadDivGraph(const TCHAR* FileName, DivImage& divImage);

  static int bossImage; //ボス画像のハンドラ(読込画像番号)
  static int player; //プレイヤ画像のハンドラ
  static int playerBullet; //プレイヤの弾画像のハンドラ
  static int enemyBullet16; //敵弾 画像のハンドラ(読込画像番号)
  static int zako0; //ザコ0 画像のハンドラ(読込画像番号)
  static int zako1; //ザコ1 画像のハンドラ(読込画像番号)
  static int zako2; //ザコ2 画像のハンドラ(読込画像番号)
  static int zako3; //ザコ3 画像のハンドラ(読込画像番号)
  static int boss1; //ボス通常 画像のハンドラ(読込画像番号)
  static int boss2; //ボス2気絶 画像のハンドラ(読込画像番号)
  static int boss3; //ボス3発狂 画像のハンドラ(読込画像番号)
  static DivImage explosion; // [分割画像]爆発エフェクト
  static int item0; //アイテム0 画像のハンドラ(読込画像番号)
  static int item1; //アイテム1 画像のハンドラ(読込画像番号)

private:

};
#endif


次に対となるcppファイルも変更します。
Image.cppの内容を次のようにします:

#include "Image.h"

int Image::bossImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{-1};
int Image::playerBullet{-1};
int Image::enemyBullet16{-1};
int Image::zako0{-1};
int Image::zako1{-1};
int Image::zako2{-1};
int Image::zako3{-1};
int Image::boss1{-1};
int Image::boss2{-1};
int Image::boss3{-1};
// ★分割画像は初期化のときに{X方向画像数, Y方向画像数, 画像横幅XSize,画像縦幅YSize}を指定
DivImage Image::explosion{ 8, 2, 64, 64 };
int Image::item0{-1};
int Image::item1{-1};

void Image::Load()
{
  bossImage = LoadGraph("Image/boss1.png");
  assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  player= LoadGraph("Image/player.png");
  assert(player!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  playerBullet = LoadGraph("Image/player_bullet.png");
  assert(playerBullet!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  enemyBullet16 = LoadGraph("Image/enemy_bullet_16.png");
  assert(enemyBullet16!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  zako0 = LoadGraph("Image/zako0.png");
  assert(zako0!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  zako1 = LoadGraph("Image/zako1.png");
  assert(zako1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  zako2 = LoadGraph("Image/zako2.png");
  assert(zako2!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  zako3 = LoadGraph("Image/zako3.png");
  assert(zako3!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  boss1 = LoadGraph("Image/boss1.png");
  assert(boss1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  boss2 = LoadGraph("Image/boss2.png");
  assert(boss2!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  boss3 = LoadGraph("Image/boss3.png");
  assert(boss3!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  // div分割画像をロード
  Image::LoadDivGraph("Image/explosion.png", explosion);
  for (int i = 0; i < explosion.AllNum; i++)
  { // 画像読込失敗
    if (explosion.HandleArray[i] == -1) assert("爆発分割画像読込失敗" == "");
  }

  item0 = LoadGraph("Image/heart.png");
  assert(item0!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  item1 = LoadGraph("Image/star.png");
  assert(item1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる


}

// DXライブラリのLoadDivGraphを使いやすくラッピング
// ★関数に参照渡しを行う
// 関数の引数に参照渡しを行うことでdivImageの中身を書き換えることができる(値渡しでは書換不可)
// https://qiita.com/agate-pris/items/05948b7d33f3e88b8967
// https://qiita.com/RuthTaro/items/f35c3a26779c0ca1a41a
int Image::LoadDivGraph(const TCHAR* FileName, DivImage& divImage)
{
  return DxLib::LoadDivGraph(FileName, divImage.XNum * divImage.YNum,
    divImage.XNum, divImage.YNum, divImage.XSize, divImage.YSize, divImage.HandleArray);
};


Item.hに敵弾と衝突した際に呼び出されるベースとなるコールバック関数を定義します。

#ifndef ITEM_H_
#define ITEM_H_

#include "GameObject.h"

#include "GameManager.h"

// アイテムのベース基底クラス
class Item : public GameObject
{
public:
    //【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
   
    float collisionRadius; // 当たり判定の半径
   
    enum class CollisionTo // 当たり判定の対象(ビット32桁に1を立てて表現)
    {
        None = 0,
        Player      = 0b0000'0000'0000'0000'0000'0000'0000'0001,
        EnemyBullet = 0b0000'0000'0000'0000'0000'0000'0000'0010,
    };
   
    CollisionTo collisionTo = CollisionTo::Player; // デフォルトはプレイヤとの当たりを判定する


    // コンストラクタ v:移動速度(方向) radius:当たり判定の半径
    Item(Vector3 pos, Vector3 v, float radius = 8)
        : collisionRadius{ radius }, GameObject( pos ) // GameObjectをベースとして初期化処理を継承する
    {
        this->typeTag = "Item"; // オブジェクトの種類判別タグ
        this->v = v; // GameObjectの速度vを利用する
    }

    // 更新処理。派生クラスでオーバーライドして使う
    virtual void Update() = 0;//virtual ~ = 0;で【純粋仮想関数に】これで【C#のabstract型に】

    // 描画処理。派生クラスでオーバーライドして使う
    virtual void Draw() = 0; //virtual ~ = 0;で【純粋仮想関数に】【関数 = 0で未定義状態を表現】

    // 自機に当たったときの処理 派生クラスでオーバーライドして使う
    virtual void OnCollisionPlayer(std::shared_ptr<GameObject> object)
    {
        // アイテムごとに継承でoverrideして個別処理を書く
    }
    
    // 敵弾とぶつかった時の処理
    virtual void OnCollisionEnemyBullet(std::shared_ptr<GameObject> object)
    {
        // アイテムごとに継承でoverrideして個別処理を書く
    }

};

#endif


Item1.hを新規作成します。

#ifndef ITEM_1_H_
#define ITEM_1_H_

#include "Item.h"
#include "Image.h"
#include "Screen.h"
#include "MyMath.h"
#include <cmath>

// アイテム1クラス プレイヤを敵弾から守り身代わりになるバリアアイテム
class Item1 : public Item
{
public:
    int life; // バリアー(星)の耐える回数
    float xForce = 0.1f; // 加速の力(流れ星の加速)
    enum class State
    {
        Popping, // ポップ(飛び出した勢いのあるあいだ)
        Shooting,// 流れ星(加速して流れていく)
        Barrier, // バリアモードのとき
    };
    State state = State::Popping; // 動作状態
    GameObject* pParent{ nullptr }; // 親となっているプレイヤなどへのリンク(バリアの中心点)
    float barrierRadius; // バリアの円回転の半径
    float barrierAngle = 0.0f; // バリアの回転角度
    bool isHiting = false; // 敵弾にヒットした時に一瞬拡大して表示

    // コンストラクタ
    Item1(Vector3 pos,
          Vector3 v = Vector3{ -0.5f,0.0f }, // デフォルトは-0.5fで遅めのスクロール量
          int life = 3, float xForce = 0.8f, float barrierRadius = 40.0f)
        : life{ life }, xForce{ xForce }, barrierRadius{ barrierRadius },
          Item(pos, v, 8)
    {
        this->tag = "Item1"; // オブジェクトの判別タグ
    }

    // 更新処理
    virtual void Update() override
    {
        if (state == State::Popping) // 飛び出した勢いは段々減る
        {
            position += v; // vのx,y量ぶん移動
            v *= 0.85f; // 速度が減衰
            if (v.magnitude() < 0.01f)
                state = State::Shooting; // 流れ星モードに移行
        }
        else if (state == State::Shooting)
        {
            position += v; // vのx,y量ぶん移動
            v.x -= xForce; // 流れ星が少しずつ加速する
        }
        else if (state == State::Barrier && pParent != nullptr)
        {
            barrierAngle += 5.5f; // 毎フレーム5.5度ずつ回転
            position.x = pParent->position.x + barrierRadius * std::cos(barrierAngle * MyMath::Deg2Rad);
            position.y = pParent->position.y + barrierRadius * std::sin(barrierAngle * MyMath::Deg2Rad);
            
            if (pParent->isDead)
                isDead = true; // 親のプレイヤリンク先がisDeadなら連動して消す
        }
        
        if (state != State::Barrier) // バリア状態でないときに画面外に出たら消す
        {
            // スクリーンの画面外になったらisDeadにして消す
            if (position.x + collisionRadius < 0 || Screen::Width < position.x - collisionRadius
             || position.y + collisionRadius < 0 || Screen::Height < position.y - collisionRadius )
            {
                isDead = true;
            }
        }
    }

    // 描画処理
    virtual void Draw() override
    {
        DrawRotaGraphF(position.x, position.y, (isHiting)? 2.0f : 1, 0, Image::item1, TRUE);
    }

    // 自機に当たったときの処理 上書きoverrideする
    virtual void OnCollisionPlayer(std::shared_ptr<GameObject> object) override;

    // 敵弾とぶつかった時の処理
    virtual void OnCollisionEnemyBullet(std::shared_ptr<GameObject> object) override
    {
        if (!((int)collisionTo & (int)CollisionTo::EnemyBullet))
            return; // 敵弾と当たり判定しない設定のときは何もせずreturn
        
        if (life <= 0) // ライフ(バリアの耐久回数)が0以下になったら
        {
            isDead = true; // 当たった直後に消える
            isHiting = true; // 一瞬拡大して表示
        }
        
        --life; // ライフ(バリアの耐久回数)を減らす
    }
};

#endif



Item1.cppを新規作成します。

#include "Item1.h"

#include "Player.h" // アイテムとプレイヤは相互にインクルードしそうなのでcppで#include

// 自機に当たったときの処理 上書きoverrideする
void Item1::OnCollisionPlayer(std::shared_ptr<GameObject> object)
{
    if (!((int)collisionTo & (int)CollisionTo::Player))
        return; // プレイヤと当たり判定しないenumビット設定のときは何もせずreturn
   
    //[共有ポインタをダウンキャスト(親GameObject → 子Playerに)]
    // https://cpprefjp.github.io/reference/memory/shared_ptr/dynamic_pointer_cast.html
    if (std::shared_ptr<Player> pPlayer = std::dynamic_pointer_cast<Player>(object))
    {
        pParent = pPlayer.get(); // プレイヤへのリンクを親リンクとしてリンクされた(x,y)を中心に回転
        state = State::Barrier; // バリアモードへ移行
        collisionTo = CollisionTo::EnemyBullet; // 敵弾との当たり判定モードに移行
    }
}



Item0.cppにも当たり判定の種類の判定(PlayerかEnemyBulletかの判定)を追加します。

#include "Item0.h"

#include "Player.h" // アイテムとプレイヤは相互にインクルードしそうなのでcppで#include

// 自機に当たったときの処理 上書きoverrideする
void Item0::OnCollisionPlayer(std::shared_ptr<GameObject> object)
{
    if (!((int)collisionTo & (int)CollisionTo::Player))
        return; // プレイヤと当たり判定しないenumビット設定のときは何もせずreturn

   
    //[共有ポインタをダウンキャスト(親GameObject → 子Playerに)]
    // https://cpprefjp.github.io/reference/memory/shared_ptr/dynamic_pointer_cast.html
    if (std::shared_ptr<Player> pPlayer = std::dynamic_pointer_cast<Player>(object))
    {
           pPlayer->RecoverLife(amount); // ライフを回復する
    }
    isDead = true; // 取ったら消される
}


例としてBoss.hで弾が当たったy位置が ±5 以内のときクリティカルと判定してバリアーの星を出す処理を追加します。

#ifndef BOSS_H_
#define BOSS_H_

#include "Enemy.h"

#include "Image.h"

#include <cmath> // Sin Cosの計算に使う
#include "MyMath.h"
#include "MyRandom.h" // 乱数方向弾とバリアーの星の飛び出す方向の抽選に使う
#include "Item1.h"
#include "PlayerBullet.h"


// ボスクラス。Enemyを継承して作る
class Boss : public Enemy
{
public:
    // ボスの状態種別
    enum class State
    {
        Appear, // 登場
        Normal, // 通常時
        Swoon, // 気絶時
        Angry, // 発狂モード
        Dying, // 死亡
    };

    State state = State::Appear; // 現在の状態
    int swoonTime = 120; // 残り気絶時間
    int dyingTime = 180; // 死亡時のアニメーション時間

    int coolTime = 0; // クールタイム(冷却時間。0になるまで次の弾が撃てない)
    float sinMove = 0; // Sinの動きをする際の0~360度
    float sinRadius = 120; // Sinの動きをするときの半径

    bool isCritical = false; // 弾に当たった瞬間の yが ±2 以内のときクリティカルと判定
    Vector3 criticalPos{0,0,0}; // クリティカルヒットした位置


    // コンストラクタ
    Boss(Vector3 pos) : Enemy( pos )
    {
        this->tag = "Boss"; // オブジェクトの種類判別タグ
        life = 100; // ライフ
        collisionRadius = 70; // 当たり判定半径
    }

    // 更新処理
    void Update() override
    {
        if (state == State::Appear) // 登場状態
        {
            position.x -= 1; // 左へ移動

            if (position.x <= 750) // x座標が750以下になったら
            {
                state = State::Normal; // 通常状態へ移行
            }
        }
        else if (state == State::Normal) // 通常状態
        {
            coolTime--;
           
            if (coolTime <= 0)
            {   // ボスの弾はスピード16で2倍の速度で撃ち出す
                gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, 180 * MyMath::Deg2Rad, 16));
                coolTime += 200; // そのかわり発射間隔は長め
            }
           
            float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
            sinMove += 2.5f; // Sinの波運動の角度を進める
            float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
           
            // (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
            position.y += currentSinY - prevSinY; // 上下方向のSinの波運動
        }
        else if (state == State::Swoon) // 気絶状態
        {
            swoonTime--; // タイマー減少

            if (swoonTime <= 0) // タイマーが0になったら
            {
                coolTime = 0; // クールタイムを0にリセットしてから
                state = State::Angry; // 発狂モードへ
            }
        }
        else if (state == State::Angry) // 発狂モード
        {
            coolTime--;
           
            if (coolTime <= 0)
            {
                gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, 180 * MyMath::Deg2Rad, 16));
                coolTime += 20;
            }
           
            float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
            sinMove += 10.5f; // Sinの波運動の角度を進める
            float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
           
            // (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
            position.y += currentSinY - prevSinY; // 上下方向のSinの波運動
        }
        else if (state == State::Dying) // 死亡中
        {
            dyingTime--; // タイマー減少

            if (dyingTime <= 0) // タイマーが0になったら
            {
                isDead = true; // 完全に消滅
            }
        }
    }

    // 描画処理
    void Draw() override
    {
        if (state == State::Appear || state == State::Normal) // 登場時と通常時
        {
            DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss1, TRUE); // 普通の画像
        }
        else if (state == State::Swoon) // 気絶時
        {
            DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss2, TRUE); // 気絶時の画像
        }
        else if (state == State::Angry) // 発狂モード
        {
            DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss3, TRUE); // 怒ってる画像
        }
        else if (state == State::Dying) // 死亡中
        {
            DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss2, TRUE); // 気絶時の画像
        }
       
        if (isCritical)
        {
            // 画面のスクリーンと同じサイズの大きな白い四角(フラッシュ)を描く
            DrawBox(0, 0, Screen::Width, Screen::Height, GetColor(200, 200, 200), TRUE);
            // 白フラッシュの上に黒丸を描くとクリティカルヒットした位置が目立つ(衝撃波っぽい)
            DrawCircle(position.x, position.y, 10, GetColor(0, 0, 0), TRUE);
            isCritical = false; // クリティカルフラグをfalseにすぐ戻す(フラッシュ点滅)
        }

    }

    // 自機弾に当たったときの処理をoverride(上書き)する
    void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet) override
    {
        // 登場時、気絶時、死亡時は被弾しても何もしない
        if (state == State::Appear || state == State::Swoon || state == State::Dying)
            return;

        life -= 1; // ライフを減らす

        float posY = position.y; // [ボス中心y]と[当たった瞬間の弾のy]が ±5 ならクリティカル
        if (posY - 5 <= playerBullet->position.y && playerBullet->position.y <= posY + 5)
        {
            isCritical = true; // クリティカルフラグをtrueに
            criticalPos = playerBullet->position; // クリティカルヒットした位置を記録する
            DropItem(); // バリアの星アイテムを落とす
        }


        if (life <= 0)
        {
            // ライフが無くなったら、すぐ消滅するのではなく、死亡状態へ移行
            state = State::Dying;
        }
        else if (state == State::Normal && life <= 50)
        {
            // 通常状態でライフが50以下になったら、気絶する
            state = State::Swoon;
        }
    }
    
    // アイテムを落とす処理
    void DropItem() override
    {
        if (MyRandom::Percent(100.0f)) // 100%の確率でバリアになる星をドロップ
        {
            float itemSpeed = 25.0f; // アイテムの飛び出すスピード
            float angle = MyRandom::Range(0.0f, 360.0f) * MyMath::Deg2Rad; // 飛び出す角度
            Vector3 v_Item{ std::cos(angle) * itemSpeed, std::sin(angle) * itemSpeed }; // 飛ぶ方向ベクトル
            int barrierLife = 3; // バリアは敵弾に3回当たるまでガードしてくれる
            gm.items.emplace_back(std::make_shared<Item1>(position, v_Item, barrierLife));
        }
    }

};

#endif


EnemyBullet.hにアイテムと衝突したさいの関数を追加します。

#ifndef ENEMYBULLET_H_
#define ENEMYBULLET_H_

#include <cmath> //sin,cosを使うのに必要

#include "DxLib.h"
#include "Image.h"
#include "Screen.h"
#include "Vector3.h"

#include "GameObject.h"

// 敵弾クラス
class EnemyBullet: public GameObject
{
public:
  const int VisibleRadius = 8; // 見た目の半径
  

  float angle; // 移動角度(垂直Vertical)
  float angleH=0; // 移動角度(水平Horizontal)


  // コンストラクタ
  EnemyBullet(Vector3 pos, float angle, float speed) : GameObject( pos )
  {
    this->angle = angle;

    // 角度からx方向の移動速度を算出
    v.x = (float)std::cos(angle) * speed;
    // 角度からy方向の移動速度を算出
    v.y = (float)std::sin(angle) * speed;
  }

  // コンストラクタ
  EnemyBullet(Vector3 pos, float angleV, float angleH, float speed) : GameObject( pos )
  {
    this->angle = angleV;
    this->angleH = angleH;

    // [3Dの角度は水平・垂直に分割]https://teratail.com/questions/126004
    float cosAngleV = (float)std::cos(angle);
    // 角度からx方向の移動速度を算出
    v.x = cosAngleV * (float)std::sin(angleH) * speed;
    // 角度からy方向の移動速度を算出
    v.y = (float)std::sin(angle) * speed;
    // 角度からz方向の移動速度を算出
    v.z = cosAngleV * (float)std::cos(angleH) * speed;
  }

  // 更新処理
  void Update()
  {
    // 速度の分だけ移動
    position += v;

    // 画面外に出たら死亡フラグを立てる
    if (position.y + VisibleRadius < 0 || position.y - VisibleRadius > Screen::Height ||
      position.x + VisibleRadius < 0 || position.x - VisibleRadius > Screen::Width)
    {
      isDead = true;
    }
  }

  // 描画処理
  void Draw()
  {
    DrawRotaGraphF(position.x, position.y, 1, angle, Image::enemyBullet16,TRUE);
  }

  // プレイヤにぶつかった時の処理
  void OnCollisionPlayer(std::shared_ptr<GameObject> other)
  {
    isDead = true;
  }

  // アイテム(プレイヤの回りをまわるバリア)にぶつかった時の処理
  void OnCollisionItem(std::shared_ptr<GameObject> other)
  {
    isDead = true;
  }

};

#endif

Game.cppにアイテムの当たり判定の処理を追加します。

#include "Game.h"

#include "MyRandom.h"

#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "EnemyBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Explosion.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako2.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako3.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako3.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Item.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理

  (中略)..................
};

void Game::Update()
{// 更新処理

  Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

  (中略)..................

  
  // 爆発エフェクトの更新処理
  for (const auto& e : gm.explosions)
  {
    e->Update();
  }
  
  // アイテムの更新処理
  for (const auto& itm : gm.items)
  {
    itm->Update();
  }
  
  
  // 自機とアイテム、敵弾とアイテムの衝突判定
  for (const auto& itm : gm.items)
  {
    // 自機が死んでたらこれ以上判定しない
    if (gm.player->isDead)
      break;
    
    // アイテムが死んでたらスキップ
    if (itm->isDead)
      continue;
    
    // 円同士の衝突判定で調べる
    if (MyMath::CircleCircleIntersection(
        gm.player->position, gm.player->collisionRadius,
        itm->position, itm->collisionRadius) )
    {
      itm->OnCollisionPlayer(gm.player);
    }
    
    for (const auto& enemyBullet : gm.enemyBullets)
    {
      // 敵弾が死んでたらスキップ
      if (enemyBullet->isDead)
        continue;
      
      // 円同士の衝突判定で調べる
      if (MyMath::CircleCircleIntersection(
          itm->position, itm->collisionRadius,
          enemyBullet->position, enemyBullet->collisionRadius) )
      {
        enemyBullet->OnCollisionItem(itm);
        itm->OnCollisionEnemyBullet(enemyBullet);
      }
    }

  }
  

  //return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
  
  // 自機弾のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.playerBullets,
    [](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  // 敵のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.enemies,
    [](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  // 敵弾のリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.enemyBullets,
    [](std::shared_ptr<EnemyBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  //爆発エフェクトのリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.explosions,
    [](std::shared_ptr<Explosion>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  //アイテムのリストから死んでるものを除去する
  gm.EraseRemoveIf(gm.items,
    [](std::shared_ptr<Item>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
  
};

void Game::Draw()
{// 描画処理
  (中略)......
}



いかがですか、ボスに弾を打ち込むとときどきクリティカルヒットして、星が飛び出し、
左方向に加速しながら流れ星して、取るとプレイヤの周りをまわるバリアとなり、敵の弾から身代わりとなってガードしてくれれば成功です。

流れ星の動き方は、今回はアイテムとして作りましたが、
敵から秘密兵器のミサイルとして「EnemyBullet2」クラスとして自作すれば、敵のミサイル攻撃としても使える動きになると思います。

また、星もバリアじゃなくカービーのようなエフェクトとしての使い方や、取るとスコアアップなど色んな使い方が企画できます。


[C++]シューティングゲーム 4


  1. シーンを分ける(循環インクルードを避けメモリも管理するには?)
    【高難度】画面やステージごとにImageのメモリを開放する
    サウンドの読込みと再生機能
    【高難度】画面やステージごとにサウンドをメモリ読込み・解放(音量調整機能も追加)
  2. CSVを読み込んで敵を生成するプログラム
    カスタムしたデータ構造DataCsv.hでデータ自身にLoadと幅高さ管理を任せる
    【高難度】敵データの全方位読出しアルゴリズム
  3. 自機狙い弾
  4. 自機に向かってくる敵

  5. 次のテキスト5はこちら。


    [C++]シューティングゲーム 5【応用1】

シーンを分ける(循環インクルードを避けメモリも管理するには?)

タイトルやプレイ画面やゲームオーバーのシーンを分けましょう。
シーンは構造的にタイトル⇒プレイ画面⇒ゲームオーバー⇒タイトル...とループするので
普通に考えれば循環インクルードしやすいですね。
ということで基本方針としては

  1. マネージャの.hヘッダに現在のシーンと一つ前のシーンを保持
  2. マネージャの.cppで各シーンをインクルードして画面チェンジ機能の処理を作成する
  3. 各シーンではマネージャの.hをインクルードして画面チェンジ機能を呼び出す

上記の構造であれば、マネージャの.hヘッダで循環は止まります、なぜか?
マネージャの.hヘッダには【画面チェンジ機能の定義だけ】があり、各シーンをインクルード必要としません。
実際の【画面チェンジ機能の処理はcppに書き】【cppで各シーンの.hヘッダをインクルードします】

さて、実際のプログラムをしてみましょう。
【シーン専門のマネージャーSceneManagerを作ることにしましょう】(GameManagerでやっても良いですが)。
管理方針は人によりますが、現状のマネージャーの役割は一度ここで整理しておいた方がよいですね

  1. 【ゲームのマネージャ役】敵リストなどメモリへのリンクを保持する
  2. 【ゲームのマネージャ役2】メモリからisDeadの要素などを高速削除する機能
  3. 【ゲームのマネージャ役3】メモリの要素同士の連絡の【参照を集約】して隠れた参照をしないように整理する
  4. 【シーンのマネージャ役】現在と前の画面のメモリへのリンクを保持する
  5. 【シーンのマネージャ役2】次の画面へシーンをチェンジする(チェンジするだけでなく前画面の弾などのメモリ参照を外さないとメモリに残り続ける)
  6. 【シーンのマネージャ役3】前の画面の弾など【終了時のメモリ参照の掃除】【各シーンに号令をかける】

シーンの遷移こそがある意味【一番のメモリの管理の「要所」】。

なぜなら【画面チェンジは実質≒前の画面のメモリへの参照リンクお掃除】に近いですから。

気をつけてくださいね【メモリの回し読みポインタの参照リンクお掃除しないと】前の画面のメモリはフリーになりません

シーンマネージャで画面遷移を作成する(Game.cppからコードを画面に分けて移植)

Scene.hを新規作成してベースとなるシーンの基底クラスを定義しましょう。
ポイント要点は【開始Initialize()】と【終了Finalize()】を区別していることです。
いままでもInit()や初期化コンストラクタはありましたが
【終了の Finalize() 】を分けることでシーンの切替えのタイミングで弾のメモリなどのリセットの処理を書けます。
【Initialize()には生成や初期化】【Finalize()にはメモリなどのリセット処理】を書き分けましょう。
つまり【シーンの移り変わりが出てきたからこそ】【終了のFinalize()の概念も必要になってきた】ということですね。
逆に今までは1つの画面しかなかったので初期化のInit()だけでよかったわけですね。

Scene.hを新規作成してベースとなるシーンの基底クラスを定義します。

#ifndef SCENE_H_
#define SCENE_H_

#include <string> // シーンの判別文字列tagに使う

// シーンの基底クラス あいまいなabstract型タイプ
class Scene
{
public:
    std::string tag = ""; // シーンの種類の判別に使う

    // コンストラクタ
    Scene()
    {

    }
    //★仮想デストラクタ【忘れるとメモリがヤバいダメ絶対】
    //【注意!】ベースの基底では必ず定義しないとstringなどが浪費され極悪なメモリ被害に発展
    virtual ~Scene()
    {
        // 特に配列とかなくても★string文字列も内部では実質配列だから!5文字分のメモリとかがすり減ってく..
        // 仮想デストラクタはstringなど暗黙に内部に配列を持つ要素のデストラクタを暗黙に呼んでくれている!
    }

    //★純粋仮想関数=0は継承したPlaySceneなどの必須機能(継承したら絶対override必須縛り)
    // 必須縛りによってSceneを継承したものは【Update,Drawが実装されてるはずの確証があるので】
    // for文であいまいなSceneのまま【まとめてUpdate,Drawできる】のだ。
    
    // 初期化処理
    virtual void Initialize() = 0; //純粋仮想関数に(継承したらoverride必須)
    
    // 終了処理(大抵Initializeと同じくリセット処理を行うがInitと区別して終了時だけやりたいリセットもある)
    virtual void Finalize() = 0; //純粋仮想関数に(継承したらoverride必須)

    // 更新処理
    virtual void Update() = 0; //純粋仮想関数に(継承したらoverride必須)

    // 描画処理
    virtual void Draw() = 0; //純粋仮想関数に(継承したらoverride必須)
};

#endif


SceneManager.hを新規作成してシーンを管理するクラスを定義しましょう。

#ifndef SCNENEMANAGER_H_
#define SCNENEMANAGER_H_

#include <vector>
#include <memory>
#include <string>
#include <assert.h> // シーン読み込みの失敗表示用

#include "Singleton.h"

class Scene; //クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる

class SceneManager : public Singleton<SceneManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
    friend class Singleton<SceneManager>; // Singleton でのインスタンス作成は許可

   
    // マネージャを【どこからでもアクセスしやすい「変数の掲示板」として使えばシーンをまたぐ変数も定義できる】
    std::string selectStage = "stage1"; // 選択中のステージ名など
    int scoreMax = -1; // 【シーンをまたぐスコアなど】はマネージャに定義すれば【シーンをまたいだあとも】消えず残る
   
    std::shared_ptr<Scene> prevScene{ nullptr }; // 一つ前のシーン
    std::shared_ptr<Scene> currentScene{ nullptr }; // 現在のシーン

    std::string currentSceneName = ""; // 現在のシーン名を保管しておく
    std::string changingSceneName = ""; // 予約された次のシーンのクラス名(次に移動予定のシーン名を保管しておく)

    // シーン名を比較、何の変化もなければfalseでシーン移動不必要の判定
    bool isChanging(const std::string& nextSceneName);

    // 次のシーンの予約だけしてLoadScene()を引数なしで呼び出したタイミングで遷移する(ループの途中じゃないキリのいいタイミングを狙って遷移させる)
    void NextScene(const std::string& nextSceneName);

    // シーンをチェンジし遷移する(前のシーンのリセット処理もする)
    void LoadScene(const std::string& sceneName = "");

protected:
    SceneManager() {}; // 外部からのインスタンス作成は禁止
    virtual ~SceneManager() {}; //外部からのインスタンス破棄も禁止
};

#endif


TitleScene.hを新規作成してタイトル画面のひな型を作成しましょう。

#ifndef TITLESCENE_H_
#define TITLESCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Input.h"
#include "Image.h"

#include "GameManager.h"
#include "SceneManager.h"

class TitleScene : public Scene
{
public:
    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
    
    // コンストラクタ
    TitleScene() : Scene()
    {
        this->tag = "TitleScene";
    }

    // 初期化処理
    void Initialize() override
    {

    }

    // 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
    void Finalize() override
    {

    }

    // 更新処理
    void Update() override
    {
        if (Input::GetButtonDown(PAD_INPUT_1))
        {
            sm.LoadScene("PlayScene"); //シーン遷移
            return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
        }
    }

    // 描画処理
    void Draw() override
    {
        DrawString(0, 0, "TitleSceneです。ボタン押下でPlaySceneへ。", GetColor(255, 255, 255));
        if (sm.scoreMax >= 0)
        {
            DrawString(0, 200, "MAXスコア: " + sm.scoreMax, GetColor(255, 255, 255));
        }
    }
};

#endif


GameOverScene.hを新規作成してゲームオーバー画面のひな型を作成しましょう。

#ifndef GAMEOVERSCENE_H_
#define GAMEOVERSCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Input.h"
#include "Image.h"

#include "GameManager.h"
#include "SceneManager.h"

class GameOverScene : public Scene
{
public:
    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

    // コンストラクタ
    GameOverScene() : Scene()
    {
        this->tag = "GameOverScene";
    }

    // 初期化処理
    void Initialize() override
    {

    }

    // 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
    void Finalize() override
    {

    }

    // 更新処理
    void Update() override
    {
        if (Input::GetButtonDown(PAD_INPUT_1))
        {
            sm.LoadScene("TitleScene"); //シーン遷移
            return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
        }
    }

    // 描画処理
    void Draw() override
    {
        DrawString(0, 0, "GameOver....ボタン押下でPlaySceneへ", GetColor(255, 255, 255));
    }
};

#endif


PlayScene.hを新規作成してプレイ画面のひな型を作成しましょう。
なお、実際のプレイに関するコードはGame.cppから頑張って移植してみましょう。
ここに全部書くと逆にシーン遷移に関するコードがわかりづらくなりますから。
個々人で作成中のGame.cppのゲームのコードをUpdate(),Draw(),Init()はInitialize()へ移植してみましょう。

#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Input.h"
#include "Image.h"

#include "GameManager.h"
#include "SceneManager.h"

#include "Player.h"

class PlayScene : public Scene
{
public:
    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

    int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる

    // コンストラクタ
    PlayScene() : Scene()
    {
        this->tag = "PlayScene";
    }

    // 初期化処理
    void Initialize() override
    {
        // ここでプレイヤの初期化などのプレイ開始時の処理
    }

    // 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
    void Finalize() override
    {
        // ここにプレイ終了時のメモリのリセット処理(ゲームクリアやゲームオーバーなど)

        // ゲーム終了時のスコアがMAXなら記録して終了
        if(score > sm.scoreMax) sm.scoreMax = score;
    }

    // 更新処理
    void Update() override
    {

        // ここにプレイ中の更新処理を持ってくる


        // ゲームオーバー判定と画面チェンジ処理
        bool isGameover = (gm.player == nullptr);
        if (!isGameover && gm.player->isDead) isGameover = true;
        if (isGameover)
        {
            sm.LoadScene("GameOverScene"); //シーン遷移
            return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
        }
    }

    // 描画処理
    void Draw() override
    {
        // ここにプレイ画面の描画処理を持ってくる
    }
};

#endif


SceneManager.cppを新規作成してシーンを遷移する機能を作成しましょう。

#include "SceneManager.h"

#include "Scene.h"
#include "TitleScene.h"
#include "PlayScene.h"
#include "GameOverScene.h"

bool SceneManager::isChanging(const std::string& nextSceneName)
{
    if (currentSceneName != nextSceneName) //シーン名不一致
        return true; // シーン名が変更
    
    return false; // シーン名に変更無し
}

void SceneManager::NextScene(const std::string& nextSceneName)
{
    // 遷移予定nextシーン名と現在のシーン名を比較、何の変化もなければfalseでシーン移動不必要の判定
    if (isChanging(nextSceneName) == false) return; //特に変更なしでシーン移動の必要なし
    
    changingSceneName = nextSceneName; // 次に移動予定のシーン名を保管しておく
}

void SceneManager::LoadScene(const std::string& sceneName)
{
    std::string loadSceneName = sceneName;     // sceneNameの指定が空の場合にはNextScene()での事前のシーン変更予約があるかチェック
    if (sceneName == "" && changingSceneName != "")
        loadSceneName = changingSceneName;
    
    if (loadSceneName == "") return; // シーン指定がないときは終了
    // 現在のシーンの終了処理
    
    if(currentScene !=nullptr)
        currentScene->Finalize(); // 終了処理を呼び出す
    
    if (loadSceneName == "TitleScene")
    {
        // 次のシーンの生成
        currentScene = std::make_shared<TitleScene>();
    }
    else if (loadSceneName == "PlayScene")
    {
        // 次のシーンの生成
        currentScene = std::make_shared<PlayScene>();
    }
    else if (loadSceneName == "GameOverScene")
    {
        // 次のシーンの生成
        currentScene = std::make_shared<GameOverScene>();
    }
    else
        assert("指定されたシーン名の生成処理が見つからなかった→SceneManager.cppを見直しましょう" == "");
    
    currentSceneName = loadSceneName; // 現在のシーン名の更新
    changingSceneName = ""; // NextScenceのシーン予約名をリセット
    
    // 次のシーンの初期化
    currentScene->Initialize(); // 初期化を呼び出す
}




Game.cppからPlayScene.h、TitleScene.hにプレイに関するコードを移植してみます
ですが、あくまでも移植【例】です。
皆さん独自の敵などいますからあくまで移植の【例】です。



Game.cppからTitleScene.hにテスト用のボス表示のコードを移植してみます。

#ifndef TITLESCENE_H_
#define TITLESCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"

#include "GameManager.h"
#include "SceneManager.h"

class TitleScene : public Scene
{
public:
    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
    
    // テスト用のボス画像をタイトル画面で左右移動させる
    int x = 0; //staticじゃないintはここで=0で初期化できる
    int vx = 0;


    // コンストラクタ
    TitleScene() : Scene()
    {
        this->tag = "TitleScene";
    }

    // 初期化処理
    void Initialize() override
    {
        // テスト用のボス画像をタイトル画面で左右移動させる
        x = 100; // Xの初期位置
        vx = 10; // ボスの初期速度

    }

    // 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
    void Finalize() override
    {

    }

    // 更新処理
    void Update() override
    {
        if (Input::GetButtonDown(PAD_INPUT_1))
        {
            sm.LoadScene("PlayScene"); //シーン遷移
            return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
        }

        // テスト用のボス画像をタイトル画面で左右移動させる
        if (x < 0)
        {
            vx = 10; //速度の変更
        }
        else if (x > Screen::Width)
        {
            vx = -10; //画面端で移動方向反転
        }
        x += vx; // ボス画像のX位置の更新

    }

    // 描画処理
    void Draw() override
    {
        // テスト用のボス画像をタイトル画面で左右移動させる
        DrawRotaGraphF((float)x, 200, 0.9f, 0, Image::bossImage, TRUE);


        DrawString(0, 0, "TitleSceneです。ボタン押下でPlaySceneへ。", GetColor(255, 255, 255));
        if (sm.scoreMax >= 0)
        {
            DrawString(0, 200, "MAXスコア: " + sm.scoreMax, GetColor(255, 255, 255));
        }

    }
};

#endif



Game.hから移植したコードを削除します。

#ifndef GAME_H_
#define GAME_H_

#include <vector>//配列std::vectorを使う
#include <memory>//スマートなポインタを使うのに必要(shared_ptrなど)

#include "Image.h"
#include "Screen.h"
#include "Input.h"

#include "GameManager.h"

class Game
{
public:
    /*        ↓{ }を.hに書くことでcppに分けて書くはずの処理ぶぶんを.hに書いてもよい */
    Game() {  }; // 初期化コンストラクタ
    ~Game() {  }; // 破棄処理デストラクタ

    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

    void Init(); // Init処理(定義だけ)
    void Update(); // 更新処理(定義だけ)
    void Draw();// 描画処理(定義だけ)

    int x = 0; //staticじゃないintはここで=0で初期化できる
    int vx = 0;


};
#endif

Game.cppから移植したコードを削除し、ImageやRandomなどの共通機能の初期化だけに絞ります。

#include "Game.h"

#include "MyRandom.h"
#include "Player.h" // ★初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
....
(中略)... その他いろいろなヘッダ............
....
#include "Zako2.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako3.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Boss.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】

void Game::Init()
{// Init処理
    Image::Load(); //画像の読込み
    MyRandom::Init(); // 乱数シードの初期化

    x = 100; // Xの初期位置
    vx = 10; // ボスの初期速度

   
    gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
    // とりあえず適当に敵を生成
    gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)600, (float)100)));
    (中略).....
    gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
}

void Game::Update()
{// 更新処理

    Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

    if (x < 0)
    {
        vx = 10; //速度の変更
    }
    else if (x > Screen::Width)
    {
        vx = -10; //画面端で移動方向反転
    }
    x += vx; // ボス画像のX位置の更新


    if (!gm.player->isDead) // 自機が死んでいなければ
        gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】

    (中略)......................
}

void Game::Draw()
{// 描画処理
    DrawRotaGraphF((float)x, 200, 0.9f, 0, Image::bossImage, TRUE);

    (中略)..................
}


PlayScene.hにGame.cppからプレイ中に関するコードを移植してみます。

#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"

#include "GameManager.h"
#include "SceneManager.h"

#include "Player.h"
#include "Boss.h"
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"
#include "Item.h"


class PlayScene : public Scene
{
public:
    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

    int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる

    // コンストラクタ
    PlayScene() : Scene()
    {
        this->tag = "PlayScene";
    }

    // 初期化処理
    void Initialize() override
    {
        // ここでプレイヤの初期化などのプレイ開始時の処理
        gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
        // 敵の仮生成
        gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));

    }

    // 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
    void Finalize() override
    {
        // ここにプレイ終了時のメモリのリセット処理(ゲームクリアやゲームオーバーなど)
        gm.player = nullptr; // プレイヤのメモリからの解放
        gm.enemies.clear(); // 敵のメモリからの解放
        gm.playerBullets.clear(); // プレイヤの弾のメモリからの解放
        gm.enemyBullets.clear(); // 敵弾のメモリからの解放
        gm.explosions.clear(); // 爆発エフェクトのメモリからの解放
        gm.items.clear(); // アイテムのメモリからの解放


        // ゲーム終了時のスコアがMAXなら記録して終了
        if(score > sm.scoreMax) sm.scoreMax = score;
    }

    // 更新処理
    void Update() override
    {
        // ここにプレイ中の更新処理を持ってくる
       
        if (!gm.player->isDead) // 自機が死んでいなければ
            gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】

        // 自機弾の更新処理
        // for文で全自機弾をループで回してUpdateで更新する
        for (const auto& b : gm.playerBullets)
        {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
            b->Update();
        }

        // 敵弾の更新処理
        for (const auto& b : gm.enemyBullets)
        {
            b->Update();
        }

        // 自機と敵弾の衝突判定
        for (const auto& enemyBullet : gm.enemyBullets)
        {
            // 自機が死んでたらこれ以上判定しない
            if (gm.player->isDead)
                break;

            // 敵弾が死んでたらスキップ
            if (enemyBullet->isDead)
                continue;

            // 円同士の衝突判定で調べる
            if (MyMath::CircleCircleIntersection(
                gm.player->position, gm.player->collisionRadius,
                enemyBullet->position, enemyBullet->collisionRadius))
            {
                gm.player->OnCollisionEnemyBullet(enemyBullet);
            }
        }

        // 敵の更新処理
        for (const auto& b : gm.enemies)
        {
            b->Update();
        }

        // 自機弾と敵の衝突判定
        for (const auto& playerBullet : gm.playerBullets)
        {
            // 自機弾が死んでたらスキップする
            if (playerBullet->isDead)
                continue;

            for (const auto& enemy : gm.enemies)
            {
                // 敵が死んでたらスキップする
                if (enemy->isDead)
                    continue;

                // 自機弾と敵が重なっているか?
                if (MyMath::CircleCircleIntersection(
                    playerBullet->position, playerBullet->collisionRadius,
                    enemy->position, enemy->collisionRadius))
                {
                    // 重なっていたら、それぞれのぶつかったときの処理を呼び出す
                    enemy->OnCollisionPlayerBullet(playerBullet);
                    playerBullet->OnCollisionEnemy(enemy);

                    // 衝突の結果、自機弾が死んだら、この弾のループはおしまい
                    if (playerBullet->isDead)
                        break;
                }
            }
        }

        // 自機と敵の衝突判定
        for (const auto& enemy : gm.enemies)
        {
            // 自機が死んでたらこれ以上判定しない
            if (gm.player->isDead)
                break;

            // 敵が死んでたらスキップ
            if (enemy->isDead)
                continue;

            // 円同士の衝突判定で調べる
            if (MyMath::CircleCircleIntersection(
                gm.player->position, gm.player->collisionRadius,
                enemy->position, enemy->collisionRadius))
            {
                gm.player->OnCollisionEnemy(enemy);
            }
        }

        // 爆発エフェクトの更新処理
        for (const auto& e : gm.explosions)
        {
            e->Update();
        }

        // アイテムの更新処理
        for (const auto& itm : gm.items)
        {
            itm->Update();
        }

        // 自機とアイテム、敵弾とアイテムの衝突判定
        for (const auto& itm : gm.items)
        {
            // 自機が死んでたらこれ以上判定しない
            if (gm.player->isDead)
                break;
            
            // アイテムが死んでたらスキップ
            if (itm->isDead)
                continue;
            
            // 円同士の衝突判定で調べる
            if (MyMath::CircleCircleIntersection(
                gm.player->position, gm.player->collisionRadius,
                itm->position, itm->collisionRadius) )
            {
                itm->OnCollisionPlayer(gm.player);
            }
            
            for (const auto& enemyBullet : gm.enemyBullets)
            {
                // 敵弾が死んでたらスキップ
                if (enemyBullet->isDead)
                    continue;
                
                // 円同士の衝突判定で調べる
                if (MyMath::CircleCircleIntersection(
                    itm->position, itm->collisionRadius,
                    enemyBullet->position, enemyBullet->collisionRadius) )
                {
                    enemyBullet->OnCollisionItem(itm);
                    itm->OnCollisionEnemyBullet(enemyBullet);
                }
            }
        }

        //return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験

        // 自機弾の削除処理
        gm.EraseRemoveIf(gm.playerBullets,
            [](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

        // 敵のリストから死んでるものを除去する
        gm.EraseRemoveIf(gm.enemies,
            [](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

        // 敵弾のリストから死んでるものを除去する
        gm.EraseRemoveIf(gm.enemyBullets,
            [](std::shared_ptr<EnemyBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

        //爆発エフェクトのリストから死んでるものを除去する
        gm.EraseRemoveIf(gm.explosions,
            [](std::shared_ptr<Explosion>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

        //アイテムのリストから死んでるものを除去する
        gm.EraseRemoveIf(gm.items,
            [](std::shared_ptr<Item>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除



        // ゲームオーバー判定と画面チェンジ処理
        bool isGameover = (gm.player == nullptr);
        if (!isGameover && gm.player->isDead) isGameover = true;
        if (isGameover)
        {
            sm.LoadScene("GameOverScene"); //シーン遷移
            return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
        }
    }

    // 描画処理
    void Draw() override
    {
        // ここにプレイ画面の描画処理を持ってくる

        // 敵の描画処理
        for (const auto& b : gm.enemies)
        {
            b->Draw();
        }
        
        // 爆発エフェクトの描画処理
        for (const auto& e : gm.explosions)
        {
            e->Draw();
        }
        
        if (!gm.player->isDead) // 自機が死んでいなければ
            gm.player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
        
        // 自機弾の描画処理
        // for文で全自機弾をループで回してUpdateで更新する
        for (const auto& b : gm.playerBullets)
        {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
            b->Draw();
        }
        
        // 敵弾の描画処理
        for (const auto& b : gm.enemyBullets)
        {
            b->Draw();
        }
    
        // アイテムの描画処理
        for (const auto& itm : gm.items)
        {
            itm->Draw();
        }

    }
};

#endif



Game.cppから移植したコードを削除します。

#include "Game.h"

#include "MyRandom.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"
#include "Zako0.h"
#include "Zako1.h"
#include "Zako2.h"
#include "Zako3.h"
#include "Item.h"
#include "Boss.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】


void Game::Init()
{// Init処理
    Image::Load(); //画像の読込み
    MyRandom::Init(); // 乱数シードの初期化

    gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
    // とりあえず適当に敵を生成
    gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)600, (float)100)));
    gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)1000, (float)500)));
    gm.enemies.emplace_back(std::make_shared<Zako1>(Vector3((float)1000, (float)150)));
    gm.enemies.emplace_back(std::make_shared<Zako2>(Vector3((float)1200, (float)250)));
    gm.enemies.emplace_back(std::make_shared<Zako3>(Vector3((float)1300, (float)400)));

    gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));


}

void Game::Update()
{// 更新処理

    Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

    if (!gm.player->isDead) // 自機が死んでいなければ
        gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】

    // 自機弾の更新処理
    // for文で全自機弾をループで回してUpdateで更新する
    for (const auto& b : gm.playerBullets)
    {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
        b->Update();
    }

    // 敵弾の更新処理
    for (const auto& b : gm.enemyBullets)
    {
        b->Update();
    }

    (中略)................

    // 自機と敵の衝突判定
    for (const auto& enemy : gm.enemies)
    {
        // 自機が死んでたらこれ以上判定しない
        if (gm.player->isDead)
            break;

        // 敵が死んでたらスキップ
        if (enemy->isDead)
            continue;

        // 円同士の衝突判定で調べる
        if (MyMath::CircleCircleIntersection(
            gm.player->position, gm.player->collisionRadius,
            enemy->position, enemy->collisionRadius))
        {
            gm.player->OnCollisionEnemy(enemy);
        }
    }

    // 爆発エフェクトの更新処理
    for (const auto& e : gm.explosions)
    {
        e->Update();
    }

    (中略)................

    //return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験

    // 自機弾の削除処理
    gm.EraseRemoveIf(gm.playerBullets,
        [](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

    (中略)..............

    //爆発エフェクトのリストから死んでるものを除去する
    gm.EraseRemoveIf(gm.explosions,
        [](std::shared_ptr<Explosion>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

    //アイテムのリストから死んでるものを除去する
    gm.EraseRemoveIf(gm.items,
        [](std::shared_ptr<Item>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除





}

void Game::Draw()
{// 描画処理
   
    // 敵の描画処理
    for (const auto& b : gm.enemies)
    {
        b->Draw();
    }

    // 爆発エフェクトの描画処理
    for (const auto& e : gm.explosions)
    {
        e->Draw();
    }

    if (!gm.player->isDead) // 自機が死んでいなければ
        gm.player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】

    // 自機弾の描画処理
    // for文で全自機弾をループで回してUpdateで更新する
    for (const auto& b : gm.playerBullets)
    {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
        b->Draw();
    }

    // 敵弾の描画処理
    for (const auto& b : gm.enemyBullets)
    {
        b->Draw();
    }
   
    // アイテムの描画処理
    for (const auto& itm : gm.items)
    {
        itm->Draw();
    }

}




さて、移植しただけではゲームは動きません。
Game.hとGame.cppにシーンの初期遷移、更新、描画の処理を追加しましょう。



Game.hにシーンのマネージャへの連絡先smを追加しましょう。

#ifndef GAME_H_
#define GAME_H_

#include <vector>//配列std::vectorを使う
#include <memory>//スマートなポインタを使うのに必要(shared_ptrなど)

#include "Image.h"
#include "Screen.h"
#include "Input.h"

#include "GameManager.h"
#include "SceneManager.h"
#include "Scene.h"


class Game
{
public:
    /*        ↓{ }を.hに書くことでcppに分けて書くはずの処理ぶぶんを.hに書いてもよい */
    Game() {  }; // 初期化コンストラクタ
    ~Game() {  }; // 破棄処理デストラクタ

    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

    void Init(); // Init処理(定義だけ)
    void Update(); // 更新処理(定義だけ)
    void Draw();// 描画処理(定義だけ)
};
#endif

Game.cppにシーンの初期遷移、更新、描画の処理を追加しましょう。

#include "Game.h"

#include "MyRandom.h"

void Game::Init()
{// Init処理
    Image::Load(); //画像の読込み
    MyRandom::Init(); // 乱数シードの初期化

    // ゲーム開始して最初はタイトルシーンに遷移
    sm.LoadScene("TitleScene");

}

void Game::Update()
{// 更新処理

    Input::Update(); //【注意】これ忘れると全シーン反応しない

    // シーンの更新処理
    sm.currentScene->Update();

}

void Game::Draw()
{// 描画処理
   
    // シーンの描画処理
    sm.currentScene->Draw();

}





さて、ここまでで画面遷移できるようになりました。
一度実行して画面遷移できるか確認してみてください。




【高難度】画面やステージごとにImageのメモリを開放する

さて、画面遷移は実現しましたが、現状ではメモリの節約効果は薄いです
なぜか? 答えはImageクラスではゲーム開始時に全画像データを読み込んだままずっと持ち続けてます。
ゆえにゲームが壮大になれば【冒険で使う全画像データがメモリにのり続けてしまい】ます。
よって【画面やステージマップごと】【使用する画像以外をメモリから解放する】処理を取り入れましょう。

【補足と考察】
(実はプレイ中の敵や弾[1個=数byte]の削除処理はメモリより【当たり判定のCPU計算を軽くする】効果の方が強かった)
(敵や弾[1個=数byte]×1000個でも1000バイト=1KBキロバイト、一方で【大きめの背景画像ファイルは1個で1~1000KBキロバイト=1MBメガバイト】)
(つまり画像1つ分で弾数万発ぶんのメモリ節約、まあでも【数万発の当たり判定したらCPU重くて画面フリーズしますが】)



Image.hにロード済み画像の辞書に関する定義を追加しましょう(mapタイプ型 = 辞書配列型)。

#ifndef IMAGE_H_
#define IMAGE_H_

#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
#include <unordered_map> // ロード済の画像辞書に使う
#include <string> // 文字列string
#include <memory> // 辞書の指す先を回し読みポインタにする
#include <vector> // シーンのリストに使う
#include <algorithm> // シーンのリストからのfindに使う

#include "DivImage.h" // 分割画像の読込みに使う

class Image
{
public:
    Image() {}; // 初期化コンストラクタの定義だけ
    ~Image() {}; // 破棄する処理デストラクタの定義だけ
    static void Load(std::string scene=""); // シーンを指定して使う画像だけロードすればメモリを節約できる
    static int LoadDivGraph(const TCHAR* FileName, DivImage& divImage, std::string scene="", std::vector<std::string> useScenes={""});
    static int LoadGraph(const TCHAR* FileName, std::string scene = "", std::vector<std::string> useScenes = { "" });
    static std::unordered_map<std::string, DivImage*> loadDic; // ロード済の画像辞書

    static int bossImage; //ボス画像のハンドラ(読込画像番号)
    static int player; //プレイヤ画像のハンドラ
    static int playerBullet; //プレイヤの弾画像のハンドラ
    static int enemyBullet16; //敵弾 画像のハンドラ(読込画像番号)
    static int zako0; //ザコ0 画像のハンドラ(読込画像番号)
    static int zako1; //ザコ1 画像のハンドラ(読込画像番号)
    static int zako2; //ザコ2 画像のハンドラ(読込画像番号)
    static int zako3; //ザコ3 画像のハンドラ(読込画像番号)
    static int boss1; // ボス通常時
    static int boss2; // ボス気絶
    static int boss3; // ボス発狂
    static DivImage explosion; // [分割画像]爆発エフェクト
    static int item0; // アイテム0 画像のハンドラ(読込画像番号)

private:

};
#endif


Image.cppにロード済み画像を辞書に登録しシーンで使わない画像を開放する処理を追加しましょう。

#include "Image.h"

std::unordered_map<std::string, DivImage*> Image::loadDic;

int Image::bossImage{ -1 }; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{ -1 };
int Image::playerBullet{ -1 };
int Image::enemyBullet16{ -1 };
int Image::zako0{ -1 };
int Image::zako1{ -1 };
int Image::zako2{ -1 };
int Image::zako3{ -1 };
int Image::boss1{ -1 };
int Image::boss2{ -1 };
int Image::boss3{ -1 };
// ★分割画像は初期化のときに{X方向画像数, Y方向画像数, 画像横幅XSize,画像縦幅YSize}を指定
DivImage Image::explosion{ 8, 2, 64, 64 };
int Image::item0{ -1 };

void Image::Load(std::string scene)
{
    bossImage = LoadGraph("Image/boss1.png", scene, { "TitleScene" });
    assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

    player = LoadGraph("Image/player.png", scene, { "PlayScene" });
    assert(player != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

    playerBullet = LoadGraph("Image/player_bullet.png", scene, { "PlayScene" });
    assert(playerBullet != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

    enemyBullet16 = LoadGraph("Image/enemy_bullet_16.png", scene, { "PlayScene" });
    assert(enemyBullet16 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

    zako0 = LoadGraph("Image/zako0.png", scene, { "PlayScene" });
    assert(zako0 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

    zako1 = LoadGraph("Image/zako1.png", scene, { "PlayScene" });
    assert(zako1 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

    zako2 = LoadGraph("Image/zako2.png", scene, { "PlayScene" });
    assert(zako2 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

    zako3 = LoadGraph("Image/zako3.png", scene, { "PlayScene" });
    assert(zako3 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

    boss1 = LoadGraph("Image/boss1.png", scene, { "PlayScene" });
    assert(boss1 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

    boss2 = LoadGraph("Image/boss2.png", scene, { "PlayScene" });
    assert(boss2 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

    boss3 = LoadGraph("Image/boss3.png", scene, { "PlayScene" });
    assert(boss3 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

    // div分割画像をロード or メモリ開放
    if( Image::LoadDivGraph("Image/explosion.png", explosion, scene, { "PlayScene" }) != -2 );
        for (int i = 0; i < explosion.AllNum; i++)
        {    // 画像読込失敗
            if (explosion.HandleArray[i] == -1)
                assert("画像読込失敗(画像ファイル名やフォルダが正しい?)" == "");
        }

    item0 = LoadGraph("Image/heart.png", scene, { "PlayScene" });
    assert(item0 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

    item1 = LoadGraph("Image/star.png", scene, { "PlayScene" });
    assert(item1 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

}

// DXライブラリのLoadGraphを同じ関数名でラッピング(既存コードの書換えを最小限にする工夫)
// シーンごとにすでにメモリにロードしてある画像でシーンに使わないものはメモリから解放する
int Image::LoadGraph(const TCHAR* FileName, std::string scene, std::vector<std::string> useScenes)
{
    DivImage divImage{ 1,1,1,1 }; // 1×1の1分割画像として分割画像とロードを共通化
    return Image::LoadDivGraph(FileName, divImage, scene, useScenes);
}


// DXライブラリのLoadDivGraphを使いやすくラッピング
// ★関数に参照渡しを行う
// 関数の引数に参照渡しを行うことでdivImageの中身を書き換えることができる(値渡しでは書換不可)
// https://qiita.com/agate-pris/items/05948b7d33f3e88b8967
// https://qiita.com/RuthTaro/items/f35c3a26779c0ca1a41a
int Image::LoadDivGraph(const TCHAR* FileName, DivImage& divImage, std::string scene, std::vector<std::string> useScenes)
{
    int result = -1;
    // 次のシーンで使う画像かの判別、useScenesリストにあれば使う判定
    bool use = std::find(useScenes.begin(), useScenes.end(), scene) != useScenes.end();
    if (use)
    {    // ロード済み辞書に指定したファイルがある(count=1)のとき
        if (loadDic.count(FileName) == 1
            && loadDic[FileName] != nullptr && loadDic[FileName]->HandleArray != nullptr)
        {    // 画像ハンドルをfor文で書き写す(単一:AllNum=1,分割画像:AllNum>1)
            for (int i = 0; i < loadDic[FileName]->AllNum; i++)
                divImage.HandleArray[i] = loadDic[FileName]->HandleArray[i];
            // ロード済みなら画像ハンドルを返す(ロード時間短縮)
            if (loadDic[FileName]->AllNum == 1)
                return loadDic[FileName]->HandleArray[0];
            else return 0; // 分割画像なら成功:0を返す(本家LoadDivGraphに合わせて)
        }

        // DXライブラリで画像をロードし辞書に登録
        if (divImage.AllNum == 1)
        {    // 単一画像のロードと画像サイズ取得
            result = divImage.HandleArray[0] = DxLib::LoadGraph(FileName);
            GetGraphSize(result, &(divImage.XSize), &(divImage.YSize)); // 画像サイズ取得
        }
        else if (divImage.AllNum > 1) // 複数分割画像の読込み
            result =
DxLib::LoadDivGraph(FileName, divImage.XNum * divImage.YNum,
                divImage.XNum, divImage.YNum, divImage.XSize, divImage.YSize, divImage.HandleArray);

        for (int i = 0; i < divImage.AllNum; i++)
        { // 画像読込失敗
            if (divImage.HandleArray[i] == -1)
                assert("画像読込失敗(画像ファイル名やフォルダが正しい?)" == "");
        }
        // 辞書用にdivImageの【★コピーを取ってポインタ】として保管
        if (result != -1)
        {
            if (loadDic.count(FileName) == 1 && loadDic[FileName] != nullptr)
            {    // 上書き登録のために既存の情報を消す
                DivImage* oldHandle = loadDic[FileName];
                delete oldHandle;// 辞書に保管している情報削除
                loadDic.erase(FileName);// 辞書から索引も削除
            }
            // 新たに辞書登録
            loadDic[FileName] = new DivImage(divImage);//←★コピーコンストラクタ
        }
       
        return result; // ロードした結果を返す
    } // ↓ここまでの↑returnに引っかからずif抜けてきたら【シーン利用しない画像】

    // 今回のシーンでは利用しない画像の時はメモリから画像を開放して節約
    if (loadDic.count(FileName) == 1 &&
        loadDic[FileName] != nullptr && loadDic[FileName]->HandleArray != nullptr)
    {    // メモリから画像開放 https://dxlib.xsrv.jp/function/dxfunc_graph1.html#R3N15
        for (int i = 0; i < loadDic[FileName]->AllNum; i++)
            DxLib::DeleteGraph(loadDic[FileName]->HandleArray[i]);
        DivImage* oldHandle = loadDic[FileName];
        delete oldHandle;// 辞書に保管している情報削除
        loadDic.erase(FileName); // 辞書からも索引削除
    }
    // 今回のシーンでは利用しない画像の時は-2を返す
    return -2; // 画像読込み失敗の-1と区別するため-2を返します

};


DivImage.hにコピーコンストラクタをpublic定義にして画像辞書に分割画像のコピーへのポインタを保存できるようにする。
下記スライドで変更理由に目を通しておきましょう(C++でクラス内でnewやdeleteのした際の根の深いコピー時の*ポインタとデータ分離問題)。


#ifndef DIVIMAGE_H_
#define DIVIMAGE_H_

#include "DxLib.h"
#include <assert.h> // 画像の2のべき乗チェックに使う

// 分割画像を読込むためのデータ構造(画像数や画像ハンドル配列、3Dに使う場合の2のべき乗チェック機能をもつ)
class DivImage
{  // 2Dの分割画像や3Dに分割Div画像を使うと画像全体が使われてしまうのを回避するための情報
public:
    int XNum = 0;
    int YNum = 0;
    int XSize = 0;
    int YSize = 0;
    int* HandleArray = nullptr;
    int AllNum = 0;
    bool is3DTexture = false;

    // 初期化 : X方向分割画像数 XNum, Y方向分割画像数 YNum, 画像 横 幅XSizeピクセル, 画像 縦 高さYSizeピクセル
    DivImage(int XNum, int YNum, int XSize, int YSize, bool is3DTexture = false)
    { // 初期化コンストラクタ
        this->XNum = XNum; // X方向分割画像数
        this->YNum = YNum; // Y方向分割画像数
        this->XSize = XSize; // 画像 横 幅XSize(ピクセルドット数)
        this->YSize = YSize; // 画像 縦 高さYSize(ピクセルドット数)
        AllNum = XNum * YNum; // トータルの分割画像数
        this->is3DTexture = is3DTexture; // 3Dテクスチャ分割画像かtrueだと2のべき乗チェックが入る
        // ★div分割画像読込ハンドラ保存用のint配列メモリを確保し-1で初期化
        this->HandleArray = new int[AllNum]; // 配列を確保し-1で初期化
        for (int i = 0; i < AllNum; i++) HandleArray[i] = -1;
        //↑【演習★】初期化後の動的配列をデバッガで見てみよう[ary,256として再評価ボタン]
        // http://visualstudiostudy.blog.fc2.com/blog-entry-17.html
        int* ary = HandleArray;
#ifdef _DEBUG // デバッグ機能(デバッグの時だけ _DEBUG が定義)
        if (is3DTexture == true) ImagePowCheck((*this)); // *(this)でthisポインタの示す変数内容を表す
#endif
    };

    ~DivImage()
    { // デストラクタでメモリを解放
        if (this->HandleArray != nullptr)
            delete[] this->HandleArray;
    };

    // コピーコンストラクタ(動的new配列をクラス内に持つ場合にはカスタムが必要)
    DivImage(const DivImage& divImage)
    {
        // 基本はただのコピー書き移し
        XNum = divImage.XNum; YNum = divImage.YNum;
        XSize = divImage.XSize; YSize = divImage.YSize;
        AllNum = XNum * YNum;
        is3DTexture = divImage.is3DTexture;
        // ↓★この配列だけはデフォルトのコピーコンストラクタではコピーされない!
        HandleArray = new int[AllNum]; //★[勉強]こちらでもnew確保
        for (int i = 0; i < AllNum; i++)
             HandleArray[i] = divImage.HandleArray[i];
    };


    //【勉強 []添え字演算子を独自に定義】https://programming.pc-note.net/cpp/operator.html
    // 内部のHandleArrayに今までの様にImage::explosion[5]とかで簡単アクセスできるように【あいだに噛ます】
    int const& operator [](int index) const
    {
        if (index < 0 || AllNum <= index) assert("画像データのアクセス番号がおかしい!" == "");
        return HandleArray[index];
    }
    int& operator [](int index)
    {
        if (index < 0 || AllNum <= index) assert("画像データのアクセス番号がおかしい!" == "");
        return HandleArray[index];
    }

#ifdef _DEBUG // デバッグ機能(デバッグの時だけ _DEBUG が定義)
    bool is_pow2(unsigned int x) // 2のべき乗か計算
    { // https://programming-place.net/ppp/contents/c/rev_res/math012.html
        return (x != 0) && (x & (x - 1)) == 0;
    }

    void ImagePowCheck(DivImage& divImage)
    { // ★3Dに使う画像は2のべき乗でなければ受け付けないようにする
      // https://yttm-work.jp/gmpg/gmpg_0031.html
      // https://yappy-t.hatenadiary.org/entry/20100110/1263138881
        if (divImage.XSize > 0 && divImage.YSize > 0
            && is_pow2(divImage.XSize) && is_pow2(divImage.YSize)) return;
        else assert("3Dに使うなら2のべき乗の画像サイズにしなきゃ" == "");
    }
#endif

private: // コピーと代入をプライベートにして禁止する
    // コピーコンストラクタの禁止privateオーバーロード
    DivImage(const DivImage& divImage) {};

    // いままでコピー禁止してたのはnew,deleteの面倒なデータと*ポインタ分離問題を察知するため

    // 代入演算子の禁止privateオーバーロード
    void operator=(const DivImage& divImage) {};
};

#endif



TitleScene.hにシーンごとの画像読込みとメモリ開放する処理を追加しましょう。

(中略)........

    // 初期化処理
    void Initialize() override
    {
        Image::Load("TitleScene"); //画像の読込みとメモリ開放

        // テスト用のボス画像をタイトルでアニメで左右移動させる
        x = 100; // Xの初期位置
        vx = 10; // ボスの初期速度
    }

(中略)........


PlayScene.hにシーンごとの画像読込みとメモリ開放する処理を追加しましょう。

(中略)........

    // 初期化処理
    void Initialize() override
    {
        Image::Load("PlayScene"); //画像の読込みとメモリ開放

        // ここでプレイヤの初期化などのプレイ開始時の処理
        gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
        // 敵の仮生成
        gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
    }

(中略)........


GameOverScene.hにシーンごとの画像読込みとメモリ開放する処理を追加します。
(現状シーンで画像使ってないけどプレイ中画面のメモリを解放する効果が出る)。

(中略)........

    // 初期化処理
    void Initialize() override
    {
        Image::Load("GameOverScene"); //画像の読込みとメモリ開放
    }

(中略)........


Game.cppでやっていたImageのLoadを削除します。

#include "Game.h"

#include "MyRandom.h"

void Game::Init()
{// Init処理
    Image::Load(); //画像の読込み
    MyRandom::Init(); // 乱数シードの初期化

    // ゲーム開始して最初はタイトルシーンに遷移
    sm.LoadScene("TitleScene");
}

(以下略)............



これで一応画面ごとにメモリから画像データが開放される構造になりました。
でも、ほとんどメモリのグラフには変化がないかもしれません(タイトル動画など重いデータあれば威力発揮)



サウンドの読込みと再生機能

実は画像より音声のほうがメモリ消費が大きかったりします。
なぜか?画像はアニメしなければ1枚ですよね。
一方【音声には時間の長さがあります】再生時間が10分など長くなれば数MBになります。
特に.mp3など圧縮された音声ファイルはメモリ上では非圧縮の.wavになります!
ゲーム開始前は小さいmp3の音声ファイルがゲーム開始すると【メモリ上で数十倍に膨れ上がります】
しかも最悪なことに【圧縮をもとの音声に戻すためロードにめちゃくちゃ時間がかかります】
ゆえにロードが重くなるのを防ぐためLoadSoundMemする音声ファイルは【あらかじめ.wav変換したほうがLoadが早い】です。
以下リンクの変換ツールでmp3などを.wavの非圧縮の音声ファイルに変換して使いましょう。
https://forest.watch.impress.co.jp/library/software/flicaudiocon/

今回はまず「webでさがしたBGM素材サイト」から音声ファイルを取ってきてSoundクラスを作成してみましょう。
素材サイト例)
https://maou.audio/category/bgm/
https://maou.audio/category/se/

自分でシンセをダウンロードして効果音やBGMを自分で作っちゃうことだってできる。
(人は真にクリエイティブになればなるほど借り物に飽き足らずそれを作る道具の方に興味が行くものです)


以下のファイルを作成中のC++のプロジェクト下にImageフォルダの時のように「SEフォルダ」や「BGMフォルダ」を作成してコピーしておきましょう。

まずは簡易版のSoundクラスを作ってみましょう。基本はImage.hと似ています。

Sound.hを新規作成します。

#ifndef SOUND_H_
#define SOUND_H_

#include "DxLib.h"
#include <assert.h> // 音声読み込みの読込み失敗表示用
#include <string>

class Sound
{
public:
    Sound() {}; // 初期化コンストラクタの定義
    ~Sound() {}; // 破棄処理デストラクタの定義
    
    // メモリ再生音源管理番号
    static int start; // ピロピロリン
    static int bomb; // ボン!

    static std::string playingMusic; // 再生中のBGM(同一BGMをPlayMusic指定しても再生し直さない)

    static void Load();
    // メモリから音声を再生(個別の音量設定も効かせられる)
    static void Play(int handle, int PlayType = DX_PLAYTYPE_BACK);
    // ディスク上の音楽を直接再生する(メモリ上に展開しない)
    static void PlayMusic(std::string file, int PlayType = DX_PLAYTYPE_LOOP);
    
private:

};
#endif



Sound.cppを新規作成します。
すでにプレイ中のBGMは同じBGMをPlayMusicしようとしても再生し直さない工夫をしています。
これで再生ボタンUpdate連打状態で重くなるストッパーになります

#include "Sound.h"

// メモリ再生音源管理番号
int Sound::start; // ピロピロリン
int Sound::bomb; // ボン!

std::string Sound::playingMusic; // 再生中のBGM(同一BGMをプレイ指定しても初めから再生しない)

void Sound::Load()
{
    start = LoadSoundMem("SE/start.ogg");
    assert(start != -1); // 読込失敗、ファイル名かフォルダ名が間違ってる
   
    bomb = LoadSoundMem("SE/bomb.wav");
    assert(bomb != -1); // 読込失敗、ファイル名かフォルダ名が間違ってる
}

// メモリ上の音声を再生する。
void Sound::Play(int handle, int PlayType)
{   
    // メモリ上から音声再生
    PlaySoundMem(handle, PlayType);
}

// ディスク上の音楽を直接再生する(メモリ上に展開しない)
void Sound::PlayMusic(std::string file, int PlayType)
{   
    // プレイ中のBGMを再指定しても再生し直さず即時return
    if (file == playingMusic) return;

    playingMusic = file; // プレイ中のBGM名を記録


    DxLib::PlayMusic(file.c_str(), PlayType); //ディスクから直接再生(メモリにのせない)
}



Game.cppにSoundのロード処理を追加します。

#include "Game.h"

#include "MyRandom.h"
#include "Sound.h"

void Game::Init()
{// Init処理
(中略)......
    MyRandom::Init(); // 乱数シードの初期化
    Sound::Load(); //音声の読込み

    // ゲーム開始して最初はタイトルシーンに遷移
    sm.LoadScene("TitleScene");
}

void Game::Update()
{// 更新処理

    Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

    // シーンの更新処理
    sm.currentScene->Update();
}

void Game::Draw()
{// 描画処理
  
    // シーンの描画処理
    sm.currentScene->Draw();
}


TitleScene.hにサウンドの読込みとBGM再生、ゲームスタート時の再生処理と再生終わり待ちで画面遷移する処理を追加します。

#ifndef TITLESCENE_H_
#define TITLESCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"

#include "GameManager.h"
#include "SceneManager.h"

class TitleScene : public Scene
{
public:
    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
    
    // スタートボタンを押したら音声再生完了を待って画面移動
    bool isStartPushed = false;


    // テスト用のボス画像をタイトルでアニメで左右移動させる
    int x = 0; //staticじゃないintはここで=0で初期化できる
    int vx = 0;

    // コンストラクタ
    TitleScene() : Scene()
    {
        this->tag = "TitleScene";
    }

    // 初期化処理
    void Initialize() override
    {
(中略).....
        
        Sound::PlayMusic("BGM/title.mp3"); //タイトルBGMを再生
        isStartPushed = false; // ボタンのフラグをリセット


        // テスト用のボス画像をタイトルでアニメで左右移動させる
        x = 100; // Xの初期位置
        vx = 10; // ボスの初期速度
    }

    // 終了処理(大抵Initializeと同じリセットだが終了時だけやりたいリセットの仕方もあるかも)
    void Finalize() override
    {

    }

    // ボタン入力に応じた処理
    void HandleInput()
    {
        if (Input::GetButtonDown(PAD_INPUT_1))
        {    // ボタンを押したらスタート音声を再生
            Sound::Play(Sound::start);
            isStartPushed = true; // 画面遷移トリガーON
        }
    }


    // 更新処理
    void Update() override
    {
        // スタートの効果音の再生が終わるまで画面遷移を待つ
        if (isStartPushed && CheckSoundMem(Sound::start) == 0)
        {    // スタート音声再生終了をトリガーとして画面遷移
            sm.LoadScene("PlayScene"); //シーン遷移
        }

        HandleInput(); // キー入力処理


        // テスト用のボス画像をタイトルでアニメで左右移動させる
        if (x < 0)
        {
            vx = 10; //速度の変更
        }
        else if (x > Screen::Width)
        {
            vx = -10; //画面端で移動方向反転
        }
        x += vx; // ボス画像のX位置の更新
    }

    // 描画処理
    void Draw() override
    {
        DrawString(0, 0, "TitleSceneです。ボタン押下でPlaySceneへ。", GetColor(255, 255, 255));
        if (sm.scoreMax >= 0)
        {
            DrawString(0, 200, "MAXスコア: " + sm.scoreMax, GetColor(255, 255, 255));
        }

        // テスト用のボス画像をタイトルでアニメで左右移動させる
        DrawRotaGraphF((float)x, 200, 1.0f, 0, Image::bossImage, TRUE);
    }
};

#endif



PlayScene.hにプレイBGM再生と敵の被弾の効果音の処理を追加します。
音声のハンドルを辞書管理してメモリから解放したり個別音量調整できるようにします。

#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"

#include "GameManager.h"
#include "SceneManager.h"

#include "Player.h"
#include "Boss.h"
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"
#include "Item.h"

class PlayScene : public Scene
{
public:
    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

    int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる

    // コンストラクタ
    PlayScene() : Scene()
    {
        this->tag = "PlayScene";
    }

    // 初期化処理
    void Initialize() override
    {
(中略).....
       
        Sound::PlayMusic("BGM/gameplay.mp3"); //プレイ画面BGMを再生

        // ここでプレイヤの初期化などのプレイ開始時の処理
        gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
        // 敵の仮生成
        gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
    }

(中略)...................

    // 更新処理
    void Update() override
    {
       
        (中略)...................

        // 自機弾と敵の衝突判定
        for (const auto& playerBullet : gm.playerBullets)
        {
            // 自機弾が死んでたらスキップする
            if (playerBullet->isDead)
                continue;

            for (const auto& enemy : gm.enemies)
            {
                // 敵が死んでたらスキップする
                if (enemy->isDead)
                    continue;

                // 自機弾と敵が重なっているか?
                if (MyMath::CircleCircleIntersection(
                    playerBullet->position, playerBullet->collisionRadius,
                    enemy->position, enemy->collisionRadius))
                {
                    // 重なっていたら、それぞれのぶつかったときの処理を呼び出す
                    enemy->OnCollisionPlayerBullet(playerBullet);
                    playerBullet->OnCollisionEnemy(enemy);

                    Sound::Play(Sound::bomb); // 効果音の再生

                    // 衝突の結果、自機弾が死んだら、この弾のループはおしまい
                    if (playerBullet->isDead)
                        break;
                }
            }
        }

        (中略)...................


        // ゲームオーバー判定と画面チェンジ処理
        bool isGameover = (gm.player == nullptr);
        if (!isGameover && gm.player->isDead) isGameover = true;
        if (isGameover)
        {
            Sound::PlayMusic("BGM/ending.mp3");
            sm.LoadScene("GameOverScene"); //シーン遷移
            return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
        }
           
    }

    // 描画処理
    void Draw() override
    {
        (中略)...................
    }
};

#endif



ここまでで簡易的なサウンドの再生できるようになります。
実行してみてうまく音声が再生できるか試してみましょう。
次は高度なサウンドの機能を作成しますが、難易度が上がりますので
簡単に再生できるだけでよい人は高度なサウンド機能は一旦後回しでもよいかもしれません。




【高難度】画面やステージごとにサウンドをメモリ読込み・解放(音量調整機能も追加)

さて、一応基本的なサウンド再生機能は実現しました。
この項では少し高度なサウンドの音量管理とメモリ管理を辞書配列で実現します。
とりあえず再生できればよいや、という人はあとから取り入れる形でもよいかもしれません。

辞書配列には辞書配列mapの【高速版unordered_map】を使用しました。
普通の辞書は【あいうえお順に毎度並べ替えに時間かけてる】ですが、
高速版辞書は【いちいち、あいうえお並び替えしないから高速】と覚えておきましょう。
並べ替え順のない高速辞書を【ハッシュ】と呼びます。
ハッシュは【辞書のキー⇒順番の必要ないデータへの高速アクセス可】と暗記しておきましょう。
配列などの中では【条件付きでほぼ最速】という認識で大丈夫です。
まとめて複数の音声をフェードアウトするときはUpdateで音声辞書を秒間60回連続検索することになるので導入しました。
ちなみにどれくらい高速かは下記リンク参照(ハッシュはO(1)という最速水準の単位です)
https://qiita.com/drken/items/872ebc3a2b5caaa4a0d0

Sound.hに辞書配列や音量管理関数の定義を追加します。

#ifndef SOUND_H_
#define SOUND_H_

#include "DxLib.h"
#include <assert.h> // 音声読み込みの読込み失敗表示用
#include <string>
#include <unordered_map> // 音声の辞書【map高速版】https://qiita.com/sileader/items/a40f9acf90fbda16af51
#include <vector> // シーンのリストに使う
#include <algorithm> // シーンのリストからのfindに使う


class Sound
{
public:
    Sound() {}; // 初期化コンストラクタの定義
    ~Sound() {}; // 破棄処理デストラクタの定義
   
    // メモリ再生音源管理番号
    static int start; // ピロピロリン
    static int bomb; // ボン!

    // ディスク再生音源管理番号
    static int title; // タイトル画面BGM
    static int gameplay; // プレイ画面BGM
    static int ending; // ゲームオーバ画面BGM


    static std::unordered_map<std::string, int> loadDic; // ロード済の音声辞書
    static std::unordered_map<int, std::string> SE_Dic; // SEファイル名辞書
    static std::unordered_map<int, std::string> BGM_Dic; // BGMファイル名辞書
    static std::unordered_map<std::string, float> volumeDic; // 【音声ごと】の[音量]辞書


    static float volumeSE; // 効果音のベース音量(0~100%で指定)
    static float volumeBGM; // 音楽BGMのベース音量(0~100%で指定)

    static std::string playingMusic; // 再生中のBGM(同一BGMをPlayMusic指定しても再生し直さない
    static int musicId; // 音楽ID -3,-4,-5..メモリ上にのせないが番号を管理し個別に音量設定できるよう

    // シーンを指定してそのシーンで使うサウンドだけロードすればメモリ節約できる
    static void Load(std::string scene = "");
    // メモリから音声を再生(個別の音量設定も効かせられる)
    static void Play(int handle, int PlayType = DX_PLAYTYPE_BACK);
    // ディスク上の音楽を直接再生する(メモリ上に展開しない)
    static void PlayMusic(std::string file, int PlayType = DX_PLAYTYPE_LOOP);
    // IDからディスク上の音楽を直接再生する(メモリ上に展開しない)
    static void PlayMusic(int musicId, int PlayType = DX_PLAYTYPE_LOOP);
    // ファイルを辞書登録しmusicIDで管理する(メモリにはのせずファイル名とIDだけ登録)
    static int SetMusicFile(std::string file, bool isSE = false);
    // 音声ファイルごとのボリューム設定
    static void SetVolume(float VolumeParcent, int SoundHandle);
    // メモリ音源の音量の更新(再生中に減らせばフェードアウト)
    static int UpdateVolume(int SoundHandle = -1);
    // 再生中のディスク音楽の音量の更新
    static int UpdateMusicVolume();
    // 音声をメモリへ読込み番号GET or シーンで使わない場合はメモリから解放
    static int LoadSoundMem(const TCHAR *FileName, std::string scene, std::vector<std::string> useScenes
, bool isBGM=false, int BufferNum = 3);

private:

};
#endif



Sound.cppに音声辞書とメモリ開放と音量調節処理を追加します。
音声のハンドルを辞書管理してメモリから解放したり個別音量調整できるようにします。

#include "Sound.h"

// メモリ再生音源管理番号
int Sound::start; // ピロピロリン
int Sound::bomb; // ボン!

// ディスク再生音源管理番号
int Sound::title; // タイトル画面BGM
int Sound::gameplay; // プレイ画面BGM
int Sound::ending; // ゲームオーバ画面BGM


std::unordered_map<std::string, int> Sound::loadDic; // ロード済の音声辞書
std::unordered_map<int, std::string> Sound::SE_Dic; // SEファイル名辞書
std::unordered_map<int, std::string> Sound::BGM_Dic; // BGMファイル名辞書
std::unordered_map<std::string, float> Sound::volumeDic; // 【音声ごと】の[音量]辞書

float Sound::volumeSE{ 100 }; // 効果音のベース音量(0~100%で指定)
float Sound::volumeBGM{ 100 }; // 音楽BGMのベース音量(0~100%で指定)

std::string Sound::playingMusic{ "" }; // 再生中のBGM(同一BGMをPlayMusic指定しても再生し直さない
int Sound::musicId{ -3 }; // 音楽ID -3,-4,-5..メモリ上にのせないが番号を管理し個別に音量設定できるよう

// シーンを指定してそのシーンで使うサウンドだけロードすればメモリ節約できる
void Sound::Load(std::string scene)
{
    start = Sound::LoadSoundMem("SE/start.ogg", scene, { "TitleScene" });
    assert(start != -1); // 読込失敗、ファイル名かフォルダ名が間違ってる
    // オプションで同時[8つ]爆発音再生を可能に(デフォルトは3)falseをtrueにするとBGM扱い
    bomb = Sound::LoadSoundMem("SE/bomb.wav",scene,{"TitleScene","PlayScene"},false, 8);
   
assert(bomb != -1); // 読込失敗、ファイル名かフォルダ名が間違ってる

    title = SetMusicFile("BGM/title.mp3");
    gameplay = SetMusicFile("BGM/gameplay.mp3");
    ending = SetMusicFile("BGM/ending.mp3");

}

// メモリ上の音声を再生する。
void Sound::Play(int handle, int PlayType)
{
    assert(handle != -1 && "プレイする音声がメモリにない?" != "");

    // 音量の更新
    UpdateVolume(handle);


    // メモリ上から音声再生
    PlaySoundMem(handle, PlayType);
}

// ディスク上の音楽を直接再生する(メモリ上に展開しない)
void Sound::PlayMusic(std::string file, int PlayType)
{
    // プレイ中のBGMを再指定しても初めから再生せず即時return
    if (file == playingMusic) return;

    playingMusic = file; // プレイ中のBGM名を設定

    DxLib::PlayMusic(file.c_str(), PlayType); //ディスクから直接再生(メモリにのせない)

    Sound::UpdateMusicVolume(); // // 音量の更新(プレイ【中】の音楽だから↑PlayMusicしてから)
}

// IDからディスク上の音楽を直接再生する(メモリ上に展開しない)
void Sound::PlayMusic(int musicId, int PlayType)
{
    if (musicId > -1) return;//音楽IDはマイナスのID番号を割り振ったはず

    std::string file = "";
    // 辞書から対応するファイル名を取得
    if (BGM_Dic.count(musicId) == 1)
        file = BGM_Dic[musicId];
    else if (SE_Dic.count(musicId) == 1)
        file = SE_Dic[musicId];

    if (file == "") return;//対応する音楽ファイルが辞書に未登録
    // ファイル名から音楽ファイルを再生
    Sound::PlayMusic(file, PlayType);
}

// ファイルを辞書登録しmusicIDで管理する(メモリにはのせずファイル名とIDだけ登録)
int Sound::SetMusicFile(std::string file, bool isSE)
{    // 既に辞書登録済なら即時return
    if (loadDic.count(file) == 1) return loadDic[file];

    // 新規に辞書にファイル登録
    loadDic[file] = musicId; //IDの辞書に登録
    if (isSE) SE_Dic[musicId] = file; //SEの辞書に登録
    else BGM_Dic[musicId] = file; //BGMの辞書に登録

    return musicId--; // 後ろに--つけるとカウントダウン前をreturn
}

// 音声ファイルごとのボリューム設定
void Sound::SetVolume(float VolumeParcent, int SoundHandle)
{
    if (SoundHandle == -1) return;

    if (SE_Dic.count(SoundHandle) == 1)
    {    // ハンドル番号がSEの辞書で見つかれば
        std::string file = SE_Dic[SoundHandle];
        volumeDic[file] = VolumeParcent;//個別音量を辞書登録
    }
    else if (BGM_Dic.count(SoundHandle) == 1)
    {    // ハンドル番号がBGMの辞書で見つかれば
        std::string file = BGM_Dic[SoundHandle];
        volumeDic[file] = VolumeParcent;//個別音量を辞書登録
    }

    // セットした後の音声には音量が反映されるが
    // すでに再生中の音量は変わらないので↓UpdateVolumeとセットで実行
}

// メモリ音源の音量の更新(再生中に減らせばフェードアウト)
int Sound::UpdateVolume(int SoundHandle)
{
    // 特に指定がなければすべての音量を更新
    if (SoundHandle == -1)
    {    // 辞書のfor文はキーとバリューがペアになってる
        int result = 0, resultAll = 0;
        for (const auto& kv : loadDic)
        {    // 辞書のキーがkv.first,バリューがkv.second
            int handle = kv.second;
            // 【★勉強】自分自身を呼出す奥の手
            if (handle != -1) //↓【再起呼出し】
                result = Sound::UpdateVolume(handle);
            if (result == -1)
                resultAll += -1; // 失敗した数ぶん-1がカウントされる
        }
        return resultAll; // 全音量更新完了【再起呼出しループで】
    }

    // 音源指定があるときはhandleを指定して音量を更新
    int handle = SoundHandle;

    int volume = 255;
    float volumeParcent = 100;
    std::string file = "";
    if (SE_Dic.count(handle) == 1)
    {    // サウンド番号がSE辞書で見つかれば
        file = SE_Dic[handle];
        volumeParcent = volumeSE; //音量をSEのベース音量に
    }
    else if (BGM_Dic.count(handle) == 1)
    {    // サウンド番号がBGM辞書で見つかれば
        file = BGM_Dic[handle];
        volumeParcent = volumeBGM; //音量をSEのベース音量に
    }

    if (volumeDic.count(file) == 1)
    {    // 音量辞書をファイル名で検索して
        float fileParcent = volumeDic[file];
        volumeParcent *= fileParcent / 100; //個別音量%割合反映
    }

    if (volumeParcent < 100) // 音量に%パーセントを掛ける
        volume = (int)(((float)volume) * volumeParcent / 100);
    // 音量反映
    int result = ChangeVolumeSoundMem(volume, handle);

    if (result == 0) // 音量変更成功
        return volume;
    else return result; // 音量変更失敗
}

// 再生中のディスク音楽の音量の更新
int Sound::UpdateMusicVolume()
{
    std::string file = playingMusic;

    int volume = 255;
    float volumeParcent = volumeBGM;
    if (volumeDic.count(file) == 1)
    {    // 音量辞書をファイル名で検索して
        float fileParcent = volumeDic[file];
        volumeParcent *= fileParcent / 100; //個別音量%割合反映
    }

    if (volumeParcent < 100) // 音量に%パーセントを掛ける
        volume = (int)(((float)volume) * volumeParcent / 100);

    // 音量反映
    int result = SetVolumeMusic(volume);

    if (result == 0) // 音量変更成功
        return volume;
    else return result; // 音量変更失敗
}

// 音声をメモリへ読込み番号GET or シーンで使わない場合はメモリから解放
int Sound::LoadSoundMem(const TCHAR *FileName, std::string scene, std::vector<std::string> useScenes, bool isBGM, int BufferNum)
{
    int result = -1;
    // 次のシーンで使う音声かの判別、useScenesリストにあれば使う判定
    bool use = std::find(useScenes.begin(), useScenes.end(), scene) != useScenes.end();
    if (use)
    {    // ロード済み辞書に指定したファイルがある(count=1)のとき
        if (loadDic.count(FileName) == 1 && loadDic[FileName] != -1)
        {    // ロード済みなら音声ハンドルを返す(ロード時間短縮)
            return loadDic[FileName];
        }

        // DXライブラリで音声をロード、BufferNumは同時再生可能数
        result = DxLib::LoadSoundMem(FileName, BufferNum);
        if (result != -1)
        {    // 辞書にサウンドハンドル番号を保管
            int handle = result; //ロード結果からハンドル番号取得
            loadDic[FileName] = handle;//ロード済辞書に登録
            if (isBGM) BGM_Dic[handle] = FileName;//BGMの辞書に登録
            else SE_Dic[handle] = FileName;//SEの辞書に登録
        }

        return result; // ロードした結果を返す
    } // ↓ここまでの↑returnに引っかからずif抜けてきたら【シーン利用しないサウンド】

    // 今回のシーンでは利用しないサウンドの時はメモリからサウンドを開放して節約
    if (loadDic.count(FileName) == 1 && loadDic[FileName] != -1)
    {    // メモリからサウンド開放 https://dxlib.xsrv.jp/function/dxfunc_sound.html#R8N8
        int handle = loadDic[FileName];
        result = DxLib::DeleteSoundMem(handle);
        assert(result != -1 && "音声メモリ開放失敗" != "");
        // 辞書からハンドルを削除
        if (SE_Dic.count(handle) == 1)
            SE_Dic.erase(handle);//SEの辞書から削除
        else if (BGM_Dic.count(handle) == 1)
            BGM_Dic.erase(handle);//BGMの辞書から削除
        loadDic.erase(FileName); // ID辞書からも索引削除
    }
    // 今回のシーンでは利用しないサウンドの時は-2を返す
    return -2; // サウンド読込み失敗の-1と区別するため-2を返します
}



TitleScene.hにシーンごとのサウンドのロードと音量調節機能を追加します。
タイトル画面で左右キーを押すと音量調整できるようになり上下キーでSE⇔BGM調整モードを切り替えられます。

#ifndef TITLESCENE_H_
#define TITLESCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"

#include "GameManager.h"
#include "SceneManager.h"

class TitleScene : public Scene
{
public:
    // 設定モード
    enum class SettingMode
    {
        BGM=0, // BGM設定モード
        SE, // SE設定モード

        NUM // 列挙数[enumの数を得る] https://qiita.com/k-satoda/items/2985e81216b3ee500956
    };
    SettingMode settingMode = SettingMode::BGM; // 現在の設定モード


    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
    
    // スタートボタンを押したら音声再生完了を待って画面移動
    bool isStartPushed = false;

    // テスト用のボス画像をタイトルでアニメで左右移動させる
    int x = 0; //staticじゃないintはここで=0で初期化できる
    int vx = 0;

    // コンストラクタ
    TitleScene() : Scene()
    {
        this->tag = "TitleScene";
    }

    // 初期化処理
    void Initialize() override
    {
        Image::Load("TitleScene"); //画像の読込みとメモリ開放
        Sound::Load("TitleScene"); //音声の読込みとメモリ開放
        
        Sound::PlayMusic(Sound::title); //タイトルBGMを再生
        isStartPushed = false; // ボタンのフラグをリセット

        // テスト用のボス画像をタイトルでアニメで左右移動させる
        x = 100; // Xの初期位置
        vx = 10; // ボスの初期速度
    }

    // 終了処理(大抵Initializeと同じリセットだが終了時だけやりたいリセットの仕方もあるかも)
    void Finalize() override
    {

    }

    // ボタン入力に応じた処理
    void HandleInput()
    {
        if (Input::GetButtonDown(PAD_INPUT_1))
        {    // ボタンを押したらスタート音声を再生
            Sound::Play(Sound::start);
            isStartPushed = true; // 画面遷移トリガーON
        }

        if (Input::GetButtonDown(PAD_INPUT_UP))
        {    //上を押すと設定モードを-1する
            int index = ((int)settingMode - 1);
            if (index < 0) //↓番号ループ 0→最大数-1
                index = ((int)SettingMode::NUM) - 1;
            settingMode = (SettingMode)index;
        }
        else if (Input::GetButtonDown(PAD_INPUT_DOWN))
        {    //下を押すと設定モードを+1する
            int index = ((int)settingMode + 1);
            if (index >= ((int)SettingMode::NUM))
                index = 0; //番号ループ
            settingMode = (SettingMode)index;
        }

        if (Input::GetButton(PAD_INPUT_RIGHT))
        {
            if (settingMode == SettingMode::BGM)
            {
                Sound::volumeBGM++;//音量をプラス
                if (Sound::volumeBGM > 100)
                    Sound::volumeBGM = 100;
            }
            else if (settingMode == SettingMode::SE)
            {
                Sound::volumeSE++;//音量をプラス
                if (Sound::volumeSE > 100)
                    Sound::volumeSE = 100;
                Sound::Play(Sound::bomb); //効果音を聞きながら調節
            }
            Sound::UpdateMusicVolume(); //再生中の音量の更新
        }
        else if (Input::GetButton(PAD_INPUT_LEFT))
        {
            if (settingMode == SettingMode::BGM)
            {
                Sound::volumeBGM--;//音量をマイナス
                if (Sound::volumeBGM < 0)
                    Sound::volumeBGM = 0;
            }
            else if (settingMode == SettingMode::SE)
            {
                Sound::volumeSE--;//音量をマイナス
                if (Sound::volumeSE <0)
                    Sound::volumeSE = 0;
                Sound::Play(Sound::bomb); //効果音を聞きながら調節
            }
            Sound::UpdateMusicVolume(); //再生中の音量の更新
        }

    }

    // 更新処理
    void Update() override
    {
        // スタートの効果音の再生中は遷移を待つ
        if (isStartPushed && CheckSoundMem(Sound::start) == 0)
        {    // スタート音声再生終了をトリガーとして画面遷移
            sm.LoadScene("PlayScene"); //シーン遷移
        }

        HandleInput(); // キー入力処理

        // テスト用のボス画像をタイトルでアニメで左右移動させる
        if (x < 0)
        {
            vx = 10; //速度の変更
        }
        else if (x > Screen::Width)
        {
            vx = -10; //画面端で移動方向反転
        }
        x += vx; // ボス画像のX位置の更新
    }

    // 描画処理
    void Draw() override
    {
        DrawString(0, 0, "TitleSceneです。ボタン押下でPlaySceneへ。", GetColor(255, 255, 255));
        if (sm.scoreMax >= 0)
        {
            DrawString(0, 200, "MAXスコア: " + sm.scoreMax, GetColor(255, 255, 255));
        }

        // 音量設定目盛りの文字列の生成
        std::string volumeBGM = " ";
        if (settingMode == SettingMode::BGM)
            volumeBGM = "⇒"; //[選択中]⇒BGM音量設定モード
        volumeBGM += "BGM音量設定:" + std::to_string((int)Sound::volumeBGM);
        int volBGM_NUM = (int)(Sound::volumeBGM / 10);
        if (volBGM_NUM != 10) volumeBGM += " ";
        for (int i = 0; i < volBGM_NUM; i++) volumeBGM += "■";//音量目盛り表示

        std::string volumeSE = " ";
        if (settingMode == SettingMode::SE)
            volumeSE = "⇒"; //[選択中]⇒BGM音量設定モード
        volumeSE += " SE音量設定:" + std::to_string((int)Sound::volumeSE);
        int volSE_NUM = (int)(Sound::volumeSE / 10);
        if (volSE_NUM != 10) volumeSE += " ";
        for (int i = 0; i < volSE_NUM; i++) volumeSE += "■";//音量目盛り表示
       
        // 音量設定の表示
        DrawString(0, Screen::Height - 60, volumeBGM.c_str(), GetColor(255, 255, 255));
        DrawString(0, Screen::Height - 30, volumeSE.c_str(), GetColor(255, 255, 255));




        // テスト用のボス画像をタイトルでアニメで左右移動させる
        DrawRotaGraphF((float)x, 200, 0.9f, 0, Image::bossImage, TRUE);
    }
};

#endif



Game.cppからSoundのロード処理を削除します(シーンごとのロードに変更)。

#include "Game.h"

#include "MyRandom.h"
#include "Sound.h"

void Game::Init()
{// Init処理
    MyRandom::Init(); // 乱数シードの初期化
    Sound::Load(); //音声の読込み

    // ゲーム開始して最初はタイトルシーンに遷移
    sm.LoadScene("TitleScene");
}

void Game::Update()
{// 更新処理

    Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない

    // シーンの更新処理
    sm.currentScene->Update();
}

void Game::Draw()
{// 描画処理
  
    // シーンの描画処理
    sm.currentScene->Draw();
}


PlayScene.hに画面ごとのサウンドのロード処理を追加しBGM再生をBGM番号経由に変更します。

#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"

#include "GameManager.h"
#include "SceneManager.h"

#include "Player.h"
#include "Boss.h"
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"

class PlayScene : public Scene
{
public:
    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

    int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる

    // コンストラクタ
    PlayScene() : Scene()
    {
        this->tag = "PlayScene";
    }

    // 初期化処理
    void Initialize() override
    {
        Image::Load("PlayScene"); //画像の読込みとメモリ開放
        Sound::Load("PlayScene"); //音声の読込みとメモリ開放
       
        Sound::PlayMusic(Sound::gameplay); //プレイ画面BGMを再生

        // ここでプレイヤの初期化などのプレイ開始時の処理
        gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
        // 敵の仮生成
        gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
    }

(中略)...................

    // 更新処理
    void Update() override
    {
       

        (中略)...................


        // ゲームオーバー判定と画面チェンジ処理
        bool isGameover = (gm.player == nullptr);
        if (!isGameover && gm.player->isDead) isGameover = true;
        if (isGameover)
        {
            Sound::PlayMusic(Sound::ending);
            sm.LoadScene("GameOverScene"); //シーン遷移
        }
           
    }

    // 描画処理
    void Draw() override
    {
        (中略)...................
    }
};

#endif



いかがでしょうか?これでタイトル画面で音量調整ができ、
画面が変わるタイミングでシーンで使わない音声のメモリを解放する処理を実現できました。
一度実行して試してみましょう。



CSVを読み込んで敵を生成するプログラム


以下のタイルマップエディタをダウンロードしてインストールしてください(寄付リンクをクリックしなければ無料)
・Tiledエディタ: https://qiita.com/muzudho1/items/ac3602a29f8536031fe4
以下のファイルをダウンロードしてください。
tile_id.png (マップに配置する敵のID配置用のタイルチップ)
tile_id.xcf (上記pngタイルチップのGIMPもとファイル。番号の隣に見やすいようにZako0などの画像をGIMPで書き込んでTiledエディタで配置すれば勘違いしにくい?)


[Tiledエディタを起動]して[新しいマップ]を作成します。


[新しいタイルセット]もしくは[アイコン]をクリックしてダウンロードしたtile_id.pngの画像を[参照]して、新しいタイルセットを作成します。


新しいタイルセットを[ファイル名をつけて保存]します。


tile_id.tsxというファイル名.ファイル形式で保存します。


idがずらりとならんだタイルセットができました。


Zako0や1や2..などのidをステージに配置します。


[ファイル]→[ファイルに名前をつけて保存]でタイルマップ形式(.tmx)で保存します。


[ファイル]→[名前をつけてエクスポート]でcsv形式(.csv)でstage.csvをDXのプロジェクトのMapフォルダにエクスポート保存します。


Tiledエディタで敵マップ(stage.csv)を作成したら、
MapフォルダをImageのフォルダを作ったときのように新規作成してその中に
stage1.csvを
Map/stage1.csvにcsv形式でエクスポートした前提条件でそれをロードするプログラムを作成していきます。

csvファイルの準備ができたらマップの読込みプログラムを作成します。

Map.hを新規作成してマップのCSVファイルの読込みとスクロールを作成しましょう。

#ifndef MAP_H_
#define MAP_H_

#include <assert.h> // 読込み失敗表示用
#include <memory> // 回し読みポインタをザコやボスの生成に使う
#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列に必要
#include <fstream> // ファイル読み出しifstreamに必要
#include <sstream> // 文字列ストリームに必要

#include "DxLib.h"
#include "Screen.h"
#include "GameManager.h"
#include "Zako0.h"
#include "Zako1.h"
#include "Zako2.h"
#include "Boss.h"

class Map
{
public:
    int CellSize = 32; // マップの1マスのピクセル数
    int Width = 0; // マップデータの横のマス数
    int Height = 0; // マップデータの縦のマス数
    
    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

    std::vector<std::vector<int>> enemyData;// 敵配置データ
   
    float position = 0; // マップ読出し位置のスクロール量

    // コンストラクタ
    // filePath : CSVファイルのパス
    // startPosition : 開始位置(デバッグやリスタート時に、面の途中から開始できるように)
    Map(std::string filePath, float startPosition=0)
    {
        position = startPosition;
        Load(filePath); // csvファイルをロード
    }

    //デストラクタ
    ~Map()
    {
        enemyData.clear();

    }

    // csvファイルの読み込み
    void Load(std::string filePath)
    {
        // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
        std::ifstream ifs_csv_file(filePath);

        std::string line; //1行単位でcsvファイルから文字列を読み込む

        Width = 0; //初期化
        int MaxWidth = 0; // 1行の数字の最大個数
        Height = 0; //初期化
        while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
        {
            std::vector<int> valuelist; // 1行の数字リスト
            std::istringstream linestream(line); // 各行の文字列ストリーム
            std::string splitted; // カンマで分割された文字列
            int widthCount = 0; //この行の幅をカウント
            while (std::getline(linestream, splitted, { ',' }))
            {
                std::istringstream ss; //文字列ストリームの初期化
                ss = std::istringstream(splitted); //文字列ストリーム
                int num; // 数字単体
                ss >> num; // 文字列ストリーム>>で数字へ変換
                valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
                widthCount++; //この行のカンマで区切られた数字の数をカウントアップ
            }
            // 1行の数字の数が記録を更新してMAXになるかチェック
            if (widthCount > MaxWidth) MaxWidth = widthCount; //暫定Max幅を更新

            // 数字1行分をvectorに追加
            if (valuelist.size() != 0) enemyData.emplace_back(valuelist);
            Height++; //マップの高さをカウントアップ
        }
        Width = MaxWidth; //マップ幅は一番数字の個数の多かった行に合わせる
        //↓読込んだCSVの幅と高さをチェック
        assert(Width > 0 &&"マップ読込み失敗ファイル名間違いでは?"!="");
        assert(Height > 0 &&"マップ読込み失敗ファイル名間違いでは?"!="");
    }

    // 画面スクロール(位置の更新)
    void Scroll(float delta)
    {
        // スクロールする前の、画面右端のセルの列番号(x)
        int prevRightCell = (int)(position + Screen::Width) / CellSize;

        position += delta; // スクロールする

        // スクロールした後の、画面右端のセルの列番号(x)
        int currentRightCell = (int)(position + Screen::Width) / CellSize;

        // スクロールした後の画面右端のセルがマップの範囲外に出ちゃったら何もしないで終了
        if (currentRightCell >= Width)
            return;

        // スクロールする前と後で、画面右端のセルが同じ場合は、何もしないで終了
        if (prevRightCell == currentRightCell)
            return;

        // 画面右端のセルの左端のx座標
        float x = currentRightCell * CellSize - position;

        // 上端のセルから下端のセルまで舐めて、敵データが配置されていたら、敵を生成する
        for (int cellY = 0; cellY < Height; cellY++)
        {
            float y = cellY * CellSize;

            //【★注意!】2次元vectorではxとyがC#と逆![行Y][列X]⇒行列の順と覚えよう
            int id = enemyData[cellY][currentRightCell];

            // 番号に応じて敵を生成する
            if (id == -1) continue; // -1は空白なので、何もしない
            else if (id == 0) gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3(x + 32, y + 32)));
            else if (id == 1) gm.enemies.emplace_back(std::make_shared<Zako1>(Vector3(x + 32, y + 32)));
            else if (id == 2) gm.enemies.emplace_back(std::make_shared<Zako2>(Vector3(x + 32, y + 32)));
            else if (id == 100) gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(x + 90, y + 32))); // ボスは横幅180pxだから90px右に生成
            else assert("まだ追加してない敵IDがマップCSVにありました。"=="");
        }
    }
};
#endif



GameManager.hにMapの回し読みポインタの参照リンクを追加します。

#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_

#include <memory>
#include <vector>

#include "Singleton.h"

class Map; //クラス宣言だけでインクルードしないので循環しない(前方宣言)
class Player;
class PlayerBullet;
class Enemy;
class EnemyBullet;
class Explosion;
class Item;

class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
    friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可

    std::shared_ptr<Map> map{ nullptr }; // マップ
    std::shared_ptr<Player> player{ nullptr }; // 自機
    std::list<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
    std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
    std::list<std::shared_ptr<EnemyBullet>> enemyBullets; // 敵弾のリスト
    std::list<std::shared_ptr<Explosion>> explosions; // 爆発エフェクトのリスト
    std::list<std::shared_ptr<Item>> items; // アイテムのリスト
    // 複数リスト↑list ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる


   (中略)..........

protected:
    GameManager() {}; // 外部からのインスタンス作成は禁止
    virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};

#endif



PlayScene.hにMapの初期化とメモリ参照クリアとスクロール処理を追加します。(シーン分けしていない人はGame.cppで)

#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"
#include "Map.h"

#include "GameManager.h"
#include "SceneManager.h"

#include "Player.h"
#include "Boss.h"
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"
#include "Item.h"

class PlayScene : public Scene
{
public:
    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

    int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる

    float scrollSpeed = 1.5f; // スクロール速度

    // コンストラクタ
    PlayScene() : Scene()
    {
        this->tag = "PlayScene";
    }

    // 初期化処理
    void Initialize() override
    {
(中略)......
        
        Sound::PlayMusic(Sound::gameplay); //プレイ画面BGMを再生

        // マップの初期化
        gm.map = std::make_shared<Map>("Map/stage1.csv");


        // ここでプレイヤの初期化などのプレイ開始時の処理
        gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
        // 敵の仮生成
        //gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
    }

    // 終了処理(大抵Initializeと同じリセットだが終了時だけやりたいリセットの仕方もあるかも)
    void Finalize() override
    {
        // ここにプレイ終了時のメモリのリセット処理(ゲームクリアやゲームオーバー時など)
        gm.map = nullptr; // マップのメモリからの解放
        gm.player = nullptr; // プレイヤのメモリからの解放
        gm.enemies.clear(); // 敵のメモリからの解放
        gm.playerBullets.clear(); // プレイヤの弾のメモリからの解放
        gm.enemyBullets.clear(); // 敵弾のメモリからの解放
        gm.explosions.clear(); // 爆発エフェクトのメモリからの解放
        gm.items.clear(); // アイテムのメモリからの解放

        // ゲーム終了時のスコアがMAXなら記録して終了
        if(score > sm.scoreMax) sm.scoreMax = score;
    }

    // 更新処理
    void Update() override
    {
        // ここにプレイ中の更新処理を持ってくる

        // マップデータの読出し位置のスクロール
        gm.map->Scroll(scrollSpeed);

        
        if (!gm.player->isDead) // 自機が死んでいなければ
            gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】

        (中略)....................
            
    }

    // 描画処理
    void Draw() override
    {
        (中略)....................
    }
};

#endif




さて、ここまでで敵が画面の右端から出て来たでしょうか?
出てこない場合は、大抵以下の原因が多いです。


サンプルのstage.csvを下記に置いておくのでうまくTileMapエディタで書き出せない人やとりあえずプレイしてみたい人はダウンロードしてMapフォルダにコピーしてプレイしてみましょう。
サンプルのstage1.csv


カスタムしたデータ構造DataCsv.hでデータ自身にLoadと幅高さ管理を任せる

さて、マップ読込みうまくいきましたか?
現状のコードは敵のみ読込みですが【地形やその他アイテムなどのデータが増えると面倒です】
いちいちMap.hにint stageWidth, int terrainWidth, int itemWidth...などと定義を増やしますか?
いちいちMap.hにLoadTerrain(),LoadItem()など長いロード処理を個別に書きますか?
そんなの僕は嫌だ!
という人はDataCsv.hを取り入れましょう。
データはデータ自身がLoad処理を持ち、幅Width高さHeightを持つ構造になればよいのです。

コードでは【[]演算子を独自定義】しています。
これはぜひ勉強してほしいテクニックです。
DataCsv型のデータは[]演算子を使わないとenemyData.Data[y][x]といったアクセスの仕方になってしまいます。
すると、DataCsvを導入するとすでに書いたコードすべて.Data[y][x]の書き足しが必要になっちゃいますよね(めんどくさい)。
[]演算子を間に噛ますだけで【既存のコードの書き直しがゼロ】になるのです!

どうでしょう?[]演算子の威力を実感できそうですか?

同じ目的で【関数名かぶらせテクニック】も使っています。
clear()の定義も同様です。enemyData.Data.clear()と書き直さずenemyData.clear()のままで済むのです。
size()の定義も同様です。enemyData.Data.size()と書き直さずenemyData.size()のままで済むのです。

どうでしょう?【関数名かぶらせテクニック】を実感できそうですか?(C#でも使えるテクニック)

DataCsv.hを新規作成します。

#ifndef DATACSV_H_
#define DATACSV_H_

#include <assert.h> // 読込み失敗表示用
#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列に必要
#include <fstream> // ファイル読み出しifstreamに必要
#include <sstream> // 文字列ストリームに必要

// CSVファイルを読込み幅や高さとデータ本体を保持するデータ型
struct DataCsv // ←structはC++ではほぼclassと同じ【違いはデフォルトがpublic】
{   // 読込んだデータファイルの情報
    int Width = 0; // csvファイルの表の幅
    int Height = 0;// csvファイルの表の高さ
    bool isInitialized = false; //[2重ロード対策]1度ロードしたらtrueにしてclear()されるまでロード抑止

    std::string FilePath { "" };
    std::vector<std::vector<int>> Data;// csvデータ

    
    // 初期化コンストラクタでファイル名を指定して初期化と同時にファイル読込
    DataCsv(std::string filePath = "") :FilePath{ filePath }
    {// csvファイルの読込み★【初期化と同時なのでファイルとデータ型が一心同体で使いやすい】
        if (FilePath != "") Load(FilePath); // ファイル読込み
    };
    virtual ~DataCsv()
    {// 仮想デストラクタ
        Data.clear();// 2次元配列データのお掃除
    };
    
    // ★スムーズに[][]でアクセスできるように[]演算子を独自定義する
    std::vector<int>& operator[](std::size_t index) { // ★ &参照にしないといちいちデータのコピーを返すので遅くなるよ
        return Data[index]; // 書き込み
    }
    std::vector<int> operator[](std::size_t index) const { // ★constは添え字[]読み取りの処理を定義
        return Data[index]; // 読み取り
    }


    std::size_t size()
    {   // size()関数の名前をvectorと被らせることで使う側はvectorインvectorのままのコードで使える
        return Data.size();
    }


    // データをクリアしてメモリを解放する
    virtual void clear()
    {   // データをクリアしてメモリを解放する
        Data.clear();// 2次元配列データのお掃除
        isInitialized = false; //ロード済みフラグをOFF
    }


    // csvファイルの読み込み
    virtual void Load(std::string filePath)
    {
        if (filePath == "" || isInitialized) return; //ファイル名がないもしくはロード済
        this->FilePath = filePath; // ファイル名を保管
        Data.clear(); //データを一旦クリア

        // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
        std::ifstream ifs_csv_file(filePath);

        std::string line; //1行単位でcsvファイルから文字列を読み込む

        int readWidth = 0; //読込みデータの幅
        int maxWidth = 0; // 1行の数字の最大個数
        int readHeight = 0; //初期化
        //↓2重while文でCSVファイルを読み取る
        while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
        {
            std::vector<int> valuelist; // 1行の数字リスト
            std::istringstream linestream(line); // 各行の文字列ストリーム
            std::string splitted; // カンマで分割された文字列
            int widthCount = 0; //この行の幅をカウント
            while (std::getline(linestream, splitted, { ',' }))
            {
                std::istringstream ss; //文字列ストリームの初期化
                ss = std::istringstream(splitted); //文字列ストリーム
                int num; // 数字単体
                ss >> num; // 文字列ストリーム>>で数字へ変換
                valuelist.emplace_back(num); // 数字を数字のリスト(valuelist)に追加
                ++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
            }
            // 1行の幅の数が記録を更新してMAXになるかチェック
            if (widthCount > maxWidth) maxWidth = widthCount; //暫定Max幅を更新

            // 1行分をvectorに追加
            if (valuelist.size() != 0) Data.emplace_back(valuelist);
            ++readHeight; //読み込んだ行(縦)の数カウントアップ
        }
        readWidth = maxWidth; //読み込んだ列(横)の数は一番数字の個数の多かった行に合わせる
        //↓読込んだCSVの幅と高さをチェック
        assert(readWidth > 0 && "CSV読込み失敗ファイル名間違いでは?" != "");
        assert(readHeight > 0 && "CSV読込み失敗ファイル名間違いでは?" != "");

        this->Width = readWidth; // 読込み成功したデータの幅を記録
        this->Height = readHeight; // 読込み成功したデータの高さを記録

        // コンストラクタで初期化でロードの場合とLoad関数経由で読む経路があるから2重ロード対策
        isInitialized = true; // 読込み初期化済みフラグをON

        return;
    }
};

#endif


Map.hで敵データを2次元vectorからDataCsvの使用に変更しましょう。

#ifndef MAP_H_
#define MAP_H_

#include <assert.h> // 失敗表示用
#include <memory> // 回し読みポインタをザコやボスの生成に使う
#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列に必要
#include <fstream> // ファイル読み出しifstreamに必要
#include <sstream> // 文字列ストリームに必要


#include "DxLib.h"
#include "Screen.h"
#include "GameManager.h"
#include "Zako0.h"
#include "Zako1.h"
#include "Zako2.h"
#include "Boss.h"

#include "DataCsv.h"

class Map
{
public:
    int CellSize = 32; // マップの1マスのピクセル数
    int Width = 0; // マップデータの横のマス数
    int Height = 0; // マップデータの縦のマス数

    
    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

    DataCsv enemyData;// 敵配置データ
    float position; // 現在の位置(画面左端の座標)

    // コンストラクタ
    // filePath : CSVファイルのパス
    // startPosition : 開始位置(デバッグやリスタート時に、面の途中から開始できるように)
    Map(std::string filePath, float startPosition=0)
    {
        position = startPosition;
        enemyData.Load(filePath); // csvファイルをロード
    }

    //デストラクタ
    ~Map()
    {
        enemyData.clear();
    }

    // csvファイルの読み込み
    void Load(std::string filePath)
    {
        // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
        std::ifstream ifs_csv_file(filePath);

        std::string line; //1行単位でcsvファイルから文字列を読み込む

        Width = 0; //初期化
        int MaxWidth = 0; // 1行の数字の最大個数
        Height = 0; //初期化
        while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
        {
            std::vector<int> valuelist; // 1行の数字リスト
            std::istringstream linestream(line); // 各行の文字列ストリーム
            std::string splitted; // カンマで分割された文字列
            int widthCount = 0; //この行の幅をカウント
            while (std::getline(linestream, splitted, { ',' }))
            {
                std::istringstream ss; //文字列ストリームの初期化
                ss = std::istringstream(splitted); //文字列ストリーム
                int num; // 数字単体
                ss >> num; // 文字列ストリーム>>で数字へ変換
                valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
                widthCount++; //この行のカンマで区切られた数字の数をカウントアップ
            }
            // 1行の数字の数が記録を更新してMAXになるかチェック
            if (widthCount > MaxWidth) MaxWidth = widthCount; //暫定Max幅を更新

            // 数字1行分をvectorに追加
            if (valuelist.size() != 0) enemyData.emplace_back(valuelist);
            Height++; //マップの高さをカウントアップ
        }
        Width = MaxWidth; //マップ幅は一番数字の個数の多かった行に合わせる
        //↓読込んだマップの幅と高さをチェック
        assert(Width > 0 &&"マップ読込み失敗ファイル名間違いでは?"!="");
        assert(Height > 0 &&"マップ読込み失敗ファイル名間違いでは?"!="");
    }


    // 画面スクロール(位置の更新)
    void Scroll(float delta)
    {
        // スクロールする前の、画面右端のセルの列番号(x)
        int prevRightCell = (int)(position + Screen::Width) / CellSize;

        position += delta; // スクロールする

        // スクロールした後の、画面右端のセルの列番号(x)
        int currentRightCell = (int)(position + Screen::Width) / CellSize;

        // スクロールした後の画面右端のセルがマップの範囲外に出ちゃったら何もしないで終了
        if (currentRightCell >= enemyData.Width)
            return;

        // スクロールする前と後で、画面右端のセルが同じ場合は、何もしないで終了
        if (prevRightCell == currentRightCell)
            return;

        // 画面右端のセルの左端のx座標
        float x = currentRightCell * CellSize - position;

        // 上端のセルから下端のセルまで舐めて、敵データが配置されていたら、敵を生成する
        for (int cellY = 0; cellY < enemyData.Height; cellY++)
        {
            float y = cellY * CellSize;

            //【★注意!】2次元vectorではxとyがC#と逆![行Y][列X]⇒行列の順と覚えよう
            int id = enemyData[cellY][currentRightCell];

            // 番号に応じて敵を生成する
            if (id == -1) continue; // -1は空白なので、何もしない
            else if (id == 0) gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3(x + 32, y + 32)));
            else if (id == 1) gm.enemies.emplace_back(std::make_shared<Zako1>(Vector3(x + 32, y + 32)));
            else if (id == 2) gm.enemies.emplace_back(std::make_shared<Zako2>(Vector3(x + 32, y + 32)));
            else if (id == 100) gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(x + 90, y + 32))); // ボスは横幅180pxだから90px右に生成
            else assert("まだ追加してない敵IDがマップCSVにありました。"=="");
        }
    }
};
#endif




さて、できるだけ最小限の変更でDataCsvを導入することができたのが実感できましたか?
これで地形データやアイテムデータなどが増えてきてもすっきりするはずです。
一度実行してみて、無事に敵が出てくるかは確認してみましょう。



【3D向け】【高難度】敵データの全方位読出しアルゴリズム

さて現状のコードでは縦スクロールの一方向スクロール限定です。
全方向進めるようなRPGや3Dゲームなどには今のままでは使いづらいです。
シンプルな解決法としては開始時に全敵キャラを読み込むのも一つの手です。
ある程度敵の数が限られるゲームであれば有効な策です。
しかし広大なマップや大量の敵の出るゲームでやると
【大量の画面にいない敵も一気に生成してしまうので、大量の当たり判定計算などにより計算が追いつかなくなります】
ゆえにある程度の敵の規模のあるゲームのためには、キャラの移動にあわせて順次【敵マップの全方向読み出し】が必要です。
敵の読出しのアルゴリズムについては以下スライドを基本的な考え方としています。

CellXY.hを新規作成してマス目の型を定義して、std::unordered_map辞書配列のキーにできるように独自operatorを定義しましょう。

#ifndef CELLXY_H_
#define CELLXY_H_

#include <unordered_map> // ハッシュ値計算 std::hashに必要

#include <cmath>
#include <algorithm> // std::maxに必要


// マス目のXとYを保持するデータ型(std::mapやstd::unordered_mapのキーにするためのoperator定義を持つ)
struct CellXY
{
    int X = 0, Y = 0, dist = 0; bool distModeOn = false;
    CellXY() = default;

    ~CellXY() {};

    CellXY(int X, int Y, bool distModeOn = false) : X{ X }, Y{ Y }, distModeOn{ distModeOn }
    {
        if (distModeOn == true) dist = X * X + Y * Y; // 原点(0,0)からの距離比較機能オンの時
    };

    // ★連想配列mapのキーで使うため < 比較演算子(連想配列並べ替えのため)をカスタム定義 http://programming-tips.info/use_struct_as_key_of_map/cpp/index.html
    virtual bool operator < (const CellXY& other) const {
        //★原点からの距離dist順になるようにするとstd::mapにいれれば自動で原点からの距離順にならぶ(敵の近い順に使える)
        if (this->distModeOn && other.distModeOn) return this->dist < other.dist;
        if (this->Y < other.Y) return true; // Yを比較CSVのように左上から列→行の順にならぶ
        else if (this->Y > other.Y) return false;
        if (this->X < other.X) return true; // Yが同じ時はXを比較
        else if (this->X > other.X) return false;
        return false; // X と Yがすべて等しい場合
    }

    //★std::unordered_map のキーで使うため == 比較演算子カスタム定義 https://sygh.hatenadiary.jp/entry/2014/02/13/222356
    virtual inline bool operator==(const CellXY& other) const {
        const CellXY& self = *this;
        return self.X == other.X && self.Y == other.Y;
    }

    virtual inline bool operator!=(const CellXY& other) const {
        return !(this->operator==(other));
    }

    virtual size_t GetHashCode() const //X,Yの値からハッシュ値を計算 https://sygh.hatenadiary.jp/entry/2014/02/13/222356
    {
        return
            std::hash<decltype(X)>()(this->X) ^
            std::hash<decltype(Y)>()(this->Y);
    }

    struct Hash // ハッシュ値取得用の関数オブジェクト(std::unordered_map のキーで使うため) https://sygh.hatenadiary.jp/entry/2014/02/13/222356
    {
        size_t operator ()(const CellXY& cellXY) const
        {
            return cellXY.GetHashCode();
        }
    };
};

#endif


Map.hで敵データを全方位読み出しできるように変更しましょう。

#ifndef MAP_H_
#define MAP_H_

#include <assert.h> // 読込み失敗表示用
#include <memory> // 回し読みポインタをザコやボスの生成に使う
#include <string> // 文字列に必要
#include <cmath> // 敵出現楕円射程のsin cos計算に

#include "DxLib.h"
#include "Screen.h"
#include "GameManager.h"
#include "Zako0.h"
#include "Zako1.h"
#include "Zako2.h"
#include "Boss.h"

#include "DataCsv.h"

#include "CellXY.h"

// 空間をグリッドでマス目として分割する(世界位置worldXをマス目位置番号cellXに変換したりできる)
class MapGridXY
{
public:
    int cellWidth = 32; // マス目セルのサイズ
    int cellHeight = 32; // マス目セルのサイズ

    // CellXYZをキーにそのマス目にあるboolを得る辞書配列
    std::unordered_map<CellXY, bool, CellXY::Hash> cellXY; // 読み込み済マス目の辞書
    std::unordered_map<CellXY, bool, CellXY::Hash> rangeXY; // 視界レンジは読み込み済cellXYと別に保管

    virtual int cellX(float worldX)
    {    // マイナス方向にも対応
        return (worldX >= 0) ? (int)worldX / cellWidth : (int)worldX / cellWidth - 1;
    }

    virtual int cellY(float worldY)
    {    // マイナス方向にも対応
        return (worldY >= 0) ? (int)worldY / cellHeight : (int)worldY / cellHeight - 1;
    }

    bool& operator[](CellXY searchXYZ) { // マス目に入っている先頭の参照を得る
        return cellXY[searchXYZ];
    }

    virtual void clear()
    {    // 空のテンポラリオブジェクトでリセット https://stackoverflow.com/questions/42114044/how-to-release-unordered-map-memory/42115076
        std::unordered_map<CellXY, bool, CellXY::Hash>().swap(cellXY);
    };

    MapGridXY(int cellWidth = 32, int cellHeight = 32)
        : cellWidth{ cellWidth }, cellHeight{ cellHeight }
    {
        clear();
    }
    virtual ~MapGridXY()
    {
        clear();
    }
};



class Map
{
public:
    int CellSize = 32; // マップの1マスのピクセル数
    
    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

    int spawnRangeX = 0; // 敵出現射程スポーンレンジ【注意!Xの単位はマス目】
    int spawnRangeY = 0; // 敵出現射程スポーンレンジ【注意!Yの単位はマス目】
    MapGridXY spawnGrid{ CellSize, CellSize }; // 敵データの読み込み済のマス目を管理するグリッド


    DataCsv enemyData;// 敵配置データ
   
    float positionX = 0; // X位置(マップ読出し位置のXスクロール量)
    float positionY = 0; // Y位置(マップ読出し位置のYスクロール量)


    // コンストラクタ
    // filePath : CSVファイルのパス
    // startPosition : 開始位置(デバッグやリスタート時に、面の途中から開始できるように)
    Map(std::string filePath, float startPositionX = 0, float startPositionY = 0)
    {
        positionX = startPositionX;
        positionX = startPositionY;

        enemyData.Load(filePath); // csvファイルをロード
       
        spawnRangeX = Screen::Width / CellSize; // 敵出現射程スポーンレンジ【注意!Xの単位はマス目】
        spawnRangeY = Screen::Height / CellSize; // 敵出現射程スポーンレンジ【注意!Yの単位はマス目】
       
        //【注意!】辞書の初期化をわすれると敵が出てこなくなる
        InitSpawnGrid(spawnRangeX, spawnRangeY);

    }

    //デストラクタ
    ~Map()
    {
        enemyData.clear();
    }

    // 敵出現射程の辞書初期化【先に計算して辞書化】すると計算が必要なくなり【高速化する】
    //【注意!XとYの単位はマス目】
    void InitSpawnGrid(int rangeCellX, int rangeCellY)
    {
        spawnGrid.clear();//一旦辞書をクリアするのでゲーム中の再設定も可(だが処理時間はかかる)
        // 敵出現射程の辞書初期化
        // ★ X = A cosθ Y = B sinθ(←楕円の方程式)
        // ★ 楕円の半径 r = √(A×A×cosθ×cosθ + B×B×sinθ×sinθ)
        // ★ xの2乗 + yの2乗 < rならば楕円の内側
        float A2 = (float)(rangeCellX * rangeCellX); // 2乗
        float B2 = (float)(rangeCellY * rangeCellY); // 2乗
        for (int x = -rangeCellX; x <= rangeCellX; x++)
        {
            for (int y = -rangeCellY; y <= rangeCellY; y++)
            {   //★[逆三角関数] https://cpprefjp.github.io/reference/cmath/atan2.html
                //float theta = (float)std::atan2(y, x); // ★[逆三角関数]
                //float cos_t = std::cos(theta);
                //float sin_t = std::sin(theta);
                //float r2 = A2 * cos_t * cos_t + B2 * sin_t * sin_t;
                //if (x * x + y * y <= r2) // ★ xの2乗 + yの2乗 < rならば楕円の内側
                //{   //楕円の内側なら辞書spawnGrid.rangeXY[(x,y)] = true;として登録
                //    //【★楕円にしたいときはこちら】spawnGrid.rangeXY[CellXY(x,y)] = true; //【例】spawnGrid.rangeXY[(3,2)] = true;
                //}
                //【★四角形にしたいときはこちら】
                spawnGrid.rangeXY[CellXY(x, y)] = true;
            }
        }
    }



    // 画面スクロール(位置の更新)
    void Scroll(float deltaX, float deltaY=0)
    {
        int diffX = spawnGrid.cellX(positionX + deltaX) - spawnGrid.cellX(positionX);
        int diffY = spawnGrid.cellY(positionY + deltaY) - spawnGrid.cellY(positionY);

        float prevPosX = positionX; // デルタの変化を足す前の位置を保管する
        float prevPosY = positionY;

        positionX += deltaX; // 現在の位置を更新
        positionY += deltaY; // 現在の位置を更新

        if (diffX == 0 && diffY == 0)
           return; // 同じマス目のままなら何も変化なし

        // 読み込むマス目の絞り込み判定 (参考:説明スライド)
        std::vector<CellXY> targetCells;
        for (auto iter = spawnGrid.rangeXY.begin(); iter != spawnGrid.rangeXY.end(); ++iter)
        {  // ★連想辞書配列のfor文 https://qiita.com/_EnumHack/items/f462042ec99a31881a81
           CellXY cell = iter->first;
           CellXY target = CellXY(cell.X + diffX, cell.Y + diffY);
           if (spawnGrid.rangeXY.count(target) == 0)
              targetCells.emplace_back(target); // 移動後にレンジの外側にはみ出たマス目を収集
        }

        // 敵新規出現マスを探す
        for (auto& target : targetCells)
        {
           CellXY cell = CellXY(spawnGrid.cellX(prevPosX) + target.X, spawnGrid.cellY(prevPosY) + target.Y);

           // [判定] count(cell) == 0なら未ロードマス、count(cell) == 1 && cellXY[cell] == falseならロード後falseに戻されたマス
           if (spawnGrid.cellXY.count(cell) == 0 || spawnGrid.cellXY.count(cell) == 1 && spawnGrid.cellXY[cell] == false)
           {
              int objectID = -1;//出現させる敵ID
              if (0 <= cell.Y && 0 <= cell.X && cell.Y < enemyData.size() && cell.X < enemyData[cell.Y].size())
              {   //データがアクセスできればIDを取得
                 objectID = enemyData[cell.Y][cell.X];
                 // ★一度出現させたマスはIDを上書きして二重に敵が出現しないように
                 enemyData[cell.Y][cell.X] = -1;
              }

              // ★(cell.X,cell.Y)の位置のマップデータを読み出し!!
              SpawnObject(cell.X, cell.Y, objectID);
           }
        }

    }


    // オブジェクトを生成・配置する
    void SpawnObject(int mapX, int mapY, int id)
    {
        // 生成位置
        float x = (float)(mapX * CellSize) - positionX;
        float y = (float)(mapY * CellSize) - positionY;

        //[コメント外すとテスト表示]if (objectID != -1)printfDx("newID %d (%d,%d,%f,%f)\n", id, mapX, mapY, x, y);

        // 番号に応じて敵を生成する
        if (id == -1) return; // -1は空白なので、何もしない
        else if (id == 0) gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3(x + 32, y + 32)));
        else if (id == 1) gm.enemies.emplace_back(std::make_shared<Zako1>(Vector3(x + 32, y + 32)));
        else if (id == 2) gm.enemies.emplace_back(std::make_shared<Zako2>(Vector3(x + 32, y + 32)));
        else if (id == 100) gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(x + 90, y + 32))); // ボスは横幅180pxだから90px右に生成
        else assert("まだ追加してない敵IDがマップCSVにありました。"=="");
    }

};
#endif



うまくいけば、これで全方向のスクロールが可能です。
Game.cppやPlayScene.hでmap->Scroll(ScrollSpeedX, ScrollSpeedY);でY方向のスクロールもできるようになりました。
今回はシューティングなのでY方向はデフォルトの0のままで大丈夫です。



自機狙い弾

点と点を結んだ線の角度を調べる関数

インクルードするのは#include <cmath>になります。cmathはスタンダード(STandarD)ライブラリ所属なので
つかうときはstd::cos()とかstd::atan2()になります。(stdはスタンダードの略)

MyMath.hに逆三角関数でプレイヤへの角度を求める関数を追加しましょう。

#ifndef _MYMATH_H
#define _MYMATH_H

#include <cmath>
#include "Vector3.h"

// 数学関連クラス
class MyMath
{
public:
  // C++標準では、静的static定数は【整数型】または【列挙型】のみを.hのクラス内で初期化できます。
  // これが、intやenumの初期化はエラーにならないのに他のstatic floatなどがエラーになる理由です。

    // float小数を定義するためだけにcppファイルを作成しないと
    //static const float Sqrt2 =【このイコールでエラー】 1.41421356237f;

    // ルート2(変数名の定義だけ)
    static const float Sqrt2;// = 1.41421356237f;
    static const float PI; // 円周率 3.14159265359f;
    static const float Deg2Rad; // 度からラジアンに変換する定数 PI / 180f;

    /// <summary>
    /// 円と円が重なっているかを調べる
    /// </summary>
    /// <param name="pos1">円1の中心</param>
    /// <param name="radius1">円1の半径</param>
    /// <param name="pos2">円2の中心</param>
    /// <param name="radius2">円2の半径</param>
    /// <returns>重なっていればtrue、重なっていなければfalseを返却する</returns>
    static bool CircleCircleIntersection(
        Vector3 pos1, float radius1,
        Vector3 pos2, float radius2)
    {
        return ((pos1 - pos2).sqrMagnitude()
            < (radius1 + radius2) * (radius1 + radius2));
    }

    /// <summary>
    /// 点から点への角度(ラジアン)を求める。
    /// </summary>
    /// <param name="fromXY">始点</param>
    /// <param name="toXY">終点</param>
    /// <returns></returns>
    static float PointToPointAngle(Vector3 fromXY, Vector3 toXY)
    {
        return (float)std::atan2(toXY.y - fromXY.y, toXY.x - fromXY.x);
    }


}; // 【注意】セミコロン抜けで【宣言が必要ですエラー】

#endif // 【宣言が必要ですエラーは上のセミコロン抜け】



自機狙い弾を撃たせる

Zako0を改造して自機狙い弾を撃たせてみます(別に他の敵クラスでも構いません)。
ついでにZako0の画面外に出たら消す処理も入れましょう【敵が消えないとCPUが当たり判定で激重くなりますよ】
これを機にZako1,Zako2,Zako3の画面外で消す判定も追加しておいた方がいいですよ。
敵がステージに100体出て(撃ち漏らしたら)プレイヤが10発弾を撃つとすると【100×10=1000回の当たり判定】になりますから。

#ifndef ZAKO_0_H_
#define ZAKO_0_H_

#include "Enemy.h"
#include "Image.h"
#include "MyMath.h"
#include "EnemyBullet.h" // make_sharedなど【実際の弾を撃ち生成処理を行うのでインクルード必要】

#include "Player.h" // Player.hでもZako0.hをインクルードすると循環するので注意

// ザコ0クラス
class Zako0 : public Enemy
{
public:
    int coolTime = 0; // クールタイム(冷却時間。0になるまで次の弾が撃てない)

    // コンストラクタ
    Zako0(Vector3 pos) : Enemy( pos )
    {
        this->tag = "Zako0";
    }

    // 更新処理
    void Update() override
    {
        position.x -= 1; // とりあえず左へ移動

        coolTime--;

        if (coolTime <= 0)
        {
            // 自機狙い弾
            float angle = MyMath::PointToPointAngle(position, gm.player->position);

            gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, angle, 8));
            coolTime += 10;
        }

        // 【重要】画面外の削除処理【CPU処理重くなる原因】
        if (position.x < 0)
        {    // 左端の画面外に出たらisDeadしないと
            // 画面外の敵の当たり判定で【だんだん処理が重くなるZako1,Zako2..も】
            isDead = true;
        }

    }

    // 描画処理
    void Draw() override
    {
        DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako0, TRUE);
    }
};

#endif


ばらまき自機狙い弾を撃たせる

Zako2を改造してばらまき自機狙い弾を撃たせてみます(別に他の敵クラスでも構いません)。

#ifndef ZAKO_2_H_
#define ZAKO_2_H_

#include "Enemy.h"

#include <cmath> // Sin Cosの計算に使う

#include "Image.h"
#include "Screen.h"
#include "MyMath.h"
#include "MyRandom.h"
#include "EnemyBullet.h" // make_sharedなど【実際の弾を撃ち生成処理を行うのでインクルード必要】

#include "Player.h" // Player.hでもZako2.hをインクルードすると循環するので注意

// ザコ2クラス
class Zako2 : public Enemy
{
public:
    int coolTime = 0; // クールタイム(冷却時間。0になるまで次の弾が撃てない)

    // コンストラクタ
    Zako2(Vector3 pos) : Enemy( pos )
    {
        this->tag = "Zako2";
    }

    // 更新処理
    void Update() override
    {
        if (position.x > Screen::Width - 200)
        { // スクリーンの端から200の位置で止まる
            position.x -= 1; // とりあえず左へ移動
        }

        coolTime--;

        if (coolTime <= 0)
        {
            // 自機狙いばらまき弾
            float angle = MyMath::PointToPointAngle(position, gm.player->position);
            float randomAngle = MyRandom::Range(-15.0f, 15.0f); // ±15度の乱数の角度を取得

            gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, angle + randomAngle * MyMath::Deg2Rad, 8));
            coolTime += 10;
        }

        // 200の位置で止まるので左端にこないから削除処理はなくて済む
    }

    // 描画処理
    void Draw() override
    {
        DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako2, TRUE);
    }
};

#endif



さて、ザコ2の場合は画面内の200で止まるので削除処理を入れませんでした。
そう、削除処理も敵により臨機応変にやるのですね。例えば下から出て上に向かう敵はyが0以下で削除です。
つまり、削除処理も敵の個性の一つの扱いというわけです。


自機に向かってくる敵

自機に向かってくる敵も基本は追尾弾と同じですね。

Zako1.hにプレイヤへ向かってくる処理を追加しましょう。

#ifndef ZAKO_1_H_
#define ZAKO_1_H_

#include "Enemy.h"

#include <cmath> // Sinの計算に使う
#include "MyMath.h"
#include "Image.h"

#include "MyRandom.h" // 乱数でアイテムのドロップを抽選する
#include "Item0.h"

#include "Player.h" // Player.hでもZako1.hをインクルードすると循環するので注意

// ザコ1クラス
class Zako1 : public Enemy
{
public:

    // コンストラクタ
    Zako1(Vector3 pos) : Enemy( pos )
    {
        this->tag = "Zako1";//オブジェクトの種類判別タグ
        life = 5;
    }

    // 更新処理
    void Update() override
    {
        // 自機をおいかけてくる処理
        float angleToPlayer = MyMath::PointToPointAngle(position, gm.player->position);
        float speed = 3;
        position += Vector3((float)std::cos(angleToPlayer) * speed, (float)std::sin(angleToPlayer) * speed);

    }

    // 描画処理
    void Draw() override
    {
        DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako1, TRUE);
    }

    // アイテムを落とす処理
    void DropItem() override
    {
        (中略).......
    }
};

#endif




ゆらゆらしながら【自機に】向かってくる敵にしてみましょうか。

Zako1.hにSin波動でゆらゆら【自機に向かってくる】処理を追加しましょう。

#ifndef ZAKO_1_H_
#define ZAKO_1_H_

#include "Enemy.h"

#include <cmath> // Sinの計算に使う
#include "MyMath.h"
#include "Image.h"

#include "MyRandom.h" // 乱数でアイテムのドロップを抽選する
#include "Item0.h"

#include "Player.h" // Player.hでもZako1.hをインクルードすると循環するので注意

// ザコ1クラス
class Zako1 : public Enemy
{
public:
    float sinMove = 0; // Sinの動きをする際の0~360度
    float sinRadius = 70; // Sinの動きをするときの半径


    // コンストラクタ
    Zako1(Vector3 pos) : Enemy( pos )
    {
        this->tag = "Zako1";//オブジェクトの種類判別タグ
        life = 5;
    }

    // 更新処理
    void Update() override
    {
        // 自機をおいかけてくる処理
        float angleToPlayer = MyMath::PointToPointAngle(position, gm.player->position);
        float speed = 3;
        position += Vector3((float)std::cos(angleToPlayer) * speed, (float)std::sin(angleToPlayer) * speed);

        //ゆらゆらSin波動の動きをy上下方向にプラス
        float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
        sinMove += 2.5f; // Sinの波運動の角度を進める
        float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;

        // (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
        position.y += currentSinY - prevSinY; // 上下方向のSinの波運動

    }

    // 描画処理
    void Draw() override
    {
        DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako1, TRUE);
    }

    // アイテムを落とす処理
    void DropItem() override
    {
        (中略).......
    }
};

#endif


ゆらゆら向かってくるほうが理論上は避けやすいはずなのに、見た目上は逆に怖く見えませんか?


あれ?何か2回、2種類の y += 〜;やってるけどアリなの?と思った人。
今から数字のマジックを示すので実感してください。
毎回 y += 〜;されるということは、つまり

y = (今の角度のY)-(前の角度のY) + (前の角度のY)-(前の前の角度のY) + (前の前の角度のY)-(前の前の前の角度のY)...

となりますよね。

そしてここで数字のマジックを見てみましょう

y = (今の角度のY) -(前の角度のY) + (前の角度のY) -(前の前の角度のY) + (前の前の角度のY) - (前の前の前の角度のY)........
y = (今の角度のY) - (一番最初の角度のY=0)←sin 0°=0だから0

おお!見事に打ち消しあって(今の角度のY)だけ残りましたね(一番最初の角度0°のYは0になるので)
じゃあ、最初からy += せずにy = (今の角度のY);にすればいいんじゃ?って思う人もいると思います。
そうするとどうなるか?2つの違う動きの合成が難しくなります。

y += 1; // 【メイン】の動き(少しづつ下に行く)
y = (今の角度のY); // あ、上書き固定値で y += 1;の効果消えちゃう..

なので【動きを += ~;で表現しつつも毎回打ち消しあう】必要が出てくるのです。

y += 1; // 【メイン】の動き(少しづつ下に行く)
y += (今の角度のY) - (前の角度のY); // これなら y += 1;の効果が消えない!

これなら色々な動きを【合成した動きなのに】【お互いの動きを消す心配】がなくなります。

これが±プラマイ0で合成テクニック⇒微分•積分的テクニックです。
え、微分積分て難しい式なんじゃなかったっけ?って思うでしょうが
これもすこしトリッキーですが積分的な式です。(離散数学的に n と n-1 の漸化式を使っている感じ?)
積分は【ちりが積もれば山となる式】です。
で、この場合【「ちり」が微分です】(1フレームごとの値、それを0~60フレーム1秒間ぶん足し合わせるとの移動量が積分されて求まる)
今回の例では【yが山です】
そして【「ちり」が(今の角度のY) - (前の角度のY)です】
これを【(今の角度のsin) - (前の角度のsin)の積分がsinθになった】
逆に言うと【sinθを微分すると(今の角度のsin) - (前の角度のsin)になった】とも言えます。
式で書くとy += 半径×sinθ1(←今の角度のY) - 半径×sinθ2(←前の角度のY)となってます。

どうでしょう意外と微分積分を身近に感じられませんか?
数学の難しい式、実際使う場合は意外とシンプルなやり方になったりします。
この【今と前の差】を+= するテクニックはSin以外でも使えるので心に留めておきましょう。
たとえばxも同じようにすれば、円を描きながら追ってくる敵も作れます。
x += 半径×cosθ1(←今の角度のX) - 半径×cosθ2(←前の角度のX)
気になった人は試してみてくださいね。

追加でcosMove,cosRadiusを定義して..

class Zako1 : public Enemy
{
public:
    float sinMove = 0; // Sinの動きをする際の0~360度
    float sinRadius = 70; // Sinの動きをするときの半径

    float cosMove = 0; // cosの動きをする際の0~360度
    float cosRadius = 70; // cosの動きをするときの半径

Update()関数内にX方向の動きも追加すると..

      //ゆらゆらCos波動の動きをx左右方向にプラス
      float prevCosX = std::cos(cosMove * MyMath::Deg2Rad) * cosRadius;
      cosMove += 2.5f; // Cosの波運動の角度を進める
      float currentCosX = std::cos(cosMove * MyMath::Deg2Rad) * cosRadius;

      // (前の角度でのX) - (今の角度でのX)の差を足すことで【ベースのxの位置 ± cosの動き】になる
      position.x += currentCosX - prevCosX; // 上下方向のSinの波運動

[C++]シューティングゲーム 5【応用1】


  1. 複数コントローラでプレイ
    Inputクラスの複数プレイ対応
    複数プレイヤの参加処理
    [DXのバグ!?仕様?]複数コントローラは4以降があやしい件

  2. 背景画像を作ってループ表示させてみる
  3. スコアを画面表示させてみる
  4. フォントを変えてみる
  5. スコアの加算と背景の加速
  6. スコアのランキング表示
  7. スコアのセーブ保存とCSVのセルを受けint,float,文字列に柔軟に化けるCsvValue型
  8. キーボードとマウスの入力
  9. ゲームクリア画面への遷移
  10. 【課題】シューティングとして完成させるための残された課題



複数コントローラでプレイ

Inputクラスの複数プレイ対応

複数コントローラーでプレイするためにはまずはInputクラスの改造が必要です。

Input.hのボタン状態を配列にしてゆきましょう。

#ifndef INPUT_H_
#define INPUT_H_

#include "DxLib.h"

// コントローラのパッド番号の列挙型enum
enum class Pad
{
    None=-1, // パッド割当て無(やられたり待機中のプレイヤに)
    Key=0, // キーボード
    One, // コントローラ1
    Two, // コントローラ2
    Three, // コントローラ3
    Four, // コントローラ4
    NUM, // コントローラの数NUM=5(ちょうど配列の数にもなる)
};


// 入力クラス
class Input
{
public:
    static int prevStates[(int)Pad::NUM]; // 1フレーム前の状態
    static int currentStates[(int)Pad::NUM]; // 現在の状態

    // 初期化。タイトルなどキー入力状態リセット時に
    static void Init()
    {
        // キー状態をゼロリセット
        prevStates[(int)Pad::Key] = currentStates[(int)Pad::Key] = 0;
        prevStates[(int)Pad::One] = currentStates[(int)Pad::One] = 0;
        prevStates[(int)Pad::Two] = currentStates[(int)Pad::Two] = 0;
        prevStates[(int)Pad::Three] = currentStates[(int)Pad::Three] = 0;
        prevStates[(int)Pad::Four] = currentStates[(int)Pad::Four] = 0;

    }

    // 最新の入力状況に更新する処理。
    // 毎フレームの最初に(ゲームの処理より先に)呼んでください。
    static void Update()
    {
        prevStates[(int)Pad::Key] = currentStates[(int)Pad::Key];
        currentStates[(int)Pad::Key] = GetJoypadInputState(DX_INPUT_KEY);
        prevStates[(int)Pad::One] = currentStates[(int)Pad::One];
        currentStates[(int)Pad::One] = GetJoypadInputState(DX_INPUT_PAD1);
        prevStates[(int)Pad::Two] = currentStates[(int)Pad::Key];
        currentStates[(int)Pad::Two] = GetJoypadInputState(DX_INPUT_PAD2);
        prevStates[(int)Pad::Three] = currentStates[(int)Pad::Key];
        currentStates[(int)Pad::Three] = GetJoypadInputState(DX_INPUT_PAD3);
        prevStates[(int)Pad::Four] = currentStates[(int)Pad::Key];
        currentStates[(int)Pad::Four] = GetJoypadInputState(DX_INPUT_PAD4);
       
        // ↑ちょ!...ちょい待てーい!!待たれ給えよ!!
        // 【こんなのいちいち書いてられるかー!!!!!】

       
    }
(以下略)..................


はい、ダメでござる。こんなのいちいち拙者書きたくないでござるよ!!
こういう時こそ、for文や辞書配列の出番でござるよ。絶対そのほうが良いでござるよ。


Input.hの複数プレイのボタン状態を辞書配列とfor文を駆使して書くでござるよ。

#ifndef INPUT_H_
#define INPUT_H_

#include <unordered_map> // 辞書配列【map高速版】https://qiita.com/sileader/items/a40f9acf90fbda16af51

#include "DxLib.h"

// コントローラのパッド番号の列挙型enum
enum class Pad
{
    None=-1, // パッド割当て無(やられたり待機中のプレイヤに)
    Key=0, // キーボード
    One, // コントローラ1
    Two, // コントローラ2
    Three, // コントローラ3
    Four, // コントローラ4
    NUM, // コントローラの数NUM=5(ちょうど配列の数にもなる)
};

// 入力クラス
class Input
{
public:
    static int prevStates[(int)Pad::NUM]; // 1フレーム前の状態
    static int currentStates[(int)Pad::NUM]; // 現在の状態

    static std::unordered_map<int, int> padDic; // パッド番号からDXの定義への変換辞書

    // 辞書配列の初期化
    static void InitPadDictionary()
    {
        // 辞書配列で対応関係の辞書を作成しておく
        padDic[(int)Pad::Key] = DX_INPUT_KEY;
        padDic[(int)Pad::One] = DX_INPUT_PAD1;
        padDic[(int)Pad::Two] = DX_INPUT_PAD2;
        padDic[(int)Pad::Three] = DX_INPUT_PAD3;
        padDic[(int)Pad::Four] = DX_INPUT_PAD4;
    }


    // 初期化。タイトルなどキー入力状態リセット時に
    static void Init()
    {
        InitPadDictionary(); // パッドの辞書配列の初期化

        // キー状態をゼロリセット
        for (int i = 0; i < (int)Pad::NUM; i++)
            prevStates[i] = currentStates[i] = 0;

    }

    // 最新の入力状況に更新する処理。
    // 毎フレームの最初に(ゲームの処理より先に)呼んでください。
    static void Update()
    {
        for (int i = 0; i < (int)Pad::NUM; i++)
        {    // 現在の状態を一つ前の状態として保存してGetJoypad..

            prevStates[i] = currentStates[i];
            currentStates[i] = GetJoypadInputState(padDic[i]);
        }
    }

    // ボタンが押されているか?
    static bool GetButton(Pad pad, int buttonId)
    {
        if(pad == Pad::None) return false; // Noneなら判別不要
        // 今ボタンが押されているかどうかを返却
        return (currentStates[(int)pad] & buttonId) != 0;
    }

    // ボタンが押された瞬間か?
    static bool GetButtonDown(Pad pad, int buttonId)
    {
        if(pad == Pad::None) return false; // Noneなら判別不要
        // 今は押されていて、かつ1フレーム前は押されていない場合はtrueを返却
        return ((currentStates[(int)pad] & buttonId) & ~(prevStates[(int)pad] & buttonId)) != 0;
    }

    // ボタンが離された瞬間か?
    static bool GetButtonUp(Pad pad, int buttonId)
    {
        if(pad == Pad::None) return false; // Noneなら判別不要
        // 1フレーム前は押されていて、かつ今は押されている場合はtrueを返却
        return ((prevStates[(int)pad] & buttonId) & ~(currentStates[(int)pad] & buttonId)) != 0;
    }
}; //←【注意】クラス定義の終わりにはコロンが必要だよ!

#endif  //ここでエラー出た人は↑【注意】の;コロン忘れ


Input.cppの変数定義を変更します。

#include "Input.h"

int Input::prevStates[]; // 1フレーム前の状態
int Input::currentStates[]; // 現在の状態

std::unordered_map<int, int> Input::padDic; // パッド番号からDXの定義への辞書


これでボタン判定にはPad番号の指定が必須になりました。
したがって既存のボタン入力コードにはエラーが出るはずです。
むしろ出たほうが良いと思い GetButtonDown(int buttonId, Pad pad = Pad::key)みたいな
デフォルト = Pad::key設定をしませんでした。(デフォルト=があれば省略できたが)
なぜなら明確にエラーが出ないと複数プレイ対応直し忘れがでちゃいますからね。


Player.hにそのプレイヤを操るコントローラpadの定義を追加しましょう。

#ifndef PLAYER_H_
#define PLAYER_H_

#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"

#include "GameObject.h"

#include "GameManager.h"

class Player : public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:
    Pad pad = Pad::None; // デフォルトは未割当て
    const float MoveSpeed = 6; // 移動速度
    int MutekiJikan = 120; // 無敵時間

    float collisionRadius = 32; // 当たり判定半径

    int lifeMax = 20; // 最大ライフ
    int life = 3; // ライフ
    int mutekiTimer = 0; // 残り無敵時間。0以下なら無敵じゃないってこと

    //【★注意!】ここに【x,yの定義が残ってるとGameObjectと被る】ので
    // 名前の同じ変数が2つ【パラレルワールドに存在してしまう!】
    // Playerからx=13,y=5に変えたつもりでも【ベースのGameObjectはx=0,y=0のままの不思議バグで混乱!】

    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る

    // コンストラクタ
    // pad : 対応するコントローラ
    // pos : 初期位置
    Player(Pad pad, Vector3 pos) : GameObject(pos) //【忘れると】GameObjectの初期化処理が飛ばされて大混乱!
    {
        this->pad = pad;
    };

    // 更新処理
    void Update();

    // 描画処理
    void Draw();

    (中略)....

    // 敵にぶつかった時の処理
    void OnCollisionEnemy(std::shared_ptr<GameObject> other);

    // 敵弾とぶつかった時の処理
    void OnCollisionEnemyBullet(std::shared_ptr<GameObject> other);

    // ダメージを受ける処理
    void TakeDamage();

    // ライフが回復する処理
    void RecoverLife(int amount);

    // ライフをバーで表示する処理
    void DrawLifeBar();
};

#endif


Player.cppのInput判定にコントローラpadの指定を追加しましょう。
なおデフォルトのPad::Noneの状態のままの時は反応しなくなります!

#include "Player.h"
#include "MyMath.h"

#include "PlayerBullet.h" // ★弾を生成して【使う処理があるのでインクルード必要】

#include "Explosion.h"

// 更新処理
void Player::Update()
{
    v = Vector3(0, 0, 0); // xyz方向移動速度

    if (Input::GetButton(pad, PAD_INPUT_LEFT))
    {
        v.x = -MoveSpeed; // 左
    }
    else if (Input::GetButton(pad, PAD_INPUT_RIGHT))
    {
        v.x = MoveSpeed; // 右
    }
    if (Input::GetButton(pad, PAD_INPUT_UP))
    {
        v.y = -MoveSpeed; // 上
    }
    else if (Input::GetButton(pad, PAD_INPUT_DOWN))
    {
        v.y = MoveSpeed; // 下
    }

    // 斜め移動も同じ速度になるように調整
    if (v.magnitude() > 0)
    {
        v /= MyMath::Sqrt2;
    }

    // 実際に位置を動かす
    position += v;

    // ボタン押下で自機弾を発射
    if (Input::GetButtonDown(pad, PAD_INPUT_1))
    {
        // C++ではAddがemplace_backに↓ newがmake_shared↓に
        gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, 0));//右
        gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, -15 * MyMath::Deg2Rad));//右上
        gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, +15 * MyMath::Deg2Rad));//右下
    }

    mutekiTimer--;// 無敵タイマーは無条件で毎フレームカウントダウン
}

(以下略)................


TitleScene.hのInput判定にコントローラpadの指定を書換えましょう。

#ifndef TITLESCENE_H_
#define TITLESCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"

#include "GameManager.h"
#include "SceneManager.h"

class TitleScene : public Scene
{
public:
    (中略).....................

    // ボタン入力に応じた処理
    void HandleInput()
    {
        // 全部のパッドぶんボタンが押されてるか検査
        for (int i = 0; i < (int)Pad::NUM; i++)
        {

            if (Input::GetButtonDown((Pad)i, PAD_INPUT_1))
            {    // ボタンを押したらスタート音声を再生
                Sound::Play(Sound::start);
                isStartPushed = true; // 画面遷移トリガーON
            }
        }
        // いや..ちょ、ちょい待たれよー!いちいちfor文書くの??

(以下略)................

はい、来ました!使い勝手の悪さを発見しましたね!
いちいち全部のコントローラ確かめるときはfor文で囲うのがめんどくさいですよね。
よいアイデアがあります。Inputの方を改造しましょう。



Input.hのコントローラpadの種類にAll(すべての内どれか押されたら)を指定できるように改造しましょう。

#ifndef INPUT_H_
#define INPUT_H_

#include <unordered_map> // 辞書配列【map高速版】https://qiita.com/sileader/items/a40f9acf90fbda16af51

#include "DxLib.h"

// コントローラのパッド番号定義
enum class Pad
{
    All = -2, // すべてのうちどれか1つでも押されたとき
    None = -1, // パッド割当て無(やられたり待機中のプレイヤに)
    Key=0, // キーボード
    One, // コントローラ1
    Two, // コントローラ2
    Three, // コントローラ3
    Four, // コントローラ4
    NUM, // コントローラの数=5
};

// 入力クラス
class Input
{
public:
    static int prevStates[(int)Pad::NUM]; // 1フレーム前の状態
    static int currentStates[(int)Pad::NUM]; // 現在の状態

    static std::unordered_map<int, int> padDic; // パッド番号からDXの定義への辞書

    // 辞書配列の初期化
    static void InitPadDictionary()
    {
        // 辞書配列で対応関係を結び付けておく
        padDic[(int)Pad::Key] = DX_INPUT_KEY;
        padDic[(int)Pad::One] = DX_INPUT_PAD1;
        padDic[(int)Pad::Two] = DX_INPUT_PAD2;
        padDic[(int)Pad::Three] = DX_INPUT_PAD3;
        padDic[(int)Pad::Four] = DX_INPUT_PAD4;
    }

    // 初期化。タイトルなどキー入力状態リセット時に
    static void Init()
    {
        InitPadDictionary(); // パッドの辞書配列の初期化

        // キー状態をゼロリセット
        for (int i = 0; i < (int)Pad::NUM; i++)
            prevStates[i] = currentStates[i] = 0
    }

    // 最新の入力状況に更新する処理。
    // 毎フレームの最初に(ゲームの処理より先に)呼んでください。
    static void Update()
    {
        for (int i = 0; i < (int)Pad::NUM; i++)
        {    // 現在の状態を一つ前の状態として保存してGetJoypad..
            prevStates[i] = currentStates[i];
            currentStates[i] = GetJoypadInputState(padDic[i]);
        }
    }

    // ボタンが押されているか?
    static bool GetButton(Pad pad, int buttonId)
    {
        if (pad == Pad::None) return false; // Noneなら判別不要
        else if (pad == Pad::All) // All指定の時は全てのパッド
        {    // GetButtonの中でGetButtonを呼ぶ【再起呼出し】テクニック
            for (int i = 0; i < (int)Pad::NUM; i++)
                if (GetButton((Pad)i, buttonId))
                    return true; //押されているPadを発見!
            return false; // 一つも押されていなかった
        }


        // 今ボタンが押されているかどうかを返却
        return (currentStates[(int)pad] & buttonId) != 0;
    }

    // ボタンが押された瞬間か?
    static bool GetButtonDown(Pad pad, int buttonId)
    {
        if (pad == Pad::None) return false; // Noneなら判別不要
        else if (pad == Pad::All) // All指定の時は全てのパッド
        {    // 再起呼出しテクニック
            for (int i = 0; i < (int)Pad::NUM; i++)
                if (GetButtonDown((Pad)i, buttonId))
                    return true; //押されているPadを発見!
            return false; // 一つも押されていなかった
        }


        // 今は押されていて、かつ1フレーム前は押されていない場合はtrueを返却
        return ((currentStates[(int)pad] & buttonId) & ~(prevStates[(int)pad] & buttonId)) != 0;
    }

    // ボタンが離された瞬間か?
    static bool GetButtonUp(Pad pad, int buttonId)
    {
        if (pad == Pad::None) return false; // Noneなら判別不要
        else if (pad == Pad::All) // All指定の時は全てのパッド
        {    // 再起呼出しテクニック
            for (int i = 0; i < (int)Pad::NUM; i++)
                if (GetButtonUp((Pad)i, buttonId))
                    return true; //押されているPadを発見!
            return false; // 一つも押されていなかった
        }


        // 1フレーム前は押されていて、かつ今は押されている場合はtrueを返却
        return ((prevStates[(int)pad] & buttonId) & ~(currentStates[(int)pad] & buttonId)) != 0;
    }
}; //←【注意】クラス定義の終わりにはコロンが必要だよ!

#endif  //ここでエラー出た人は↑【注意】の;コロン忘れ

極意【再起呼出し】(自分自身を呼び出すこと)が炸裂しましたね!(再起はきれいにハマると気持ちいいです)
自分自身を呼び出すなんてそんなのアリ!?ですよね。
でもこのテク便利です。【if条件の絞りミスると呼出し無限ループしますがね】
あと、もう一つのテク【enumの種類をAll=-2でマイナス方向に拡張してます】
生真面目な人は0から始めたらプラス方向しかできないと思いがちですよね。
頭が固いです。柔軟にマイナスのenum番号も有効活用すればいいんです。
0から上はコントローラ番号に割り当てることでfor(int i=0;i< (int)Pad::NUM; i++)で全チェックのiに使えます。
0から上の番号にALLとか例外の番号が割り込んでくるとforで回すのに邪魔ですからね。
だからマイナスのenum番号は意外に便利なんです。普通はfor文でマイナスのiを使わないからこそ【マイナスの配列番号は絶対空いてるんです】



さあ、これで使う側はPad::Allを指定するだけでコントローラのどれか1つでもボタンが押されたら検知できます。

TitleScene.hのInput判定にコントローラpadの指定を書換えましょう。

#ifndef TITLESCENE_H_
#define TITLESCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"

#include "GameManager.h"
#include "SceneManager.h"

class TitleScene : public Scene
{
public:
    (中略)....................

    // ボタン入力に応じた処理
    void HandleInput()
    {
        if (Input::GetButtonDown(Pad::All, PAD_INPUT_1))
        {    // ボタンを押したらスタート音声を再生
            Sound::Play(Sound::start);
            isStartPushed = true; // 画面遷移トリガーON
        }

        if (Input::GetButtonDown(Pad::All, PAD_INPUT_UP))
        {    //上を押すと設定モードを-1する
            int index = ((int)settingMode - 1);
            if (index < 0) //↓番号ループ 0→最大数-1
                index = ((int)SettingMode::NUM) - 1;
            settingMode = (SettingMode)index;
        }
        else if (Input::GetButtonDown(Pad::All, PAD_INPUT_DOWN))
        {    //下を押すと設定モードを+1する
            int index = ((int)settingMode + 1);
            if (index >= ((int)SettingMode::NUM))
                index = 0; //番号ループ
            settingMode = (SettingMode)index;
        }

        if (Input::GetButton(Pad::All, PAD_INPUT_RIGHT))
        {
            if (settingMode == SettingMode::BGM)
            {
                Sound::volumeBGM++;//音量をプラス
                if (Sound::volumeBGM > 100)
                    Sound::volumeBGM = 100;
            }
            else if (settingMode == SettingMode::SE)
            {
                Sound::volumeSE++;//音量をプラス
                if (Sound::volumeSE > 100)
                    Sound::volumeSE = 100;
                Sound::Play(Sound::bomb); //効果音を聞きながら調節
            }
            Sound::UpdateMusicVolume(); //再生中の音量の更新
        }
        else if (Input::GetButton(Pad::All, PAD_INPUT_LEFT))
        {
            if (settingMode == SettingMode::BGM)
            {
                Sound::volumeBGM--;//音量をマイナス
                if (Sound::volumeBGM < 0)
                    Sound::volumeBGM = 0;
            }
            else if (settingMode == SettingMode::SE)
            {
                Sound::volumeSE--;//音量をマイナス
                if (Sound::volumeSE <0)
                    Sound::volumeSE = 0;
                Sound::Play(Sound::bomb); //効果音を聞きながら調節
            }
            Sound::UpdateMusicVolume(); //再生中の音量の更新
        }
    }

(以下略).....................


PlayScene.hのプレイヤの生成にパッド番号指定を書き加えましょう。

#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_

(中略)...........

class PlayScene : public Scene
{
public:
    (中略)............

    // 初期化処理
    void Initialize() override
    {
        (中略)....................

        // ここでプレイヤの初期化などのプレイ開始時の処理
        gm.player = std::make_shared<Player>(Pad::Key, Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
        // 敵の仮生成
        //gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
    }

(以下略).....................


GameOverScene.hのInputボタン判定にPad::Allの指定を書き加えましょう。

#ifndef GAMEOVERSCENE_H_
#define GAMEOVERSCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Input.h"
#include "Image.h"

#include "GameManager.h"
#include "SceneManager.h"

class GameOverScene : public Scene
{
public:
   
(中略).....................

    // 更新処理
    void Update() override
    {
        if (Input::GetButtonDown(Pad::All, PAD_INPUT_1))
        {
            sm.LoadScene("TitleScene"); //シーン遷移
        }
    }

    // 描画処理
    void Draw() override
    {
        DrawString(0, 0, "GameOver....ボタン押下でPlaySceneへ", GetColor(255, 255, 255));
    }
};

#endif



さて、これで大丈夫か!...と思いきや反応しなくなりました。
原因はInput::Init(); // 入力コントローラの初期化 忘れでした!
逆にいままでなぜ大丈夫だったかというとint型や配列currentStatesの初期値は0だったからです(たまたま実質0初期化になってた)

Game.cppに忘れていたInputの初期化を書き加えましょう。

#include "Game.h"

#include "MyRandom.h"
#include "Input.h"

void Game::Init()
{// Init処理
    Input::Init(); // コントローラ入力の初期化

    (中略)............
}

(以下略).....................


これでコントローラをつなげばどのコントローラからでもタイトル画面を操作できます。
なぜ各シーンではなくGame.cppでInput入力とMyRandom乱数はInit初期化するのか?
それはコントローラ入力と乱数はシーンによらない共通機能だからです。



複数プレイヤの参加処理

さて、次はプレイヤの参加処理を書いてゆきましょう。
プレイヤの参加は色んな画面で使うのでInputクラスで共通処理とした方が良さそうです。
すでにInputを使い慣れた人も辞書配列の使い道に注目してください。
辞書配列は【対応関係を結ぶのにめちゃ便利】です。
辞書配列に慣れると【vector配列が辞書配列の劣化版のようにすら感じるほど】です。
ですがvector配列はデータがぎっしりとかっつめられているというメモリ節約の利点があります、機動力とメモリ節約の両観点から選択しましょう。

Input.hに参加中パッドの辞書と参加チェック処理を書き加えましょう。

#ifndef INPUT_H_
#define INPUT_H_

#include <unordered_map> // 辞書配列【map高速版】https://qiita.com/sileader/items/a40f9acf90fbda16af51

#include "DxLib.h"

// コントローラのパッド番号定義
enum class Pad
{
    All = -2, // すべてのうちどれか1つでも押されたとき
    None = -1, // パッド割当て無(やられたり待機中のプレイヤに)
    Key=0, // キーボード
    One, // コントローラ1
    Two, // コントローラ2
    Three, // コントローラ3
    Four, // コントローラ4
    NUM, // コントローラの数=5
};

// 入力クラス
class Input
{
public:
    static int prevStates[(int)Pad::NUM]; // 1フレーム前の状態
    static int currentStates[(int)Pad::NUM]; // 現在の状態
    static std::unordered_map<Pad, bool> isJoin; // 参加中のパッド番号辞書
    // 参加中のパッドかどうか?
    static bool IsJoin(Pad pad) { return isJoin.count(pad) == 1 && Input::isJoin[pad]; };


    static std::unordered_map<int, int> padDic; // パッド番号からDXの定義への辞書

    // 辞書配列の初期化
    static void InitPadDictionary()
    {
        // 辞書配列で対応関係を結び付けておく
        padDic[(int)Pad::Key] = DX_INPUT_KEY;
        padDic[(int)Pad::One] = DX_INPUT_PAD1;
        padDic[(int)Pad::Two] = DX_INPUT_PAD2;
        padDic[(int)Pad::Three] = DX_INPUT_PAD3;
        padDic[(int)Pad::Four] = DX_INPUT_PAD4;
    }

    // 初期化。タイトルなどキー入力状態リセット時に
    static void Init()
    {
        InitPadDictionary(); // 辞書配列の初期化

        // キー状態をゼロリセット
        for (int i = 0; i < (int)Pad::NUM; i++)
            prevStates[i] = currentStates[i] = 0;
    }

    // 最新の入力状況に更新する処理。
    // 毎フレームの最初に(ゲームの処理より先に)呼んでください。
    static void Update()
    {
        for (int i = 0; i < (int)Pad::NUM; i++)
        {    // 現在の状態を一つ前の状態として保存してGetJoypad..
            prevStates[i] = currentStates[i];
            currentStates[i] = GetJoypadInputState(padDic[i]);
        }
    }

    // プレイヤの参加処理(参加辞書登録して参加中の数も返す)
    static int JoinCheck()
    {
        int joinCount = 0;
        for (int i = 0; i < (int)Pad::NUM; i++)
        {    // 参加ボタンが押されたら
            if (Input::GetButtonDown((Pad)i, PAD_INPUT_1))
                isJoin[(Pad)i] = true; // 参加パッド辞書に登録
            if (isJoin.count((Pad)i) == 1 && isJoin[(Pad)i] == true)
                joinCount++; // 参加中の人数をカウント
        }
        return joinCount; // 参加中のコントローラ数を返す
    }


(以下略)...............

Input.cppに変数定義を追加します。

#include "Input.h"

int Input::prevStates[]; // 1フレーム前の状態
int Input::currentStates[]; // 現在の状態
std::unordered_map<Pad, bool> Input::isJoin; // 参加中のパッド番号辞書

std::unordered_map<int, int> Input::padDic; // パッド番号からDXの定義への辞書


辞書配列、使う際にはvector配列のようにisJoin[(Pad)i] = true;と書けば使えるんですね。
if文の時はあえてcountを間にはさんでます。
直接isJoin[(Pad)i] == trueの判別するとisJoin[(Pad)i]の登録がなかった時に辞書にfalseで登録が増えちゃうんです。

辞書に未登録のものの判定を行うと気を利かせてデフォルトのfalseとか0を勝手に登録してくれちゃうのです。
勝手に登録してくれてありがとうとはなりません。辞書の数が増えると検索数も増えるので無駄なんです。
.count(~)==1のときだけ判定を行えば辞書に無駄なデフォルト登録が発生せずに済みます。
【辞書配列の判定は.count(~)==1の時だけ行わないと勝手に無駄な項目が増えバグ原因】と覚えておきましょう。



次に現状では、PAD_INPUT_1(Xボタン)にしか反応しないので他のボタンでも参加できるようにします。
Input.hのボタン判定処理を増やしましょう。

(中略)..............

    // プレイヤの参加処理(参加辞書登録して参加中の数も返す)
    static int JoinCheck()
    {
        int joinCount = 0;
        for (int i = 0; i < (int)Pad::NUM; i++)
        {    // 参加ボタンが押されたら
            if (Input::GetButtonDown((Pad)i, PAD_INPUT_1))
                isJoin[(Pad)i] = true; // 参加パッド辞書に登録
            // 参加ボタンが押されたら
            if (Input::GetButtonDown((Pad)i, PAD_INPUT_2))
                isJoin[(Pad)i] = true; // 参加パッド辞書に登録

            // 参加ボタンが押されたら
            if (Input::GetButtonDown((Pad)i, PAD_INPUT_3))
                isJoin[(Pad)i] = true; // 参加パッド辞書に登録

            // 参加ボタンが押されたら
            if (Input::GetButtonDown((Pad)i, PAD_INPUT_4))
                isJoin[(Pad)i] = true; // 参加パッド辞書に登録


            // ちょ待ち!、あ、これボタンの数だけめっちゃif文増えるやつだ..やだな

            if (isJoin.count((Pad)i) == 1 && isJoin[(Pad)i] == true)
                joinCount++; // 参加中の人数をカウント
        }
        return joinCount; // 参加中のコントローラ数を返す
    }

(以下略)...............



あ、このままだと【if else文がボタンの数だけめっちゃ増えちゃいます】ね。
もっと良い書きかたが実はあります。
こんな時のために
PAD_INPUT_1,PAD_INPUT_2,...の定義は2進数に直すと以下のようになってます。

パッドボタン             2進数(32ビット32桁)        ←   16進数  
PAD_INPUT_1   00000000000000000000000000010000 ← 0x00000010
PAD_INPUT_2   00000000000000000000000000100000 ← 0x00000020
PAD_INPUT_3   00000000000000000000000001000000 ← 0x00000040
PAD_INPUT_4   00000000000000000000000010000000 ← 0x00000080
PAD_INPUT_5   00000000000000000000000100000000 ← 0x00000100
PAD_INPUT_6   00000000000000000000001000000000 ← 0x00000200
PAD_INPUT_7   00000000000000000000010000000000 ← 0x00000400
PAD_INPUT_8   00000000000000000000100000000000 ← 0x00000800
PAD_INPUT_9   00000000000000000001000000000000 ← 0x00001000
PAD_INPUT_10  00000000000000000010000000000000 ← 0x00002000
............................................................
PAD_INPUT_28  10000000000000000000000000000000 ← 0x80000000

おお!見事に1が一つずつ左にずれていってますね。
偶然ではありません。人為的にこういう構造になっているのです。
この左ビットシフト構造を【うまくビット論理和で合体させてみましょう】
【ビット論理和「 | 」(または)演算子を使います】(ただの棒 | ではありませんよ!)
ビット論理和「 | 」は「または」の意味になりA | Bで AまたはB どちらかが1の場合に1となるを表します。
ボタン1とボタン4をビット論理和「 | 」でかけ合わせると以下のようになります!

パッドボタン                               2進数(32ビット32桁)      
PAD_INPUT_1               00000000000000000000000000010000
PAD_INPUT_4               00000000000000000000000010000000
............................................................
PAD_INPUT_1 | PAD_INPUT_4 00000000000000000000000010010000

はい、うまいことビットが合成できて0..010010000で1が2か所にできました。
この合成したビットの並びをButton::Joinと名付けましょう。

パッドボタン               2進数(32ビット32桁)   
PAD_INPUT_1      00000000000000000000000000010000
PAD_INPUT_4      00000000000000000000000010000000
.................................................
Button::Join     00000000000000000000000010010000


そしてこの合成したビットを判定に使うときは【ビット論理積「 & 」(かつ)演算子を使います】
ビット論理積「 & 」は「かつ」の意味になりA & Bで AかつB 両方が1の場合に1を表します。
合成したビットPad::Join と PAD_INPUT_1 の【ビット論理積&】をかけ合わせてみましょう。

パッドボタン                        2進数(32ビット32桁)   
Button::Join               00000000000000000000000100010000
PAD_INPUT_1                00000000000000000000000000010000
...........................................................
Button::Join & PAD_INPUT_1 00000000000000000000000000010000 = true
                                                  (1つでも1がある)


この【ビット論理積「 & 」(かつ)演算子】どこかで見たと思いませんか?
そうInput.hのGetButtonの中ボタン押し判定のreturnには【ビット論理積「 & 」演算子】を使っていたのです。
Input.hのGetButtonを見てみてください。

    // ボタンが押されているか?
    static bool GetButton(Pad pad, int buttonId)
    {
        if (pad == Pad::None) return false; // Noneなら判別不要
        else if (pad == Pad::All) // 全てAllの指定だった時は
        {    // GetButtonの中でGetButtonを呼ぶ再起呼出しテクニック
            for (int i = 0; i < (int)Pad::NUM; i++)
                if (GetButton((Pad)i, buttonId))
                    return true; //押されているPadを発見!
            return false; // 一つも押されていなかった
        }

        // 今ボタンが押されているかどうかを返却
        return (currentStates[(int)pad] & buttonId) != 0;
    }


ただ注意してください。【「 & 」マークは色んな異なる意味がありすぎなので】
ややこしいです【「&&」や「&メモリ参照」など文脈によってビット論理積と勘違いしないように(文脈で判断)しましょう】
まあ普通はあんまりビット演算ばかりやるコードはないので気づけると思います。


では【ビット合成】でボタンの種類を合成したButton::Joinを使って
Input.hのボタン判定処理をすっきり書きましょう。

#ifndef INPUT_H_
#define INPUT_H_

#include <unordered_map> // 辞書配列【map高速版】https://qiita.com/sileader/items/a40f9acf90fbda16af51

#include "DxLib.h"

// コントローラのパッド番号定義
enum class Pad
{
    All = -2, // すべてのうちどれか1つでも押されたとき
    None = -1, // パッド割当て無(やられたり待機中のプレイヤに)
    Key=0, // キーボード
    One, // コントローラ1
    Two, // コントローラ2
    Three, // コントローラ3
    Four, // コントローラ4
    NUM, // コントローラの数=5
};

// ある動作として反応するボタン一覧(ビット論理和「|または」)
enum class Button
{    //[反応ボタン一覧定義] https://dixq.net/g/04_05.html
    // 参加ボタン 1=Z 2=X 3=C 4=A 5=S 6=D 7=Q 8=W 9=ESC 10=SPACE
    Join = PAD_INPUT_1 | PAD_INPUT_2 | PAD_INPUT_3 | PAD_INPUT_4
         | PAD_INPUT_5 | PAD_INPUT_6 | PAD_INPUT_7 | PAD_INPUT_8
         | PAD_INPUT_9 | PAD_INPUT_10, //1Z~10SPACEキー全て反応
};


// 入力クラス
class Input
{
public:
         (中略)..............

    // プレイヤの参加処理(参加辞書登録して参加中の数も返す)
    static int JoinCheck()
    {
        int joinCount = 0;
        for (int i = 0; i < (int)Pad::NUM; i++)
        {    // 参加ボタンが押されたら
            if (Input::GetButtonDown((Pad)i, (int)Button::Join))
                isJoin[(Pad)i] = true; // 参加パッド辞書に登録
            // 参加ボタンが押されたら
            if (Input::GetButtonDown((Pad)i, PAD_INPUT_2))
                isJoin[(Pad)i] = true; // 参加パッド辞書に登録
            // 参加ボタンが押されたら
            if (Input::GetButtonDown((Pad)i, PAD_INPUT_3))
                isJoin[(Pad)i] = true; // 参加パッド辞書に登録
            // 参加ボタンが押されたら
            if (Input::GetButtonDown((Pad)i, PAD_INPUT_4))
                isJoin[(Pad)i] = true; // 参加パッド辞書に登録

            // ちょ待ち!、あ、これボタンの数だけめっちゃif文増えるやつだ..やだな


            if (isJoin.count((Pad)i) == 1 && isJoin[(Pad)i] == true)
                joinCount++; // 参加中の人数をカウント
        }
        return joinCount; // 参加中のコントローラ数を返す
    }

(以下略)...............

最終的に赤部分を消すと以下のようにすっきりします。
一目瞭然、見事にすっきりしますね【複数ボタンに反応させるときはビット合成テクニックを使いましょう】


    // プレイヤの参加処理(参加辞書登録して参加中の数も返す)
    static int JoinCheck()
    {
        int joinCount = 0;
        for (int i = 0; i < (int)Pad::NUM; i++)
        {    // 参加ボタンが押されたら
            if (Input::GetButtonDown((Pad)i, (int)Button::Join))
                isJoin[(Pad)i] = true; // 参加パッド辞書に登録

            if (isJoin.count((Pad)i) == 1 && isJoin[(Pad)i] == true)
                joinCount++; // 参加中の人数をカウント
        }
        return joinCount; // 参加中のコントローラ数を返す
    }



今度はプレイヤの数を増やすためにプレイヤをリスト化しましょう。
今までの敵リストのようにvector配列のリストでもいいですが【使い勝手を考えて辞書配列に挑戦】しましょう。

GameManager.hのプレイヤを複数にできるよう辞書配列に変更します。

#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_

#include <memory>
#include <vector>
#include <list>
#include <unordered_map> // 辞書配列【map高速版】

#include "Singleton.h"

class Map;
class Player; //クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class PlayerBullet;
class Enemy;
class EnemyBullet;
class Explosion;
class Item;

class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
    friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可

    std::shared_ptr<Map> map{ nullptr }; // マップ
    std::shared_ptr<Player> player{ nullptr }; // 自機の初期化
    std::unordered_map<int, std::shared_ptr<Player>> players; // 自機の辞書配列(複数プレイ対応)
    std::list<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
    std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
    std::list<std::shared_ptr<EnemyBullet>> enemyBullets; // 敵弾のリスト
    std::list<std::shared_ptr<Explosion>> explosions; // 爆発エフェクトのリスト
    std::list<std::shared_ptr<Item>> items; // アイテムのリスト
    // 複数リスト↑list ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる


    (中略)....................

    };

protected:
    GameManager() {}; // 外部からのインスタンス作成は禁止
    virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};

#endif

playerをplayersに変えたので【つづりの変わった部分は全部エラーになるはず】です。
逆にいうと【今から変えなきゃいけないとこがエラーになってくれた】のです。
エラーが出ないと逆に【忘れて謎のバグの原因になりますからね】
変更の必要なところの目印になってくれて逆に方針が立てやすいのです。


今度はプレイヤの数を増やすためにプレイヤをリスト化しましょう。
今までの敵リストのようにvector配列のリストでもいいですが【使い勝手を考えて辞書配列に挑戦】しましょう。

まずはプレイヤをたくさん使ってるだけあってエラーもたくさん出ている
PlayScene.hのプレイヤを単数から複数に変更するとこから修正を始めましょう。

#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Map.h"

#include "GameManager.h"
#include "SceneManager.h"

#include "Player.h"
#include "Boss.h"
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"
#include "Item.h"

class PlayScene : public Scene
{
public:
    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
   
    int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる

    float scrollSpeed = 1.5f; // スクロール速度

    // コンストラクタ
    PlayScene() : Scene()
    {
        this->tag = "PlayScene";
    }

    // 初期化処理
    void Initialize() override
    {
(中略).........

        // マップの初期化
        gm.map = std::make_shared<Map>("Map/stage1.csv");

        // ここでプレイヤの初期化などのプレイ開始時の処理
        int joinCount = Input::JoinCheck(); // プレイヤの参加検出処理
        if(joinCount > 0)
            MakePlayer(); // プレイヤの生成処理


        //gm.player = std::make_shared<Player>(Pad::Key, Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化

        // 敵の仮生成
        //gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
    }

    // 終了処理(大抵Initializeと同じリセットだが終了時だけやりたいリセットの仕方もあるかも)
    void Finalize() override
    {
        // ここにプレイ終了時のメモリのリセット処理(ゲームクリアやゲームオーバー時など)
        gm.map = nullptr; // マップのメモリからの解放
        gm.players.clear(); // プレイヤのメモリからの解放
        gm.enemies.clear(); // 敵のメモリからの解放
        gm.playerBullets.clear(); // プレイヤの弾のメモリからの解放
        gm.enemyBullets.clear(); // 敵弾のメモリからの解放
        gm.explosions.clear(); // 爆発エフェクトのメモリからの解放
        gm.items.clear(); // アイテムのメモリからの解放

        // ゲーム終了時のスコアがMAXなら記録して終了
        if(score > sm.scoreMax) sm.scoreMax = score;
    }

    // プレイヤの生成処理
    void MakePlayer()
    {
        for (int i = 0; i < (int)Pad::NUM; i++)
        {    // 参加辞書登録されたプレイヤの自機の生成
            if (Input::IsJoin((Pad)i) && gm.players.count(i) == 0)
                gm.players[i] = std::make_shared<Player>((Pad)i, Vector3((float)100, (float)(Screen::Height / 2)));
        }
    }


    // 更新処理
    void Update() override
    {
        // ここにプレイ中の更新処理を持ってくる

        // プレイヤの参加検出処理
        int joinCount = Input::JoinCheck();
        if (joinCount > 0)
            MakePlayer(); // プレイヤの生成処理


        // マップデータの読出し位置のスクロール
        if(gm.players.size() > 0) //プレイヤ配列0の内はスクロールしない
            gm.map->Scroll(scrollSpeed);
       
        // プレイヤの更新処理
        for (const auto& pair : gm.players)
        {
            auto player = pair.second;
            if (!player->isDead) // 自機が死んでいなければ

                player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
        }

        // 自機弾の更新処理
        // for文で全自機弾をループで回してUpdateで更新する
        for (const auto& b : gm.playerBullets)
        {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
            b->Update();
        }

        // 敵弾の更新処理
        for (const auto& b : gm.enemyBullets)
        {
            b->Update();
        }

        // 自機と敵弾の衝突判定
        for (const auto& enemyBullet : gm.enemyBullets)
        {
            // 敵弾が死んでたらスキップ
            if (enemyBullet->isDead)
                continue;


            for (const auto& pair : gm.players)
            {
                auto player = pair.second;

                // 自機が死んでたらこれ以上判定しない
                if (player->isDead)
                    break;

        // 敵弾が死んでたらスキップ
        if (enemyBullet->isDead)
            continue;


                // 円同士の衝突判定で調べる
                if (MyMath::CircleCircleIntersection(
                    player->position, player->collisionRadius,
                    enemyBullet->position, enemyBullet->collisionRadius))
                {
                    player->OnCollisionEnemyBullet(enemyBullet);
                }
            }
        }

        // 敵の更新処理
        for (const auto& b : gm.enemies)
        {
            b->Update();
        }

        // 自機弾と敵の衝突判定
        for (const auto& playerBullet : gm.playerBullets)
        {
            // 自機弾が死んでたらスキップする
            if (playerBullet->isDead)
                continue;

            for (const auto& enemy : gm.enemies)
            {
                // 敵が死んでたらスキップする
                if (enemy->isDead)
                    continue;

                // 自機弾と敵が重なっているか?
                if (MyMath::CircleCircleIntersection(
                    playerBullet->position, playerBullet->collisionRadius,
                    enemy->position, enemy->collisionRadius))
                {
                    // 重なっていたら、それぞれのぶつかったときの処理を呼び出す
                    enemy->OnCollisionPlayerBullet(playerBullet);
                    playerBullet->OnCollisionEnemy(enemy);

                    Sound::Play(Sound::bomb); // 効果音の再生

                    // 衝突の結果、自機弾が死んだら、この弾のループはおしまい
                    if (playerBullet->isDead)
                        break;
                }
            }
        }

        // 自機と敵の衝突判定
        for (const auto& enemy : gm.enemies)
        {
            // 敵が死んでたらスキップ
            if (enemy->isDead)
                continue;


            for (const auto& pair : gm.players)
            {
                auto player = pair.second;

                // 自機が死んでたらこれ以上判定しない
                if (player->isDead)
                    break;

            // 敵が死んでたらスキップ
            if (enemy->isDead)
                continue;


                // 円同士の衝突判定で調べる
                if (MyMath::CircleCircleIntersection(
                    player->position, player->collisionRadius,
                    enemy->position, enemy->collisionRadius))
                {
                    player->OnCollisionEnemy(enemy);
                }
            }
        }

        // 爆発エフェクトの更新処理
        for (const auto& e : gm.explosions)
        {
            e->Update();
        }

        // アイテムの更新処理
        for (const auto& itm : gm.items)
        {
            itm->Update();
        }

        // 自機とアイテムの衝突判定
        for (const auto& itm : gm.items)
        {
            // アイテムが死んでたらスキップ
            if (itm->isDead)
                continue;

            
            for (const auto& pair : gm.players)
            {
                auto player = pair.second;
                // 自機が死んでたらこれ以上判定しない
                if (player->isDead)
                    break;
            
            // アイテムが死んでたらスキップ
            if (itm->isDead)
                continue;

            
                // 円同士の衝突判定で調べる
                if (MyMath::CircleCircleIntersection(
                   player->position, player->collisionRadius,
                   itm->position, itm->collisionRadius) )
                {
                    itm->OnCollisionPlayer(player);
                }
            }
           
            for (const auto& enemyBullet : gm.enemyBullets)
            {
                // 敵弾が死んでたらスキップ
                if (enemyBullet->isDead)
                    continue;
                
                // 円同士の衝突判定で調べる
                if (MyMath::CircleCircleIntersection(
                    itm->position, itm->collisionRadius,
                    enemyBullet->position, enemyBullet->collisionRadius) )
                {
                    enemyBullet->OnCollisionItem(itm);
                    itm->OnCollisionEnemyBullet(enemyBullet);
                }
            }
        }

        //return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験

        // 自機弾の削除処理
        gm.EraseRemoveIf(gm.playerBullets,
            [](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

        // 敵のリストから死んでるものを除去する
        gm.EraseRemoveIf(gm.enemies,
            [](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

        // 敵弾のリストから死んでるものを除去する
        gm.EraseRemoveIf(gm.enemyBullets,
            [](std::shared_ptr<EnemyBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

        //爆発エフェクトのリストから死んでるものを除去する
        gm.EraseRemoveIf(gm.explosions,
            [](std::shared_ptr<Explosion>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

        //アイテムのリストから死んでるものを除去する
        gm.EraseRemoveIf(gm.items,
            [](std::shared_ptr<Item>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除

        int aliveCount = 0; // 生きているプレイヤ数のカウント
        for (const auto& pair : gm.players)
        {
            auto player = pair.second;
            if (player->isDead != true)
                aliveCount++; // 生存プレイヤ数をカウント
        }


        // ゲームオーバー判定と画面チェンジ処理
        // プレイヤの数が0じゃないのに生存数が0ならゲームオーバー
        bool isGameover = (gm.players.size() != 0 && aliveCount == 0);
        if (!isGameover && gm.player->isDead) isGameover = true;
        if (isGameover)
        {
            Sound::PlayMusic(Sound::ending); //BGMを変更する
            sm.LoadScene("GameOverScene"); //シーン遷移
            return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
        }
    }

    // 描画処理
    void Draw() override
    {
        // ここにプレイ画面の描画処理を持ってくる

        // 敵の描画処理
        for (const auto& b : gm.enemies)
        {
            b->Draw();
        }

        // 爆発エフェクトの描画処理
        for (const auto& e : gm.explosions)
        {
            e->Draw();
        }

        // プレイヤの描画処理
        for (const auto& pair : gm.players)
        {
            auto player = pair.second;

            if (!player->isDead) // 自機が死んでいなければ
                player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
        }

        // 自機弾の描画処理
        // for文で全自機弾をループで回してUpdateで更新する
        for (const auto& b : gm.playerBullets)
        {  // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
            b->Draw();
        }

        // 敵弾の描画処理
        for (const auto& b : gm.enemyBullets)
        {
            b->Draw();
        }

        // アイテムの描画処理
        for (const auto& itm : gm.items)
        {
            itm->Draw();
        }
    }
};

#endif

さてここで辞書配列のfor文に慣れてしまいましょう。
vector配列との違いは【辞書のキーとバリューを各ループで得られること】です。
あいうえおの辞書を例にすると【「あ」がキー、「アイス」がバリューですね】
あ、すみません違います!【「アイス」がキー、「アイスとは冷たくておいしいもの(説明)」がバリューです】
ただし一つのキーに複数のバリューのあるタイプの辞書もあり
その場合は【「あ」がキー、「アイス」「愛」..などがバリューですね】
プログラム上では【キーがfirst、バリューがsecondでアクセスできます】

        // プレイヤの描画処理
        for (const auto& pair : gm.players)
        {
            auto player = pair.second;
            if (!player->isDead) // 自機が死んでいなければ
                player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
        }


もう一つ辞書配列に登録する際の形も見ておきましょう。
players[i] = ~;のような配列に代入するような形で登録可能です。
すでに登録済か未登録かは.count(キー)で判別します。
このcount()でキーがあるか探せるのがvector配列より手軽なところ
5文字ぐらいでさらっと登録済かわかるので。

    // プレイヤの生成処理
    void MakePlayer()
    {
        for (int i = 0; i < (int)Pad::NUM; i++)
        {    // 参加辞書登録されたプレイヤの自機の生成
            if (Input::IsJoin((Pad)i) && gm.players.count(i) == 0)
                gm.players[i] = std::make_shared<Player>((Pad)i, Vector3((float)100, (float)(Screen::Height / 2)));
        }
    }




さてタイトル画面などにもプレイヤ参加の検出処理を加えておきましょう。
プレイに移る前に参加辞書Input::isJoinに1人は登録がないとプレイヤのいない空のプレイ画面から始まるので。

TitleScene.hにプレイヤの参加検出処理を追加します。

#ifndef TITLESCENE_H_
#define TITLESCENE_H_

(中略)..................

class TitleScene : public Scene
{
public:
          (中略)..................


    // ボタン入力に応じた処理
    void HandleInput()
    {
        Input::JoinCheck(); // プレイヤの参加検出処理

        if (Input::GetButtonDown(Pad::All, PAD_INPUT_1))
        {    // ボタンを押したらスタート音声を再生
            Sound::Play(Sound::start);
            isStartPushed = true; // 画面遷移トリガーON
        }

      (中略)..................
    }
(以下略)..................



他にもザコにプレイヤの追撃弾を撃たせている人は単体のプレイヤを複数対応しなければなりません。
プレイヤを追撃するにも複数いるとどのプレイヤを追撃するかあいまいになるので。
ここからのザコの処理の修正は人それぞれです。プレイヤを追っかけない独自のザコのままがいい!という人は不要かもしれません。


Zako0.hに複数のプレイヤから一番近いプレイヤを追撃する処理を追加します。

#ifndef ZAKO_0_H_
#define ZAKO_0_H_

#include <memory>

#include "Enemy.h"
#include "Image.h"
#include "MyMath.h"

#include "Player.h" // Player.hでもZako1.hをインクルードすると循環するので注意

// ザコ0クラス
class Zako0 : public Enemy
{
public:
    int coolTime = 0; // クールタイム(冷却時間。0になるまで次の弾が撃てない)

    // コンストラクタ
    Zako0(Vector3 pos) : Enemy( pos )
    {
        this->tag = "Zako0";
    }

    // 更新処理
    void Update() override
    {
        position.x -= 1; // とりあえず左へ移動

        coolTime--;

        if (coolTime <= 0)
        {
            float minDistance = 1000000; // 最小の距離
            std::shared_ptr<Player> minPlayer = nullptr; // 一番近いプレイヤを探す
            for (const auto& pair : gm.players)
            {
                auto p = pair.second;
                float dist = (position - p->position).magnitude();
                if (dist < minDistance)
                {    // より小さい距離のプレイヤがいれば記録更新
                    minDistance = dist;
                    minPlayer = p;
                }    
            }
            if (minPlayer != nullptr)
            {

                // 自機狙い弾
                float angle = MyMath::PointToPointAngle(position, minPlayer->position);
                gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, angle, 8));
            }

            coolTime += 10;
        }

        // 【重要】画面外の削除処理【CPU処理重くなる原因】
        if (x < 0)
        {    // 左端の画面外に出たらisDeadしないと
            // 画面外の敵の当たり判定で【だんだん処理が重くなるZako1,Zako2..も】
            isDead = true;
        }
    }

    // 描画処理
    void Draw() override
    {
        DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako0, TRUE);
    }
};

#endif



Zako1.hに複数のプレイヤから一番近いプレイヤを追撃する処理を追加します。

#ifndef ZAKO_1_H_
#define ZAKO_1_H_

#include "Enemy.h"

#include <memory>
#include <cmath> // Sinの計算に使う
#include "MyMath.h"
#include "Image.h"

#include "Player.h" // Player.hでもZako1.hをインクルードすると循環するので注意

// ザコ1クラス
class Zako1 : public Enemy
{
public:
    float sinMove = 0; // Sinの動きをする際の0~360度
    float sinRadius = 70; // Sinの動きをするときの半径

    // コンストラクタ
    Zako1(Vector3 pos) : Enemy( pos )
    {
        this->tag = "Zako1";//オブジェクトの種類判別タグ
        life = 5;
    }

    // 更新処理
    void Update() override
    {
        float minDistance = 1000000; // 最小の距離
        std::shared_ptr<Player> minPlayer = nullptr; // 一番近いプレイヤを探す
        for (const auto& pair : gm.players)
        {
            auto p = pair.second;
            float dist = (position - p->position).magnitude();
            if (dist < minDistance)
            {    // より小さい距離のプレイヤがいれば記録更新
                minDistance = dist;
                minPlayer = p;
            }
        }

        if (minPlayer != nullptr)
        {

            // 自機をおいかけてくる処理
            float angleToPlayer = MyMath::PointToPointAngle(position, minPlayer->position);
            float speed = 3;
            position += Vector3((float)std::cos(angleToPlayer) * speed, (float)std::sin(angleToPlayer) * speed);

            //ゆらゆらSin波動の動きをy上下方向にプラス
            float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
            sinMove += 2.5f; // Sinの波運動の角度を進める
            float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;

            // (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
            position.y += currentSinY - prevSinY; // 上下方向のSinの波運動
        }
    }

    // 描画処理
    void Draw() override
    {
        DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako1, TRUE);
    }
};

#endif



Zako2.hに複数のプレイヤから一番近いプレイヤを追撃する処理を追加します。

#ifndef ZAKO_2_H_
#define ZAKO_2_H_

#include "Enemy.h"

#include <memory>
#include <cmath> // Sin Cosの計算に使う

#include "Image.h"
#include "Screen.h"
#include "MyMath.h"
#include "MyRandom.h"

#include "Player.h" // Player.hでもZako2.hをインクルードすると循環するので注意

// ザコ2クラス
class Zako2 : public Enemy
{
public:
    int coolTime = 0; // クールタイム(冷却時間。0になるまで次の弾が撃てない)

    // コンストラクタ
    Zako2(Vector3 pos) : Enemy( pos )
    {
        this->tag = "Zako2";
    }

    // 更新処理
    void Update() override
    {
        if (position.x > Screen::Width - 200)
        { // スクリーンの端から200の位置で止まる
            position.x -= 1; // とりあえず左へ移動
        }

        coolTime--;

        if (coolTime <= 0)
        {
            float minDistance = 1000000; // 最小の距離
            std::shared_ptr<Player> minPlayer = nullptr; // 一番近いプレイヤを探す
            for (const auto& pair : gm.players)
            {    // より小さい距離のプレイヤがいれば記録更新
                auto p = pair.second;
                float dist = (position - p->position).magnitude();
                if (dist < minDistance)
                {
                    minDistance = dist;
                    minPlayer = p;
                }
            }
            if (minPlayer != nullptr)
            {

                // 自機狙いばらまき弾
                float angle = MyMath::PointToPointAngle(position, minPlayer->position);
                float randomAngle = MyRandom::Range(-15.0f, 15.0f); // ±15度の乱数の角度を取得
                gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, angle + randomAngle * MyMath::Deg2Rad, 8));
                coolTime += 10;
            }
        }

        // 200の位置で止まるので左端にこないから削除処理はなくて済む
    }

    // 描画処理
    void Draw() override
    {
        DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako2, TRUE);
    }
};

#endif




さて、これで独自のザコや弾など単数プレイヤを使用しているコードがなくなれば
複数プレイヤでプレイできるようになるはずです。
一度プレイして複数プレイヤでプレイできるか確かめてみましょう。




    もし複数のコントローラが連動しちゃったりした場合は次の項で対策が必要かもしれません。

[DXのバグ!?仕様?]複数コントローラは4以降があやしい件

コントローラ7を押すと3としてキャラが反応するなどコントローラ4以降の動作が怪しいのでその対策(DXのバグ?仕様?)をしておきます。

Input.hでコントローラの初回ボタンプッシュを管理して、2つのコントローラの初回プッシュが被ったら同一コントローラ扱いとしてスルーする機能を追加しましょう。

#ifndef INPUT_H_
#define INPUT_H_

#include <climits> //int型の最大値2147483647につかう
#include <unordered_map> // 辞書配列【map高速版】https://qiita.com/sileader/items/a40f9acf90fbda16af51

#include "DxLib.h"

// コントローラのパッド番号定義
enum class Pad
{
    All = -2, // すべてのうちどれか1つでも押されたとき
    None = -1, // パッド割当て無(やられたり待機中のプレイヤに)
    Key=0, // キーボード
    One, // コントローラ1
    Two, // コントローラ2
    Three, // コントローラ3
    Four, // コントローラ4
    NUM, // コントローラの数=5
};

// 入力クラス
class Input
{
public:
    const int MaxPadNum = (int)Pad::Four + 1; // 最大パッド数(Keyぶん+1)

    static int prevStates[(int)Pad::NUM]; // 1フレーム前の状態
    static int currentStates[(int)Pad::NUM]; // 現在の状態

    static std::unordered_map<Pad, bool> isJoin; // 参加中のパッド番号辞書
    // 参加中のパッドかどうか?
    static bool IsJoin(Pad pad) { return isJoin.count(pad) == 1 && Input::isJoin[pad]; };

    static std::unordered_map<int, int> padDic; // パッド番号からDXの定義への辞書

    //★【DXコントローラバグ検知】初回プッシュが被ればDXバグで同一コントローラ
    static int firstPushTiming[(int)Pad::Four]; // Pad::Fourが最後だから8とかに増やすなら要書き換え
    static int timing;// = 0;
    static bool isBugCheckMode;// = false; //バグチェックを開始するか


    // 辞書配列の初期化
    static void InitPadDictionary()
    {
        // 辞書配列で対応関係を結び付けておく
        padDic[(int)Pad::Key] = DX_INPUT_KEY;
        padDic[(int)Pad::One] = DX_INPUT_PAD1;
        padDic[(int)Pad::Two] = DX_INPUT_PAD2;
        padDic[(int)Pad::Three] = DX_INPUT_PAD3;
        padDic[(int)Pad::Four] = DX_INPUT_PAD4;
    }

    // 初期化。タイトルなどキー入力状態リセット時に
    static void Init()
    {
        InitPadDictionary(); // パッドの辞書配列の初期化

        // キー状態をゼロリセット
        for (int i = 0; i < (int)Pad::NUM; i++)
            prevStates[i] = currentStates[i] = 0;
       
        timing = 0, isBugCheckMode = false; //バグチェック

    }

    // プレイヤの参加処理(参加辞書登録して参加中の数も返す)
    static int JoinCheck()
    {
        int joinCount = 0;
        for (int i = 0; i < (int)Pad::NUM; i++)
        {    // 参加ボタンが押されたら
            if (Input::GetButtonDown((Pad)i, PAD_INPUT_1))
                isJoin[(Pad)i] = true; // 参加パッド辞書に登録
            if (isJoin.count((Pad)i) == 1 && isJoin[(Pad)i] == true)
                joinCount++; // 参加中の人数をカウント
        }
        return joinCount; // 参加中のコントローラ数を返す
    }

    // 最新の入力状況に更新する処理。
    // 毎フレームの最初に(ゲームの処理より先に)呼んでください。
    static void Update()
    {
        for (int i = 0; i < (int)Pad::NUM; i++)
        {    // 現在の状態を一つ前の状態として保存してGetJoypad..
            prevStates[i] = currentStates[i];
            currentStates[i] = GetJoypadInputState(padDic[i]);
        }
       
        isBugCheckMode = true;
        ++timing; //タイミングのカウントを+1
        if (timing == INT_MAX) timing = 0; // int型最大値になったら0にループ

    }

    // ボタンが押されているか?
    static bool GetButton(Pad pad, int buttonId)
    {    //コントローラバグチェック
        if (ControllerBugCheck(pad, buttonId)) return false;
       

        if (pad == Pad::None) return false; // Noneなら判別不要
        else if (pad == Pad::All) // All指定の時は全てのパッド
        {    // GetButtonの中でGetButtonを呼ぶ【再起呼出し】テクニック
            for (int i = 0; i < (int)Pad::NUM; i++)
                if (GetButton((Pad)i, buttonId))
                    return true; //押されているPadを発見!
            return false; // 一つも押されていなかった
        }

        // 今ボタンが押されているかどうかを返却
        return (currentStates[(int)pad] & buttonId) != 0;
    }

    // ボタンが押された瞬間か?
    static bool GetButtonDown(Pad pad, int buttonId)
    {    //コントローラバグチェック
        if (ControllerBugCheck(pad, buttonId)) return false;
       

        if (pad == Pad::None) return false; // Noneなら判別不要
        else if (pad == Pad::All) // All指定の時は全てのパッド
        {    // 再起呼出しテクニック
            for (int i = 0; i < (int)Pad::NUM; i++)
                if (GetButtonDown((Pad)i, buttonId))
                    return true; //押されているPadを発見!
            return false; // 一つも押されていなかった
        }

        // 今は押されていて、かつ1フレーム前は押されていない場合はtrueを返却
        return ((currentStates[(int)pad] & buttonId) & ~(prevStates[(int)pad] & buttonId)) != 0;
    }

    // ボタンが離された瞬間か?
    static bool GetButtonUp(Pad pad, int buttonId)
    {    //コントローラバグチェック
        if (ControllerBugCheck(pad, buttonId)) return false;
       

        if (pad == Pad::None) return false; // Noneなら判別不要
        else if (pad == Pad::All) // All指定の時は全てのパッド
        {    // 再起呼出しテクニック
            for (int i = 0; i < (int)Pad::NUM; i++)
                if (GetButtonUp((Pad)i, buttonId))
                    return true; //押されているPadを発見!
            return false; // 一つも押されていなかった
        }

        // 1フレーム前は押されていて、かつ今は押されている場合はtrueを返却
        return ((prevStates[(int)pad] & buttonId) & ~(currentStates[(int)pad] & buttonId)) != 0;
    }
   
    //コントローラのボタンの初回プッシュを検知し
    //他コントローラと初回タイミングが完全被りかチェックして重複コントローラの判定処理をスルーする対策
    //★コントローラ3を押すと7も反応するなどコントローラ4以降の動作が怪しいのでその対策(DXのバグ?仕様?)
    static bool ControllerBugCheck(Pad pad, int buttonId)
    {
        // 今は押されていて、かつ1フレーム前は押されていない場合
        bool buttonDown = ((currentStates[(int)pad] & buttonId) & ~(prevStates[(int)pad] & buttonId)) != 0;
        //if (isBugCheckMode == false) return false;
        //コントローラの初回ボタンプッシュを検知
        if (buttonDown && firstPushTiming[(int)pad] == 0)
        {
            for (int i = 0; i < (int)Pad::NUM - 1; i++)
            {    //全コントローラ初回プッシュ被りがないかチェック完全同時は怪しいので-1
                bool isDown = ((currentStates[i] & buttonId) & ~(prevStates[i] & buttonId)) != 0;
                if ((Pad)i != pad && isDown ////初回プッシュ被り検出
                  && (timing == firstPushTiming[i] || firstPushTiming[i] == 0))
                {    //初回プッシュのタイミング被りは-1フラグを記録
                    firstPushTiming[i] = -1;
                }
            }
            if (firstPushTiming[(int)pad] != -1)
                firstPushTiming[(int)pad] = timing;//初回プッシュを記録
        }
       
        if (firstPushTiming[(int)pad] == -1) return true; //DXのコントローラ被りバグ!
        else return false; //コントローラ被りOK!
    }
   

}; //←【注意】クラス定義の終わりにはコロンが必要だよ!

#endif  //ここでエラー出た人は↑【注意】の;コロン忘れ


Input.cppに変数定義を追加します。

#include "Input.h"

int Input::prevStates[]; // 1フレーム前の状態
int Input::currentStates[]; // 現在の状態
std::unordered_map<Pad, bool> Input::isJoin; // 参加中のパッド番号辞書

std::unordered_map<int, int> Input::padDic; // パッド番号からDXの定義への辞書

//★【DXコントローラバグ検知】初回プッシュが被ればDXバグで同一コントローラ
int Input::firstPushTiming[(int)Pad::Four]; // Pad::Fourが最後だから8とかに増やすなら要書き換え
int Input::timing{ 0 };
bool Input::isBugCheckMode = false; //バグチェックを開始するか




コントローラを4つ以上持っている人は中々いないでしょうが、念のため今後も導入しておいたほうが安心です



背景画像を作ってループ表示させてみる


まずはGIMPでループさせる背景画像を作ってみましょう。
GIMPを開いてスクリーンのサイズと合わせて960×540でキャンバスをつくる(横スクロールするなら960より大きくしてみてもよい)。


空の色をイメージして水色でぬりつぶします。


ブラシを使って白色で雲をえがきます。


背景画像をエクスポートでImageフォルダにbackground.jpgと名前をつけて保存します。(背景画像は縦×横サイズが大きいのでjpg形式にすることで圧縮して保存サイズが小さくなるようにした)



Image.hに背景画像の読み込み定義を追加しましょう。

#ifndef IMAGE_H_
#define IMAGE_H_

#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
(中略)..........

class Image
{
public:
    Image() {}; // 初期化コンストラクタの定義だけ
    ~Image() {}; // 破棄する処理デストラクタの定義だけ
(中略)..........

    static int background; //背景画像のハンドラ(読込画像番号)
    static int bossImage; //ボス画像のハンドラ(読込画像番号)
    static int player; //プレイヤ画像のハンドラ
(中略)..........

};
#endif


Image.cppにも背景画像の読み込みの処理を追加しましょう。

#include "Image.h"

(中略)..........

int Image::background{ -1 };
int Image::bossImage{ -1 }; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{ -1 };
(中略)..........

void Image::Load()
{
(中略)..........
    background = LoadGraph("Image/background.jpg"); //[メモリ節約機能付の場合追記→] scene, { "TitleScene" });
    assert(background != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる


(中略)..........

}

PlayScene.hに背景をえがく処理を追加します。

#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"

#include "GameManager.h"
#include "SceneManager.h"

#include "Player.h"
#include "Boss.h"
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"

class PlayScene : public Scene
{
(中略)..........

    // 描画処理
    void Draw() override
    {
        // 背景の描画(一番後ろに表示させるものから最初に描く、描く順が早い方が後ろのレイヤー)
        DrawGraphF(0, 0, Image::background, TRUE); // DrawGraphF関数DrawRotaGraphFと違い(0,0)を指定すると左上が(0,0)になるように描く。
       

        (中略)...................
    }
};

#endif


はい、実行してみて、これで静止した状態で背景が表示されたら一旦は成功です。
うまく表示されない場合は、background.jpgのつづりは合ってますか?、もしくはImageフォルダにあるかちゃんと確認してくださいね。



うまく表示出来たら今度は背景をスクロールさせる処理に挑戦してみます。




PlayScene.hに背景をスクロールさせる処理を追加します。ポイントは背景を2枚、画像の幅だけずらして描いていることです。

#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"

#include "GameManager.h"
#include "SceneManager.h"

#include "Player.h"
#include "Boss.h"
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"

class PlayScene : public Scene
{
public:
    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
   
    int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる

    float scrollSpeed = 1.5f; // スクロール速度

    float bgX = 0.0f; // 背景のスクロール位置
    float bgSpeed = 1.5f; // 背景のスクロールスピード

    // コンストラクタ
    PlayScene() : Scene()
    {
        this->tag = "PlayScene";
    }
(中略)..........

    // 描画処理
    void Draw() override
    {
        float bgWidth, bgHeight; // 画像の幅と高さ
        // &で&bgWidth と &bgHeightを参照渡し↓することでGetGraphSizeF内部で書き換えてもらう(&をつけない渡し方は変数のコピーを渡しだから関数が終わっても書き変わらない)
        GetGraphSizeF(Image::background, &bgWidth, &bgHeight); // 画像の縦横のピクセルサイズを取得する
        bgX -= bgSpeed; // マイナスで左方向にスクロール
        if (bgX < -bgWidth) bgX = 0; // 画像サイズの幅ぶんスクロールしたら0に戻してループ
       

        // 背景の描画(一番後ろに表示させるものから最初に描く、描く順が早い方が後ろのレイヤー)
        DrawGraphF(bgX, 0, Image::background, TRUE); // DrawGraphF関数はDrawRotaGraphFと違い(0,0)を指定すると左上が(0,0)になるように描く。
        // 2枚背景を画像の幅のぶんだけ右端にずらして描くことで無限にループしてるようにみせる
        DrawGraphF(bgX + bgWidth, 0, Image::background, TRUE);
       

        (中略)...................
    }
};

#endif



いかがですか?背景がスクロールすると画面に動きが出て少し気持ちよさが増したように感じませんか?



スコアを画面表示させてみる

TitleScene.hには実はすでにスコアを表示するコードがありますが実は文字列の扱い方が間違っていて表示されていません。

#ifndef TITLESCENE_H_
#define TITLESCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
(中略)...........

class TitleScene : public Scene
{
public:
(中略)...........

    // 描画処理
    void Draw() override
    {
        DrawString(0, 0, "TitleSceneです。ボタン押下でPlaySceneへ。", GetColor(255, 255, 255));
        if (sm.scoreMax >= 0)
        {
            DrawString(0, 200, "MAXスコア: " + sm.scoreMax, GetColor(255, 255, 255));
        }

(中略)...........

    }
};

#endif


DrawStringの関数の上で右クリックして「定義をここに表示」してみてください。

// 文字列描画関数
extern int DrawString( int x, int y, const TCHAR *String, unsigned int Color, unsigned int EdgeColor DEFAULTPARAM( = 0 ) ) ;


const TCHAR *String、つまりはconst TCHAR *型ですね
さらにTCHARの上で右クリックして「定義をここに表示」してみてください。

typedef char TCHAR, *PTCHAR;

typedef char ~,~...; つまりはchar型です(C言語の1byteの1文字のアスキーコード型もしくは-127 〜 128の1byteの整数型)

さて、ここまではC++なので使いやすいstd::string型でコードを書いてきました。
一方、DXライブラリはC言語時代から作られており、C言語でも動かせるように文字列はchar型の*ポインタとして表現されています。
char *型でなぜ、文字列として扱えているのでしょうか?
それはC言語時代から文字列はchar型の配列として表現されてきており、char*型はその配列の先頭を指すポインタとしての意味を持っているのです。
下記のスライドでポインタや文字列、ヌルnullptrの概念、データの読み出し方と型の定義の関連、参照&、CDやメモリなど物理データとプログラムのつながりを確認しましょう


上記を踏まえて、"MAXスコア: " + sm.scoreMaxの意味を考えると..
あ、スコアが2だとするとchar*型"MAXスコア: "に+2しても..ポインタの先頭位置が+2ずれるだけ..つまり[M][A][X]..のXをポインタが指すようになるだけ..
つまり、文字列char*型に+で数字を足したところで文字のアクセス位置が変わるだけという処理になり、バグってしまうコードになるわけです。

ではどうするかというと std::string("MAXスコア: ") としてC++のstd::string型に変換してからstd::to_string(sm.scoreMax)で数字を文字列型に変換したうえで、

std::string("MAXスコア: ") + std::to_string(sm.scoreMax)

として足し合わせて、
さらに.c_str()関数でもとのchar*型へ戻してやる必要があります。

( std::string("MAXスコア: ") + std::to_string(sm.scoreMax) ).c_str()



TitleScene.hのスコアを表示するコードを修正します。

#ifndef TITLESCENE_H_
#define TITLESCENE_H_

#include "Scene.h"

#include <string>

#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
(中略)...........

class TitleScene : public Scene
{
public:
(中略)...........

    // 描画処理
    void Draw() override
    {
        DrawString(0, 0, "TitleSceneです。ボタン押下でPlaySceneへ。", GetColor(255, 255, 255));
        if (sm.scoreMax >= 0)
        {
            DrawString(0, 200, (std::string("MAXスコア: ") + std::to_string(sm.scoreMax)).c_str(), GetColor(255, 255, 255));
        }

(中略)...........

    }
};

#endif



どうでしょうか、一度自滅してみてスコアを確定させて、タイトル画面にスコアが表示されるか確かめてみましょう。
スコアは0固定のはずです。なぜなら敵を倒してもスコアを加算する処理はまだ書いてないので(のちほどやる)。

フォントを変えてみる

フォントの表示の仕方を色々変えてみましょう。
たとえば、フォントの色やサイズを変えることもできます。

#ifndef TITLESCENE_H_
#define TITLESCENE_H_

#include "Scene.h"

#include <string>

#include "DxLib.h"
(中略)...........

class TitleScene : public Scene
{
public:
(中略)...........

    // 描画処理
    void Draw() override
    {
        SetFontLineSpace(40); // 1行ごとのスペース
        
        int prevFontSize = GetFontSize(); // フォントのサイズを一旦保管
        SetFontSize(40); // フォントのサイズを変える

        DrawString(0, 0, "TitleSceneです。\nボタン押下でPlaySceneへ。", GetColor(0, 255, 0));
        SetFontSize(prevFontSize); // フォントのサイズを元に戻す(戻さないと以降のすべての文字が40のサイズになる)
        
        if (sm.scoreMax >= 0)
        {
            DrawString(0, 200, (std::string("MAXスコア: ") + std::to_string(sm.scoreMax)).c_str(), GetColor(255, 255, 255));
        }

(中略)...........

    }
};

#endif


実行したら、フォントの色とサイズは変わりましたか?

\nは改行を意味する、アスキーコードの特殊文字です。



さて、うまくはいきましたが実はDraw()関数など1秒間に何回も繰り返される関数内でサイズや太さなどの変更処理をするのは重くなります。
https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N7
https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N8

なので、Initialize()関数など開始時に1度だけ呼ばれる関数でCreateFontToHandle関数でフォントを画像データとしてメモリ上に初期化する処理が望ましいです。
https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10


#ifndef TITLESCENE_H_
#define TITLESCENE_H_

#include "Scene.h"

#include <string>

#include "DxLib.h"
(中略)...........

class TitleScene : public Scene
{
public:
(中略)...........
    int font1 = -1; //フォントのハンドルをCreateFontToHandle関数で得る
    
    // コンストラクタ
    TitleScene() : Scene()
    {
        this->tag = "TitleScene";
    }
    
    // 初期化処理
    void Initialize() override
    {
        // "MS 明朝"のフォントで、サイズ40、太さ7
        // エッジサイズ3、DX_FONTTYPE_ANTIALIASING_EDGE_4X4 : アンチエイリアス&エッジ付きフォント( 4x4サンプリング )
        // イタリック=TRUEのフォントを作成し
        // 作成したデータの識別番号を変数 font1 に保存する
        font1 = DxLib::CreateFontToHandle("MS 明朝", 40, 7, DX_FONTTYPE_ANTIALIASING_EDGE_4X4, -1, 3, TRUE);

    
    (中略)...........
    
    }
(中略)...........
    
    // 描画処理
    void Draw() override
    {
        SetFontLineSpaceToHandle(40, font1); // 1行ごとのスペース
        
        int prevFontSize = GetFontSize(); // フォントのサイズを一旦保管
        SetFontSize(40); // フォントのサイズを変える

        DrawStringToHandle(0, 0, "TitleSceneです。\nボタン押下でPlaySceneへ。", GetColor(0, 255, 0), font1, GetColor(0, 0, 255));
        SetFontSize(prevFontSize); // フォントのサイズを元に戻す(戻さないと以降のすべての文字が40のサイズになる)
        
        if (sm.scoreMax >= 0)
        {
            DrawString(0, 200, (std::string("MAXスコア: ") + std::to_string(sm.scoreMax)).c_str(), GetColor(255, 255, 255));
        }

(中略)...........

    }
};

#endif



さてうまく"MS 明朝"のフォントで表示がされたでしょうか?
このフォント名については"MS 明朝"以外にも沢山あるうえ、半角や全角やスペースがいりまじって、つづりを間違わない方が難しいです
ゆえに下記ツールをダウンロード(64bit VersionのZIP archiveをクリック)して、Infoタブからフォント名を右クリックでコピーできるようにしましょう。
https://us.fontviewer.de/Download/

[使い方]:MS Minchoを検索したりして探して、見つけたら右下からInfoタブをクリックして、右画面スクロールで[Full font name]を探して、
必ず右クリックでコピーを選択してフォント名をテキストとしてコピーする(右クリックからやらないとCtrl+Cではうまくコピーされない..)




さて、このままでもシンプルに使いこなすことはできるでしょうが、できればImageクラスのようにFont用のクラスをつくって管理しておきたいところです。
では、さっそくつくっていきましょう。

Font.hを新規作成してフォントデータを生成して辞書配列std::unordered_mapで管理するクラスを定義しましょう。

#ifndef FONT_H_
#define FONT_H_

#include "DxLib.h"
#include <assert.h> // 読込み失敗表示用
#include <string>
#include <unordered_map>


// フォント設定の管理や読み込みを行うクラス
class Font
{
public:
    // フォントの設定の構造体
    struct Setting
    {
        int size = -1; // フォントのサイズ( おおよそのドット数 -1にするとデフォルトのサイズ )
        int thick = -1; // フォントの太さ( 0~9 -1にするとデフォルトの太さ )
        int fontType = -1; // フォントのタイプ [参考] https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10
        int charSet = -1;
        int edgeSize = -1; // フォントの縁のエッジのサイズ
        int italic = FALSE; // イタリック体で表示するか
        int handle = -1; // CreateFontToHandle関数で帰ってくるハンドル https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10

        const std::string tag = ""; // 設定にタグをつけて管理する
        std::string fontName; // 作成するフォント名(NULL にするとデフォルトのフォント)

        // std::unordered_mapの値に使うにはデフォルトコンストラクタが必要
        Setting() = default;//←これが無いと「クラス、構造体、..に規定のコンストラクタがありません」エラー

        Setting(const std::string tag, const TCHAR* FontName, int Size, int Thick,
                                int FontType = -1, int CharSet = -1, int EdgeSize = -1, int Italic = FALSE)
          : size{ Size }, thick{ Thick }, fontType{ FontType }, charSet{ CharSet }, edgeSize{ EdgeSize }, italic{ Italic },
                tag{ tag }, fontName{ (FontName == NULL) ? "" : std::string(FontName) }
        {
        }

        virtual ~Setting() {}
    };

    // 自分で設定したフォント設定の辞書
    static std::unordered_map<std::string, Setting> fonts;

    Font() {}; // 初期化コンストラクタ
    virtual ~Font() {}; // 破棄するデストラクタ

    static void Init()
    {
        // ゲーム起動時から終了までずっと使うフォントはまとめてここで読込む形もアリ
        //Setting font1{ "設定1", "MS 明朝", 40, 3 };
        //Load(&font1); // 設定に沿ってフォントをデータ化しておく
    }

    // Settingに沿ってCreateFontToHandle関数でフォントデータを作成して辞書に登録しておく https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10
    static int Load(Setting* setting = nullptr);

    // 作成しておいたフォントデータを削除する(ひとつあたり1.2MB程のメモリを使う)
    static int DeleteFontToHandle(Setting* setting = nullptr);

private:

};
#endif



Font.cppも必要になります。

Font.cppを新規作成してフォントデータを生成したり、メモリ上から削除して節約したりする処理をつくりましょう。

#include "Font.h"

std::unordered_map<std::string, Font::Setting> Font::fonts; // フォントの辞書


int Font::Load(Setting* setting)
{
    // 設定がnullptrだったらデフォルトのフォントを返す
    if (setting == nullptr)
        return GetDefaultFontHandle();

    // すでに辞書に登録があったら作成したフォントデータを削除しておく
    if (fonts.count(setting->tag) != 0)
        DxLib::DeleteFontToHandle(fonts[setting->tag].handle);

    // https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10
    // フォントを作成し作成したデータの識別番号をsetting->handle に保存する
    setting->handle = CreateFontToHandle((setting->fontName == "") ? NULL : setting->fontName.c_str(), setting->size, setting->thick,
                                            setting->fontType, setting->charSet, setting->edgeSize, setting->italic, setting->handle);

    assert(setting->handle != -1 && "Font名が間違っているかFontフォルダ指定をミスっていませんか?"); // フォント作成失敗

    // 読み込んだハンドルを辞書に登録しておく
    fonts.emplace(setting->tag, *setting);

    return setting->handle;
}

// フォントがメモリを圧迫してるときに使う(フォントデータはひとつ1.2MB程のメモリを使う)
int Font::DeleteFontToHandle(Setting* setting)
{
    // 設定がnullptrだったら何もせずreturn
    if (setting == nullptr)
        return 0;

    // すでに辞書に登録があったら
    if (fonts.count(setting->tag) != 0)
    { // 作成したフォントデータを削除
        int result = DxLib::DeleteFontToHandle(fonts[setting->tag].handle);
        if(result == 0)
            fonts.erase(setting->tag); // 辞書からも削除
    }

    setting->handle = -1; // -1に戻して未ロード状態がわかるようにする

    return setting->handle;
}


TitleScene.hを修正してFontクラスから生成したフォントデータからスコアを表示してみましょう。

#ifndef TITLESCENE_H_
#define TITLESCENE_H_

#include "Scene.h"

#include <string>

#include "DxLib.h"
(中略)...........
#include "Font.h"
(中略)...........

class TitleScene : public Scene
{
public:
(中略)...........
    // ★dp4FontViewerを使ってInfoからFont名を得て↓コピぺすればつづりを間違えない https://us.fontviewer.de/Download/
    Font::Setting font = Font::Setting("設定1", "HG丸ゴシックM-PRO", 40, 7, DX_FONTTYPE_ANTIALIASING_EDGE_4X4, -1, 3, FALSE);

    int font1 = -1; //フォントのハンドルをCreateFontToHandle関数で得る
    
    // コンストラクタ
    TitleScene() : Scene()
    {
        this->tag = "TitleScene";
    }
    
    // 初期化処理
    void Initialize() override
    {
        Font::Load(&font); // 設定に沿ってフォントをデータ化しておく
        

        // "MS 明朝"のフォントで、サイズ40、太さ7
        // エッジサイズ3、DX_FONTTYPE_ANTIALIASING_EDGE_4X4 : アンチエイリアス&エッジ付きフォント( 4x4サンプリング )
        // イタリック=TRUEのフォントを作成し
        // 作成したデータの識別番号を変数 font1 に保存する
        font1 = DxLib::CreateFontToHandle("MS 明朝", 40, 7, DX_FONTTYPE_ANTIALIASING_EDGE_4X4, -1, 3, TRUE);
    
    (中略)...........
    
    } (中略)...........
    // 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
    void Finalize() override
    {
        Font::DeleteFontToHandle(&font); // フォントデータを削除してメモリ節約(1フォントあたり1.2MB節約できる)
        

    (中略)...........
    } (中略)...........
    
    // 描画処理
    void Draw() override
    {
        SetFontLineSpaceToHandle(40, font1); // 1行ごとのスペース
        
        DrawStringToHandle(0, 0, "TitleSceneです。\nボタン押下でPlaySceneへ。", GetColor(0, 255, 0), font1, GetColor(0, 0, 255));
        
        //[テストしやすくするため一旦コメントアウト] if (sm.scoreMax >= 0)
        {
            DrawStringToHandle(0, 200, (std::string("MAXスコア: ") + std::to_string(sm.scoreMax)).c_str(), GetColor(255, 255, 255), Font::fonts["設定1"].handle, GetColor(255, 0, 0));
        }

(中略)...........

    }
};

#endif




どうでしょうか?表示されましたか?

ひとつ問題として、同じWindows同士でも入っているフォントが違っていたりすると、
ゲームをプレイしてもらう相手にフォントがない場合は、フォントが表示されなくなるおそれがあります。
これを解決するにはDXライブラリをインストールしたフォルダに付属しているToolツールフォルダにある CreateDXFontData.exeをコマンドプロンプトで使用して書き出されるDFT形式を使用する対策があります。
Windowsのデフォルトで入っているフォントも著作物ですから、安全を考えて個人商用フリーのフォントをWEBでさがしてみましょう。(利用規約はしっかり読みましょう)
https://coliss.com/articles/freebies/japanese-free-fonts.html

フリーフォントのインストールの仕方は下記を参考に
https://www.pc-koubou.jp/magazine/58097





フリーフォントのフォント名がコピー出来たら、左下のWindowsの検索窓からコマンドプロンプトを検索して起動しましょう。
コマンドプロンプトはGUI(Gグラフィカルな Userユーザー Interfaceインターフェース)と違い、
1990年代のCUI(Commandコマンド形式のUserユーザー Interfaceインターフェース)で、cdなどのコマンドを文字で打ち込み、フォルダをたどる仕組みです。
cd \コマンドでWindow C:直下のルートフォルダに移動します。
cd と打って、タブTabボタンを押すとC:フォルダの中にあるフォルダが順番に表示されますのでDxLib_VCフォルダを探しましょう。
あとは順々にDxLib_VC/Tool/CreateDXFontDataフォルダまでたどりましょう。
DxLib_VC/Tool/CreateDXFontDataフォルダにはCreateDXFontData.exeがありますので
Sampleフォルダにある~.batやreadme.txtをメモ帳に書いてあることを参考に
CreateDXFontData.exe /F"コピーしたフォント名" /B4 /S32 /O"MyFont1.dft"などのコマンドをうちEnterで実行してMyFont1.dftができたら成功です。




MyFont1.dftができたらImageフォルダと同じフォルダにFontフォルダを作成して、生成したMyFont1.dftファイルをコピーしておきましょう

Font.hを修正してDFT形式のファイルからフォントデータを生成して表示する処理を定義しましょう。

#ifndef FONT_H_
#define FONT_H_

#include "DxLib.h"
#include <assert.h> // 読込み失敗表示用
#include <string>
#include <unordered_map>


// フォント設定の管理や読み込みを行うクラス
class Font
{
public:
    // フォントの設定の構造体
    struct Setting
    {
        int size = -1; // フォントのサイズ( おおよそのドット数 -1にするとデフォルトのサイズ )
        int thick = -1; // フォントの太さ( 0~9 -1にするとデフォルトの太さ )
        int fontType = -1; // フォントのタイプ [参考] https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10
        int charSet = -1;
        int edgeSize = -1; // フォントの縁のエッジのサイズ
        int italic = FALSE; // イタリック体で表示するか
        int handle = -1; // CreateFontToHandle関数で帰ってくるハンドル https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10

        bool isDFTFont = false; // DXライブラリ付属ツール C:\DxLib_VC\Tool\CreateDXFontData で書き出したDFT形式を使用するかどうか
        const std::string tag = ""; // 設定にタグをつけて管理する
        std::string fontName; // 作成するフォント名(NULL にするとデフォルトのフォント)

        // std::unordered_mapの値に使うにはデフォルトコンストラクタが必要
        Setting() = default;//←これが無いと「クラス、構造体、..に規定のコンストラクタがありません」エラー

        Setting(const std::string tag, const TCHAR* FontName, int Size, int Thick,
                                int FontType = -1, int CharSet = -1, int EdgeSize = -1, int Italic = FALSE)
          : size{ Size }, thick{ Thick }, fontType{ FontType }, charSet{ CharSet }, edgeSize{ EdgeSize }, italic{ Italic },
                tag{ tag }, fontName{ (FontName == NULL) ? "" : std::string(FontName) }
        {
            this->isDFTFont = false; // 引数を4つ以上指定する初期化ならCreateFontToHandleを使用する

        }

        Setting(const std::string tag, const TCHAR* DFTFontPath, int EdgeSize = -1)
          : tag{ tag }, fontName{ (DFTFontPath == NULL) ? "" : std::string(DFTFontPath) }, edgeSize{ EdgeSize }
        {
            this->isDFTFont = true; // 引数を3つ以下指定する初期化ならDFT形式のフォントをロードする
        }


        virtual ~Setting() {}
    };

    // 自分で設定したフォント設定の辞書
    static std::unordered_map<std::string, Setting> fonts;

    Font() {}; // 初期化コンストラクタ
    virtual ~Font() {}; // 破棄するデストラクタ

    static void Init()
    {
        // ゲーム起動時から終了までずっと使うフォントはまとめてここで読込む形もアリ
        //Setting font1{ "設定1", "MS 明朝", 40, 3 };
        //Load(&font1); // 設定に沿ってフォントをデータ化しておく
    }

    // Settingに沿ってCreateFontToHandle関数でフォントデータを作成して辞書に登録しておく https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10
    static int Load(Setting* setting = nullptr);

    // 作成しておいたフォントデータを削除する(ひとつあたり1.2MB程のメモリを使う)
    static int DeleteFontToHandle(Setting* setting = nullptr);

private:

};
#endif



Font.cppも修正が必要になります。

Font.cppを修正してLoadFontDataToHandle関数でDFT形式のフォントファイルを読み込む処理をつくりましょう。

#include "Font.h"

std::unordered_map<std::string, Font::Setting> Font::fonts; // フォントの辞書


int Font::Load(Setting* setting)
{
    // 設定がnullptrだったらデフォルトのフォントを返す
    if (setting == nullptr)
        return GetDefaultFontHandle();

    // すでに辞書に登録があったら作成したフォントデータを削除しておく
    if (fonts.count(setting->tag) != 0)
        DxLib::DeleteFontToHandle(fonts[setting->tag].handle);

    // https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10
    // フォントを作成し作成したデータの識別番号をsetting->handle に保存する
    if (setting->isDFTFont == false || setting->fontName == "")
        
setting->handle = CreateFontToHandle((setting->fontName == "") ? NULL : setting->fontName.c_str(), setting->size, setting->thick,
                                                setting->fontType, setting->charSet, setting->edgeSize, setting->italic, setting->handle);
    else // DXフォントデータファイルを読込み、フォントハンドルを保存する
        setting->handle = LoadFontDataToHandle(setting->fontName.c_str(), setting->edgeSize);


    assert(setting->handle != -1 && "Font名が間違っているかFontフォルダ指定をミスっていませんか?"); // フォント作成失敗

    // 読み込んだハンドルを辞書に登録しておく
    fonts.emplace(setting->tag, *setting);

    return setting->handle;
}

// フォントがメモリを圧迫してるときに使う(フォントデータはひとつ1.2MB程のメモリを使う)
int Font::DeleteFontToHandle(Setting* setting)
{
    // 設定がnullptrだったら何もせずreturn
    if (setting == nullptr)
        return 0;

    // すでに辞書に登録があったら
    if (fonts.count(setting->tag) != 0)
    { // 作成したフォントデータを削除
        int result = DxLib::DeleteFontToHandle(fonts[setting->tag].handle);
        if(result == 0)
            fonts.erase(setting->tag); // 辞書からも削除
    }

    setting->handle = -1; // -1に戻して未ロード状態がわかるようにする

    return setting->handle;
}



TitleScene.hを修正してFontクラスから生成したフォントデータからスコアを表示してみましょう。

#ifndef TITLESCENE_H_
#define TITLESCENE_H_

#include "Scene.h"

#include <string>

#include "DxLib.h"
(中略)...........
#include "Font.h"
(中略)...........

class TitleScene : public Scene
{
public:
(中略)...........
    // ★dp4FontViewerを使ってInfoからFont名を得て↓コピぺすればつづりを間違えない https://us.fontviewer.de/Download/
    //Font::Setting font = Font::Setting("設定1", "HG丸ゴシックM-PRO", 40, 7, DX_FONTTYPE_ANTIALIASING_EDGE_4X4, -1, 3, FALSE);
    Font::Setting font = Font::Setting("設定1", "Font/MyFont1.dft", 2);
    int font1 = -1; //フォントのハンドルをCreateFontToHandle関数で得る
    
    // 初期化処理
    void Initialize() override
    {
        Font::Load(&font); // 設定に沿ってフォントをデータ化しておく
        
        // "MS 明朝"のフォントで、サイズ40、太さ7
        // エッジサイズ3、DX_FONTTYPE_ANTIALIASING_EDGE_4X4 : アンチエイリアス&エッジ付きフォント( 4x4サンプリング )
        // イタリック=TRUEのフォントを作成し
        // 作成したデータの識別番号を変数 font1 に保存する
        font1 = DxLib::CreateFontToHandle("MS 明朝", 40, 7, DX_FONTTYPE_ANTIALIASING_EDGE_4X4, -1, 3, TRUE);
    
    (中略)...........
    
    } (中略)...........
    
    // 描画処理
    void Draw() override
    {
        SetFontLineSpaceToHandle(40, font1); // 1行ごとのスペース
        
        DrawStringToHandle(0, 0, "TitleSceneです。\nボタン押下でPlaySceneへ。", GetColor(0, 255, 0), font1, GetColor(0, 0, 255));
        
        //[テストしやすくするため一旦コメントアウト] if (sm.scoreMax >= 0)
        {
            DrawStringToHandle(0, 200, (std::string("MAXスコア: ") + std::to_string(sm.scoreMax)).c_str(), GetColor(255, 255, 255), Font::fonts["設定1"].handle, GetColor(0, 255, 255));
        }

(中略)...........

    }
};

#endif


どうでしょうか?これで他の人のPCに入っていないフリーのフォントでも.dtf形式のファイルをFontフォルダにおいてゲームを送れば、文字が表示されない心配がなくなります。

かっこいいフリーのフォントを探して取り入れるだけでもゲームの印象はがらりと宇宙っぽくもデジタルっぽくもポップにもなります。
センスとはチョイスと組み合わせです。そしてそこをさぼるとユーザーには確実に作り手の熱量を見抜かれてしまいます
探して選んで組み合わせること。その地道な積み上げは暗黙に人に伝わってしまうのです。
センスとは積み上げられるものです。過去の試行錯誤が結果的に知らない人から見るとひらめきセンスのように見えてしまうだけです




スコアの加算処理と背景の加速

さて現状では、スコアは表示されますが、敵を倒してもスコアは増えていません。
今度はそこを改造して、スコアをカウントしてみましょう。


PlayScene.hのプレイヤ弾と敵の衝突判定にスコアをふやす処理を追加してみましょう。

#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_

#include "Scene.h"

(中略)................

class PlayScene : public Scene
{
public:
(中略)................
int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる (中略)................

    // 更新処理
    void Update() override
    {
        // ここにプレイ中の更新処理を持ってくる

(中略)................

        // 自機弾と敵の衝突判定
        for (const auto& playerBullet : gm.playerBullets)
        {
            // 自機弾が死んでたらスキップする
            if (playerBullet->isDead)
                continue;

            for (const auto& enemy : gm.enemies)
            {
                // 敵が死んでたらスキップする
                if (enemy->isDead)
                    continue;

                // 自機弾と敵が重なっているか?
                if (MyMath::CircleCircleIntersection(
                    playerBullet->position, playerBullet->collisionRadius,
                    enemy->position, enemy->collisionRadius))
                {
                    // 重なっていたら、それぞれのぶつかったときの処理を呼び出す
                    enemy->OnCollisionPlayerBullet(playerBullet);
                    playerBullet->OnCollisionEnemy(enemy);

                    Sound::Play(Sound::bomb); // 効果音の再生
                    ++score; // 敵を倒すとスコアが+1される
                    bgSpeed += 0.05f; // 敵を倒せば倒すほど背景を加速させる演出


                    // 衝突の結果、自機弾が死んだら、この弾のループはおしまい
                    if (playerBullet->isDead)
                        break;
                }
            }
        }

(中略)................
    }

(中略)................
};

#endif


さて敵をたくさん倒してタイトル画面に戻るとスコアが増えていたでしょうか?
おまけでスコアが増えるタイミングで背景を加速させる処理も追加しておきました。

画面にはスコアを表示させていませんが、背景の加速でスコアが増えてゲームがヒートアップしていることを実感できるのではないですか?

画面にスコアを表示させるのもいいですが、数字を文字で表現するよりも数字を何かの演出につなげる方が人の直感に訴えかけるものがあります

私は自分の好きなゲームの面白さを蒐集(しゅうしゅう)して自己分析してきた結果、抽出できた構造は

        「ゲームとは状態の遷移でできている」ということです。

たとえば、RPGでレベルが上がるということは「状態(状況)が変わっていく」ということです。
逆に言うならレベルを上げている最中なかなかレベルが上がらないときには「状態が変わらず」「単調で眠くなる」といえるわけです。
ソーシャルゲームの開発現場でプレイヤのレベルが上がるのにあわせた並走する形の成長曲線で敵も強くなるという仕様を聞いたとき、
え、絶対売れないじゃん「プレイヤが俺つよいと感じられる瞬間を数値設計演出できてない → 状態が単調になる」そんな簡単なこともわかってないのか..と思いました。

逆にいうなら、今作っているこんな単純なシューティングでも「背景を加速させるというシンプルな状態の変化の演出」を入れるだけで熱量を感じさせることは可能なのです。


少なくとも自分がゲームを作るなら、パワポの上でアイデアを考えるよりも、
    ゲームの状態の遷移とユーザーの心の遷移の連動を頭の中でイメージしてユーザーがどの時点で先を予想できてあきてしまうか、までは見えてしまいます。
    自分の心の遷移の先回りをされていると感じるときはゲームに明確な作り手の意図を感じられますし、難しすぎるときは自分が無視されているような感覚を感じます
    そういう意味では作り手(自分)とユーザー(自分や他人)の「表現物としてのゲームを通じた対話」がゲームを作る繰り返しではないかと感じます。

私は自分の気持ち良いと感じる状態遷移を蒐集しました、
― マリオのスター無敵状態(曲の加速)、
― テトリスのブロックが上のほうでどんどん積みあがってどんどん追いつめられる瞬間(積みあがる間隔の加速)、
― 果てはアンドロイドスマホのブラウザスクロールは気持ちよくないのにiPhoneのブラウザのスクロールが気持ちいいのはなぜか?(スクロールの加速の仕方)。

私はおそらく「加速」の状態遷移、が好きなのだと思います。

あなたも、ぜひ「自分の面白さの蒐集(しゅうしゅう)と分析」を始めてみてください。
あるいは、逆の「面白くなさの蒐集(しゅうしゅう)と分析」もアリかもしれません(面白くなさでゲームをあきらめる「=あきらかになる」ことも一歩一歩プラスになる)。

さもなくば、知らないうちにあなたのゲームへの向き合い方の状態はずっと同じ状態【死 状態】におちいってしまうかもしれません。




スコアのランキング表示

さて、現状でもこれまでの最大MAXスコアは表示できていますが、MAXを更新しなければ、ユーザーはやりがいを感じられなくなってしまうかもしれません。
一番簡単な解決策としては、スコアを配列でランキングとして管理して、
No.1(1位)になれなくてもプレイ結果が今までのプレイと比べてどの順位にいるかわかるだけでも、繰り返しプレイするときのやりがいは変わってきます。(1プレイごとの状態の変化)

SceneManager.hにスコアのランクとランキング配列を定義してランキングを管理できるようにしましょう。

#ifndef SCNENEMANAGER_H_
#define SCNENEMANAGER_H_

(中略)..........

#include "Singleton.h"

class Scene; //クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる

class SceneManager : public Singleton<SceneManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
    friend class Singleton<SceneManager>; // Singleton でのインスタンス作成は許可

   
    // マネージャを【どこからでもアクセスしやすい「変数の掲示板」として使えばシーンをまたぐ変数も定義できる】
    std::string selectStage = "stage1"; // 選択中のステージ名など
    int scoreMax = -1; // 【シーンをまたぐスコアなど】はマネージャに定義すれば【シーンをまたいだあとも】消えず残る
    int ranking[5] = {-1,-1,-1,-1,-1}; // スコアトップ5を記録しておく配列
    int rank = 0; // 直前のゲーム終了したときの順位

   
(中略)..........

};

#endif


つぎにプレイの画面終了時にスコアを降順に並べ替えながらランキングに登録する処理をくわえます。


【勉強】順序を並べ替えるソートの仕方はいろいろ種類があります。プログラミングの仕事をするのであれば常識ですので(就職や転職のオンラインコーディングテストでよく出る)、
これを機会にぜひ勉強しておきたいところですね。おすすめはアルゴリズムビジュアル大辞典です。

QRコードからデータの動きをアニメーションで見れて、実感がわきやすいです。
    アルゴリズムビジュアル大辞典のアニメ一覧サイト:https://yutaka-watanobe.github.io/star-aida/books/
    今回使う挿入ソート: https://yutaka-watanobe.github.io/star-aida/1.0/algorithms/insertion_sort/anim.html

下記のサイトで[Run]ボタンを押す並べ替えの速度の違いを実感できるとおもいます。
    挿入ソート: https://ufcpp.net/study/algorithm/sort_insert.html
    バブルソート: https://ufcpp.net/study/algorithm/sort_bubble.html
    選択ソート: https://ufcpp.net/study/algorithm/sort_select.html
    シェルソート: https://ufcpp.net/study/algorithm/sort_shell.html
    クイックソート: https://ufcpp.net/study/algorithm/sort_quick.html
    ヒープソート: https://ufcpp.net/study/algorithm/sort_heap.html
    マージソート: https://ufcpp.net/study/algorithm/sort_merge.html
    バケットソート: https://ufcpp.net/study/algorithm/sort_bucket.html
    基数ソート: https://ufcpp.net/study/algorithm/sort_radix.html

基本的にはクイックソートが速度の王様だと記憶しておけばよいです。(問題設定の条件による)
std::qsortの使い方 : https://runebook.dev/ja/docs/cpp/algorithm/qsort
クイックソートは安定ソートではない不安定ソート(=並べ替え前とあとで同じ値を持つ人の順位の変わる可能性がある)です。
不安定な具体例. 前回 3位:Aさん(10点)、4位:Bさん(10点) → 次回 3位:Bさん(10点)、4位:Aさん(10点)→おなじ点数なのに順位が毎回不安定..

自分でソート処理を書かなくてもC++のstd::sort関数を使えば楽だが、
教養として最低限、std::sort関数クイックソートの改良版のイントロソートをベースとした不安定ソートであることを知ったうえで使うべきであり、
安定ソートが必要な場合は、必要性に応じて、std::stable_sort関数安定なマージソート系の並べ替えの仕方もあることも知識として知っている必要はあります。
(色んなソートの種類があるとを知ったうえで、std::sort関数で1,2行プログラムを書いて並べ替えるのと、何もわからないけどとりあえずstd::sort関数使うだけなのとは、素人の魔法使いと熟練の魔法使いぐらいの差があります)


さて、今回は勉強を兼ねて、比較的コードも単純で、安定ソートである挿入ソートを自分で書いて、ランキングにゲームのスコアを挿入する練習をしてみましょう。


PlayScene.hのシーンの終了時(GameOver時でもよいが..どこでやるかは迷いどころ)にスコアをランキングに順序をソートしながら登録する修正をくわえましょう。

#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_

#include "Scene.h"

#include "DxLib.h"
(中略).....................

class PlayScene : public Scene
{
public:
(中略).....................

    // 終了処理(大抵Initializeと同じリセットだが終了時だけやりたいリセットの仕方もあるかも)
    void Finalize() override
    {
(中略).....................

        // ゲーム終了時のスコアがMAXなら記録して終了
        if(score > sm.scoreMax) sm.scoreMax = score;
       
        // ゲーム終了時のスコアをranking配列に挿入ソートして記録
        int j, iSize = sizeof(sm.ranking) / sizeof(int);
        if (score < sm.ranking[iSize - 1])
            sm.rank = -1; // ランキング外
        else
        { // ranking配列の一番後ろは一番小さいのでまずはそれと入れ替え
            sm.ranking[iSize - 1] = score;
           
            // 挿入ソートでランキングを降順に並べ替え
            // https://www.momoyama-usagi.com/entry/info-algo-sort-basic#i-3
            // https://ufcpp.net/study/algorithm/sort_insert.html
            for (int i = 1; i < iSize; ++i)
            {
                j = i; // 交換要素のためのインデックス

                // while(j > 0 && sm.ranking[j-1] > sm.ranking[j]) に変えると昇順ソートになる
                while ((j > 0) && (sm.ranking[j - 1] < sm.ranking[j]))
                {
                    // 整列されていない隣り合う要素を交換する
                    int tmp = sm.ranking[j - 1];
                    sm.ranking[j - 1] = sm.ranking[j];
                    sm.ranking[j] = tmp;

                    j--; // 1つ左のデータへ
                }
            }

            // 何位かを判定
            for (int i = 0; i < iSize; ++i)
                if (sm.ranking[i] == score)
                {
                    sm.rank = i; // ランキング順位 0位~4位
                    break; // スコアが=イコールになるものを見つけたらすぐbreakでfor文を抜ける
                }
        }

    }

(中略).....................
};

#endif




TitleScene.hを修正してプレイしたスコアのランキングをFontで文字列を改行しながら表示してみましょう。

#ifndef TITLESCENE_H_
#define TITLESCENE_H_

#include "Scene.h"

#include <string>

#include "DxLib.h"
(中略)...........
#include "Font.h"
(中略)...........

class TitleScene : public Scene
{
public:
(中略)...........
    
    // 描画処理
    void Draw() override
    {
        int lineSize = 40; // 1行の縦方向の改行サイズ
        SetFontLineSpaceToHandle(lineSize, font1); // 1行ごとのスペース
        
        DrawStringToHandle(0, 0, "TitleSceneです。\nボタン押下でPlaySceneへ。", GetColor(0, 255, 0), font1, GetColor(0, 0, 255));
        
        if (sm.scoreMax >= 0)
        {
            DrawStringToHandle(0, 200, (std::string("MAXスコア: ") + std::to_string(sm.scoreMax)).c_str(), GetColor(255, 255, 255), Font::fonts["設定1"].handle, GetColor(0, 255, 255));
            
            // 順位 0位~4位を1位から5位に直すため +1する→ sm.rank + 1
            std::string myRankText = (sm.rank < 0) ? std::string("ランク イン シッパイ") : std::string("アナタ ハ イマ No.") + std::to_string(sm.rank + 1) + std::string(" ") + std::to_string(sm.ranking[sm.rank]);
            DrawStringToHandle(0, 200 + lineSize, myRankText.c_str(), GetColor(255, 255, 255), Font::fonts["設定1"].handle, GetColor(255, 0, 255));

            std::string rankText; // ランキングを改行しながら複数行のテキストに
            for (int i=0, iSize = sizeof(sm.ranking) / sizeof(int); i < iSize; ++i)
            {
                if(sm.ranking[i] >= 0) // 配列の初期値は-1は表示せずとばす
                    rankText += std::string("No.") + std::to_string(i + 1) + std::string(" ") + std::to_string(sm.ranking[i]) + std::string("\n");
            }
            DrawStringToHandle(0, 200 + lineSize*2, rankText.c_str(), GetColor(255, 255, 255), Font::fonts["設定1"].handle);
        

        }

(中略)...........

    }
};

#endif



さて、うまくランキング表示ができたでしょうか?何度か自滅して5回ぐらい自滅しないとランキングは表示されないのでバグだと勘違いしないでくださいね。



スコアのセーブ保存とCSVのセルを受けint,float,文字列に柔軟に化けるCsvValue型



DataCsv.hにCSVのセル(int,float,文字列の3種類)を読み取れるCsvValueクラスを追加して、さらにSave機能を追加します。

#ifndef DATACSV_H_
#define DATACSV_H_

#include <assert.h> // 読込み失敗表示用
#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列に必要
#include <fstream> // ファイル読み出しifstreamに必要
#include <sstream> // 文字列ストリームに必要

//★CsvValue型をint型や文字列string型や小数float型として【カメレオン】のように使える。
// 【エクセルはマス目がカメレオン】整数も小数も文字列もありうる。
// それを解消して統一して扱える便利な★CsvValue型。
class CsvValue
{
public:
    enum class Type
    {
        Int,
        Float,
        String,
        NUM
    };
    Type type = Type::String;
    
protected: // 内部データを書き換え禁止:書き換えられるとintと文字列とのずれが起きうる
    // エクセルのCsvデータはマス目がint,float,stringの3種類ありうる
    union { // ★共用体unionテクニック https://inemaru.hatenablog.com/entry/2016/03/02/005408
        int intData = 0; // 整数int型データを格納
        float floatData; // 小数float型データを格納
    };
    std::string stringData{""}; // 文字列string型データを格納
    
public:
    CsvValue() = default;
    // 3種類のCSVセル(int,float,文字列std::stringとconst char*:計4種)のコンストラクタ
    CsvValue(int intData) : type{ Type::Int }
    {
        this->intData = intData;
        this->stringData = std::to_string(intData);
    }
    
    CsvValue(float floatData) : type{ Type::Float }
    {
        this->floatData = floatData;
        this->stringData = std::to_string(floatData);
    }
    
    CsvValue(std::string& stringData) : type{ Type::String }
    {
        this->stringData = stringData;
    }
    
    CsvValue(const char* stringData) : type{ Type::String }
    {
        this->stringData = stringData;
    }
    
    virtual ~CsvValue() {}; // 仮想デストラクタ
    
    // 変換演算子によりCSVのセルをintとしてもstringとしても扱えるように
    // https://programming-place.net/ppp/contents/cpp/language/019.html#conversion_op
    // 3種類の読み取りオペレータ
    inline operator int()
    {
    if (type == Type::Int) return intData;
    else if (type == Type::Float) return (int)floatData;
    else if (type == Type::String && intData == 0)
         intData = intFromString(); // デフォルトで文字列として読込んだデータをintに変換
    return intData;
    }
    
    inline operator float()
    {
    if (type == Type::Float) return floatData;
    else if (type == Type::Int) return (float)intData;
    else if (type == Type::String && intData == 0)
         floatData = floatFromString(); // デフォルトで文字列として読込んだデータをfloatに変換
    return floatData;
    }
    
    inline operator std::string()&
    {
         return stringData;
    }
    
    int intFromString() // 文字列データからint型への変換する関数
    {
         std::istringstream ss(stringData); //文字列ストリームの初期化
         int num; // 数字単体
         ss >> num; // 文字列ストリームから数字への変換
         return num;
    }
    
    float floatFromString() // 文字列データからfloat型への変換する関数
    {
         std::istringstream ss(stringData); //文字列ストリームの初期化
         float num; // 数字単体
         ss >> num; // 文字列ストリームから数字への変換
         return num;
    }
    
private:
    void clearString() // 文字列をcapacityごとクリアしてメモリ節約
    {    // https://stackoverflow.com/questions/740030/how-to-release-the-unused-capacity-of-a-string
        std::string("").swap(stringData); // [capacityをリセットしてメモリ節約]
    }
    
public:
    CsvValue& operator = (const int other) { // 代入=演算子
        type = Type::Int; // タイプを更新
        if (type == Type::Float) floatData = (float)other;
        else intData = other;
        
        if (type == Type::String) std::to_string(other).swap(stringData); // [swapテクでcapacity節約]
        else clearString();
        
        this->stringData = std::to_string(intData);
        return *this;
    }
    
    CsvValue& operator = (const float other) { // 代入=演算子
        type = Type::Float; // タイプを更新
        if (type == Type::Int) intData = (int)other;
        else floatData = other;
        
        if (type == Type::String) std::to_string(other).swap(stringData); // [swapテクでcapacity節約]
        else clearString();
        
        this->stringData = std::to_string(floatData);
        return *this;
    }
    
    CsvValue& operator = (const std::string& other) { // 代入=演算子
        type = Type::String; // タイプを更新
        intData = 0; // 初期値 0 としてリセット
        stringData = other;
        return *this;
    }
    
    CsvValue& operator = (const char* other) { // 代入=演算子
        type = Type::String; // タイプを更新
        intData = 0; // 初期値 0 としてリセット
        stringData = std::string(other);
        return *this;
    }
};

namespace { // 無名名前空間で囲んで、.hヘッダにグローバル関数定義をする
    
    // ファイル出力ストリーム演算子<<を定義する
    std::ostream& operator << (std::ostream& stream, CsvValue& csvValue)
    {
        if (csvValue.type == CsvValue::Type::Int) stream << (int)csvValue;
        else if (csvValue.type == CsvValue::Type::Float) stream << (float)csvValue;
        else if (csvValue.type == CsvValue::Type::String) stream << (std::string)csvValue;
    
        return stream;
    }
    
}


// CSVファイルを読込み幅や高さとデータ本体を保持するデータ型
struct DataCsv // ←structはC++ではほぼclassと同じ【違いはデフォルトがpublic】
{   // 読込んだデータファイルの情報
    enum class CsvType
    {
        IntMap,
        CsvValue
    };

    int Width = 0; // csvファイルの表の幅
    int Height = 0;// csvファイルの表の高さ
    bool isInitialized = false; //[2重ロード対策]1度ロードしたらtrueにしてclear()されるまでロード抑止
    bool isForceCreate = false; // ファイルが無い、もしくは空のときにアラートを出さず無理やり(force)新しく作るか(create)
    
    // isForceCreateをセット[*thisをreturnで関数連鎖テク] https://flat-leon.hatenablog.com/entry/cpp_named_parameter
    DataCsv& IsForceCreate(bool b) { isForceCreate = b; return *this; }


    std::string FilePath { "" };
    std::vector<std::vector<CsvValue>> Data;// csvデータ
    
    // 初期化コンストラクタでファイル名を指定して初期化と同時にファイル読込
    DataCsv(std::string filePath = "", CsvType csvType = CsvType::CsvValue) :FilePath{ filePath }
    {// csvファイルの読込み★【初期化と同時なのでファイルとデータ型が一心同体で使いやすい】
        if (FilePath != "") Load(FilePath, csvType); // ファイル読込み
    };
    virtual ~DataCsv()
    {// 仮想デストラクタ
        Data.clear();// 2次元配列データのお掃除
    };
    
    // ★スムーズに[][]でアクセスできるように[]演算子を独自定義する
    std::vector<CsvValue>& operator[](std::size_t index) { // ★ &参照にしないといちいちデータのコピーを返すので遅くなるよ
        return Data[index]; // 書き込み
    }
    std::vector<CsvValue> operator[](std::size_t index) const { // ★constは添え字[]読み取りの処理を定義
        return Data[index]; // 読み取り
    }

    std::size_t size()
    {   // size()関数の名前をvectorと被らせることで使う側はvectorインvectorのままのコードで使える
        return Data.size();
    }

    // データをクリアしてメモリを解放する
    virtual void clear()
    {   // [確実にメモリを空にするには] http://vivi.dyndns.org/tech/cpp/vector.html#shrink_to_fit
        std::vector<std::vector<CsvValue>>().swap(Data); // 空のテンポラリオブジェクトでリセット

        isInitialized = false; //ロード済みフラグをOFF
    }

    // csvファイルの読み込み
    virtual void Load(std::string filePath, CsvType csvType = CsvType::CsvValue)
    {
        if (filePath == "" || isInitialized) return; //ファイル名がないもしくはロード済
        this->FilePath = filePath; // ファイル名を保管
        Data.clear(); //データを一旦クリア

        // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
        std::ifstream ifs_csv_file(filePath);
        // [.good()でファイル存在確認]https://stackoverflow.com/questions/12774207/fastest-way-to-check-if-a-file-exists-using-standard-c-c11-14-17-c
        if (isForceCreate && !ifs_csv_file.good())
        {
            std::ofstream ofs_csv_file(filePath);
            ofs_csv_file << ""; // 空文字のファイルを生成
        }


        std::string line; //1行単位でcsvファイルから文字列を読み込む

        int readWidth = 0; //読込みデータの幅
        int maxWidth = 0; // 1行の数字の最大個数
        int readHeight = 0; //初期化
        //↓2重while文でCSVファイルを読み取る
        while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
        {
            std::vector<CsvValue> valuelist; // 1行の数字リスト
            std::istringstream linestream(line); // 各行の文字列ストリーム
            std::string splitted; // カンマで分割された文字列
            int widthCount = 0; //この行の幅をカウント
            if (csvType == CsvType::IntMap)
            {
                while (std::getline(linestream, splitted, { ',' }))
                {
                    std::istringstream ss; //文字列ストリームの初期化
                    ss = std::istringstream(splitted); //文字列ストリーム
                    int num; // 数字単体
                    ss >> num; // 文字列ストリーム>>で数字へ変換
                    valuelist.emplace_back(num); // 数字を数字のリスト(valuelist)に追加
                    ++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
                }
            }
            else if (csvType == CsvType::CsvValue)
            {
                while (std::getline(linestream, splitted, { ',' }))
                {
                    valuelist.emplace_back(splitted); // 文字列としてリスト(valuelist)に追加
                    ++widthCount; //この行のカンマで区切られた数をカウントアップ
                }
            }

            
            // 1行の幅の数が記録を更新してMAXになるかチェック
            if (widthCount > maxWidth) maxWidth = widthCount; //暫定Max幅を更新

            // 1行分をvectorに追加
            if (valuelist.size() != 0) Data.emplace_back(valuelist);
            ++readHeight; //読み込んだ行(縦)をカウントアップ
        }
        readWidth = maxWidth; //読み込んだ列(横)の数は一番数字の個数の多かった行に合わせる
        if (!isForceCreate) // ファイルがないとき強制的に空ファイルを生成した場合は幅と高さのチェックをスキップする
        {
            //↓読込んだCSVの幅と高さをチェック
            assert(readWidth > 0 && "CSV読込み失敗ファイル名間違いでは?" != "");
            assert(readHeight > 0 && "CSV読込み失敗ファイル名間違いでは?" != "");
        }

        this->Width = readWidth; // 読込み成功したデータの幅を記録
        this->Height = readHeight; // 読込み成功したデータの高さを記録

        // コンストラクタで初期化でロードの場合とLoad関数経由で読む経路があるから2重ロード対策
        isInitialized = true; // 読込み初期化済みフラグをON

        return;
    }

    // csvファイルの保存
    void Save(std::string filePath = "")
    {
        std::string outputPath = filePath;
        if (filePath == "") // ファイル名がないときは読込時のパス
            outputPath = FilePath;
        if (outputPath == "") return; // ファイル名がないとき
        
        FilePath = filePath; // ファイル名を保管
        
        // 読み込むcsvファイルを開く(std::ofstreamのコンストラクタで開く)
        std::ofstream ofs_csv_file(filePath);
        
        std::string line; //1行単位でcsvファイルから文字列を読み込む
        
        //↓2重for文でCSVファイルを列i,行jごとに書き出していく
        for (auto j = 0; j < Data.size(); ++j)
        {
            for (auto i = 0; i < Data[j].size(); ++i)
            {
                if (i > 0) ofs_csv_file << ","; // 区切りのカンマを書き出す
                ofs_csv_file << Data[j][i]; // Data配列を出力
                if (Data[j][i].type == CsvValue::Type::String)
                    ofs_csv_file << "\"" << Data[j][i] << "\""; // "を両端に付け足してData配列を文字列として出力
                else
                    ofs_csv_file << Data[j][i]; // Data配列を数値として出力
            }
            ofs_csv_file << std::endl; // 1行の終わりに改行を出力
        }
        
        return;
    }

};

#endif



上記コードで メソッドチェイン(関数連鎖)のテクニックを使いました。
基本構造は 〇〇型の自分のクラスを定義した際に

〇〇 & 関数名()
{
    ........
    return *this; // 最終的に*thisで自分のクラス自身の参照を返す
}


とやるだけです。

    bool isForceCreate = false; // ファイルが無い、もしくは空のときにアラートを出さず無理やり(force)新しく作るか(create)
    
    // isForceCreateをセット[*thisをreturnで関数連鎖テク] https://flat-leon.hatenablog.com/entry/cpp_named_parameter
    DataCsv& IsForceCreate(bool b) { isForceCreate = b; return *this; }


メソッド連鎖を使うと通常2行で書くような次のような処理をすっきりと表現できるようになります。

DataCsv data;
data.isForceCreate = true;
data.Load(filePath, DataCsv::CsvType::CsvValue);

↑これを、下記↓のように書けます。

DataCsv data;
data.IsForceCreate(true).Load(filePath, DataCsv::CsvType::CsvValue);



Setする変数項目が多かったりするときに効果を発揮します。

DataCsv data;
data.SetA(true).SetB(-1).SetC("テスト").SetD(3.145f).Load(filePath, DataCsv::CsvType::CsvValue);



Load関数のデフォルト引数でisForceCreateをデフォルト引数として渡すか迷いました。

void Load(std::string filePath, CsvType csvType = CsvType::CsvValue, bool isForceCreate=false);

........
data.Load(filePath, DataCsv::CsvType::CsvValue, true); // うーん、あいだに挟まったDataCsv::CsvType::CsvValueをいちいち書かないとtrue渡せないな..省略しにくい




関数連鎖で渡せば、DataCsv::CsvType::CsvValueを省略しやすくなる。

void Load(std::string filePath, CsvType csvType = CsvType::CsvValue);

........
data.IsForceCreate(true).Load(filePath, DataCsv::CsvType::CsvValue, true); // 省略!!

data.IsForceCreate(true).Load(filePath); // 省略しても、csvTypeはデフォルトのDataCsv::CsvType::CsvValueになる





つぎにSaveデータを扱うベースとなる基底クラスSaveDataを定義しましょう(これを : 継承してRankingDataクラスをつくります)

SaveData.hを新規作成します。

#ifndef SAVEDATA_H_
#define SAVEDATA_H_

#include <vector> // セーブデータのファイルパスを複数vector配列で関数に渡すために使用
#include <string> // 文字列に必要

struct SaveData // データをSaveするタイプ あいまいなabstract型
{
    std::vector<std::string> filePaths{ {""} }; // 複数のファイルを同時に呼び出したい場合のため配列に
    SaveData(std::vector<std::string> filePaths = { "" } ) : filePaths { filePaths }
    {
    }
    virtual ~SaveData() {}; // 破棄する処理デストラクタ

// セーブデータのファイルパスを複数vector配列で関数に渡す
    virtual void Load(std::vector<std::string> filePaths = { "" }) {};
    virtual void Save(std::vector<std::string> filePaths = { "" }) {};
private:

};
#endif



RankingData.hを新規作成してSaveDataを継承してCSVのロードとセーブ機能を実装します。

#ifndef RANKING_H_
#define RANKING_H_

#include "SaveData.h"
#include "DataCsv.h"

struct RankingData : public SaveData
{
public:
    const size_t listMAX = 100; // 過去のランキングリストを100位までcsvに保管
    DataCsv list;

    std::vector<CsvValue>& operator[](std::size_t index) {
        return list[index]; // 読み書き
    }

    // コンストラクタ
    RankingData(std::vector<std::string> filePaths = { "" }) : SaveData(filePaths)
    {
         Load(filePaths);
    }

    virtual void Load(std::vector<std::string> filepaths = { "" }) override
    {
        if (filepaths.size() > 0 && filepaths[0] != "")
           filePaths = filepaths;
        
        for (std::size_t i = 0, iSize = filePaths.size(); i < iSize; ++i)
        {
            if (i == 0)
            {    // csvファイルの読み込み
                list.IsForceCreate(true).Load(filePaths[0]);
                if (list.size() == 0)
                { // 何もデータがないときは初期生成
                    std::vector<CsvValue> firstScore = { -1 }; // 0行目: スコア(最後にプレイしたとき)
                    list.Data.emplace_back(firstScore);
                    std::vector<CsvValue> firstRank = { 0 }; // 1行目: ランク順位(最後にプレイしたとき)
                    list.Data.emplace_back(firstRank);
                    std::vector<CsvValue> firstList = { -1 }; // 2行目: 過去スコアのリスト
                    list.Data.emplace_back(firstList);
                }
            }
            //else if (i == 1)
            //{
            // // 複数まとめて同時にファイルを読み込みたければi=1,2..も定義
            //}
        }
        
        return;
    }

    virtual void Save(std::vector<std::string> filepaths = { "" }) override
    {
        if (filepaths.size() > 0 && filepaths[0] != "")
            filePaths = filepaths;
        
        for (std::size_t i = 0, iSize = filePaths.size(); i < iSize; ++i)
        {
            if (i == 0)
            {    // csvファイルの保存
                list.Save(filePaths[0]);
            }
            //else if (i == 1)
            //{
            // // 複数まとめて同時にファイルを保存したければi=1,2..も定義
            //}
        }
        
        return;
    }
};

#endif



つぎにSceneManager.hをSaveDataをRankingDataクラスを使ってLoadする形に変更しますが、
必ず、Imageフォルダと同じ階層にSaveDataという名前でフォルダを新規作成しておかないと保存もロードも失敗しますので注意してください。

SceneManager.hのスコアのランクとランキング配列をRankingDataクラスからロードする形に変えてしてランキングを管理できるようにしましょう。

#ifndef SCNENEMANAGER_H_
#define SCNENEMANAGER_H_

(中略)..........
#include "DataCsv.h"
#include "RankingData.h"

#include "Singleton.h"

class Scene; //クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる

class SceneManager : public Singleton<SceneManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
    friend class Singleton<SceneManager>; // Singleton でのインスタンス作成は許可

    
    // マネージャを【どこからでもアクセスしやすい「変数の掲示板」として使えばシーンをまたぐ変数も定義できる】
    std::string selectStage = "stage1"; // 選択中のステージ名など
    RankingData ranking{ {"SaveData/ranking.csv"} }; // スコアのランキング記録するCSV
    CsvValue& scoreMax = ranking[0][0]; // 今までのMAXスコア &参照でセーブするデータの住所を参照して直接書き換えられるように
    // int ranking[5] = {-1,-1,-1,-1,-1}; // スコアトップ5を記録しておく配列
    CsvValue& rank = ranking[1][0]; // 最後にプレイしたセーブデータのランク順位への&参照
   
(中略)..........

};

#endif


さて、CSVのエクセルのセルが以下のようになっていたとしましょう
0行目:[ 5 ][   ][   ][   ]
1行目:[ 2 ][   ][   ][   ]
2行目:[ 5 ][ 3 ][ 1 ][ 1 ].......

0行目0列 : scoreMax 今までのMAXスコアを保存 ( 上図では5 )
1行目0列 : rank 最後にプレイしたセーブデータのランク順位を保存 ( 上図では2 2位)
2行目以降.. 過去のデータ 5,3,1,1.....
上記を読みだしているコードの対応関係を下記で今後使いこなせるように確認しておきましょう。

    CsvValue& scoreMax = ranking[0][0]; // 今までのMAXスコア &参照でセーブするデータの住所を参照して直接書き換えられるように
    CsvValue& rank = ranking[1][0]; // 最後にプレイしたセーブデータのランク順位への&参照


実際にデータを読み出してゆく際には[2]行目にアクセスしてやっていきます。
TitleSceneやPlaySceneのコードでは[2]に着目しておきましょう。

TitleScene.hのスコアへアクセスする際に、何型かあいまいなCsvValue型を(int)でキャストして型を確定する変更と、CSVのセーブデータ配列の[2]行目形にアクセスする形に修正をくわえましょう。

#ifndef TITLESCENE_H_
#define TITLESCENE_H_

#include "Scene.h"

#include <string>

#include "DxLib.h"
(中略)...........

class TitleScene : public Scene
{
public:
(中略)...........
    
    // 描画処理
    void Draw() override
    {
    (中略)...........
        
       if ((int)sm.scoreMax >= 0)
        {
            DrawStringToHandle(0, 200, (std::string("MAXスコア: ") + std::to_string((int)sm.scoreMax)).c_str(), GetColor(255, 255, 255), Font::fonts["設定1"].handle, GetColor(0, 255, 255));
            
            // 順位 0位~4位を1位から5位に直すため +1する→ sm.rank + 1
            std::string myRankText = ((int)sm.rank < 0) ? std::string("ランク イン シッパイ") : std::string("アナタ ハ イマ No.") + std::to_string((int)sm.rank + 1) + std::string(" ") + (std::string)(sm.ranking[2][(int)sm.rank]);
            DrawStringToHandle(0, 200 + lineSize, myRankText.c_str(), GetColor(255, 255, 255), Font::fonts["設定1"].handle, GetColor(255, 0, 255));

            std::string rankText; // ランキングを改行しながら複数行のテキストに
            for (int i=0, iSize = sm.ranking[2].size(); i < iSize; ++i)
            {
                if((int)sm.ranking[2][i] >= 0) // 配列の初期値は-1は表示せずとばす
                    rankText += std::string("No.") + std::to_string(i + 1) + std::string(" ") + std::to_string((int)sm.ranking[2][i]) + std::string("\n");
            }
            DrawStringToHandle(0, 200 + lineSize*2, rankText.c_str(), GetColor(255, 255, 255), Font::fonts["設定1"].handle);
        
        }

(中略)...........

    }
};

#endif



PlayScene.hのスコアへアクセスする部分をあいまいなCsvValue型にアクセスする部分は(int)に変換し、CSVのセーブデータ配列の[2]行目形にアクセスする形に修正をくわえましょう。

#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_

#include "Scene.h"

#include "DxLib.h"
(中略).....................

class PlayScene : public Scene
{
public:
(中略).....................

    // 終了処理(大抵Initializeと同じリセットだが終了時だけやりたいリセットの仕方もあるかも)
    void Finalize() override
    {
(中略).....................

        // ゲーム終了時のスコアがMAXなら記録
        if(score > (int)sm.scoreMax) sm.scoreMax = score;
       
        if (sm.ranking[2].size() < sm.ranking.listMAX)
            sm.ranking[2].emplace_back( -1 ); // sizeがlistMAX以下のときは後ろに-1を追加

        // ゲーム終了時のスコアをranking配列に挿入ソートして記録
        int j, iSize = sm.ranking[2].size();
        if (score < (int)sm.ranking[2][iSize - 1])
            sm.rank = -1; // ランキング外
        else
        { // ranking配列の一番後ろは一番小さいのでまずはそれと入れ替え
            sm.ranking[2][iSize - 1] = score;
           
            // 挿入ソートでランキングを降順に並べ替え
            // https://www.momoyama-usagi.com/entry/info-algo-sort-basic#i-3
            // https://ufcpp.net/study/algorithm/sort_insert.html
            for (int i = 1; i < iSize; ++i)
            {
                j = i; // 交換要素のためのインデックス

                // while(j > 0 && (int)sm.ranking[2][j-1] > (int)sm.ranking[2][j]) に変えると昇順ソートになる
                while ((j > 0) && ((int)sm.ranking[2][j - 1] < (int)sm.ranking[2][j]))
                {
                    // 整列されていない隣り合う要素を交換する
                    int tmp = sm.ranking[2][j - 1];
                    sm.ranking[2][j - 1] = sm.ranking[2][j];
                    sm.ranking[2][j] = tmp;

                    j--; // 1つ左のデータへ
                }
            }

            // 何位かを判定
            for (int i = 0; i < iSize; ++i)
                if ((int)sm.ranking[2][i] == score)
                {
                    sm.rank = i; // ランキング順位 0位~4位
                    break; // スコアが=イコールになるものを見つけたらすぐbreakでfor文を抜ける
                }
        }
       
        sm.ranking.Save(); // 並べ替えたあとにランキングをCSVファイルにセーブする
    }

(中略).....................
};

#endif


さて、実行すると
SaveDataフォルダにranking.csvファイルが自動生成されていますか?
一回ゲームオーバーになったあとranking.csvを右クリックでメモ帳で開くと中にMAXスコアやランクやランキングデータが保存されていましたか?



これがうまくいけば、あとは自分でいろいろなセーブの仕方を工夫できるようになりますね。
90's年代ファミコンやゲームボーイが出た当時は、セーブ機能自体がないゲームが普通でした。
プレイするごとにスコアは0に戻り、ドラゴンクエストでは「ふっかつのじゅもん」を紙やノートに書き写して、毎回それを打ち込んでゲームの状態を再現してプレイを再開していました。

その後、徐々にセーブ機能がでてきました。
すると、時代はロールプレイングゲームが全盛となっていきました。
つまり、ユーザーの【プレイ状態を保存できること自体がイノベーションの発火】となり【新たなゲームの形態が流行していった】とも考えられませんか?

【今日プレイした状態】を【明日に持ち越せる】ことで、1ヵ月や2ヵ月、はてはポケモンに至っては3,4年の長期間、ユーザーを引き付ける形態のゲームアイデアが実現されていったのです。

    ◆ 逆に発想するならばセーブできることによって、どんな状態を明日や1ヵ月、1年持ち越せると楽しいか?

という視点でゲームを考察したり、アイデアだしをするならば、

ポケモンやソシャゲの本質は【セーブデータの一部の交換や共有や見せあい】であり、今回作ったランキング機能とはその最も簡易な表現形式である、と考えられないでしょうか?

    ◆ 【セーブできること】自体をあたりまえと思わず、そのありがたさ自体から「再発想」してみるのも哲学的ではないですか?

    ◆ さて、あなたのゲームではなにを未来にセーブして持ち越したら面白いと発想しますか?


こののプログラムは【そのインフラを提示したにすぎません】。あとは作り手の発想しだい。どんな形態でどのCSVのセルにセーブしてどう使うか。
とんびが鷹(タカ)を生めることを願って、少々難しい「シーン分け、全方位読出、複数プレイヤ、フォント、セーブ」など基本インフラをゲーム内容より先に準備してきたわけです。

    基本インフラがあれば【作り手によるバリエーション】が生まれる余地がでます。
    プログラミングは無名のプログラマたちの歴代インフラのワンフォーオール精神のかたまりだと思ってください。
    ある程度経験のあるプログラマはまず「ゲームを作るインフラ」のほうに注心しなければゲーム制作がはかどらないことを知っています。
    ゲームよりゲームを作るインフラのほうがゲームのプログラミング現場で重宝される作業だったりします。     インフラが整ってくるとアイデアや作業の選択に幅が生まれます。
    ゲーム内容の制作側はオールフォーワンのように、がめつく、計画的に、憎らしいほど、取り込んだ機能(個性)を使いこなせるといいですね。


キーボードとマウスの入力


重要なインフラとしてキーボード入力の検知とマウスの入力の検知があります。

DXが用意してくれているキーボードの関数は
GetHitKeyStateAll()になります。
https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N28
この関数をGetButton、GetButtonDown、GetButtonUpに対応させて3種類の入力状態を得られるように対応します。

DXが用意してくれているマウスの関数は下記3つを使います。
GetMousePoint(&MouseX, &MouseY)
https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N28
GetMouseInput()
https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N8
GetMouseInputLog2(&MButton, &ClickX, &ClickY, &LogType, TRUE)
https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N40
はい、DXの元の関数だけでは単純にダブルクリックやドラッグを得られるわけではありません
プログラマが頑張ってこれら3つの関数を駆使して、独自にダブルクリックやドラッグを得られるよう頑張ってくれということですね。

がんばって、それらを得られるようにしましょう。

Input.hにキーボードとマウス入力を検知できる機能を追加しましょう。

#ifndef INPUT_H_
#define INPUT_H_

#include <climits> //int型の最大値2147483647につかう
#include <unordered_map> // 辞書配列【map高速版】https://qiita.com/sileader/items/a40f9acf90fbda16af51
#include <string> // キーボード押下の文字列出力につかう

#include "DxLib.h"

// コントローラのパッド番号定義
enum class Pad
{
    Keyboard = -3, // キーボード入力を得る
    All = -2, // すべてのうちどれか1つでも押されたとき
    None = -1, // パッド割当て無(やられたり待機中のプレイヤに)
    Key=0, // キーボード
    One, // コントローラ1
    Two, // コントローラ2
    Three, // コントローラ3
    Four, // コントローラ4
    Mouse, // マウスのボタン判定に
    NUM, // コントローラの数=6(マウス込み)
};

enum class Mouse
{
    All = -2, // すべてのうちどれか1つでも押されたとき
    None = -1,
    DownL, //[左クリック押した瞬間=1]
    DragL, //[左ドラッグ中=1]
    UpL, //[左クリック離した瞬間=1]
    DownR, //[右クリック押した瞬間=1]
    DragR, //[右ドラッグ中=1]
    UpR, //[右クリック離した瞬間=1]
    X, // 現在のマウス位置
    ReleaseX, // 離した状態のマウス位置(ドラッグ中は開始前位置)
    DownLX, //[左] 押した位置
    DragLX, //[左] ドラッグ中の位置
    UpLX, //[左] 離した位置
    DownRX,//[右] 押した位置
    DragRX, //[右] ドラッグ中の位置
    UpRX, //[右] 離した位置
    Y, // 現在のマウス位置
    ReleaseY, // 離した状態のマウス位置(ドラッグ中は開始前位置)
    DownLY, //[左] 押した位置
    DragLY, //[左] ドラッグ中の位置
    UpLY, //[左] 離した位置
    DownRY,//[右] 押した位置
    DragRY, //[右] ドラッグ中の位置
    UpRY, //[右] 離した位置
    NUM
};


// 入力クラス
class Input
{
public:
    const int MaxPadNum = (int)Pad::Four + 1; // 最大パッド数(Keyぶん+1)

    static int prevStates[(int)Pad::NUM]; // 1フレーム前の状態
    static int currentStates[(int)Pad::NUM]; // 現在の状態

    static std::unordered_map<Pad, bool> isJoin; // 参加中のパッド番号辞書
    // 参加中のパッドかどうか?
    static bool IsJoin(Pad pad) { return isJoin.count(pad) == 1 && Input::isJoin[pad]; };

    static std::unordered_map<int, int> padDic; // パッド番号からDXの定義への辞書

    static int prevMouse; // 1フレーム前のマウス状態
    static int currentMouse; // 現在のマウス状態
    static int MouseX, MouseY; // 現在のマウス位置
    static int ClickX, ClickY, MButton, LogType;
    static float MouseWheel; // マウスのホイールの回転
    static std::unordered_map<Mouse, int> Click;//<Mouse⇒int> マウスのクリックXY位置辞書

    //[キーボード入力]https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N28
    static char prevKey[256]; // 1フレーム前のキーボード状態
    static char currentKey[256]; // 現在のキーボード状態
    static std::string KeyString[256];//<キーコード⇒キー文字列>の辞書
    static std::string CapsString[256];//<Caps状態キーコード⇒キー文字列>の辞書
    static bool isCapsLocked;// CapsLock状態か?
    static bool isShifted;// Shiftが押された状態か?

    //★【DXコントローラバグ検知】初回プッシュが被ればDXバグで同一コントローラ
    static int firstPushTiming[(int)Pad::Four]; // Pad::Fourが最後だから8とかに増やすなら要書き換え
    static int timing;// = 0;
    static bool isBugCheckMode;// = false; //バグチェックを開始するか

    // 辞書配列の初期化
    static void InitPadDictionary()
    {
        // 辞書配列で対応関係を結び付けておく
        padDic[(int)Pad::Key] = DX_INPUT_KEY;
        padDic[(int)Pad::One] = DX_INPUT_PAD1;
        padDic[(int)Pad::Two] = DX_INPUT_PAD2;
        padDic[(int)Pad::Three] = DX_INPUT_PAD3;
        padDic[(int)Pad::Four] = DX_INPUT_PAD4;
    }

    // マウスの状態初期化
    static void InitMouse()
    {
        MouseX = -1, MouseY = -1; // 現在のマウス位置を-1で初期状態では画面外として初期化
        ClickX = -1, ClickY = -1, MButton = 0, LogType = 0;
       
        Click[Mouse::ReleaseX] = -1; Click[Mouse::ReleaseY] = -1;
        Click[Mouse::UpLX] = -1; Click[Mouse::UpLY] = -1;
        Click[Mouse::UpRX] = -1; Click[Mouse::UpRY] = -1;
        Click[Mouse::DownLX] = -1; Click[Mouse::DownLY] = -1;
        Click[Mouse::DownRX] = -1; Click[Mouse::DownRY] = -1;
    }

    // キーボードの状態初期化
    static void InitKeyboard()
    {
        // キー状態をゼロリセット
        for (int i = 0; i < 256; i++)
            prevKey[i] = currentKey[i] = 0;
    }

    // 初期化。タイトルなどキー入力状態リセット時に
    static void Init()
    {
        InitPadDictionary(); // パッドの辞書配列の初期化
        InitKeyString(); // キーボード辞書配列の初期化

        // キー状態をゼロリセット
        for (int i = 0; i < (int)Pad::NUM; i++)
            prevStates[i] = currentStates[i] = 0;
       
        InitMouse(); // マウスの状態初期化
        InitKeyboard(); // キーボードの状態初期化
       
        timing = 0, isBugCheckMode = false; //バグチェック
    }

    // プレイヤの参加処理(参加辞書登録して参加中の数も返す)
    static int JoinCheck()
    {
        int joinCount = 0;
        for (int i = 0; i < (int)Pad::NUM; i++)
        {    // 参加ボタンが押されたら
            if (Input::GetButtonDown((Pad)i, PAD_INPUT_1))
                isJoin[(Pad)i] = true; // 参加パッド辞書に登録
            if (isJoin.count((Pad)i) == 1 && isJoin[(Pad)i] == true)
                joinCount++; // 参加中の人数をカウント
        }
        return joinCount; // 参加中のコントローラ数を返す
    }

    // 最新の入力状況に更新する処理。
    // 毎フレームの最初に(ゲームの処理より先に)呼んでください。
    static void Update()
    {
        for (int i = 0; i < (int)Pad::NUM; i++)
        {    // 現在の状態を一つ前の状態として保存してGetJoypad..
            prevStates[i] = currentStates[i];
            currentStates[i] = GetJoypadInputState(padDic[i]);
        }
       
        UpdateMouse(); // マウス状態を更新
        UpdateKeyboard(); // キーボード状態を更新
       
        isBugCheckMode = true;
        ++timing; //タイミングのカウントを+1
        if (timing == INT_MAX) timing = 0; // int型最大値になったら0にループ
    }

    // キーボードの状態を更新する
    static void UpdateKeyboard()
    {
        memcpy(prevKey, currentKey, sizeof(prevKey));
        // キーボードの状態配列を更新する
        GetHitKeyStateAll(currentKey); // https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N28
       
        if (GetButtonUp(Pad::Keyboard, KEY_INPUT_CAPSLOCK))
            isCapsLocked = !isCapsLocked; // CapsLock状態を反転
        // シフトが押された状態かを更新
        isShifted = (GetButton(Pad::Keyboard, KEY_INPUT_LSHIFT) || GetButton(Pad::Keyboard, KEY_INPUT_RSHIFT));
    }


    // マウスの状態を更新する
    static void UpdateMouse()
    {
        Click[Mouse::DownL] = 0; Click[Mouse::DownR] = 0; //Click[Mouse.UpL] = 0; Click[Mouse.UpR] = 0;
        Click[Mouse::All] = 0; // 状態辞書の0リセット
       
        prevMouse = currentMouse; // 前フレームの最新のマウス情報を1つ前の情報とする
        prevStates[(int)Pad::Mouse] = currentMouse;
       
        MouseWheel = GetMouseWheelRotVolF(); // マウスのホイール回転量を更新
       
        // マウスの位置を取得
        GetMousePoint(&MouseX, &MouseY);
        Click[Mouse::X] = MouseX; Click[Mouse::Y] = MouseY;
        currentMouse = GetMouseInput(); // マウス状態を得る
        currentStates[(int)Pad::Mouse] = currentMouse; // マウス状態をパッド状態にも併合
       
        // 押してない離した状態のマウス位置を更新
        if (currentMouse == 0)
        {
            Click[Mouse::ReleaseX] = MouseX; Click[Mouse::ReleaseY] = MouseY;
        }
        else Click[Mouse::All] = 1; // マウスのいずれかのボタンが押されている
       
        // 左マウスドラッグ中のXY位置を更新
        if (GetButton(Pad::Mouse, MOUSE_INPUT_LEFT) && !GetButtonDown(Pad::Mouse, MOUSE_INPUT_LEFT))
        {
            Click[Mouse::DragLX] = MouseX; Click[Mouse::DragLY] = MouseY; Click[Mouse::DragL] = 1;
        }
        else
            Click[Mouse::DragL] = 0;
       
        // 右マウスドラッグ中のXY位置を更新
        if (GetButton(Pad::Mouse, MOUSE_INPUT_RIGHT) && !GetButtonDown(Pad::Mouse, MOUSE_INPUT_RIGHT))
        {
            Click[Mouse::DragRX] = MouseX; Click[Mouse::DragRY] = MouseY; Click[Mouse::DragR] = 1;
        }
        else
            Click[Mouse::DragR] = 0;
       
        //[マウスクリック検知] https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N40
        // マウスのボタンが押されたり離されたりしたかどうかの情報を取得する
        if (GetMouseInputLog2(&MButton, &ClickX, &ClickY, &LogType, TRUE) == 0)
        {    // [左ボタン]が押されたり離されたりしていた
            if ((MButton & MOUSE_INPUT_LEFT) != 0)
            {    // クリックされた瞬間と離された瞬間のマウス位置を記録
                if (LogType == MOUSE_INPUT_LOG_DOWN)
                {
                    Click[Mouse::DownLX] = ClickX; Click[Mouse::DownLY] = ClickY; Click[Mouse::DownL] = 1;
                }
                else if (LogType == MOUSE_INPUT_LOG_UP)
                {
                    Click[Mouse::UpLX] = ClickX; Click[Mouse::UpLY] = ClickY; Click[Mouse::UpL] = 1;
                }
            }// [右ボタン]が押されたり離されたりしていた
            else if ((MButton & MOUSE_INPUT_RIGHT) != 0)
            {    // クリックされた瞬間と離された瞬間のマウス位置を記録
                if (LogType == MOUSE_INPUT_LOG_DOWN)
                {
                    Click[Mouse::DownRX] = ClickX; Click[Mouse::DownRY] = ClickY; Click[Mouse::DownR] = 1;
                }
                else if (LogType == MOUSE_INPUT_LOG_UP)
                {
                    Click[Mouse::UpRX] = ClickX; Click[Mouse::UpRY] = ClickY; Click[Mouse::UpR] = 1;
                }
            }
        }
    }


    // ボタンが押されているか?
    static bool GetButton(Pad pad, int buttonId)
    {    //コントローラバグチェック
        if (ControllerBugCheck(pad, buttonId)) return false;
       
        if (pad == Pad::None) return false; // Noneなら判別不要
        else if (pad == Pad::All) // All指定の時は全てのパッド
        {    // GetButtonの中でGetButtonを呼ぶ【再起呼出し】テクニック
            for (int i = 0; i < (int)Pad::NUM; i++)
                if (GetButton((Pad)i, buttonId))
                    return true; //押されているPadを発見!
            return false; // 一つも押されていなかった
        }
       
        if(pad == Pad::Keyboard) // 今ボタンが押されているかどうかを返却
            return (currentKey[buttonId] ==1); //https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N28


        // 今ボタンが押されているかどうかを返却
        return (currentStates[(int)pad] & buttonId) != 0;
    }

    // ボタンが押された瞬間か?
    static bool GetButtonDown(Pad pad, int buttonId)
    {    //コントローラバグチェック
        if (ControllerBugCheck(pad, buttonId)) return false;
       
        if (pad == Pad::None) return false; // Noneなら判別不要
        else if (pad == Pad::All) // All指定の時は全てのパッド
        {    // 再起呼出しテクニック
            for (int i = 0; i < (int)Pad::NUM; i++)
                if (GetButtonDown((Pad)i, buttonId))
                    return true; //押されているPadを発見!
            return false; // 一つも押されていなかった
        }
       
        if (pad == Pad::Keyboard) //https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N28
            return (currentKey[buttonId] == 1) && (prevKey[buttonId] != 1);


        // 今は押されていて、かつ1フレーム前は押されていない場合はtrueを返却
        return ((currentStates[(int)pad] & buttonId) & ~(prevStates[(int)pad] & buttonId)) != 0;
    }

    // ボタンが離された瞬間か?
    static bool GetButtonUp(Pad pad, int buttonId)
    {    //コントローラバグチェック
        if (ControllerBugCheck(pad, buttonId)) return false;
       
        if (pad == Pad::None) return false; // Noneなら判別不要
        else if (pad == Pad::All) // All指定の時は全てのパッド
        {    // 再起呼出しテクニック
            for (int i = 0; i < (int)Pad::NUM; i++)
                if (GetButtonUp((Pad)i, buttonId))
                    return true; //押されているPadを発見!
            return false; // 一つも押されていなかった
        }
       
        if (pad == Pad::Keyboard) //https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N28
            return (prevKey[buttonId] == 1) && (currentKey[buttonId] != 1);


        // 1フレーム前は押されていて、かつ今は押されている場合はtrueを返却
        return ((prevStates[(int)pad] & buttonId) & ~(currentStates[(int)pad] & buttonId)) != 0;
    }
   
    //コントローラのボタンの初回プッシュを検知し
    //他コントローラと初回タイミングが完全被りかチェックして重複コントローラの判定処理をスルーする対策
    //★コントローラ3を押すと7も反応するなどコントローラ4以降の動作が怪しいのでその対策(DXのバグ?仕様?)
    static bool ControllerBugCheck(Pad pad, int buttonId)
    {
        // 今は押されていて、かつ1フレーム前は押されていない場合
        bool buttonDown = ((currentStates[(int)pad] & buttonId) & ~(prevStates[(int)pad] & buttonId)) != 0;
        //if (isBugCheckMode == false) return false;
        //コントローラの初回ボタンプッシュを検知
        if (buttonDown && firstPushTiming[(int)pad] == 0)
        {
            for (int i = 0; i < (int)Pad::NUM - 1; i++)
            {    //全コントローラ初回プッシュ被りがないかチェック完全同時は怪しいので-1
                bool isDown = ((currentStates[i] & buttonId) & ~(prevStates[i] & buttonId)) != 0;
                if ((Pad)i != pad && isDown ////初回プッシュ被り検出
                  && (timing == firstPushTiming[i] || firstPushTiming[i] == 0))
                {    //初回プッシュのタイミング被りは-1フラグを記録
                    firstPushTiming[i] = -1;
                }
            }
            if (firstPushTiming[(int)pad] != -1)
                firstPushTiming[(int)pad] = timing;//初回プッシュを記録
        }
       
        if (firstPushTiming[(int)pad] == -1) return true; //DXのコントローラ被りバグ!
        else return false; //コントローラ被りOK!
    }
   
    // キーボードのキーと文字列の対応関係配列の初期化 https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N2
    static void InitKeyString()
    {
        // 辞書配列で対応関係を結び付けておく
        KeyString[KEY_INPUT_BACK] = "BackSpace";//"\b";
        KeyString[KEY_INPUT_TAB] = "\t";
        KeyString[KEY_INPUT_RETURN] = "\n";
        KeyString[KEY_INPUT_LSHIFT] = "ShiftL";
        KeyString[KEY_INPUT_RSHIFT] = "ShiftR";
        KeyString[KEY_INPUT_LCONTROL] = "CtrlL";
        KeyString[KEY_INPUT_RCONTROL] = "CtrlR";
        KeyString[KEY_INPUT_SPACE] = " ";
        KeyString[KEY_INPUT_PGUP] = "PgUp";
        KeyString[KEY_INPUT_PGDN] = "PgDn";
        KeyString[KEY_INPUT_END] = "End";
        KeyString[KEY_INPUT_HOME] = "Home";
        KeyString[KEY_INPUT_LEFT] = "Left";
        KeyString[KEY_INPUT_UP] = "Up";
        KeyString[KEY_INPUT_RIGHT] = "Right";
        KeyString[KEY_INPUT_DOWN] = "Down";
        KeyString[KEY_INPUT_INSERT] = "Insert";
        KeyString[KEY_INPUT_DELETE] = "Delete";
        KeyString[KEY_INPUT_MINUS] = "-"; CapsString[KEY_INPUT_MINUS] = "=";
        KeyString[KEY_INPUT_YEN] = "\\"; CapsString[KEY_INPUT_YEN] = "|";
        KeyString[KEY_INPUT_PREVTRACK] = "^"; CapsString[KEY_INPUT_PREVTRACK] = "~";
        KeyString[KEY_INPUT_PERIOD] = "."; CapsString[KEY_INPUT_PERIOD] = ">";
        KeyString[KEY_INPUT_SLASH] = "/"; CapsString[KEY_INPUT_SLASH] = "?";
        KeyString[KEY_INPUT_LALT] = "AltL";
        KeyString[KEY_INPUT_RALT] = "AltR";
        KeyString[KEY_INPUT_SCROLL] = "Scroll";
        KeyString[KEY_INPUT_SEMICOLON] = ";"; CapsString[KEY_INPUT_SEMICOLON] = "+";
        KeyString[KEY_INPUT_COLON] = ":"; CapsString[KEY_INPUT_COLON] = "*";
        KeyString[KEY_INPUT_LBRACKET] = "["; CapsString[KEY_INPUT_LBRACKET] = "{";
        KeyString[KEY_INPUT_RBRACKET] = "]"; CapsString[KEY_INPUT_RBRACKET] = "}";
        KeyString[KEY_INPUT_AT] = "@"; CapsString[KEY_INPUT_AT] = "`";
        KeyString[KEY_INPUT_BACKSLASH] = "\\"; CapsString[KEY_INPUT_BACKSLASH] = "_";
        KeyString[KEY_INPUT_COMMA] = ","; CapsString[KEY_INPUT_COMMA] = "<";
        KeyString[KEY_INPUT_CAPSLOCK] = "CapsLock";
        KeyString[KEY_INPUT_PAUSE] = "Pause";
        KeyString[KEY_INPUT_NUMPAD0] = "0";
        KeyString[KEY_INPUT_NUMPAD1] = "1";
        KeyString[KEY_INPUT_NUMPAD2] = "2";
        KeyString[KEY_INPUT_NUMPAD3] = "3";
        KeyString[KEY_INPUT_NUMPAD4] = "4";
        KeyString[KEY_INPUT_NUMPAD5] = "5";
        KeyString[KEY_INPUT_NUMPAD6] = "6";
        KeyString[KEY_INPUT_NUMPAD7] = "7";
        KeyString[KEY_INPUT_NUMPAD8] = "8";
        KeyString[KEY_INPUT_NUMPAD9] = "9";
        KeyString[KEY_INPUT_MULTIPLY] = "*";
        KeyString[KEY_INPUT_ADD] = "+";
        KeyString[KEY_INPUT_SUBTRACT] = "-";
        KeyString[KEY_INPUT_DECIMAL] = ".";
        KeyString[KEY_INPUT_DIVIDE] = "/";
        KeyString[KEY_INPUT_NUMPADENTER] = "\n";
        KeyString[KEY_INPUT_F1] = "F1";
        KeyString[KEY_INPUT_F2] = "F2";
        KeyString[KEY_INPUT_F3] = "F3";
        KeyString[KEY_INPUT_F4] = "F4";
        KeyString[KEY_INPUT_F5] = "F5";
        KeyString[KEY_INPUT_F6] = "F6";
        KeyString[KEY_INPUT_F7] = "F7";
        KeyString[KEY_INPUT_F8] = "F8";
        KeyString[KEY_INPUT_F9] = "F9";
        KeyString[KEY_INPUT_F10] = "F10";
        KeyString[KEY_INPUT_F11] = "F11";
        KeyString[KEY_INPUT_F12] = "F12";
        KeyString[KEY_INPUT_A] = "a"; CapsString[KEY_INPUT_A] = "A";
        KeyString[KEY_INPUT_B] = "b"; CapsString[KEY_INPUT_B] = "B";
        KeyString[KEY_INPUT_C] = "c"; CapsString[KEY_INPUT_C] = "C";
        KeyString[KEY_INPUT_D] = "d"; CapsString[KEY_INPUT_D] = "D";
        KeyString[KEY_INPUT_E] = "e"; CapsString[KEY_INPUT_E] = "E";
        KeyString[KEY_INPUT_F] = "f"; CapsString[KEY_INPUT_F] = "F";
        KeyString[KEY_INPUT_G] = "g"; CapsString[KEY_INPUT_G] = "G";
        KeyString[KEY_INPUT_H] = "h"; CapsString[KEY_INPUT_H] = "H";
        KeyString[KEY_INPUT_I] = "i"; CapsString[KEY_INPUT_I] = "I";
        KeyString[KEY_INPUT_J] = "j"; CapsString[KEY_INPUT_J] = "J";
        KeyString[KEY_INPUT_K] = "k"; CapsString[KEY_INPUT_K] = "K";
        KeyString[KEY_INPUT_L] = "l"; CapsString[KEY_INPUT_L] = "L";
        KeyString[KEY_INPUT_M] = "m"; CapsString[KEY_INPUT_M] = "M";
        KeyString[KEY_INPUT_N] = "n"; CapsString[KEY_INPUT_N] = "N";
        KeyString[KEY_INPUT_O] = "o"; CapsString[KEY_INPUT_O] = "O";
        KeyString[KEY_INPUT_P] = "p"; CapsString[KEY_INPUT_P] = "P";
        KeyString[KEY_INPUT_Q] = "q"; CapsString[KEY_INPUT_Q] = "Q";
        KeyString[KEY_INPUT_R] = "r"; CapsString[KEY_INPUT_R] = "R";
        KeyString[KEY_INPUT_S] = "s"; CapsString[KEY_INPUT_S] = "S";
        KeyString[KEY_INPUT_T] = "t"; CapsString[KEY_INPUT_T] = "T";
        KeyString[KEY_INPUT_U] = "u"; CapsString[KEY_INPUT_U] = "U";
        KeyString[KEY_INPUT_V] = "v"; CapsString[KEY_INPUT_V] = "V";
        KeyString[KEY_INPUT_W] = "w"; CapsString[KEY_INPUT_W] = "W";
        KeyString[KEY_INPUT_X] = "x"; CapsString[KEY_INPUT_X] = "X";
        KeyString[KEY_INPUT_Y] = "y"; CapsString[KEY_INPUT_Y] = "Y";
        KeyString[KEY_INPUT_Z] = "z"; CapsString[KEY_INPUT_Z] = "Z";
        KeyString[KEY_INPUT_0] = "0";
        KeyString[KEY_INPUT_1] = "1"; CapsString[KEY_INPUT_1] = "!";
        KeyString[KEY_INPUT_2] = "2"; CapsString[KEY_INPUT_2] = "\"";
        KeyString[KEY_INPUT_3] = "3"; CapsString[KEY_INPUT_3] = "#";
        KeyString[KEY_INPUT_4] = "4"; CapsString[KEY_INPUT_4] = "$";
        KeyString[KEY_INPUT_5] = "5"; CapsString[KEY_INPUT_5] = "%";
        KeyString[KEY_INPUT_6] = "6"; CapsString[KEY_INPUT_6] = "&";
        KeyString[KEY_INPUT_7] = "7"; CapsString[KEY_INPUT_7] = "'";
        KeyString[KEY_INPUT_8] = "8"; CapsString[KEY_INPUT_8] = "(";
        KeyString[KEY_INPUT_9] = "9"; CapsString[KEY_INPUT_9] = ")";
    }


}; //←【注意】クラス定義の終わりにはコロンが必要だよ!

#endif  //ここでエラー出た人は↑【注意】の;コロン忘れ


Input.cppに変数定義を追加します。

#include "Input.h"

int Input::prevStates[]; // 1フレーム前の状態
int Input::currentStates[]; // 現在の状態
std::unordered_map<Pad, bool> Input::isJoin; // 参加中のパッド番号辞書

std::unordered_map<int, int> Input::padDic; // パッド番号からDXの定義への辞書

int Input::prevMouse; // 1フレーム前のマウス状態
int Input::currentMouse; // 現在のマウス状態
int Input::MouseX{ -1 }, Input::MouseY{ -1 }; // 現在のマウス位置
float Input::MouseWheel{ 0 };
int Input::ClickX{ -1 }, Input::ClickY{ -1 }, Input::MButton{ 0 }, Input::LogType{ 0 };
std::unordered_map<Mouse, int> Input::Click;//<Mouse⇒int> マウスのクリックXY位置辞書

//[キーボード入力]https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N28
char Input::prevKey[256]; // 1フレーム前のキーボード状態
char Input::currentKey[256]; // 現在のキーボード状態
std::string Input::KeyString[256];//<キーコード⇒キー文字列>の辞書
std::string Input::CapsString[256];//<Caps状態キーコード⇒キー文字列>の辞書
bool Input::isCapsLocked{ false };// CapsLock状態か?
bool Input::isShifted{ false };// Shiftが押された状態か?

//★【DXコントローラバグ検知】初回プッシュが被ればDXバグで同一コントローラ
int Input::firstPushTiming[(int)Pad::Four]; // Pad::Fourが最後だから8とかに増やすなら要書き換え
int Input::timing{ 0 };
bool Input::isBugCheckMode = false; //バグチェックを開始するか



TitleScene.hにマウスとキーボード入力を表示するデモのコードをくわえましょう。

#ifndef TITLESCENE_H_
#define TITLESCENE_H_

#include "Scene.h"

#include <string>

#include "DxLib.h"
(中略)...........

class TitleScene : public Scene
{
public:
(中略)...........

    std::string keyboardText{""}; // キーボード入力テキスト
    int keyDownCount[256]; // 各キーを押している時間をカウントして長押し動作
    int longPushNum = 30; // 30フレーム0.5秒長押しで文字がだーっと出る

    // コンストラクタ
    TitleScene() : Scene()
    {
        this->tag = "TitleScene";
    }

    (中略)...........

    // ボタン入力に応じた処理
    void HandleInput()
    {
        (中略)...........

        // キーボードでテキスト入力
        bool isEntered = false; // テンキーのEnterと普通のEnterダブり問題対策
        for (int i = 0; i < 256; ++i)
        {
            if (i == KEY_INPUT_RETURN || i == KEY_INPUT_NUMPADENTER)
                if (!isEntered) isEntered = true;
                else continue;

            if (Input::GetButtonDown(Pad::Keyboard, i))
                keyDownCount[i] = 0; // キーが押された瞬間からカウントアップ開始
            else if(Input::GetButton(Pad::Keyboard, i))
                ++keyDownCount[i]; // キーが押されてる時間をカウントアップ
        
            if (0 < keyDownCount[i] && keyDownCount[i] < longPushNum)
                continue; // Downの瞬間=0かlongPush以上時間経過してない場合は文字を増やさない
            
            if (Input::GetButton(Pad::Keyboard, i))
            {
                if (i == KEY_INPUT_BACK || i == KEY_INPUT_DELETE)
                {
                    if (keyboardText.size() > 0)
                        keyboardText.pop_back(); // 末尾の文字を1つ削除する
                }
                else if (i == KEY_INPUT_LSHIFT || i == KEY_INPUT_RSHIFT)
                {
                    // シフト文字は出力しない SHIFT + a ⇒ A 大文字表示に
                }
                else if (Input::isCapsLocked || Input::isShifted) //CapsLockかShift押下中は大文字
                    keyboardText += (Input::CapsString[i] != "") ? Input::CapsString[i] : Input::KeyString[i];
                else
                    keyboardText += Input::KeyString[i]; // 押された文字をテキスト列に追加
            }
        }

    
    }
(中略)...........
    
    // 描画処理
    void Draw() override
    {
    (中略)...........
        
        // キーボードのデモ
        DrawString(0, 100, keyboardText.c_str(),GetColor(255,255,255));
        
        // マウスのデモ
        if (Input::Click[Mouse::All] == 1)
        {    // ドラッグしたら
            if (Input::Click[Mouse::DragL] == 1)
            {    // ドラッグしたら開始XYと四角 □ を表示
                std::string drawString = "Drag開始(X,Y)=(" + std::to_string(Input::Click[Mouse::DownLX]) + "," + std::to_string(Input::Click[Mouse::DownLY]) + ")";
                DrawString(Input::Click[Mouse::DownLX], Input::Click[Mouse::DownLY], drawString.c_str(), GetColor(255, 255, 255));
                // 選択範囲の四角を表示
                DrawLineBox(Input::Click[Mouse::DownLX], Input::Click[Mouse::DownLY], Input::Click[Mouse::DragLX], Input::Click[Mouse::DragLY], GetColor(255, 255, 255));
            }
            // [1] if(Input::GetButton(Pad::Mouse, MOUSE_INPUT_RIGHT) と
            // [2] if(Input::Click[Mouse::DragR] == 1) は 同じ意味になる([2]のほうが短く書ける)
            if (Input::Click[Mouse::DragR] == 1 && Input::MouseX >= 0 && Input::MouseY >= 0)
            {
                std::string drawString = "マウス右(X,Y)=(" + std::to_string(Input::MouseX) + "," + std::to_string(Input::MouseY) + ")";
                DrawString(Input::MouseX, Input::MouseY, drawString.c_str(), GetColor(255, 255, 255));
            }
        }
        else if (Input::MouseX >= 0 && Input::MouseY >= 0)
        {
            std::string drawString = "(X,Y)=(" + std::to_string(Input::MouseX) + "," + std::to_string(Input::MouseY) + ")";
            DrawString(Input::MouseX, Input::MouseY, drawString.c_str(), GetColor(255, 255, 255));
        }
        // クリックを離した位置は記録が残る
        if (Input::Click[Mouse::UpLX] != -1 && Input::Click[Mouse::UpLY] != -1)
        {
            std::string drawString = "離した(X,Y)=(" + std::to_string(Input::Click[Mouse::UpLX]) + "," + std::to_string(Input::Click[Mouse::UpLY]) + ")";
            DrawString(Input::Click[Mouse::UpLX], Input::Click[Mouse::UpLY], drawString.c_str(), GetColor(255, 255, 255));
        }
        // ドラッグ開始XYも残るので範囲エリアの四角内のキャラ選択などにも使える
        if (Input::Click[Mouse::DownLX] != -1 && Input::Click[Mouse::DownLY] != -1)
        {
            std::string drawString = "Drag開始(X,Y)=(" + std::to_string(Input::Click[Mouse::DownLX]) + "," + std::to_string(Input::Click[Mouse::DownLY]) + ")";
            DrawString(Input::Click[Mouse::DownLX], Input::Click[Mouse::DownLY], drawString.c_str(), GetColor(255, 255, 255));
        }

(中略)...........

    }
};

#endif



いかがでしょうか?実行するとタイトル画面でマウスドラッグや右クリックやキーボードのテキスト入力が表示されたでしょうか?
全角や日本語入力についてはMakeKeyInput関数を使ったまた別のやり方になります。
https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N13
気が向いたらトライしてみるのもよいかもしれません。


ゲームクリア画面への遷移


GameClearScene.hを新規作成して、ベースのScene.hを継承してクリア画面を作成しましょう。

#ifndef GAMECLEARSCENE_H_
#define GAMECLEARSCENE_H_

#include "Scene.h"

#include "DxLib.h"
#include "Input.h"
#include "Image.h"

#include "GameManager.h"
#include "SceneManager.h"

class GameClearScene : public Scene
{
public:
    //★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
    SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
    GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
    
    // コンストラクタ
    GameClearScene() : Scene()
    {
        this->tag = "GameClearScene";
    }

    // 初期化処理
    void Initialize() override
    {

    }

    // 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
    void Finalize() override
    {

    }

    // 更新処理
    void Update() override
    {
        if (Input::GetButtonDown(Pad::All, PAD_INPUT_1))
        {
            sm.LoadScene("TitleScene"); //シーン遷移
        }
    }

    // 描画処理
    void Draw() override
    {
        DrawString(0, 0, "おめでとう!!!!GameClear....ボタン押下でPlaySceneへ", GetColor(255, 255, 255));
    }
};

#endif


SceneManager.cppにGameClearシーンの生成処理を追加してクリアシーンへ遷移する機能を作成しましょう。

#include "SceneManager.h"

#include "Scene.h"
#include "TitleScene.h"
#include "PlayScene.h"
#include "GameOverScene.h"
#include "GameClearScene.h"

bool SceneManager::isChanging(const std::string& nextSceneName)
{
    if (currentSceneName != nextSceneName) //シーン名不一致
        return true; // シーン名が変更
    
    return false; // シーン名に変更無し
}

void SceneManager::NextScene(const std::string& nextSceneName)
{
    // 遷移予定nextシーン名と現在のシーン名を比較、何の変化もなければfalseでシーン移動不必要の判定
    if (isChanging(nextSceneName) == false) return; //特に変更なしでシーン移動の必要なし
    
    changingSceneName = nextSceneName; // 次に移動予定のシーン名を保管しておく
}

void SceneManager::LoadScene(const std::string& sceneName)
{
    std::string loadSceneName = sceneName;     // sceneNameの指定が空の場合にはNextScene()での事前のシーン変更予約があるかチェック
    if (sceneName == "" && changingSceneName != "")
        loadSceneName = changingSceneName;
    
    if (loadSceneName == "") return; // シーン指定がないときは終了
    // 現在のシーンの終了処理
    
    if(currentScene !=nullptr)
        currentScene->Finalize(); // 終了処理を呼び出す
    
    if (loadSceneName == "TitleScene")
    {
        // 次のシーンの生成
        currentScene = std::make_shared<TitleScene>();
    }
    else if (loadSceneName == "PlayScene")
    {
        // 次のシーンの生成
        currentScene = std::make_shared<PlayScene>();
    }
    else if (loadSceneName == "GameOverScene")
    {
        // 次のシーンの生成
        currentScene = std::make_shared<GameOverScene>();
    }
    else if (loadSceneName == "GameClearScene")
    {
        // 次のシーンの生成
        currentScene = std::make_shared<GameClearScene>();
    }

    else
        assert("指定されたシーン名の生成処理が見つからなかった→SceneManager.cppを見直しましょう" == "");
    
    currentSceneName = loadSceneName; // 現在のシーン名の更新
    changingSceneName = ""; // NextScene関数のシーン予約名をリセット(リセットしないと次回LoadSceneを引数なしで呼ぶと残存したchangingSceneNameへ飛んじゃう)
    
    // 次のシーンの初期化
    currentScene->Initialize(); // 初期化を呼び出す
}


PlayScene.hにゲームクリア画面への遷移予約がある場合に遷移させるため、引数無しのLoadScene();関数呼び出しを追加しましょう。

#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_

#include "Scene.h"

(中略).........

class PlayScene : public Scene
{
public:

    (中略).........

    // 更新処理
    void Update() override
    {
        (中略).........
       
       
        (中略).........
        if (isGameover)
        {
            Sound::PlayMusic(Sound::ending); //BGMを変更する
            sm.LoadScene("GameOverScene"); //シーン遷移
            return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
        }
       
        sm.LoadScene(); // シーン遷移予約がNextScneneで事前予約されていたら遷移
    }

    (中略).........


};

#endif



例としてBoss.hがisDead=true;になったタイミングで連動してGameClearSceneに遷移するようにNextScene関数の呼び出しを追加します(ついでにボスの爆発も乱数でハデにします)

#ifndef BOSS_H_
#define BOSS_H_

#include "Enemy.h"

(中略).........


#include "SceneManager.h"

// ボスクラス。Enemyを継承して作る
class Boss : public Enemy
{
public:
    (中略).........
   
    SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る

    // コンストラクタ
    Boss(Vector3 pos) : Enemy( pos )
    {
        this->tag = "Boss"; // オブジェクトの種類判別タグ
        life = 100; // ライフ
        collisionRadius = 70; // 当たり判定半径
    }

    // 更新処理
    void Update() override
    {
        if (state == State::Appear) // 登場状態
        {
            (中略).........
        }
        else if (state == State::Normal) // 通常状態
        {
            (中略).........
        }
        else if (state == State::Swoon) // 気絶状態
        {
            (中略).........
        }
        else if (state == State::Angry) // 発狂モード
        {
            (中略).........
        }
        else if (state == State::Dying) // 死亡中
        {
            dyingTime--; // タイマー減少
           
            if (dyingTime % 5 == 0) // カウント 5 おきに
            {
               // やられたら ±(当たり判定の半径 + 40.0の乱数) の位置に爆発を出す
               float rangeX = MyRandom::Range(-40.0f - collisionRadius, 40.0f + collisionRadius);
               float rangeY = MyRandom::Range(-40.0f - collisionRadius, 40.0f + collisionRadius);
               // 爆発を生成
               gm.explosions.emplace_back(std::make_shared<Explosion>(Vector3(position.x + rangeX, position.y + rangeY)));
            }

           
            if (dyingTime <= 0) // タイマーが0になったら
            {
                isDead = true; // 完全に消滅
                sm.NextScene("GameClearScene"); // ゲームクリア画面への遷移を予約
            }
        }
    }

    // 描画処理
    void Draw() override
    {
        (中略).........
    }

    // 自機弾に当たったときの処理をoverride(上書き)する
    void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet) override
    {
        (中略).........
    }
    
    (中略).........
};

#endif




いかがですか?ボスを倒したらゲームクリア画面に遷移するようになりましたか?



【課題】シューティングとして完成度を上げるための残された課題


さて、ここまでで、ひととおりのインフラは整いました。
ここまであえて残してきた、下記の未完成ポイントをここまで導入してきた要素を応用して埋めてみてください。
[1][2]は愛でカバーできる努力義務の範囲、[3][4][5]はプログラミング応用力と拡張力(Zakoなどストラテジーパターンなど)の証明、[6][7][8]はシーン遷移とゲームシステムの拡張力の証明。

・[1]タイルマップエディタでステージを自作してゲームバランスを自分で調整する。

・[2]タイトル画面の機能を整理して、背景画像などを自作したりフォントのロゴを工夫して「タイトル画面らしくする」

・[3]プレイヤの画面外へのはみ出しをif, else if, else文で判定して画面はしっこに沿わせる処理(ヒント:Player.cppで左端position.x = collisionRadius や 右端position.y = Screen::Width - collisionRadius で画面はしっこに沿わせる)

・[4]Zako4,Zako5..や Item2,Item3....Boss2,Boss3.hなど自作のクラスを作成してオブジェクト指向のクラスの設計を体得、練習する。

・[5]Map.hのSpawnObject関数のなかにザコ0などだけではなく、Item0.hなどの生成処理を自作:例えばアイテムはid=20~30に対応させてタイルエディタでアイテム配置できるようにする。

・[6]ボスを倒した場合以外の自作のゲームクリア条件で別のゲームクリア画面2へ進む処理を考えてみる(スコアが一定数を超えると隠しクリア画面へ進むなど)。

・[7]ScoreScene.hやSoundScene.hなどを自作して、スコアのランキング専用のページやサウンド設定専用のページを自作する。

・[8]タイトル画面 → ステージ選択画面などステージ1,ステージ2などを選べる画面SelectStageScene.hなどを自作して、複数ステージで遊べるようにする。

◆[1]-[8]ができれば「ゲームの基本ベース構造についての免許が皆伝」(あとはシューティング以外の「ゲーム内容」にフォーカスできる準備が整ったということ)


ここまでを総括してゲーム制作の最小構成要件を押さえておきます。
◆ゲームの原材料は
・「画像、音など素材系」
・「ステージやパラメータやセーブなどデータ系」
◆ゲームのプログラミング要項は
・「Inputなど操作系」
・「フォントや音量オプションなどOS系」
・「Vector3やMathなど数学系」
・「跳ね返りやジャンプや物理などのダイナミクス系」
・「ZakoやBossなどのルーチン・AI系」
上記の構成要素を体感したうえで、ゲームの原材料と作業量をイメージした上で
「完成させられそうな現実路線のゲームアイデア」= 基本作業時間 と 拡張作業時間 を分けて考えたうえでゲームの制作に入ることが大事です。(人の時間は有限なので)