[C++]アクションゲームで基本練習

  1. 静止したキャラを出すだけのプログラム
  2. キャラを上下左右キーで動かせるプログラム
  3. キャラを左右キーで加速移動させるプログラム
  4. キャラをZキーでジャンプさせるプログラム
  5. 初代マリオ風のふわっとジャンプ(上昇と下降フェーズの引力を別々に設定できる)プログラム
  6. ダッシュ中にジャンプすると高く飛べる(加速段階によって複数のジャンプパラメータを持てる)プログラム
  7. ダッシュで2速状態でジャンプすると空で羽ばたけるプログラム
  8. キャラチップ配列アニメで「左右ダッシュ」と「2速状態ジャンプでキャラを±60度回転させ空を飛ぶ」プログラム
  9. ジャンプの効果音を鳴らすプログラム
  10. 地形ファイルを読み込むプログラム
  11. 地形マップチップを画面に描くプログラム
  12. 地形マップチップの当たり判定(めり込みの余りのぶんだけ押し戻す)プログラム
  13. キャラの移動に連動したcamera_x,camera_yで「描く地形のほうをスクロールでずらす」プログラム
  14. トゲトゲに当たったらキャラがくるくる回ってから下に落ちて最初のスタート位置に戻されるプログラム
  15. ブロックを下から叩いたらy位置をアニメさせてバウンスするプログラム
  16. ブロックidを辞書で変化させて叩いたらidが3 → 2 → -1 に変化してブロックを壊せるプログラム
  17. for文で複数ブロックidを辞書で変化させてON OFFブロックを叩いたらidが12⇔13、14⇔15にスイッチできるプログラム
  18. Imageのロードをclass分けして画像のロード処理をファイル分けするプログラム
  19. Soundのロードをclass分けして音声のロード処理をファイル分けするプログラム
  20. Cameraの描画ずらし処理をclass分けしてファイル分けしてさらにズーム機能を付加するプログラム
  21. 地形をBlock(アニメ機能付き)とTile(データ読込とブロック描画)という概念に切り分けたプログラム
  22. PlayerクラスをGameObjectクラスを継承して作成し、実行時型識別(RTTI)もできるようにするプログラム
  23. PlayerやGameObjectクラスにmain.cppのプレイヤ関連の処理を移植したプログラム

静止したキャラを出すだけのプログラム


まずは最小限のプログラムでif文などの練習をしていきましょう

以下の【キャラを出すだけの最小限サンプル】でキャラクターが出るかを確認しましょう

GIMPでのキャラ画像の作り方はこちらを参考にしてください

main.cppを編集します:

#include "DxLib.h"
#include <assert.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;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerImage = -1;
  playerImage = LoadGraph("Image/Player.png");
  assert(playerImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  float x = 200, y = 100; // 初期位置

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

    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}


キャラクターが出なかった人はImageフォルダにPlayer.pngファイルがちゃんとあるか確かめてみてください。

キャラを上下左右キーで動かせるプログラム


では次は【Screen.h、Input.hとInput.cppを追加してキャラを上下左右キーで動かせる】ようにしてみましょう

Screen.hの作り方はこちらを参考にしてください

Input.hとInput.cppの作り方はこちらを参考にしてください


以下の【キャラを上下左右ボタンで動かすだけの最小限サンプル】でキャラクターを動かせるかを確認しましょう

main.cppを編集します:

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

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerImage = -1;
  playerImage = LoadGraph("Image/Player.png");
  assert(playerImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  
  float x = 200, y = 100; // 初期位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    vx = 0; // 速度を毎ターンリセット初期化(しない場合はどんどん加速)
    vy = 0;

    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      vx = -5; // Xマイナス方向の速度
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      vx = 5; // Xプラス方向の速度
    }
    if (Input::GetButton(PAD_INPUT_UP))
    {
      vy = -5; // Yマイナス方向の速度
    }
    if (Input::GetButton(PAD_INPUT_DOWN))
    {
      vy = 5; // Yプラス方向の速度
    }

    
    x += vx; // 速度のぶん現在の位置から移動
    y += vy;

    
    
    // 描画処理
    DrawRotaGraphF(x, y, 1.0f, 0, playerImage, TRUE);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}


どうでしょう、キーボードの上下左右キーでキャラを動かすことができたでしょうか?
うまく反応しない場合はInput::Update();を忘れていることが多いです。



キャラを左右キーで加速移動させるプログラム



今度は【キャラを左右キーで加速させる】ように改造してみましょう


以下の【キャラを左右キーで加速させるだけの最小限サンプル】でキャラクターを加速させられるか挑戦しましょう

main.cppを編集します:

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

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerImage = -1;
  playerImage = LoadGraph("Image/Player.png");
  assert(playerImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  
  float x = 200, y = Screen::Height; // 初期位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  
  float vxSpeedMax = 15; // X左右方向の限界速度
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(playerImage, &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる

  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    vx = 0; // 速度を毎ターンリセット初期化(しない場合はどんどん加速)
    vy = 0;

    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;

      vx = -5; // Xマイナス方向の速度
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;

      vx = 5; // Xプラス方向の速度
    }
    if (Input::GetButton(PAD_INPUT_UP))
    {
      vy = -5; // Yマイナス方向の速度
    }
    if (Input::GetButton(PAD_INPUT_DOWN))
    {
      vy = 5; // Yプラス方向の速度
    }

    
    x += vx; // 速度のぶん現在の位置から移動
    y += vy;
    
    vx *= 0.97f; // 減速率
    
    // 描画処理
    DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 0, playerImage, TRUE);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}


さて、いかがでしょうか?氷が床の表面を滑るような動きができたでしょうか?
DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2.. としてimgSizeX / 2を引いているのは
DrawRotaGraphF関数は画像を描く基準点が画像の中心なので、画像の縦と横の幅の1/2を引くことで中心のぶんのつじつまを合わせるため
です。
単純なプログラムですが、
突き詰めると、スマホのiPhoneのSafariの【ブラウザのスクロールの気持ちよさ】のような独自のブランディング(差別化)につながるわけですから、あなどれません。
たかが加速、されど加速減速率やvxMaxSpeedなどの数字を色々変えながら自分の気持ちいい数値を探してみてください。





キャラをZキーでジャンプさせるプログラム



さて、こうなったら横方向だけではなく、縦方向のジャンプにも挑戦してみましょう

以下の【キャラをZキーでジャンプさせるサンプル】でキャラクターをジャンプさせられるか挑戦しましょう

main.cppを編集します:

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

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerImage = -1;
  playerImage = LoadGraph("Image/Player.png");
  assert(playerImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  
  float x = 200, y = Screen::Height; // 初期位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed = 16, vyDownSpeedMax = 16; // ジャンプの上昇と降下スピードのリミット
  float vyGravity = 0.8f;
  bool isGround = true; // 着地しているか

  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(playerImage, &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vy = -vyJumpSpeed; // ジャンプ方向はY上方向(マイナス方向)
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy < 0 && Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      vy = 0; // ジャンプで上昇中(vy > 0)にボタンを離すと勢いが止まる(小ジャンプ)
    }

    
    x += vx; // 速度のぶん現在の位置から移動
    y += vy;
    
    vx *= 0.97f; // 減速率
    
    if(!isGround)
      vy += vyGravity; // 重力で下方向に加速

    
    if (vy >= vyDownSpeedMax) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax;

    
    // 着地判定
    if (y > Screen::Height)
    {
      y = Screen::Height; // この行が無いと勢いで地面にめり込んだぶんを地面の位置に沿わせられない
      vy = 0;
      isGround = true;
    }

    
    // 描画処理
    DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 0, playerImage, TRUE);
    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f\n", y, vy);

    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}





いかがでしょう、うまくジャンプできましたか?
bool isGroundがtrueのとき(着地状態のとき)にボタンを押した瞬間にジャンプ力の数値をvyに設定して、
if (!isGround)のとき(isGroundがfalseのとき)に毎ターン vy += vyGravity;することで下方向に重力がかかるようにしています。
vyJumpSpeed = 16 やvyDownSpeedMax = 16 や yGravity = 0.8fなどの数字を自分で変えてみてイメージにあうジャンプを調整してみましょう




初代マリオ風のふわっとジャンプ(上昇と下降フェーズの引力を別々に設定できる)プログラム



さて、さらに凝ったプログラムにトライして、初代マリオのようなジャンプ処理に挑戦してみましょう

以下の【初代マリオ風の処理(小数のbit演算を省いた簡易化したもの)】でジャンプさせられるか挑戦しましょう

main.cppを編集します:

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

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerImage = -1;
  playerImage = LoadGraph("Image/Player.png");
  assert(playerImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  
  float x = 200, y = Screen::Height; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed = 16, vyDownSpeedMax = 16; // ジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump = 0.5f, vyGravity = 0.8f; // 上昇フェーズと下落フェーズの力を別々に設定できるようになった
  bool isGround = true; // 着地しているか
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(playerImage, &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vy = -vyJumpSpeed; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }

    else if (vy < 0 && Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      vy = 0; // ジャンプで上昇中(vy > 0)にボタンを離すと勢いが止まる(小ジャンプ)
    }

    
    x += vx; // 速度のぶん現在の位置から移動
    y += vy;
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    if(!isGround)
      vy += vyGravity; // 重力で下方向に加速

    
    if (vy >= vyDownSpeedMax) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax;
    
    // 着地判定
    if (y > Screen::Height)
    {
      y = Screen::Height; // この行が無いと勢いで地面にめり込んだぶんを地面の位置に沿わせられない
      vy = 0;
      vyForce = 0;
      isGround = true;
    }
    
    // 描画処理
    DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 0, playerImage, TRUE);
    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f\n", y, vy);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}



いかがでしょう、一番の違いは上昇フェーズと下落フェーズの力を別々に設定できるようになったことです。
たしかに、上昇フェーズは伸びがあるようにして、下落フェーズではストンと落としたいなど、
力・フォースを上昇と下落2つのフェーズで切り替えられるほうが表現力のバリエーションの作りこみがより深まりますよね。

こういった細かいこだわりまでをプログラミングで表現できたことこそが、
Nintendoの家庭用ファミコンが日本国内を席巻するだけでなく、
かつてアップルのジョブスも若き時代在籍した米国のアタリ社がぶっ壊しかけた家庭用ゲーム機市場を復活・開拓できた勝因かもしれません。
(任天堂に先んじて家庭用ゲーム機を出したがアタリショック:粗悪ソフトの大量在庫余りで米国クリスマス商戦でそのゲーム機が大爆死を引き起こした)

今の時代もスマホアプリは粗悪な課金アプリであふれていますが、アップル自体はソフトの在庫を抱えず審査するビジネスモデルを作りあげました(そこにゲームや音楽ソフトへの愛はあるでしょうか)。
(ジョブズがアタリショックからも学んだかどうかはわかりませんがゲームも音楽もソフトが売れたら巨大IT側にパーセンテージを、売れなくてもアカウント費を払う時代です)
そう、今の時代は機器メーカー側ではなく、作り手側が絶対的なリスクを負わされている時代にまた戻ったわけです。

いつかゲーム制作企業に勤めれば、こだわれるなどと考えるのは甘いです。
プログラムなんて勉強するつもりもないようなプランナやプロデューサーたちが牛耳っている現代の制作現場おいては、
こだわる時間と愛と余裕のあるのはゲームの制作を学んでいるフェーズの学生ぐらいです。今しかこだわれないと常に思うことです。

学者タイプの秀才に言わせれば、上昇フェーズも下降フェーズも重力加速度は同じはずだ(ドヤ)となりますが、
それにとらわれず、上昇と下落フェーズで別の力を設定できたほうが面白くなるかも、とのこだわりを表現できたプログラマと開発現場があの時代の日本の片隅にあったことは確かです。

「ただのゲーム好き」が最初期のゲーム業界を支えていったのではなく
雑誌を見ながらプログラムを打ち込んだ「プログラミング好き」な人がゲームを作っていったのです。
ゲームプログラマは本当は「ゲーム好きだけどプログラミングがもっと好き」な人が向いている仕事かもしれません。
(世代を経るごとに日本のゲーム開発力はどんどん空洞化していくでしょうが、次世代にささやかな希望を託す意味をこめて、少し踏み込んだことを書きました)







ダッシュ中にジャンプすると高く飛べる(加速段階によって複数のジャンプパラメータを持てる)プログラム



さて、次はダッシュ中にジャンプすると高く飛べる仕組みにトライしてみましょう

以下の【ダッシュの加速段階によってパラメータを切り替えるサンプル】でダッシュの段階に応じた高さでジャンプさせられるか挑戦しましょう

main.cppを編集します:

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

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerImage = -1;
  playerImage = LoadGraph("Image/Player.png");
  assert(playerImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  
  float x = 200, y = Screen::Height; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16 }, vyDownSpeedMax[] = { 16, 16, 16 }; // ダッシュの段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f }, vyGravity[] = { 0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(playerImage, &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる

        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    x += vx; // 速度のぶん現在の位置から移動
    y += vy;
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    // 着地判定
    if (y > Screen::Height)
    {
      y = Screen::Height; // この行が無いと勢いで地面にめり込んだぶんを地面の位置に沿わせられない
      vy = 0;
      vyForce = 0;
      vx_idx = 0;
      isGround = true;
    }
    
    // 描画処理
    DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 0, playerImage, TRUE);
    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}



いかがでしょう、ダッシュして加速している状態のときのジャンプの高さと、その場でダッシュせずにジャンプした時のジャンプの高さの違いが出れば成功です。
float vyForceJump[] = { 0.5f,0.4f,0.3f }, vyGravity[] = { 0.8f,0.8f,0.8f };のように
複数のパラメータを持ち、それをvyForce = vyGravity[vx_idx];のように、vx_idxで切り替えているのがポイントです。
加速の段階はジャンプする瞬間にif (vx_abs > 6) ++vx_idx;のようにif(vx_abs > 6)やif(vx_abs > 9)で現在の加速度の大きさで段階を切り替えています。





ダッシュで2速状態でジャンプすると空で羽ばたけるプログラム



さて、次はダッシュで2速状態でジャンプすると空で羽ばたける仕組みにトライしてみましょう。

以下の【ダッシュで2速状態でジャンプすると空で羽ばたけるサンプル】で2速でダッシュして飛んだら空で羽ばたく仕組みに挑戦しましょう

main.cppを編集します:

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

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerImage = -1;
  playerImage = LoadGraph("Image/Player.png");
  assert(playerImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  
  float x = 200, y = Screen::Height; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16, 4 }, vyDownSpeedMax[] = { 16, 16, 16, 8 }; // ダッシュ段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f,0.1f }, vyGravity[] = { 0.8f,0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  bool isFlying = false; // 飛行モード
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(playerImage, &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      vy = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
    }

    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs > 9) {
          isFlying = true; // 飛行モードにする
        }

        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    x += vx; // 速度のぶん現在の位置から移動
    y += vy;
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    // 着地判定
    if (y > Screen::Height)
    {
      y = Screen::Height; // この行が無いと勢いで地面にめり込んだぶんを地面の位置に沿わせられない
      vy = 0;
      vyForce = 0;
      vx_idx = 0;
      isGround = true;
      isFlying = false;
    }
    
    // 描画処理
    DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 0, playerImage, TRUE);
    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}




いかがでしょう、空でパタパタ羽ばたく感じが出せたでしょうか?
パラメーターを自分で変えてみて自分なりの羽ばたきを実験してみてください。
( ぼくはvyJumpSpeed[]をあえて少なめの4にして、たくさん連打しても上に上がりすぎないように調理して、
vyForceJump[]もそれに比例して少なくすることで、長押しで羽ばたくとギュンと上に伸びをだせるようにしました )

昔のプログラマは少しのアイデアと少しのコード変更でUnityの物理機能に頼らずとも、独創性を発揮してきたようですね。
こういった小ネタだけでもFlappy Birdのように人を熱中させ大ヒットすることもあるわけです。

シンプルな面白さの発見は、日々プログラミングしていてこそ生み出される可能性があります
ついつい、連打したくなる、とか、グイっと伸びて気持ちいいとか、そういう手触りは頭でっかちの企画書ベースの開発では生み出されません

自分なりの手触りやパラメーターや面白さの体感は自分のオリジナル料理の大事なレシピとなるうるのです。







キャラチップ配列アニメで「左右ダッシュ」と「2速状態ジャンプでキャラを±60度回転させ空を飛ぶ」プログラム



さて、次はキャラクターをアニメーションさせる表現にトライしてみましょう。
RPGなどのキャラチップ自作ツールキャラメル-CharaMEL-をダウンロードしていろいろな機能を駆使してキャラを自作すれば再配布自由な自作キャラが完成です。
作るのが面倒な人向けに自作したファイルを以下に置いておきます。
キャラの設定jsonファイル[←リンクを右クリックして名前を付けてリンク保存からダウンロード](CharaMELアプリメニューのファイル→キャラクタのインポートから読込可能)
書き出したpng画像ファイル(メニュー下のpng↓ボタンから書き出せる)

書き出したpngファイルを私は「Image/pikkoron.png」としてDXライブラリのImageフォルダに保存(またはコピー)しておきました。
このキャラチップ画像ファイルをパラパラアニメさせることで「キャラクターをダッシュさせたり、空を飛行させたり」してみましょう。

キャラチップ画像のキャラは32×32(書き出し時のサイズによる)などに分割されており、それをDXライブラリのLoadDivGraph関数で読み取る際に対応する画像番号の見極めが必要です。(GIMPでグリッドを引いて番号を見極めましょう)


以下の【キャラチップ配列のダッシュ左[3][4][5]、右[6][7][8]と立ち(左右)[4][7]を±60度回転させ空を飛ぶサンプル】でキャラチップ画像をパラパラアニメさせてみましょう。

main.cppを編集します:

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

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerImage = -1;
  playerImage = LoadGraph("Image/Player.png");
  assert(playerImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  int playerChips[12]; // キャラのチップのグリッドの分割数に応じて3×4 = 12のぶんだけ配列を事前に確保
  LoadDivGraph("Image/pikkoron.png", 12, 3, 4, 32, 32, playerChips); // キャラのチップのサイズは32×32
  assert(playerChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  
  
  float x = 200, y = Screen::Height; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  enum Direction { Left, Right }; // enumでキャラの向いてる方向をDirection型として2種類(LeftとRight)定義
  Direction direction = Direction::Right; // キャラの向いてる方向
  int animeTime = 0; // アニメ時間

  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16, 4 }, vyDownSpeedMax[] = { 16, 16, 16, 8 }; // ダッシュ段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f,0.1f }, vyGravity[] = { 0.8f,0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  bool isFlying = false; // 飛行モード
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(playerChips[0], &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      direction = Direction::Left;
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      direction = Direction::Right;
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      vy = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs > 9) {
          isFlying = true; // 飛行モードにする
        }
        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    x += vx; // 速度のぶん現在の位置から移動
    y += vy;
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    // 着地判定
    if (y > Screen::Height)
    {
      y = Screen::Height; // この行が無いと勢いで地面にめり込んだぶんを地面の位置に沿わせられない
      vy = 0;
      vyForce = 0;
      vx_idx = 0;
      isGround = true;
      isFlying = false;
    }
    
    // ダッシュ段階に応じてキャラクタのアニメ時間を進める
    float vx_dash = (vx < 0) ? -vx : vx; // 絶対値でプラスだけにする
    if (vx_dash > 0.5f) animeTime += 1; // 時間 +1 ( 合計 1 )
    if (vx_dash > 6) animeTime += 1; // 時間 +1 (計 1+1=2 ) 1フレームごとに2アニメ時間が進む
    if (vx_dash > 9) animeTime += 2; // 最終段階ではアニメ時間をさらに +2 (計 1+1+2=4 )

    
    int animeStep = animeTime / 10; // 10を5に変えたらアニメ速度が上がる(10ごとに1コマアニメが進む)
    if (animeStep >= 3) // アニメのステップ数が0,1,2の3段階までなら3を超えたら0へループ
    {
      animeTime = 0; // 0へ戻り再びアニメ時間を再ループ
      animeStep = 0;
    }

    
    
    // 描画処理
    DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 0, playerImage, TRUE);
    if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
      DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f,-60.0f / 360.0f * 2 * DX_PI, playerChips[4], TRUE);
    else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
      DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 60.0f / 360.0f * 2 * DX_PI, playerChips[7], TRUE);
    else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
      DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 0.0f, playerChips[5 - animeStep], TRUE);
    else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
      DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 0.0f, playerChips[6 + animeStep], TRUE);

    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}



いかがでしょう、キャラクタがパラパラとアニメされて走っているように見えるようになりましたか?

キャラクタが小さくて見にくいという人は描画処理のコードの以下の部分を変更すれば、キャラを2倍の大きさで表示させられます。

    
    // 描画処理
    if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
      DrawRotaGraphF(x - imgSizeX, y - imgSizeY, 2.0f,-60.0f / 360.0f * 2 * DX_PI, playerChips[4], TRUE);
    else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
      DrawRotaGraphF(x - imgSizeX, y - imgSizeY, 2.0f, 60.0f / 360.0f * 2 * DX_PI, playerChips[7], TRUE);
    else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
      DrawRotaGraphF(x - imgSizeX, y - imgSizeY, 2.0f, 0.0f, playerChips[5 - animeStep], TRUE);
    else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
      DrawRotaGraphF(x - imgSizeX, y - imgSizeY, 2.0f, 0.0f, playerChips[6 + animeStep], TRUE);
    






ジャンプの効果音を鳴らすプログラム




まずは、せっかくなのでジャンプの効果音をブラウザで動くシンセサイザーJFXRで作ってみましょう。
簡単な使い方だけなら以下のサイトが参考になります。
シンセの調節に慣れてきたらiPhone用のシンセで夜、寝転がりながらスマホで効果音制作にも挑戦だ。
結構本格的なシンセ機能があるのでDAWのユーザーも結構おすすめ↑してる

シンセの基本は下記の動画を暇な夜の時間のあるときにみておくと音づくりのスタート地点に立てると思います(センスのある天才肌なら開眼するはず)。
https://youtu.be/7tMbJ6f6qf0?t=341

まあ、ジャンプ音ぐらいなら下記画像のボタンやバーを適当にいじれば音の知識がゼロからでも、なんとなくセンスがつかめるはず。




例として、わたくしの引き当てたジャンプ音のシンセ調節ファイルを下記リンクに共有します。
わたくしの引き当てたジャンプ音のシンセ調節ファイル←右クリックで[名前をつけてリンク先を保存]でダウンロードして
↑ダウンロードしたファイルをブラウザで動くシンセJFXRの左上の[Open]ボタンから開いてみよう


まずは「小ジャンプ」の効果音として下記設定をして、[Export]ボタンからJump0.wavというファイル名DXライブラリのプロジェクトにSEフォルダを作ってそこに保存しましょう。
Jump0.wav←右クリックで[名前をつけてリンク先を保存]でダウンロード


つぎは「大ジャンプ」の効果音として下記設定をして、[Export]ボタンからJump1.wavというファイル名DXライブラリのプロジェクトのSEフォルダに保存しましょう。
Jump1.wav←右クリックで[名前をつけてリンク先を保存]でダウンロード


今度は「空を飛び始める」効果音として下記設定をして、[Export]ボタンからJump2.wavというファイル名DXライブラリのプロジェクトのSEフォルダに保存しましょう。
Jump2.wav←右クリックで[名前をつけてリンク先を保存]でダウンロード


最後は「空でホップする」効果音として下記設定をして、[Export]ボタンからJump3.wavというファイル名DXライブラリのプロジェクトのSEフォルダに保存しましょう。
Jump3.wav←右クリックで[名前をつけてリンク先を保存]でダウンロード



あとは以下の【効果音をSEフォルダから読み込んで大小ジャンプと空を飛ぶ音を出すサンプル】でキャラの動きに合わせて効果音を出してみましょう。

main.cppを編集します:

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

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerChips[12]; // キャラのチップのグリッドの分割数に応じて3×4 = 12のぶんだけ配列を事前に確保
  LoadDivGraph("Image/pikkoron.png", 12, 3, 4, 32, 32, playerChips); // キャラのチップのサイズは32×32
  assert(playerChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  // 効果音ファイルを事前にメモリに読み込む https://dxlib.xsrv.jp/function/dxfunc_sound.html#R8N4
  int jumpSE[4]; // 4種類のジャンプ音を読み込む
  jumpSE[0] = LoadSoundMem("SE/Jump0.wav");
  jumpSE[1] = LoadSoundMem("SE/Jump1.wav");
  jumpSE[2] = LoadSoundMem("SE/Jump2.wav");
  jumpSE[3] = LoadSoundMem("SE/Jump3.wav");
  assert(jumpSE[0] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[1] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[2] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[3] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる

  
  float x = 200, y = Screen::Height; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  enum Direction { Left, Right }; // enumでキャラの向いてる方向をDirection型として2種類(LeftとRight)定義
  Direction direction = Direction::Right; // キャラの向いてる方向
  int animeTime = 0; // アニメ時間
  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16, 4 }, vyDownSpeedMax[] = { 16, 16, 16, 8 }; // ダッシュ段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f,0.1f }, vyGravity[] = { 0.8f,0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  bool isFlying = false; // 飛行モード
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(playerChips[0], &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      direction = Direction::Left;
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      direction = Direction::Right;
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      vy = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
      // 加速段階に応じて違うジャンプ音を鳴らす
      PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);

    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs > 9) {
          isFlying = true; // 飛行モードにする
        }
        
        // 加速段階に応じて違うジャンプ音を鳴らす
        PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);

        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    x += vx; // 速度のぶん現在の位置から移動
    y += vy;
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    // 着地判定
    if (y > Screen::Height)
    {
      y = Screen::Height; // この行が無いと勢いで地面にめり込んだぶんを地面の位置に沿わせられない
      vy = 0;
      vyForce = 0;
      vx_idx = 0;
      isGround = true;
      isFlying = false;
    }
    
    // ダッシュ段階に応じてキャラクタのアニメ時間を進める
    float vx_dash = (vx < 0) ? -vx : vx; // 絶対値でプラスだけにする
    if (vx_dash > 0.5f) animeTime += 1; // 時間 +1 ( 合計 1 )
    if (vx_dash > 6) animeTime += 1; // 時間 +1 (計 1+1=2 ) 1フレームごとに2アニメ時間が進む
    if (vx_dash > 9) animeTime += 2; // 最終段階ではアニメ時間をさらに +2 (計 1+1+2=4 )
    
    int animeStep = animeTime / 10; // 10を5に変えたらアニメ速度が上がる(10ごとに1コマアニメが進む)
    if (animeStep >= 3) // アニメのステップ数が0,1,2の3段階までなら3を超えたら0へループ
    {
      animeTime = 0; // 0へ戻り再びアニメ時間を再ループ
      animeStep = 0;
    }
    
    
    // 描画処理
    if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
      DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f,-60.0f / 360.0f * 2 * DX_PI, playerChips[4], TRUE);
    else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
      DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 60.0f / 360.0f * 2 * DX_PI, playerChips[7], TRUE);
    else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
      DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 0.0f, playerChips[5 - animeStep], TRUE);
    else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
      DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 0.0f, playerChips[6 + animeStep], TRUE);
    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}



いかがでしょう、ジャンプ音が鳴りましたか?
うまくいかない人は、音のファイルをDXのプロジェクトのSEフォルダに入れてありますか?
ジャンプ音のファイル名はちゃんとJump1.wavが大文字のJから始まっていますか?Jump 1.wavのようにスペースが開いてませんか?

コード自体の変更は少ないですが、理想のジャンプ音に出会えるまで繰り返し色んな音を試しているだけですぐに時間は過ぎてゆくはずです。
WEBでひとさまの作った効果音を漁るのと、シンセで理想のジャンプ音を自作するのと、どちらを選んでも結局、時間がかかると思います。
クリエイターの端くれとしては、ぼくは自分でシンセをいじってイメージどおりの音を探る作業のほうが楽しいかもしれません。

たかがジャンプの効果音一つと思うでしょうか?
ゲームの手触り感の50%は効果音で決まるとぼくは思っています。
どんなにプログラミングや企画にこだわったゲームでも効果音が気持ちよくないゲームはメリハリがなくて眠くなってしまいます。

マリオワールドの効果音をはじめて聞いたとき、ぼくはゲームというより「楽器」を感じました
ひとつひとつの効果音が高周波が効いて目覚ましのような目の覚める音だと思います

おそらく、ぼくが最近のマリオや任天堂作品と昔の作品を比べて、昔のほうが逆にきらきらしてると感じるのは「効果音」の作者が変わったからなのかな..と思います。
ポケモンも最近の作品は昔の作品と変わってなぜかバトル中の音楽で眠くなってしまうのは...曲のメリハリの問題?..(それともぼくの老化か)

ゲームの快適さも、手触りも、メリハリも、簡単に見えて音の影響はかなり大きいと思います。






地形ファイルを読み込むプログラム




つぎは地形ファイルを読み込むプログラムに挑戦してみます。

まずはGIMPでためしに簡単な32×32のブロックを描いてみましょう。

[ファイル] → [新しい画像]から256×256、256×512..など2の何乗かのサイズでキャンバスを作成しましょう。


右下のレイヤの上で右クリックして[アルファチャンネルの追加]してから[消しゴムで背景を消して透明]にしましょう。


[画像] → [グリッドの設定]から16×16,32×32,64×64..などマップで描きたいブロックのサイズにグリッド補助線表示の大きさの設定をしましょう。


[表示] → [グリッドの表示]でグリッドを表示させて、[表示] → [表示倍率]でドットを描きやすいように800%ほどに拡大表示をしましょう。


範囲選択でぬりつぶしたい32×32の四角形を選択して、黒色でぬりつぶしましょう。


ぬりつぶした黒色の内側を範囲選択でべつの色でぬりつぶして、ブロックの内側に色の層を積み重ねましょう。


さらに内側を範囲選択でべつの色でぬりつぶして、ブロックの色の層を積み重ねて立体感を出しましょう。


マウス長押しで鉛筆を選択して内側に白いドットでテカリを描いて、ブロックのテカリによる立体感を出しましょう。


同じような手順で好きな色のブロックを自作して、自分のオリジナル色のブロックのバリエーションを増やしましょう。

マップチップを自作出来たらGIMPファイルをmaphip.xcfで、画像ファイルをmapchip.pngというファイル名でエクスポートしましょう。



GIMPが苦手な人向けに私の自作したマップチップを以下に共有しておきます。
以下の地形のマップチップはGIMPで1ブロック32×32として自作してみました。自作したGIMPファイルとエクスポート画像を下記リンクに共有しておきます。
mapchip.xcf(GIMPの編集ファイル)←右クリック[名前をつけてリンク先を保存]でダウンロードすればGIMPで自作ブロックを書き足せる
mapchip.png(↑GIMPから名前をつけてエクスポートしたpng画像ファイル)←右クリック[名前をつけてリンク先を保存]でダウンロード


地形のマップチップはタイルマップエディタで配置して[ファイル→名前をつけてエクスポート]からcsvファイルで書き出せます。
下記リンクの2つのファイルをダウンロードして、mapchip.pngと同じフォルダに置いた状態でタイルマップエディタで[ファイル→Open File or Project]でstage.tmxを開けばマップを編集できます。
mapchip.tsx(png画像の32×32など「グリッド分割設定など」の保存ファイル)←右クリック[名前をつけてリンク先を保存]でダウンロード
stage.tmx(タイルマップ編集ファイル)←右クリック[名前をつけてリンク先を保存]でダウンロード


タイルマップエディタで[ファイル→名前をつけてエクスポート]から書き出したcsvファイルを下記リンクに共有しておきます。
stage.csv(地形番号カンマ区切りで書き出したファイル)←右クリック[名前をつけてリンク先を保存]でダウンロード
書き出したcsvファイル↑はDXのプロジェクトのフォルダにMapフォルダを作ってMap/stage.csvとして保存してください。


さてまずは【地形csvファイルをMapフォルダから読み込んで地形番号を表示させるサンプル】でcsvファイルの中身には地形番号がカンマ区切りで書き出されていることを実感しましょう。

main.cppを編集します:

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

#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列std::stringに必要
#include <fstream> // ファイル読み出しstd::ifstreamに必要
#include <sstream> // 文字列ストリームに必要 ss >> num;で文字列から数字に変換できる


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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerChips[12]; // キャラのチップのグリッドの分割数に応じて3×4 = 12のぶんだけ配列を事前に確保
  LoadDivGraph("Image/pikkoron.png", 12, 3, 4, 32, 32, playerChips); // キャラのチップのサイズは32×32
  assert(playerChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  // 効果音ファイルを事前にメモリに読み込む https://dxlib.xsrv.jp/function/dxfunc_sound.html#R8N4
  int jumpSE[4]; // 4種類のジャンプ音を読み込む
  jumpSE[0] = LoadSoundMem("SE/Jump0.wav");
  jumpSE[1] = LoadSoundMem("SE/Jump1.wav");
  jumpSE[2] = LoadSoundMem("SE/Jump2.wav");
  jumpSE[3] = LoadSoundMem("SE/Jump3.wav");
  assert(jumpSE[0] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[1] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[2] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[3] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  
  int CellSize = 32; // マップの1マスのピクセル数
  
  
  std::vector<std::vector<int>> mapData;// タイルマップデータ
  std::ifstream ifs_csv_file("Map/stage.csv"); // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
  
  int Width = 0, Height = 0; // マップデータの横(Width) 縦(Height)のマスの数
  int MaxWidth = 0; // 1行の数字の最大個数
  std::string line; // 1行単位ごとにcsvファイルから文字列をstd::getlineで読み込んで受け取る
  while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
  {
    std::vector<int> valuelist; // 1行ぶんの数字リスト配列
    std::istringstream linestream( line ); // 各行の文字列ストリーム
    std::string comma_part; // カンマで分割された文字列
    int widthCount = 0; // カンマで区切られた行の数字の数(各行の幅)をカウント
    while (std::getline(linestream, comma_part, { ',' })) // 1行をgetlineでカンマで区切って列を得る
    {
      std::istringstream ss( comma_part ); // カンマで区切られた文字をssを通して>>で数字に
      int num; // 数字単体
      ss >> num; // 文字列ストリーム>>で数字へ変換
      valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
      ++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
    }
    // 1行の数字の数がMAX記録を更新するかチェック
    if (widthCount > MaxWidth)
      MaxWidth = widthCount; // 暫定Maxの列の幅を更新
    
    // 1行ぶんの数字の配列をとりまとめてmapDataに1行ずつ追加
    if (valuelist.size() != 0) // 配列に数字が1つでもあるなら.size() != 0
      mapData.emplace_back(valuelist); // mapDataに1行ずつ数字配列valuelistを追加
    
    ++Height; //マップの高さをカウントアップ
  }
  Width = MaxWidth; // マップの幅Widthは一番数字の個数の多かった行に合わせる
  // ↓読込んだCSVの幅と高さをチェック
  assert(Width > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  assert(Height > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");

  
  
  float x = 200, y = Screen::Height; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  enum Direction { Left, Right }; // enumでキャラの向いてる方向をDirection型として2種類(LeftとRight)定義
  Direction direction = Direction::Right; // キャラの向いてる方向
  int animeTime = 0; // アニメ時間
  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16, 4 }, vyDownSpeedMax[] = { 16, 16, 16, 8 }; // ダッシュ段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f,0.1f }, vyGravity[] = { 0.8f,0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  bool isFlying = false; // 飛行モード
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(playerChips[0], &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      direction = Direction::Left;
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      direction = Direction::Right;
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      vy = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
      // 加速段階に応じて違うジャンプ音を鳴らす
      PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs > 9) {
          isFlying = true; // 飛行モードにする
        }
        
        // 加速段階に応じて違うジャンプ音を鳴らす
        PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    x += vx; // 速度のぶん現在の位置から移動
    y += vy;
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    // 着地判定
    if (y > Screen::Height)
    {
      y = Screen::Height; // この行が無いと勢いで地面にめり込んだぶんを地面の位置に沿わせられない
      vy = 0;
      vx_idx = 0;
      isGround = true;
      isFlying = false;
    }
    
    // ダッシュ段階に応じてキャラクタのアニメ時間を進める
    float vx_dash = (vx < 0) ? -vx : vx; // 絶対値でプラスだけにする
    if (vx_dash > 0.5f) animeTime += 1; // 時間 +1 ( 合計 1 )
    if (vx_dash > 6) animeTime += 1; // 時間 +1 (計 1+1=2 ) 1フレームごとに2アニメ時間が進む
    if (vx_dash > 9) animeTime += 2; // 最終段階ではアニメ時間をさらに +2 (計 1+1+2=4 )
    
    int animeStep = animeTime / 10; // 10を5に変えたらアニメ速度が上がる(10ごとに1コマアニメが進む)
    if (animeStep >= 3) // アニメのステップ数が0,1,2の3段階までなら3を超えたら0へループ
    {
      animeTime = 0; // 0へ戻り再びアニメ時間を再ループ
      animeStep = 0;
    }
    
    
    // 描画処理
    if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
      DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f,-60.0f / 360.0f * 2 * DX_PI, playerChips[4], TRUE);
    else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
      DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 60.0f / 360.0f * 2 * DX_PI, playerChips[7], TRUE);
    else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
      DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 0.0f, playerChips[5 - animeStep], TRUE);
    else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
      DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 0.0f, playerChips[6 + animeStep], TRUE);
    
    // 地形マップを[ 行のfor文×列のfor文 ]の2重ループで描く
    for (int cellY = 0, ySize = mapData.size(); cellY < ySize; ++cellY)
    {
      float chip_y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
      for (int cellX = 0, xSize = mapData[cellY].size(); cellX < xSize; ++cellX)
      {
        float chip_x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
        int id = mapData[cellY][cellX]; // [行][列] の位置のcsvの地形番号(チップ番号)を得る
        if (id < 0) continue; // -1のときは描かない
        
        // [デバッグ]用にcsvのidの数字を描く
        DrawString(chip_x, chip_y, std::to_string(id).c_str(), GetColor(255, 255, 255));
      }
    }

    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}



いかがでしょうか?このようにcsvの中身は地形のチップ番号が,カンマ区切りで書き出されています。



プログラムではそのcsvファイルを std::ifstream ifs_csv_file("Map/stage.csv"); で読みだして
カンマ区切りをwhile文とstd::getlineを使い while (std::getline(linestream, comma_part, { ',' })) で分割して
分割したcomma_partを文字列ストリームssを使い std::istringstream ss( comma_part );
ストリーム演算子>> を通してint型のnumに変換 ss >> num; して
その数字numをまずは1次元配列std::vector<int>型のvaluelistvaluelist.emplace_back(num);1つずつ入れて、
その数字配列の1行ぶん(valuelist)を2次元数字配列std::vector<std::vector<int>>型のmapDatamapData.emplace_back(valuelist);行単位で1行ずつ格納しています。




地形マップチップを画面に描くプログラム




さてつぎは【地形のマップチップMap/mapchip.pngを読み込んだstage.csvの地形番号にあわせて描き出すサンプル】で画面に地形を描き出しましょう(注意:まだ当たり判定はない)

main.cppを編集します:

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

#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列std::stringに必要
#include <fstream> // ファイル読み出しstd::ifstreamに必要
#include <sstream> // 文字列ストリームに必要 ss >> num;で文字列から数字に変換できる

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerChips[12]; // キャラのチップのグリッドの分割数に応じて3×4 = 12のぶんだけ配列を事前に確保
  LoadDivGraph("Image/pikkoron.png", 12, 3, 4, 32, 32, playerChips); // キャラのチップのサイズは32×32
  assert(playerChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  // 効果音ファイルを事前にメモリに読み込む https://dxlib.xsrv.jp/function/dxfunc_sound.html#R8N4
  int jumpSE[4]; // 4種類のジャンプ音を読み込む
  jumpSE[0] = LoadSoundMem("SE/Jump0.wav");
  jumpSE[1] = LoadSoundMem("SE/Jump1.wav");
  jumpSE[2] = LoadSoundMem("SE/Jump2.wav");
  jumpSE[3] = LoadSoundMem("SE/Jump3.wav");
  assert(jumpSE[0] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[1] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[2] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[3] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  
  int CellSize = 32; // マップの1マスのピクセル数
  int mapChips[64]; // マップのチップのグリッドの分割数に応じて8×8 = 64など配列で確保
  LoadDivGraph("Map/mapchip.png", 64, 8, 8, CellSize, CellSize, mapChips); // マップのチップのサイズは32×32(=CellSize)
  assert(mapChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  
  std::vector<std::vector<int>> mapData;// タイルマップデータ
  std::ifstream ifs_csv_file("Map/stage.csv"); // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
  
  int Width = 0, Height = 0; // マップデータの横(Width) 縦(Height)のマスの数
  int MaxWidth = 0; // 1行の数字の最大個数
  std::string line; // 1行単位ごとにcsvファイルから文字列をstd::getlineで読み込んで受け取る
  while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
  {
    std::vector<int> valuelist; // 1行ぶんの数字リスト配列
    std::istringstream linestream( line ); // 各行の文字列ストリーム
    std::string comma_part; // カンマで分割された文字列
    int widthCount = 0; // カンマで区切られた行の数字の数(各行の幅)をカウント
    while (std::getline(linestream, comma_part, { ',' })) // 1行をgetlineでカンマで区切って列を得る
    {
      std::istringstream ss( comma_part ); // カンマで区切られた文字をssを通して>>で数字に
      int num; // 数字単体
      ss >> num; // 文字列ストリーム>>で数字へ変換
      valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
      ++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
    }
    // 1行の数字の数がMAX記録を更新するかチェック
    if (widthCount > MaxWidth)
      MaxWidth = widthCount; // 暫定Maxの列の幅を更新
    
    // 1行ぶんの数字の配列をとりまとめてmapDataに1行ずつ追加
    if (valuelist.size() != 0) // 配列に数字が1つでもあるなら.size() != 0
      mapData.emplace_back(valuelist); // mapDataに1行ずつ数字配列valuelistを追加
    
    ++Height; //マップの高さをカウントアップ
  }
  Width = MaxWidth; // マップの幅Widthは一番数字の個数の多かった行に合わせる
  // ↓読込んだCSVの幅と高さをチェック
  assert(Width > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  assert(Height > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  
  
  float x = 200, y = Screen::Height; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  enum Direction { Left, Right }; // enumでキャラの向いてる方向をDirection型として2種類(LeftとRight)定義
  Direction direction = Direction::Right; // キャラの向いてる方向
  int animeTime = 0; // アニメ時間
  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16, 4 }, vyDownSpeedMax[] = { 16, 16, 16, 8 }; // ダッシュ段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f,0.1f }, vyGravity[] = { 0.8f,0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  bool isFlying = false; // 飛行モード
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(playerChips[0], &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      direction = Direction::Left;
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      direction = Direction::Right;
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      vy = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
      // 加速段階に応じて違うジャンプ音を鳴らす
      PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs > 9) {
          isFlying = true; // 飛行モードにする
        }
        
        // 加速段階に応じて違うジャンプ音を鳴らす
        PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    x += vx; // 速度のぶん現在の位置から移動
    y += vy;
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    // 着地判定
    if (y > Screen::Height)
    {
      y = Screen::Height; // この行が無いと勢いで地面にめり込んだぶんを地面の位置に沿わせられない
      vy = 0;
      vx_idx = 0;
      isGround = true;
      isFlying = false;
    }
    
    // ダッシュ段階に応じてキャラクタのアニメ時間を進める
    float vx_dash = (vx < 0) ? -vx : vx; // 絶対値でプラスだけにする
    if (vx_dash > 0.5f) animeTime += 1; // 時間 +1 ( 合計 1 )
    if (vx_dash > 6) animeTime += 1; // 時間 +1 (計 1+1=2 ) 1フレームごとに2アニメ時間が進む
    if (vx_dash > 9) animeTime += 2; // 最終段階ではアニメ時間をさらに +2 (計 1+1+2=4 )
    
    int animeStep = animeTime / 10; // 10を5に変えたらアニメ速度が上がる(10ごとに1コマアニメが進む)
    if (animeStep >= 3) // アニメのステップ数が0,1,2の3段階までなら3を超えたら0へループ
    {
      animeTime = 0; // 0へ戻り再びアニメ時間を再ループ
      animeStep = 0;
    }
    
    
    // 描画処理
    if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
      DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f,-60.0f / 360.0f * 2 * DX_PI, playerChips[4], TRUE);
    else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
      DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 60.0f / 360.0f * 2 * DX_PI, playerChips[7], TRUE);
    else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
      DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 0.0f, playerChips[5 - animeStep], TRUE);
    else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
      DrawRotaGraphF(x - imgSizeX / 2, y - imgSizeY / 2, 1.0f, 0.0f, playerChips[6 + animeStep], TRUE);
    
    float rotaShiftX = CellSize / 2, rotaShiftY = CellSize / 2; // DrawRotaGraphF関数は画像の真ん中が起点なので1/2ずらした位置計算
    // 地形マップを[ 行のfor文×列のfor文 ]の2重ループで描く
    for (int cellY = 0, ySize = mapData.size(); cellY < ySize; ++cellY)
    {
      float chip_y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
      for (int cellX = 0, xSize = mapData[cellY].size(); cellX < xSize; ++cellX)
      {
        float chip_x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
        int id = mapData[cellY][cellX]; // [行][列] の位置のcsvの地形番号(チップ番号)を得る
        if (id < 0) continue; // -1のときは描かない
        
        // マップチップを描く
        DrawRotaGraphF(chip_x + rotaShiftX, chip_y + rotaShiftY, 1.0f, 0.0f, mapChips[id], TRUE);

        // [デバッグ]用にcsvのidの数字を描く
        DrawString(chip_x, chip_y, std::to_string(id).c_str(), GetColor(255, 255, 255));
      }
    }
    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}



いかがでしょう、マップ地形が画面に描かれたでしょうか?

チップを描く起点の座標(x,y)=(chip_x, chip_y)は、マス目番号×32(CellSize:チップ画像の分割サイズ)で求められます。
float chip_y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズ
float chip_x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズ

下図はマス目の1マス(セル)のサイズが32の場合の位置の計算方法のイメージです。





地形マップチップの当たり判定(めり込みの余りのぶんだけ押し戻す)プログラム




さてつぎは【キャラの四隅が壁のマップチップか調べて、めり込みの余りのぶんだけ押し戻すサンプル】で地形のブロックの当たり判定でブロックに乗れるようにしましょう。

main.cppを編集します:

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

#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列std::stringに必要
#include <fstream> // ファイル読み出しstd::ifstreamに必要
#include <sstream> // 文字列ストリームに必要 ss >> num;で文字列から数字に変換できる
#include <unordered_map> // std::unordered_map 高速辞書配列に必要(チップ番号から壁などを表すビットへ変換する辞書)

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerChips[12]; // キャラのチップのグリッドの分割数に応じて3×4 = 12のぶんだけ配列を事前に確保
  LoadDivGraph("Image/pikkoron.png", 12, 3, 4, 32, 32, playerChips); // キャラのチップのサイズは32×32
  assert(playerChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  // 効果音ファイルを事前にメモリに読み込む https://dxlib.xsrv.jp/function/dxfunc_sound.html#R8N4
  int jumpSE[4]; // 4種類のジャンプ音を読み込む
  jumpSE[0] = LoadSoundMem("SE/Jump0.wav");
  jumpSE[1] = LoadSoundMem("SE/Jump1.wav");
  jumpSE[2] = LoadSoundMem("SE/Jump2.wav");
  jumpSE[3] = LoadSoundMem("SE/Jump3.wav");
  assert(jumpSE[0] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[1] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[2] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[3] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  
  int CellSize = 32; // マップの1マスのピクセル数
  int mapChips[64]; // マップのチップのグリッドの分割数に応じて8×8 = 64など配列で確保
  LoadDivGraph("Map/mapchip.png", 64, 8, 8, CellSize, CellSize, mapChips); // マップのチップのサイズは32×32(=CellSize)
  assert(mapChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  std::vector<std::vector<int>> mapData;// タイルマップデータ
  std::ifstream ifs_csv_file("Map/stage.csv"); // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
  
  int Width = 0, Height = 0; // マップデータの横(Width) 縦(Height)のマスの数
  int MaxWidth = 0; // 1行の数字の最大個数
  std::string line; // 1行単位ごとにcsvファイルから文字列をstd::getlineで読み込んで受け取る
  while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
  {
    std::vector<int> valuelist; // 1行ぶんの数字リスト配列
    std::istringstream linestream( line ); // 各行の文字列ストリーム
    std::string comma_part; // カンマで分割された文字列
    int widthCount = 0; // カンマで区切られた行の数字の数(各行の幅)をカウント
    while (std::getline(linestream, comma_part, { ',' })) // 1行をgetlineでカンマで区切って列を得る
    {
      std::istringstream ss( comma_part ); // カンマで区切られた文字をssを通して>>で数字に
      int num; // 数字単体
      ss >> num; // 文字列ストリーム>>で数字へ変換
      valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
      ++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
    }
    // 1行の数字の数がMAX記録を更新するかチェック
    if (widthCount > MaxWidth)
      MaxWidth = widthCount; // 暫定Maxの列の幅を更新
    
    // 1行ぶんの数字の配列をとりまとめてmapDataに1行ずつ追加
    if (valuelist.size() != 0) // 配列に数字が1つでもあるなら.size() != 0
      mapData.emplace_back(valuelist); // mapDataに1行ずつ数字配列valuelistを追加
    
    ++Height; //マップの高さをカウントアップ
  }
  Width = MaxWidth; // マップの幅Widthは一番数字の個数の多かった行に合わせる
  // ↓読込んだCSVの幅と高さをチェック
  assert(Width > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  assert(Height > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  
  
  float x = 200, y = Screen::Height - 100; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  enum Direction { Left, Right }; // enumでキャラの向いてる方向をDirection型として2種類(LeftとRight)定義
  Direction direction = Direction::Right; // キャラの向いてる方向
  int animeTime = 0; // アニメ時間
  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16, 4 }, vyDownSpeedMax[] = { 16, 16, 16, 8 }; // ダッシュ段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f,0.1f }, vyGravity[] = { 0.8f,0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  bool isFlying = false; // 飛行モード
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(playerChips[0], &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  int hitOffsetLeft = 2; // 当たり判定の左端のオフセット
  int hitOffsetRight = 2; // 当たり判定の右端のオフセット
  int hitOffsetTop = 1; // 当たり判定の上端のオフセット
  int hitOffsetBottom = 0; // 当たり判定の下端のオフセット

  
  // 当たり判定のはしっこを計算するための一時関数
  auto getLeft = [&hitOffsetLeft](float worldX) { return worldX + hitOffsetLeft; };
  auto getTop = [&hitOffsetTop](float worldY) { return worldY + hitOffsetTop; };
  auto getRight = [&hitOffsetRight, &imgSizeX](float worldX) { return worldX + imgSizeX - hitOffsetRight; };
  auto getBottom = [&hitOffsetBottom, &imgSizeY](float worldY) { return worldY + imgSizeY - hitOffsetBottom; };

  
  enum BlockBit // ブロックの特性を32桁のビットで表現
  {
    None = 0,
    Wall = 0b0000'0000'0000'0000'0000'0000'0000'0001,
  };

  
  std::unordered_map<int, unsigned int> blockBits = {
    {-1,BlockBit::None},
    { 0,BlockBit::Wall},
    { 2,BlockBit::Wall},
    { 3,BlockBit::Wall},
  };

  
  // 指定された座標(worldX,worldY)の地形が 壁 や ダメージブロックかなどをチェックする一時関数
  auto checkID = [&blockBits, &CellSize](BlockBit checkBit, std::vector<std::vector<int>>& terrain, float worldX, float worldY)
    {
      int terrainID = -1; // 指定された座標の地形のID
      
      if (worldX < 0 || worldY < 0) return false; // 負の座標が指定された場合
      
      // マップ座標系(二次元配列の行と列)に変換する
      int mapX = (int)(worldX / CellSize), mapY = (int)(worldY / CellSize);
      // 二次元配列の範囲内か判定
      if (mapY < terrain.size() && mapX < terrain[mapY].size())
        terrainID = terrain[mapY][mapX]; // 二次元配列から地形IDを取り出す
      
      return (checkBit & blockBits[terrainID]) != 0; // ビット論理積 & で地形のchekckBitを確かめる
    };

  
  float debug_x = -1, debug_y = -1; // [デバッグ] めり込みすぎのラインを記録しておく変数
  
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      direction = Direction::Left;
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      direction = Direction::Right;
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      vy = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
      // 加速段階に応じて違うジャンプ音を鳴らす
      PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs > 9) {
          isFlying = true; // 飛行モードにする
        }
        
        // 加速段階に応じて違うジャンプ音を鳴らす
        PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    x += vx; // 速度のぶん現在の位置から移動
    {//←スコープ{}で下記xLeftなどの変数名を{}の内側でリセットして、外側でかぶっても大丈夫なようにする
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      // 左端が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yMiddle) || // 左中が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) ) // 左下が壁か?
      {
        debug_x = xLeft; // [デバック] めり込んだラインを記録しておく
        // std::fmodで小数点の余りを計算 めり込みすぎの余り = std::fmod(xLeft, CellSize)
        float wallRight = xLeft + CellSize - (float)std::fmod(xLeft, CellSize); // 壁の右端
        x = wallRight - hitOffsetLeft; // 左端を壁の右端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      } // ↓右端が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xRight, yTop) || // 右上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yMiddle) || // 右中が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_x = xRight; // [デバック] めり込んだラインを記録しておく
        float wallLeft = xRight - (float)std::fmod(xRight, CellSize); // 壁の左端
        x = wallLeft + hitOffsetRight - imgSizeX; // 右端を壁の左端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      }
    }

    
    y += vy;
    {//←スコープ{}で下記xLeftなどの変数名を{}の内側でリセットして、外側でかぶっても大丈夫なようにする
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      
      // 頭の部分が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yTop) || // 中上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yTop) ) // 右上が壁か?
      {
        debug_y = yTop; // [デバック] めり込んだラインを記録しておく
        float wallBottom = yTop + CellSize - (float)std::fmod(yTop, CellSize); // 壁の下側の境界
        y = wallBottom - hitOffsetTop; // 頭を壁の下端に沿わす(めり込んだぶんを押し戻す)
        vy = 0; // 壁に当たったら速度をストップ
      } // ↓足元が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) || // 左下が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yBottom) || // 中下が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_y = yBottom; // [デバック] めり込んだラインを記録しておく
        float wallTop = yBottom - (float)std::fmod(yBottom, CellSize); // 壁の上側の境界
        y = wallTop + hitOffsetBottom - imgSizeX; // 足元を壁の上端に沿わす(めり込んだぶんを押し戻す)
        
        isGround = true;
        isFlying = false;
        vx_idx = 0;
        
        vy = 0; // 床に当たったら速度をストップ
      }
      else
        isGround = false; // ブロックに当たってないからfalseにして落下するように
    }

    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    // 着地判定
    if (y > Screen::Height)
    {
      y = Screen::Height; // この行が無いと勢いで地面にめり込んだぶんを地面の位置に沿わせられない
      vy = 0;
      vx_idx = 0;
      isGround = true;
      isFlying = false;
    }

    
    // ダッシュ段階に応じてキャラクタのアニメ時間を進める
    float vx_dash = (vx < 0) ? -vx : vx; // 絶対値でプラスだけにする
    if (vx_dash > 0.5f) animeTime += 1; // 時間 +1 ( 合計 1 )
    if (vx_dash > 6) animeTime += 1; // 時間 +1 (計 1+1=2 ) 1フレームごとに2アニメ時間が進む
    if (vx_dash > 9) animeTime += 2; // 最終段階ではアニメ時間をさらに +2 (計 1+1+2=4 )
    
    int animeStep = animeTime / 10; // 10を5に変えたらアニメ速度が上がる(10ごとに1コマアニメが進む)
    if (animeStep >= 3) // アニメのステップ数が0,1,2の3段階までなら3を超えたら0へループ
    {
      animeTime = 0; // 0へ戻り再びアニメ時間を再ループ
      animeStep = 0;
    }
    
    
    // 描画処理
    if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
      DrawRotaGraphF(x + imgSizeX / 2, y + imgSizeY / 2, 1.0f,-60.0f / 360.0f * 2 * DX_PI, playerChips[4], TRUE);
    else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
      DrawRotaGraphF(x + imgSizeX / 2, y + imgSizeY / 2, 1.0f, 60.0f / 360.0f * 2 * DX_PI, playerChips[7], TRUE);
    else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
      DrawRotaGraphF(x + imgSizeX / 2, y + imgSizeY / 2, 1.0f, 0.0f, playerChips[5 - animeStep], TRUE);
    else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
      DrawRotaGraphF(x + imgSizeX / 2, y + imgSizeY / 2, 1.0f, 0.0f, playerChips[6 + animeStep], TRUE);
    
    float rotaShiftX = CellSize / 2, rotaShiftY = CellSize / 2; // DrawRotaGraphF関数は画像の真ん中が起点なので1/2ずらした位置計算
    // 地形マップを[ 行のfor文×列のfor文 ]の2重ループで描く
    for (int cellY = 0, ySize = mapData.size(); cellY < ySize; ++cellY)
    {
      float chip_y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
      for (int cellX = 0, xSize = mapData[cellY].size(); cellX < xSize; ++cellX)
      {
        float chip_x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
        int id = mapData[cellY][cellX]; // [行][列] の位置のcsvの地形番号(チップ番号)を得る
        if (id < 0) continue; // -1のときは描かない
        
        // マップチップを描く
        DrawRotaGraphF(chip_x + rotaShiftX, chip_y + rotaShiftY, 1.0f, 0.0f, mapChips[id], TRUE);
        // [デバッグ]用にcsvのidの数字を描く
        DrawString(chip_x, chip_y, std::to_string(id).c_str(), GetColor(255, 255, 255));
      }
    }
    
    // [デバッグ] 当たり判定の四隅を赤枠で描画
    DrawBox(getLeft(x), getTop(y), getRight(x), getBottom(y), GetColor(255, 0, 0), FALSE);
    // [デバッグ] キャラ原点(x,y)を緑丸で描画
    DrawCircle(x, y, 2, GetColor(0, 255, 0), TRUE);
    // [デバッグ] 当たった際の行き過ぎの めりこみ座標(debug_x,debug_y) を青線で描画
    if (debug_x != -1)
      DrawLine(debug_x, 0, debug_x, Screen::Height, GetColor(0, 0, 255));
    if (debug_y != -1)
      DrawLine(0, debug_y, Screen::Width, debug_y, GetColor(0, 0, 255));

    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}



いかがでしょう、マップ地形のブロックに当たったら立てるようになりましたか?

キャラによって当たり判定のオフセットの数字を変えてキャラの背の高さや幅にフィットさせるようにするとよいでしょう。
int hitOffsetLeft = 2; // 当たり判定の左端のオフセット
int hitOffsetRight = 2; // 当たり判定の右端のオフセット
int hitOffsetTop = 1; // 当たり判定の上端のオフセット
int hitOffsetBottom = 0; // 当たり判定の下端のオフセット






キャラの移動に連動したcamera_x,camera_yで「描く地形のほうをスクロールでずらす」プログラム




さてつぎは【キャラのx座標からスクリーンの幅の1/2を引いたぶん(camera_x)だけ背後の地形を描く起点をずらすサンプル】で地形のスクロールに挑戦しましょう。

main.cppを編集します:

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

#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列std::stringに必要
#include <fstream> // ファイル読み出しstd::ifstreamに必要
#include <sstream> // 文字列ストリームに必要 ss >> num;で文字列から数字に変換できる
#include <unordered_map> // std::unordered_map 高速辞書配列に必要(チップ番号から壁などを表すビットへ変換する辞書)

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerChips[12]; // キャラのチップのグリッドの分割数に応じて3×4 = 12のぶんだけ配列を事前に確保
  LoadDivGraph("Image/pikkoron.png", 12, 3, 4, 32, 32, playerChips); // キャラのチップのサイズは32×32
  assert(playerChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  // 効果音ファイルを事前にメモリに読み込む https://dxlib.xsrv.jp/function/dxfunc_sound.html#R8N4
  int jumpSE[4]; // 4種類のジャンプ音を読み込む
  jumpSE[0] = LoadSoundMem("SE/Jump0.wav");
  jumpSE[1] = LoadSoundMem("SE/Jump1.wav");
  jumpSE[2] = LoadSoundMem("SE/Jump2.wav");
  jumpSE[3] = LoadSoundMem("SE/Jump3.wav");
  assert(jumpSE[0] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[1] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[2] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[3] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  
  int CellSize = 32; // マップの1マスのピクセル数
  int mapChips[64]; // マップのチップのグリッドの分割数に応じて8×8 = 64など配列で確保
  LoadDivGraph("Map/mapchip.png", 64, 8, 8, CellSize, CellSize, mapChips); // マップのチップのサイズは32×32(=CellSize)
  assert(mapChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  std::vector<std::vector<int>> mapData;// タイルマップデータ
  std::ifstream ifs_csv_file("Map/stage.csv"); // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
  
  int Width = 0, Height = 0; // マップデータの横(Width) 縦(Height)のマスの数
  int MaxWidth = 0; // 1行の数字の最大個数
  std::string line; // 1行単位ごとにcsvファイルから文字列をstd::getlineで読み込んで受け取る
  while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
  {
    std::vector<int> valuelist; // 1行ぶんの数字リスト配列
    std::istringstream linestream( line ); // 各行の文字列ストリーム
    std::string comma_part; // カンマで分割された文字列
    int widthCount = 0; // カンマで区切られた行の数字の数(各行の幅)をカウント
    while (std::getline(linestream, comma_part, { ',' })) // 1行をgetlineでカンマで区切って列を得る
    {
      std::istringstream ss( comma_part ); // カンマで区切られた文字をssを通して>>で数字に
      int num; // 数字単体
      ss >> num; // 文字列ストリーム>>で数字へ変換
      valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
      ++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
    }
    // 1行の数字の数がMAX記録を更新するかチェック
    if (widthCount > MaxWidth)
      MaxWidth = widthCount; // 暫定Maxの列の幅を更新
    
    // 1行ぶんの数字の配列をとりまとめてmapDataに1行ずつ追加
    if (valuelist.size() != 0) // 配列に数字が1つでもあるなら.size() != 0
      mapData.emplace_back(valuelist); // mapDataに1行ずつ数字配列valuelistを追加
    
    ++Height; //マップの高さをカウントアップ
  }
  Width = MaxWidth; // マップの幅Widthは一番数字の個数の多かった行に合わせる
  // ↓読込んだCSVの幅と高さをチェック
  assert(Width > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  assert(Height > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  
  
  float x = 200, y = Screen::Height - 100; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  enum Direction { Left, Right }; // enumでキャラの向いてる方向をDirection型として2種類(LeftとRight)定義
  Direction direction = Direction::Right; // キャラの向いてる方向
  int animeTime = 0; // アニメ時間
  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16, 4 }, vyDownSpeedMax[] = { 16, 16, 16, 8 }; // ダッシュ段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f,0.1f }, vyGravity[] = { 0.8f,0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  bool isFlying = false; // 飛行モード
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(playerChips[0], &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  int hitOffsetLeft = 2; // 当たり判定の左端のオフセット
  int hitOffsetRight = 2; // 当たり判定の右端のオフセット
  int hitOffsetTop = 1; // 当たり判定の上端のオフセット
  int hitOffsetBottom = 0; // 当たり判定の下端のオフセット
  
  // 当たり判定のはしっこを計算するための一時関数
  auto getLeft = [&hitOffsetLeft](float worldX) { return worldX + hitOffsetLeft; };
  auto getTop = [&hitOffsetTop](float worldY) { return worldY + hitOffsetTop; };
  auto getRight = [&hitOffsetRight, &imgSizeX](float worldX) { return worldX + imgSizeX - hitOffsetRight; };
  auto getBottom = [&hitOffsetBottom, &imgSizeY](float worldY) { return worldY + imgSizeY - hitOffsetBottom; };
  
  enum BlockBit // ブロックの特性を32桁のビットで表現
  {
    None = 0,
    Wall = 0b0000'0000'0000'0000'0000'0000'0000'0001,
  };
  
  std::unordered_map<int, unsigned int> blockBits = {
    {-1,BlockBit::None},
    { 0,BlockBit::Wall},
    { 2,BlockBit::Wall},
    { 3,BlockBit::Wall},
  };
  
  // 指定された座標(worldX,worldY)の地形が 壁 や ダメージブロックかなどをチェックする一時関数
  auto checkID = [&blockBits, &CellSize](BlockBit checkBit, std::vector<std::vector<int>>& terrain, float worldX, float worldY)
    {
      int terrainID = -1; // 指定された座標の地形のID
      
      if (worldX < 0 || worldY < 0) return false; // 負の座標が指定された場合
      
      // マップ座標系(二次元配列の行と列)に変換する
      int mapX = (int)(worldX / CellSize), mapY = (int)(worldY / CellSize);
      // 二次元配列の範囲内か判定
      if (mapY < terrain.size() && mapX < terrain[mapY].size())
        terrainID = terrain[mapY][mapX]; // 二次元配列から地形IDを取り出す
      
      return (checkBit & blockBits[terrainID]) != 0; // ビット論理積 & で地形のchekckBitを確かめる
    };
  
  float camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
  if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ

  
  
  float debug_x = -1, debug_y = -1; // [デバッグ] めり込みすぎのラインを記録しておく変数
  float debug_x_yTop = -1, debug_x_yBottom = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の上下端も記録
  
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      direction = Direction::Left;
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      direction = Direction::Right;
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      vy = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
      // 加速段階に応じて違うジャンプ音を鳴らす
      PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs > 9) {
          isFlying = true; // 飛行モードにする
        }
        
        // 加速段階に応じて違うジャンプ音を鳴らす
        PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    x += vx; // 速度のぶん現在の位置から移動
    {//←スコープ{}で下記xLeftなどの変数名を{}の内側でリセットして、外側でかぶっても大丈夫なようにする
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      // 左端が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yMiddle) || // 左中が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) ) // 左下が壁か?
      {
        debug_x = xLeft; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        // std::fmodで小数点の余りを計算 めり込みすぎの余り = std::fmod(xLeft, CellSize)
        float wallRight = xLeft + CellSize - (float)std::fmod(xLeft, CellSize); // 壁の右端
        x = wallRight - hitOffsetLeft; // 左端を壁の右端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      } // ↓右端が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xRight, yTop) || // 右上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yMiddle) || // 右中が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_x = xRight; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        float wallLeft = xRight - (float)std::fmod(xRight, CellSize); // 壁の左端
        x = wallLeft + hitOffsetRight - imgSizeX; // 右端を壁の左端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      }
    }
    
    y += vy;
    {//←スコープ{}で下記xLeftなどの変数名を{}の内側でリセットして、外側でかぶっても大丈夫なようにする
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      
      // 頭の部分が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yTop) || // 中上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yTop) ) // 右上が壁か?
      {
        debug_y = yTop; // [デバック] めり込んだラインを記録しておく
        float wallBottom = yTop + CellSize - (float)std::fmod(yTop, CellSize); // 壁の下側の境界
        y = wallBottom - hitOffsetTop; // 頭を壁の下端に沿わす(めり込んだぶんを押し戻す)
        vy = 0; // 壁に当たったら速度をストップ
      } // ↓足元が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) || // 左下が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yBottom) || // 中下が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_y = yBottom; // [デバック] めり込んだラインを記録しておく
        float wallTop = yBottom - (float)std::fmod(yBottom, CellSize); // 壁の上側の境界
        y = wallTop + hitOffsetBottom - imgSizeX; // 足元を壁の上端に沿わす(めり込んだぶんを押し戻す)
        
        isGround = true;
        isFlying = false;
        vx_idx = 0;
        
        vy = 0; // 床に当たったら速度をストップ
      }
      else
        isGround = false; // ブロックに当たってないからfalseにして落下するように
    }
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    
    camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
    if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ

    
    
    // ダッシュ段階に応じてキャラクタのアニメ時間を進める
    float vx_dash = (vx < 0) ? -vx : vx; // 絶対値でプラスだけにする
    if (vx_dash > 0.5f) animeTime += 1; // 時間 +1 ( 合計 1 )
    if (vx_dash > 6) animeTime += 1; // 時間 +1 (計 1+1=2 ) 1フレームごとに2アニメ時間が進む
    if (vx_dash > 9) animeTime += 2; // 最終段階ではアニメ時間をさらに +2 (計 1+1+2=4 )
    
    int animeStep = animeTime / 10; // 10を5に変えたらアニメ速度が上がる(10ごとに1コマアニメが進む)
    if (animeStep >= 3) // アニメのステップ数が0,1,2の3段階までなら3を超えたら0へループ
    {
      animeTime = 0; // 0へ戻り再びアニメ時間を再ループ
      animeStep = 0;
    }
    
    
    // 描画処理
    if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
      DrawRotaGraphF(x - camera_x + imgSizeX / 2, y + imgSizeY / 2, 1.0f,-60.0f / 360.0f * 2 * DX_PI, playerChips[4], TRUE);
    else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
      DrawRotaGraphF(x - camera_x + imgSizeX / 2, y + imgSizeY / 2, 1.0f, 60.0f / 360.0f * 2 * DX_PI, playerChips[7], TRUE);
    else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
      DrawRotaGraphF(x - camera_x + imgSizeX / 2, y + imgSizeY / 2, 1.0f, 0.0f, playerChips[5 - animeStep], TRUE);
    else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
      DrawRotaGraphF(x - camera_x + imgSizeX / 2, y + imgSizeY / 2, 1.0f, 0.0f, playerChips[6 + animeStep], TRUE);
    
    float rotaShiftX = CellSize / 2, rotaShiftY = CellSize / 2; // DrawRotaGraphF関数は画像の真ん中が起点なので1/2ずらした位置計算
    // 地形マップを[ 行のfor文×列のfor文 ]の2重ループで描く
    for (int cellY = 0, ySize = mapData.size(); cellY < ySize; ++cellY)
    {
      float chip_y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
      for (int cellX = 0, xSize = mapData[cellY].size(); cellX < xSize; ++cellX)
      {
        float chip_x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
        int id = mapData[cellY][cellX]; // [行][列] の位置のcsvの地形番号(チップ番号)を得る
        if (id < 0) continue; // -1のときは描かない
        
        // マップチップを描く
        DrawRotaGraphF(chip_x - camera_x + rotaShiftX, chip_y + rotaShiftY, 1.0f, 0.0f, mapChips[id], TRUE);
        // [デバッグ]用にcsvのidの数字を描く
        DrawString(chip_x - camera_x, chip_y, std::to_string(id).c_str(), GetColor(255, 255, 255));
      }
    }
    
    // [デバッグ] 当たり判定の四隅を赤枠で描画
    DrawBox(getLeft(x) - camera_x, getTop(y), getRight(x) - camera_x, getBottom(y), GetColor(255, 0, 0), FALSE);
    // [デバッグ] キャラ原点(x,y)を緑丸で描画
    DrawCircle(x - camera_x, y, 2, GetColor(0, 255, 0), TRUE);
    // [デバッグ] 当たった際の行き過ぎの めりこみ座標(debug_x,debug_y) を青線で描画
    if (debug_x != -1)
      DrawLine(debug_x - camera_x, debug_x_yTop, debug_x - camera_x, debug_x_yBottom, GetColor(0, 0, 255));
    if (debug_y != -1)
      DrawLine(-camera_x, debug_y, Screen::Width - camera_x, debug_y, GetColor(0, 0, 255));
    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}



いかがでしょうか、キャラが右端に行こうとすると画面がスクロールするようになりましたか?

今回のプログラムは一番の昔のマリオ形式のように地形が横方向(x方向)のみにスクロールするように限定しました。
つぎは地形をタイルエディタで書き足して、縦方向(y方向)にもスクロールするように改造してみましょう。




さてつぎは【キャラのy座標からスクリーンの高さの1/2を引いたぶん(camera_y)だけ背後の地形を描く起点をずらすサンプル】で地形のスクロールに挑戦しましょう。

main.cppを編集します:

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

#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列std::stringに必要
#include <fstream> // ファイル読み出しstd::ifstreamに必要
#include <sstream> // 文字列ストリームに必要 ss >> num;で文字列から数字に変換できる
#include <unordered_map> // std::unordered_map 高速辞書配列に必要(チップ番号から壁などを表すビットへ変換する辞書)

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerChips[12]; // キャラのチップのグリッドの分割数に応じて3×4 = 12のぶんだけ配列を事前に確保
  LoadDivGraph("Image/pikkoron.png", 12, 3, 4, 32, 32, playerChips); // キャラのチップのサイズは32×32
  assert(playerChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  // 効果音ファイルを事前にメモリに読み込む https://dxlib.xsrv.jp/function/dxfunc_sound.html#R8N4
  int jumpSE[4]; // 4種類のジャンプ音を読み込む
  jumpSE[0] = LoadSoundMem("SE/Jump0.wav");
  jumpSE[1] = LoadSoundMem("SE/Jump1.wav");
  jumpSE[2] = LoadSoundMem("SE/Jump2.wav");
  jumpSE[3] = LoadSoundMem("SE/Jump3.wav");
  assert(jumpSE[0] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[1] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[2] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[3] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  
  int CellSize = 32; // マップの1マスのピクセル数
  int mapChips[64]; // マップのチップのグリッドの分割数に応じて8×8 = 64など配列で確保
  LoadDivGraph("Map/mapchip.png", 64, 8, 8, CellSize, CellSize, mapChips); // マップのチップのサイズは32×32(=CellSize)
  assert(mapChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  std::vector<std::vector<int>> mapData;// タイルマップデータ
  std::ifstream ifs_csv_file("Map/stage.csv"); // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
  
  int Width = 0, Height = 0; // マップデータの横(Width) 縦(Height)のマスの数
  int MaxWidth = 0; // 1行の数字の最大個数
  std::string line; // 1行単位ごとにcsvファイルから文字列をstd::getlineで読み込んで受け取る
  while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
  {
    std::vector<int> valuelist; // 1行ぶんの数字リスト配列
    std::istringstream linestream( line ); // 各行の文字列ストリーム
    std::string comma_part; // カンマで分割された文字列
    int widthCount = 0; // カンマで区切られた行の数字の数(各行の幅)をカウント
    while (std::getline(linestream, comma_part, { ',' })) // 1行をgetlineでカンマで区切って列を得る
    {
      std::istringstream ss( comma_part ); // カンマで区切られた文字をssを通して>>で数字に
      int num; // 数字単体
      ss >> num; // 文字列ストリーム>>で数字へ変換
      valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
      ++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
    }
    // 1行の数字の数がMAX記録を更新するかチェック
    if (widthCount > MaxWidth)
      MaxWidth = widthCount; // 暫定Maxの列の幅を更新
    
    // 1行ぶんの数字の配列をとりまとめてmapDataに1行ずつ追加
    if (valuelist.size() != 0) // 配列に数字が1つでもあるなら.size() != 0
      mapData.emplace_back(valuelist); // mapDataに1行ずつ数字配列valuelistを追加
    
    ++Height; //マップの高さをカウントアップ
  }
  Width = MaxWidth; // マップの幅Widthは一番数字の個数の多かった行に合わせる
  // ↓読込んだCSVの幅と高さをチェック
  assert(Width > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  assert(Height > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  
  
  float x = 200, y = Screen::Height - 100; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  enum Direction { Left, Right }; // enumでキャラの向いてる方向をDirection型として2種類(LeftとRight)定義
  Direction direction = Direction::Right; // キャラの向いてる方向
  int animeTime = 0; // アニメ時間
  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16, 4 }, vyDownSpeedMax[] = { 16, 16, 16, 8 }; // ダッシュ段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f,0.1f }, vyGravity[] = { 0.8f,0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  bool isFlying = false; // 飛行モード
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(playerChips[0], &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  int hitOffsetLeft = 2; // 当たり判定の左端のオフセット
  int hitOffsetRight = 2; // 当たり判定の右端のオフセット
  int hitOffsetTop = 1; // 当たり判定の上端のオフセット
  int hitOffsetBottom = 0; // 当たり判定の下端のオフセット
  
  // 当たり判定のはしっこを計算するための一時関数
  auto getLeft = [&hitOffsetLeft](float worldX) { return worldX + hitOffsetLeft; };
  auto getTop = [&hitOffsetTop](float worldY) { return worldY + hitOffsetTop; };
  auto getRight = [&hitOffsetRight, &imgSizeX](float worldX) { return worldX + imgSizeX - hitOffsetRight; };
  auto getBottom = [&hitOffsetBottom, &imgSizeY](float worldY) { return worldY + imgSizeY - hitOffsetBottom; };
  
  enum BlockBit // ブロックの特性を32桁のビットで表現
  {
    None = 0,
    Wall = 0b0000'0000'0000'0000'0000'0000'0000'0001,
  };
  
  std::unordered_map<int, unsigned int> blockBits = {
    {-1,BlockBit::None},
    { 0,BlockBit::Wall},
    { 2,BlockBit::Wall},
    { 3,BlockBit::Wall},
  };
  
  // 指定された座標(worldX,worldY)の地形が 壁 や ダメージブロックかなどをチェックする一時関数
  auto checkID = [&blockBits, &CellSize](BlockBit checkBit, std::vector<std::vector<int>>& terrain, float worldX, float worldY)
    {
      int terrainID = -1; // 指定された座標の地形のID
      
      if (worldX < 0 || worldY < 0) return false; // 負の座標が指定された場合
      
      // マップ座標系(二次元配列の行と列)に変換する
      int mapX = (int)(worldX / CellSize), mapY = (int)(worldY / CellSize);
      // 二次元配列の範囲内か判定
      if (mapY < terrain.size() && mapX < terrain[mapY].size())
        terrainID = terrain[mapY][mapX]; // 二次元配列から地形IDを取り出す
      
      return (checkBit & blockBits[terrainID]) != 0; // ビット論理積 & で地形のchekckBitを確かめる
    };
  
  float camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
  if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ
  float camera_y = y - Screen::Height / 2; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
  if (camera_y <= 0) camera_y = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Height * CellSize - Screen::Height <= camera_y) camera_y = Height * CellSize - Screen::Height; // ステージのはしっこ

  
  
  float debug_x = -1, debug_y = -1; // [デバッグ] めり込みすぎのラインを記録しておく変数
  float debug_x_yTop = -1, debug_x_yBottom = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の上下端も記録
  float debug_y_xLeft = -1, debug_y_xRight = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の左右端も記録
  
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      direction = Direction::Left;
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      direction = Direction::Right;
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      vy = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
      // 加速段階に応じて違うジャンプ音を鳴らす
      PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs > 9) {
          isFlying = true; // 飛行モードにする
        }
        
        // 加速段階に応じて違うジャンプ音を鳴らす
        PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    x += vx; // 速度のぶん現在の位置から移動
    {//←スコープ{}で下記xLeftなどの変数名を{}の内側でリセットして、外側でかぶっても大丈夫なようにする
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      // 左端が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yMiddle) || // 左中が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) ) // 左下が壁か?
      {
        debug_x = xLeft; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        // std::fmodで小数点の余りを計算 めり込みすぎの余り = std::fmod(xLeft, CellSize)
        float wallRight = xLeft + CellSize - (float)std::fmod(xLeft, CellSize); // 壁の右端
        x = wallRight - hitOffsetLeft; // 左端を壁の右端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      } // ↓右端が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xRight, yTop) || // 右上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yMiddle) || // 右中が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_x = xRight; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        float wallLeft = xRight - (float)std::fmod(xRight, CellSize); // 壁の左端
        x = wallLeft + hitOffsetRight - imgSizeX; // 右端を壁の左端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      }
    }
    
    y += vy;
    {//←スコープ{}で下記xLeftなどの変数名を{}の内側でリセットして、外側でかぶっても大丈夫なようにする
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      
      // 頭の部分が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yTop) || // 中上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yTop) ) // 右上が壁か?
      {
        debug_y = yTop; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallBottom = yTop + CellSize - (float)std::fmod(yTop, CellSize); // 壁の下側の境界
        y = wallBottom - hitOffsetTop; // 頭を壁の下端に沿わす(めり込んだぶんを押し戻す)
        vy = 0; // 壁に当たったら速度をストップ
      } // ↓足元が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) || // 左下が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yBottom) || // 中下が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_y = yBottom; // [デバック] めり込んだラインを記録しておく
        float wallTop = yBottom - (float)std::fmod(yBottom, CellSize); // 壁の上側の境界
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        y = wallTop + hitOffsetBottom - imgSizeX; // 足元を壁の上端に沿わす(めり込んだぶんを押し戻す)
        
        isGround = true;
        isFlying = false;
        vx_idx = 0;
        
        vy = 0; // 床に当たったら速度をストップ
      }
      else
        isGround = false; // ブロックに当たってないからfalseにして落下するように
    }
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    
    camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
    if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ
    camera_y = y - Screen::Height / 2; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
    if (camera_y <= 0) camera_y = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Height * CellSize - Screen::Height <= camera_y) camera_y = Height * CellSize - Screen::Height; // ステージのはしっこ

    
    
    // ダッシュ段階に応じてキャラクタのアニメ時間を進める
    float vx_dash = (vx < 0) ? -vx : vx; // 絶対値でプラスだけにする
    if (vx_dash > 0.5f) animeTime += 1; // 時間 +1 ( 合計 1 )
    if (vx_dash > 6) animeTime += 1; // 時間 +1 (計 1+1=2 ) 1フレームごとに2アニメ時間が進む
    if (vx_dash > 9) animeTime += 2; // 最終段階ではアニメ時間をさらに +2 (計 1+1+2=4 )
    
    int animeStep = animeTime / 10; // 10を5に変えたらアニメ速度が上がる(10ごとに1コマアニメが進む)
    if (animeStep >= 3) // アニメのステップ数が0,1,2の3段階までなら3を超えたら0へループ
    {
      animeTime = 0; // 0へ戻り再びアニメ時間を再ループ
      animeStep = 0;
    }
    
    
    // 描画処理
    if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
      DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f,-60.0f / 360.0f * 2 * DX_PI, playerChips[4], TRUE);
    else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
      DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 60.0f / 360.0f * 2 * DX_PI, playerChips[7], TRUE);
    else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
      DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 0.0f, playerChips[5 - animeStep], TRUE);
    else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
      DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 0.0f, playerChips[6 + animeStep], TRUE);
    
    float rotaShiftX = CellSize / 2, rotaShiftY = CellSize / 2; // DrawRotaGraphF関数は画像の真ん中が起点なので1/2ずらした位置計算
    // 地形マップを[ 行のfor文×列のfor文 ]の2重ループで描く
    for (int cellY = 0, ySize = mapData.size(); cellY < ySize; ++cellY)
    {
      float chip_y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
      for (int cellX = 0, xSize = mapData[cellY].size(); cellX < xSize; ++cellX)
      {
        float chip_x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
        int id = mapData[cellY][cellX]; // [行][列] の位置のcsvの地形番号(チップ番号)を得る
        if (id < 0) continue; // -1のときは描かない
        
        // マップチップを描く
        DrawRotaGraphF(chip_x - camera_x + rotaShiftX, chip_y - camera_y + rotaShiftY, 1.0f, 0.0f, mapChips[id], TRUE);
        // [デバッグ]用にcsvのidの数字を描く
        DrawString(chip_x - camera_x, chip_y - camera_y, std::to_string(id).c_str(), GetColor(255, 255, 255));
      }
    }
    
    // [デバッグ] 当たり判定の四隅を赤枠で描画
    DrawBox(getLeft(x) - camera_x, getTop(y) - camera_y, getRight(x) - camera_x, getBottom(y) - camera_y, GetColor(255, 0, 0), FALSE);
    // [デバッグ] キャラ原点(x,y)を緑丸で描画
    DrawCircle(x - camera_x, y - camera_y, 2, GetColor(0, 255, 0), TRUE);
    // [デバッグ] 当たった際の行き過ぎの めりこみ座標(debug_x,debug_y) を青線で描画
    if (debug_x != -1)
      DrawLine(debug_x - camera_x, debug_x_yTop- camera_y, debug_x - camera_x, debug_x_yBottom - camera_y, GetColor(0, 0, 255));
    if (debug_y != -1)
      DrawLine(debug_y_xLeft - camera_x, debug_y - camera_y, debug_y_xRight - camera_x, debug_y - camera_y, GetColor(0, 0, 255));
    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}



いかがでしょう、キャラが上下移動したら上下方向にスクロールするようになりましたか?
スクロールしない人は、地形ファイルが横長で、上下縦長の方向に地形がないのでタイルマップエディタで地形を書き足す必要がありそうです。
スクリーンのサイズが960×540ならば、タイル32×17個=544と比べると、タイルは少なくとも18以上の縦方向のタイルの数があればスクロールするようになるはずです。





トゲトゲに当たったらキャラがくるくる回ってから下に落ちて最初のスタート位置に戻されるプログラム




さてつぎは【トゲトゲに当たるとisDeadフラグをtrueにしてキャラをくるくる回しながら、ミス位置で少し浮かせて(yAnim0 ~ yAnim1)下に落ちて(yAnim1 ~ yAnim2)最初のスタート位置に戻るサンプル】でプレイヤーのミスの処理を実現しましょう。

main.cppを編集します:

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

#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列std::stringに必要
#include <fstream> // ファイル読み出しstd::ifstreamに必要
#include <sstream> // 文字列ストリームに必要 ss >> num;で文字列から数字に変換できる
#include <unordered_map> // std::unordered_map 高速辞書配列に必要(チップ番号から壁などを表すビットへ変換する辞書)

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerChips[12]; // キャラのチップのグリッドの分割数に応じて3×4 = 12のぶんだけ配列を事前に確保
  LoadDivGraph("Image/pikkoron.png", 12, 3, 4, 32, 32, playerChips); // キャラのチップのサイズは32×32
  assert(playerChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  // 効果音ファイルを事前にメモリに読み込む https://dxlib.xsrv.jp/function/dxfunc_sound.html#R8N4
  int jumpSE[4]; // 4種類のジャンプ音を読み込む
  jumpSE[0] = LoadSoundMem("SE/Jump0.wav");
  jumpSE[1] = LoadSoundMem("SE/Jump1.wav");
  jumpSE[2] = LoadSoundMem("SE/Jump2.wav");
  jumpSE[3] = LoadSoundMem("SE/Jump3.wav");
  assert(jumpSE[0] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[1] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[2] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[3] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  
  int CellSize = 32; // マップの1マスのピクセル数
  int mapChips[64]; // マップのチップのグリッドの分割数に応じて8×8 = 64など配列で確保
  LoadDivGraph("Map/mapchip.png", 64, 8, 8, CellSize, CellSize, mapChips); // マップのチップのサイズは32×32(=CellSize)
  assert(mapChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  std::vector<std::vector<int>> mapData;// タイルマップデータ
  std::ifstream ifs_csv_file("Map/stage.csv"); // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
  
  int Width = 0, Height = 0; // マップデータの横(Width) 縦(Height)のマスの数
  int MaxWidth = 0; // 1行の数字の最大個数
  std::string line; // 1行単位ごとにcsvファイルから文字列をstd::getlineで読み込んで受け取る
  while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
  {
    std::vector<int> valuelist; // 1行ぶんの数字リスト配列
    std::istringstream linestream( line ); // 各行の文字列ストリーム
    std::string comma_part; // カンマで分割された文字列
    int widthCount = 0; // カンマで区切られた行の数字の数(各行の幅)をカウント
    while (std::getline(linestream, comma_part, { ',' })) // 1行をgetlineでカンマで区切って列を得る
    {
      std::istringstream ss( comma_part ); // カンマで区切られた文字をssを通して>>で数字に
      int num; // 数字単体
      ss >> num; // 文字列ストリーム>>で数字へ変換
      valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
      ++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
    }
    // 1行の数字の数がMAX記録を更新するかチェック
    if (widthCount > MaxWidth)
      MaxWidth = widthCount; // 暫定Maxの列の幅を更新
    
    // 1行ぶんの数字の配列をとりまとめてmapDataに1行ずつ追加
    if (valuelist.size() != 0) // 配列に数字が1つでもあるなら.size() != 0
      mapData.emplace_back(valuelist); // mapDataに1行ずつ数字配列valuelistを追加
    
    ++Height; //マップの高さをカウントアップ
  }
  Width = MaxWidth; // マップの幅Widthは一番数字の個数の多かった行に合わせる
  // ↓読込んだCSVの幅と高さをチェック
  assert(Width > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  assert(Height > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  
  
  float xStart = 200, yStart = Screen::Height - 100; // キャラの初期スタート(ミス後の再開位置)
  float x = xStart, y = yStart; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  enum Direction { Left, Right }; // enumでキャラの向いてる方向をDirection型として2種類(LeftとRight)定義
  Direction direction = Direction::Right; // キャラの向いてる方向
  int animeTime = 0; // アニメ時間
  
  bool isDead = false; // キャラがやられたかどうか
  int deadAnimeTime = 0; // やられたときのアニメ時間

  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16, 4 }, vyDownSpeedMax[] = { 16, 16, 16, 8 }; // ダッシュ段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f,0.1f }, vyGravity[] = { 0.8f,0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  bool isFlying = false; // 飛行モード
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(playerChips[0], &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  int hitOffsetLeft = 2; // 当たり判定の左端のオフセット
  int hitOffsetRight = 2; // 当たり判定の右端のオフセット
  int hitOffsetTop = 1; // 当たり判定の上端のオフセット
  int hitOffsetBottom = 0; // 当たり判定の下端のオフセット
  
  // 当たり判定のはしっこを計算するための一時関数
  auto getLeft = [&hitOffsetLeft](float worldX) { return worldX + hitOffsetLeft; };
  auto getTop = [&hitOffsetTop](float worldY) { return worldY + hitOffsetTop; };
  auto getRight = [&hitOffsetRight, &imgSizeX](float worldX) { return worldX + imgSizeX - hitOffsetRight; };
  auto getBottom = [&hitOffsetBottom, &imgSizeY](float worldY) { return worldY + imgSizeY - hitOffsetBottom; };
  
  enum BlockBit // ブロックの特性を32桁のビットで表現
  {
    None = 0,
    Wall   = 0b0000'0000'0000'0000'0000'0000'0000'0001,
    Damage = 0b0000'0000'0000'0000'0000'0000'0000'0010,
  };
  
  std::unordered_map<int, unsigned int> blockBits = {
    {-1,BlockBit::None},
    { 0,BlockBit::Wall},
    { 1,BlockBit::Damage},
    { 2,BlockBit::Wall},
    { 3,BlockBit::Wall},
  };
  
  // 指定された座標(worldX,worldY)の地形が 壁 や ダメージブロックかなどをチェックする一時関数
  auto checkID = [&blockBits, &CellSize](BlockBit checkBit, std::vector<std::vector<int>>& terrain, float worldX, float worldY)
    {
      int terrainID = -1; // 指定された座標の地形のID
      
      if (worldX < 0 || worldY < 0) return false; // 負の座標が指定された場合
      
      // マップ座標系(二次元配列の行と列)に変換する
      int mapX = (int)(worldX / CellSize), mapY = (int)(worldY / CellSize);
      // 二次元配列の範囲内か判定
      if (mapY < terrain.size() && mapX < terrain[mapY].size())
        terrainID = terrain[mapY][mapX]; // 二次元配列から地形IDを取り出す
      
      return (checkBit & blockBits[terrainID]) != 0; // ビット論理積 & で地形のchekckBitを確かめる
    };
  
  // [x方向左右]地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheckX = [&checkID](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom)
    {
      // 左端がめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yTop))
        return true; // 左上がめりこんでいる
      if (checkID(checkBit, terrain, xLeft, yMiddle))
        return true; // 左中がめりこんでいる
      if (checkID(checkBit, terrain, xLeft, yBottom))
        return true; // 左下がめりこんでいる
      // 右端がめりこんでいるか?
      if (checkID(checkBit, terrain, xRight, yTop))
        return true; // 右上がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yMiddle))
        return true; // 右中がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yBottom))
        return true; // 右下がめりこんでいる
      
      return false;
    };

  
  // [y方向上下]地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheckY = [&checkID](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom)
    {
      // 頭の部分がめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yTop))
        return true; // 左上がめりこんでいる
      if (checkID(checkBit, terrain, xMiddle, yTop))
        return true; // 中上がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yTop))
        return true; // 右上がめりこんでいる
      // 足元が壁にめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yBottom))
        return true; // 左下がめりこんでいる
      if (checkID(checkBit, terrain, xMiddle, yBottom))
        return true; // 中下がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yBottom))
        return true; // 右下がめりこんでいる
      
      return false;
    };

  
  // 地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheck = [&hitcheckX, &hitcheckY](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom)
    {
      return hitcheckX(checkBit,terrain,xLeft,xMiddle,xRight,yTop,yMiddle,yBottom)
        | hitcheckY(checkBit,terrain,xLeft,xMiddle,xRight,yTop,yMiddle,yBottom);
    };

  
  float camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
  if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ
  float camera_y = y - Screen::Height / 2; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
  if (camera_y <= 0) camera_y = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Height * CellSize - Screen::Height <= camera_y) camera_y = Height * CellSize - Screen::Height; // ステージのはしっこ
  
  
  float debug_x = -1, debug_y = -1; // [デバッグ] めり込みすぎのラインを記録しておく変数
  float debug_x_yTop = -1, debug_x_yBottom = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の上下端も記録
  float debug_y_xLeft = -1, debug_y_xRight = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の左右端も記録
  
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      direction = Direction::Left;
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      direction = Direction::Right;
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      vy = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
      // 加速段階に応じて違うジャンプ音を鳴らす
      PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs > 9) {
          isFlying = true; // 飛行モードにする
        }
        
        // 加速段階に応じて違うジャンプ音を鳴らす
        PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    if (!isDead) // やられたときは移動を停止
      x += vx; // 速度のぶん現在の位置から移動
    if (!isDead) // やられたときは当たり判定も停止
    {//←スコープ{}で下記xLeftなどの変数名を{}の内側でリセットして、外側でかぶっても大丈夫なようにする
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      if(hitcheck(BlockBit::Damage, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに

      
      // 左端が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yMiddle) || // 左中が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) ) // 左下が壁か?
      {
        debug_x = xLeft; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        // std::fmodで小数点の余りを計算 めり込みすぎの余り = std::fmod(xLeft, CellSize)
        float wallRight = xLeft + CellSize - (float)std::fmod(xLeft, CellSize); // 壁の右端
        x = wallRight - hitOffsetLeft; // 左端を壁の右端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      } // ↓右端が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xRight, yTop) || // 右上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yMiddle) || // 右中が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_x = xRight; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        float wallLeft = xRight - (float)std::fmod(xRight, CellSize); // 壁の左端
        x = wallLeft + hitOffsetRight - imgSizeX; // 右端を壁の左端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      }
    }
    
    if (!isDead) // やられたときは移動を停止
      y += vy;
    if (!isDead) // やられたときは当たり判定も停止
    {//←スコープ{}で下記xLeftなどの変数名を{}の内側でリセットして、外側でかぶっても大丈夫なようにする
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      if (hitcheck(BlockBit::Damage, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに

      
      // 頭の部分が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yTop) || // 中上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yTop) ) // 右上が壁か?
      {
        debug_y = yTop; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallBottom = yTop + CellSize - (float)std::fmod(yTop, CellSize); // 壁の下側の境界
        y = wallBottom - hitOffsetTop; // 頭を壁の下端に沿わす(めり込んだぶんを押し戻す)
        vy = 0; // 壁に当たったら速度をストップ
      } // ↓足元が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) || // 左下が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yBottom) || // 中下が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_y = yBottom; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallTop = yBottom - (float)std::fmod(yBottom, CellSize); // 壁の上側の境界
        y = wallTop + hitOffsetBottom - imgSizeX; // 足元を壁の上端に沿わす(めり込んだぶんを押し戻す)
        
        isGround = true;
        isFlying = false;
        vx_idx = 0;
        
        vy = 0; // 床に当たったら速度をストップ
      }
      else
        isGround = false; // ブロックに当たってないからfalseにして落下するように
    }
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    
    camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
    if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ
    camera_y = y - Screen::Height / 2; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
    if (camera_y <= 0) camera_y = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Height * CellSize - Screen::Height <= camera_y) camera_y = Height * CellSize - Screen::Height; // ステージのはしっこ
    
    
    // ダッシュ段階に応じてキャラクタのアニメ時間を進める
    float vx_dash = (vx < 0) ? -vx : vx; // 絶対値でプラスだけにする
    if (vx_dash > 0.5f) animeTime += 1; // 時間 +1 ( 合計 1 )
    if (vx_dash > 6) animeTime += 1; // 時間 +1 (計 1+1=2 ) 1フレームごとに2アニメ時間が進む
    if (vx_dash > 9) animeTime += 2; // 最終段階ではアニメ時間をさらに +2 (計 1+1+2=4 )
    
    int animeStep = animeTime / 10; // 10を5に変えたらアニメ速度が上がる(10ごとに1コマアニメが進む)
    if (animeStep >= 3) // アニメのステップ数が0,1,2の3段階までなら3を超えたら0へループ
    {
      animeTime = 0; // 0へ戻り再びアニメ時間を再ループ
      animeStep = 0;
    }
    
    
    // 描画処理
    if (isDead) // やられたときの描画処理
    {
      float yMiss = y - camera_y + imgSizeY / 2; // ミスったy起点
      float yAnim0 = yMiss; // アニメy位置0 始まりのy位置
      float yAnim1 = yMiss - 10; // アニメy位置1 アニメの途中少し上方向に 10 浮く
      float yAnim2 = Screen::Height + 32; // アニメy位置2 最終y位置 画面の高さ+32で画面外へ
      
      float yDeadAnim; // やられた際にはyの位置をアニメさせる
      // 直線的な比例アニメの式(t=時刻) 位置1×(終時刻 - t) / アニメの長さ + 位置2×(t - 始時刻) / アニメの長さ
      if (deadAnimeTime < 40) // やられアニメが始まって40フレーム目までは yAnim0 ~ yAnim1
        yDeadAnim = yAnim0 * (40 - deadAnimeTime) / 40 + yAnim1 * (deadAnimeTime - 0) / 40;
      else if (deadAnimeTime < 60) // やられアニメが始まって60フレーム目までは yAnim1 ~ yAnim2
        yDeadAnim = yAnim1 * (60 - deadAnimeTime) / 20 + yAnim2 * (deadAnimeTime - 40) / 20;
      else
      {
        yDeadAnim = yAnim2; // アニメ終了 到着 y位置をセット
        deadAnimeTime = 0; // 次にやられたときのアニメのため0リセットしておく
        
        // 再開位置からゲーム再開(各数値もリセット)
        isDead = false;
        x = xStart, y = yStart, vx = 0, vy = 0;
        isGround = false, isFlying = false;
        vx_idx = 0;
      }
      
      int deadAnimeStep = deadAnimeTime / 3; // 3で割って[チップのパラパラの]アニメの速度は弱める
      int chipAnime = deadAnimeStep % 3; // % 3で余りを求めれば余りはチップのパラパラ番号は0~2の間に収まる
      DrawRotaGraphF(x - camera_x + imgSizeX / 2, yDeadAnim, 1.0f, 10.0f * deadAnimeStep / 360.0f * 2 * DX_PI, playerChips[chipAnime], TRUE);
      
      ++deadAnimeTime; // アニメ時間を進める
    }
    else
    {

      if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f,-60.0f / 360.0f * 2 * DX_PI, playerChips[4], TRUE);
      else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 60.0f / 360.0f * 2 * DX_PI, playerChips[7], TRUE);
      else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 0.0f, playerChips[5 - animeStep], TRUE);
      else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 0.0f, playerChips[6 + animeStep], TRUE);
    }
    
    float rotaShiftX = CellSize / 2, rotaShiftY = CellSize / 2; // DrawRotaGraphF関数は画像の真ん中が起点なので1/2ずらした位置計算
    // 地形マップを[ 行のfor文×列のfor文 ]の2重ループで描く
    for (int cellY = 0, ySize = mapData.size(); cellY < ySize; ++cellY)
    {
      float chip_y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
      for (int cellX = 0, xSize = mapData[cellY].size(); cellX < xSize; ++cellX)
      {
        float chip_x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
        int id = mapData[cellY][cellX]; // [行][列] の位置のcsvの地形番号(チップ番号)を得る
        if (id < 0) continue; // -1のときは描かない
        
        // マップチップを描く
        DrawRotaGraphF(chip_x - camera_x + rotaShiftX, chip_y - camera_y + rotaShiftY, 1.0f, 0.0f, mapChips[id], TRUE);
        // [デバッグ]用にcsvのidの数字を描く
        DrawString(chip_x - camera_x, chip_y - camera_y, std::to_string(id).c_str(), GetColor(255, 255, 255));
      }
    }
    
    // [デバッグ] 当たり判定の四隅を赤枠で描画
    DrawBox(getLeft(x) - camera_x, getTop(y) - camera_y, getRight(x) - camera_x, getBottom(y) - camera_y, GetColor(255, 0, 0), FALSE);
    // [デバッグ] キャラ原点(x,y)を緑丸で描画
    DrawCircle(x - camera_x, y - camera_y, 2, GetColor(0, 255, 0), TRUE);
    // [デバッグ] 当たった際の行き過ぎの めりこみ座標(debug_x,debug_y) を青線で描画
    if (debug_x != -1)
      DrawLine(debug_x - camera_x, debug_x_yTop - camera_y, debug_x - camera_x, debug_x_yBottom - camera_y, GetColor(0, 0, 255));
    if (debug_y != -1)
      DrawLine(debug_y_xLeft - camera_x, debug_y - camera_y, debug_y_xRight - camera_x, debug_y - camera_y, GetColor(0, 0, 255));
    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}



いかがでしょう、トゲトゲにあたったらキャラがアニメして、スタート地点に戻るようになりましたか?

今回のポイントは位置を直線的に移動させるアニメの式です。
yAnim0, yAnim1, yAnim2 など 3つのyの位置があったとすると

float yDeadAnim; // やられた際にはyの位置をアニメさせる
// 直線的な比例アニメの式(t=時刻) 位置1×(終時刻 - t) / アニメの長さ + 位置2×(t - 始時刻) / アニメの長さ
if (deadAnimeTime < 40) // やられアニメが始まって40フレーム目までは yAnim0 ~ yAnim1
  yDeadAnim = yAnim0 * (40 - deadAnimeTime) / 40 + yAnim1 * (deadAnimeTime - 0) / 40;
else if (deadAnimeTime < 60) // やられアニメが始まって60フレーム目までは yAnim1 ~ yAnim2
  yDeadAnim = yAnim1 * (60 - deadAnimeTime) / 20 + yAnim2 * (deadAnimeTime - 40) / 20;



式をさらに単純化すると、tが時刻0.0~1.0まで変わるとすると
  y = y0 × (1.0 - t) + y1 × t
この形の式にt=0.0をいれたときはy = y0 × (1.0 - 0.0) + y1 × 0.0 = y0(開始位置)
この形の式にt=0.5をいれたときはy = y0 × (1.0 - 0.5) + y1 × 0.5 = 0.5×y0 + 0.5×y1(中間位置)
この形の式にt=1.0をいれたときはy = y0 × (1.0 - 1.0) + y1 × 1.0 = y1(到着位置)

つまりt = 0.0~1.0 に変わると y が y0 から y1 までアニメーションする式が出来上がります。
そう、簡単に言うと実はy0 に 0~1 を、y1 に 1~0 を「0と1が入れ替わるように つじつま を合わせているだけの式」です。
このアニメ式を x にもペアで使えば自由に直線的にある地点からある地点まで画像をアニメで動かせるようになります。


[練習問題]
キャラのやられたときの「残念な気持ちになる効果音」を探してSEフォルダに保存して、isDead = true; にしている瞬間に連動して効果音も鳴らしてみましょう。





ブロックを下から叩いたらy位置をアニメさせてバウンスするプログラム




さてつぎは【ブロックにどの方向からめりこんだかを得られるようにして叩かれたブロックのマス目番号XYをキーに高速辞書でアニメ時間を管理して叩かれたブロックをバウンスするサンプル】でブロックに動きをつけましょう。

main.cppを編集します:

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

#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列std::stringに必要
#include <fstream> // ファイル読み出しstd::ifstreamに必要
#include <sstream> // 文字列ストリームに必要 ss >> num;で文字列から数字に変換できる
#include <unordered_map> // std::unordered_map 高速辞書配列に必要(チップ番号から壁などを表すビットへ変換する辞書)
#include <utility> // std::pair(データのペア型)を使うのに必要
// [ペアを高速辞書のキーにするためハッシュ関数が必要] https://qiita.com/hamamu/items/4d081751b69aa3bb3557
template<class TypeT> size_t HashCombine(const size_t seed, const TypeT& value)
{
  return seed ^ (std::hash<TypeT>()(value) + 0x9e3779b9 + (seed << 6) + (seed >> 2));
}

// pair型に()をつけたときに呼び出される処理をoperator()で定義してreturnでハッシュ関数を返す
template<class TypeT, class TypeS>
struct std::hash<std::pair<TypeT, TypeS>>
{
  size_t operator() (const std::pair<TypeT, TypeS>& keyval) const noexcept
  {   // returnでハッシュ関数を返すことで高速辞書のキーとして認め許されるようになる
    return HashCombine(std::hash<TypeT>()(keyval.first), keyval.second);
  }
};


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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerChips[12]; // キャラのチップのグリッドの分割数に応じて3×4 = 12のぶんだけ配列を事前に確保
  LoadDivGraph("Image/pikkoron.png", 12, 3, 4, 32, 32, playerChips); // キャラのチップのサイズは32×32
  assert(playerChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  // 効果音ファイルを事前にメモリに読み込む https://dxlib.xsrv.jp/function/dxfunc_sound.html#R8N4
  int jumpSE[4]; // 4種類のジャンプ音を読み込む
  jumpSE[0] = LoadSoundMem("SE/Jump0.wav");
  jumpSE[1] = LoadSoundMem("SE/Jump1.wav");
  jumpSE[2] = LoadSoundMem("SE/Jump2.wav");
  jumpSE[3] = LoadSoundMem("SE/Jump3.wav");
  assert(jumpSE[0] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[1] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[2] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[3] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  
  int CellSize = 32; // マップの1マスのピクセル数
  int mapChips[64]; // マップのチップのグリッドの分割数に応じて8×8 = 64など配列で確保
  LoadDivGraph("Map/mapchip.png", 64, 8, 8, CellSize, CellSize, mapChips); // マップのチップのサイズは32×32(=CellSize)
  assert(mapChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  std::vector<std::vector<int>> mapData;// タイルマップデータ
  std::ifstream ifs_csv_file("Map/stage.csv"); // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
  
  int Width = 0, Height = 0; // マップデータの横(Width) 縦(Height)のマスの数
  int MaxWidth = 0; // 1行の数字の最大個数
  std::string line; // 1行単位ごとにcsvファイルから文字列をstd::getlineで読み込んで受け取る
  while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
  {
    std::vector<int> valuelist; // 1行ぶんの数字リスト配列
    std::istringstream linestream( line ); // 各行の文字列ストリーム
    std::string comma_part; // カンマで分割された文字列
    int widthCount = 0; // カンマで区切られた行の数字の数(各行の幅)をカウント
    while (std::getline(linestream, comma_part, { ',' })) // 1行をgetlineでカンマで区切って列を得る
    {
      std::istringstream ss( comma_part ); // カンマで区切られた文字をssを通して>>で数字に
      int num; // 数字単体
      ss >> num; // 文字列ストリーム>>で数字へ変換
      valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
      ++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
    }
    // 1行の数字の数がMAX記録を更新するかチェック
    if (widthCount > MaxWidth)
      MaxWidth = widthCount; // 暫定Maxの列の幅を更新
    
    // 1行ぶんの数字の配列をとりまとめてmapDataに1行ずつ追加
    if (valuelist.size() != 0) // 配列に数字が1つでもあるなら.size() != 0
      mapData.emplace_back(valuelist); // mapDataに1行ずつ数字配列valuelistを追加
    
    ++Height; //マップの高さをカウントアップ
  }
  Width = MaxWidth; // マップの幅Widthは一番数字の個数の多かった行に合わせる
  // ↓読込んだCSVの幅と高さをチェック
  assert(Width > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  assert(Height > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  
  
  float xStart = 200, yStart = Screen::Height - 100; // キャラの初期スタート(ミス後の再開位置)
  float x = xStart, y = yStart; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  enum class Direction { Left, Right }; // enumでキャラの向いてる方向をDirection型として2種類(LeftとRight)定義
  Direction direction = Direction::Right; // キャラの向いてる方向
  int animeTime = 0; // アニメ時間
  
  bool isDead = false; // キャラがやられたかどうか
  int deadAnimeTime = 0; // やられたときのアニメ時間
  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16, 4 }, vyDownSpeedMax[] = { 16, 16, 16, 8 }; // ダッシュ段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f,0.1f }, vyGravity[] = { 0.8f,0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  bool isFlying = false; // 飛行モード
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(playerChips[0], &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  int hitOffsetLeft = 2; // 当たり判定の左端のオフセット
  int hitOffsetRight = 2; // 当たり判定の右端のオフセット
  int hitOffsetTop = 1; // 当たり判定の上端のオフセット
  int hitOffsetBottom = 0; // 当たり判定の下端のオフセット
  
  // 当たり判定のはしっこを計算するための一時関数
  auto getLeft = [&hitOffsetLeft](float worldX) { return worldX + hitOffsetLeft; };
  auto getTop = [&hitOffsetTop](float worldY) { return worldY + hitOffsetTop; };
  auto getRight = [&hitOffsetRight, &imgSizeX](float worldX) { return worldX + imgSizeX - hitOffsetRight; };
  auto getBottom = [&hitOffsetBottom, &imgSizeY](float worldY) { return worldY + imgSizeY - hitOffsetBottom; };
  
  enum class BlockBit // ブロックの特性を32桁のビットで表現
  {
    None = 0,
    Wall   = 0b0000'0000'0000'0000'0000'0000'0000'0001,
    Damage = 0b0000'0000'0000'0000'0000'0000'0000'0010,
    Bounce = 0b0000'0000'0000'0000'0000'0000'0000'0100, // 叩くと弾むブロック
  };
  
  std::unordered_map<int, unsigned int> blockBits = {
    {-1,(int)BlockBit::None},
    { 0,(int)BlockBit::Wall},
    { 1,(int)BlockBit::Damage},
    { 2,(int)BlockBit::Wall | (int)BlockBit::Bounce}, // 壁 | はずむ (|ビット論理和)
    { 3,(int)BlockBit::Wall | (int)BlockBit::Bounce},
  };
  
  // ブロックをバウンドさせるための 高速辞書<ペア<cellX,cellY>, intアニメ時間>
  std::unordered_map<std::pair<int, int>, int> blockAnime;

  
  // 指定された座標(worldX,worldY)の地形が 壁 や ダメージブロックかなどをチェックする一時関数
  auto checkID = [&blockBits, &CellSize](BlockBit checkBit, std::vector<std::vector<int>>& terrain, float worldX, float worldY,
                                int* pMapX = nullptr, int* pMapY = nullptr)

    {
      int terrainID = -1; // 指定された座標の地形のID
      
      if (worldX < 0 || worldY < 0) return false; // 負の座標が指定された場合
      
      // マップ座標系(二次元配列の行と列)に変換する
      int mapX = (int)(worldX / CellSize), mapY = (int)(worldY / CellSize);
      // 二次元配列の範囲内か判定
      if (mapY < terrain.size() && mapX < terrain[mapY].size())
        terrainID = terrain[mapY][mapX]; // 二次元配列から地形IDを取り出す
      
      bool isCheckBit = (((int)checkBit) & blockBits[terrainID]) != 0; // ビット論理積 & で地形のchekckBitを確かめる
      if (isCheckBit && pMapX != nullptr && pMapY != nullptr)
        *pMapX = mapX, *pMapY = mapY; // ブロックのcsvのセル番号をポインタに記録(nullptrの場合は記録しない)

      
      return isCheckBit;
    };
  
  enum class HitPart // 当たった箇所を表すビット
  {
    None    = 0,
    Left    = 0b0000'0000'0000'0000'0000'0000'0000'0001,
    Right   = 0b0000'0000'0000'0000'0000'0000'0000'0010,
    Top     = 0b0000'0000'0000'0000'0000'0000'0000'0100,
    Bottom  = 0b0000'0000'0000'0000'0000'0000'0000'1000,
    XMiddle = 0b0000'0000'0000'0000'0000'0000'0001'0000,
    YMiddle = 0b0000'0000'0000'0000'0000'0000'0010'0000,
  };

  
  // [x方向左右]地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheckX = [&checkID](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)

    {
      // 左端がめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yTop, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Top; // 左上がめりこんでいる
      if (checkID(checkBit, terrain, xLeft, yMiddle, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::YMiddle; // 左中がめりこんでいる
      if (checkID(checkBit, terrain, xLeft, yBottom, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Bottom; // 左下がめりこんでいる
      // 右端がめりこんでいるか?
      if (checkID(checkBit, terrain, xRight, yTop, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Top; // 右上がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yMiddle, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::YMiddle; // 右中がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yBottom, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Bottom; // 右下がめりこんでいる
      
      return (int)HitPart::None;
    };
  
  // [y方向上下]地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheckY = [&checkID](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)

    {
      // 頭の部分がめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yTop, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Top; // 左上がめりこんでいる
      if (checkID(checkBit, terrain, xMiddle, yTop, pMapX, pMapY))
        return (int)HitPart::XMiddle | (int)HitPart::Top; // 中上がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yTop, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Top; // 右上がめりこんでいる
      // 足元が壁にめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yBottom, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Bottom; // 左下がめりこんでいる
      if (checkID(checkBit, terrain, xMiddle, yBottom, pMapX, pMapY))
        return (int)HitPart::XMiddle | (int)HitPart::Bottom; // 中下がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yBottom, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Bottom; // 右下がめりこんでいる
      
      return (int)HitPart::None;
    };
  
  // 地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheck = [&hitcheckX, &hitcheckY](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr
)
    {
      return hitcheckX(checkBit,terrain,xLeft,xMiddle,xRight,yTop,yMiddle,yBottom,pMapX,pMapY)
        | hitcheckY(checkBit,terrain,xLeft,xMiddle,xRight,yTop,yMiddle,yBottom,pMapX,pMapY);
    };
  
  float camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
  if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ
  float camera_y = y - Screen::Height / 2; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
  if (camera_y <= 0) camera_y = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Height * CellSize - Screen::Height <= camera_y) camera_y = Height * CellSize - Screen::Height; // ステージのはしっこ
  
  
  float debug_x = -1, debug_y = -1; // [デバッグ] めり込みすぎのラインを記録しておく変数
  float debug_x_yTop = -1, debug_x_yBottom = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の上下端も記録
  float debug_y_xLeft = -1, debug_y_xRight = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の左右端も記録
  
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      direction = Direction::Left;
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      direction = Direction::Right;
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      vy = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
      // 加速段階に応じて違うジャンプ音を鳴らす
      PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs > 9) {
          isFlying = true; // 飛行モードにする
        }
        
        // 加速段階に応じて違うジャンプ音を鳴らす
        PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    if (!isDead) // やられたときは移動を停止
      x += vx; // 速度のぶん現在の位置から移動
    if (!isDead) // やられたときは当たり判定も停止
    {
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      if(hitcheck(BlockBit::Damage, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに
      
      // 左端が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yMiddle) || // 左中が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) ) // 左下が壁か?
      {
        debug_x = xLeft; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        // std::fmodで小数点の余りを計算 めり込みすぎの余り = std::fmod(xLeft, CellSize)
        float wallRight = xLeft + CellSize - (float)std::fmod(xLeft, CellSize); // 壁の右端
        x = wallRight - hitOffsetLeft; // 左端を壁の右端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      } // ↓右端が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xRight, yTop) || // 右上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yMiddle) || // 右中が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_x = xRight; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        float wallLeft = xRight - (float)std::fmod(xRight, CellSize); // 壁の左端
        x = wallLeft + hitOffsetRight - imgSizeX; // 右端を壁の左端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      }
    }
    
    if (!isDead) // やられたときは移動を停止
      y += vy;
    if (!isDead) // やられたときは当たり判定も停止
    {
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      if (hitcheck(BlockBit::Damage, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに
      
      int mapX = 0, mapY = 0; // ぶつかったらそのブロックのCSVのセルの行mapYと列mapXを得る
      if ( (int)HitPart::Top
          & hitcheck(BlockBit::Bounce, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        std::pair<int,int> mapXY = std::make_pair(mapX, mapY); // ぶつかったブロックのセルの行mapYと列mapXのペア
        if (blockAnime.count(mapXY) == 0) // 辞書にキーがあるかcountで数えて未登録==0のとき
          blockAnime[mapXY] = 0; // ブロックのアニメ時間を辞書に登録(時間が辞書にあるものだけアニメが走る)
      }

      
      // 頭の部分が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yTop) || // 中上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yTop) ) // 右上が壁か?
      {
        debug_y = yTop; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallBottom = yTop + CellSize - (float)std::fmod(yTop, CellSize); // 壁の下側の境界
        y = wallBottom - hitOffsetTop; // 頭を壁の下端に沿わす(めり込んだぶんを押し戻す)
        vy = 0; // 壁に当たったら速度をストップ
      } // ↓足元が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) || // 左下が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yBottom) || // 中下が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_y = yBottom; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallTop = yBottom - (float)std::fmod(yBottom, CellSize); // 壁の上側の境界
        y = wallTop + hitOffsetBottom - imgSizeX; // 足元を壁の上端に沿わす(めり込んだぶんを押し戻す)
        
        isGround = true;
        isFlying = false;
        vx_idx = 0;
        
        vy = 0; // 床に当たったら速度をストップ
      }
      else
        isGround = false; // ブロックに当たってないからfalseにして落下するように
    }
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    
    camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
    if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ
    camera_y = y - Screen::Height / 2; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
    if (camera_y <= 0) camera_y = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Height * CellSize - Screen::Height <= camera_y) camera_y = Height * CellSize - Screen::Height; // ステージのはしっこ
    
    
    // ダッシュ段階に応じてキャラクタのアニメ時間を進める
    float vx_dash = (vx < 0) ? -vx : vx; // 絶対値でプラスだけにする
    if (vx_dash > 0.5f) animeTime += 1; // 時間 +1 ( 合計 1 )
    if (vx_dash > 6) animeTime += 1; // 時間 +1 (計 1+1=2 ) 1フレームごとに2アニメ時間が進む
    if (vx_dash > 9) animeTime += 2; // 最終段階ではアニメ時間をさらに +2 (計 1+1+2=4 )
    
    int animeStep = animeTime / 10; // 10を5に変えたらアニメ速度が上がる(10ごとに1コマアニメが進む)
    if (animeStep >= 3) // アニメのステップ数が0,1,2の3段階までなら3を超えたら0へループ
    {
      animeTime = 0; // 0へ戻り再びアニメ時間を再ループ
      animeStep = 0;
    }
    
    
    // 描画処理
    if (isDead) // やられたときの描画処理
    {
      float yMiss = y - camera_y + imgSizeY / 2; // ミスったy起点
      float yAnim0 = yMiss; // アニメy位置0 始まりのy位置
      float yAnim1 = yMiss - 10; // アニメy位置1 アニメの途中少し上方向に 10 浮く
      float yAnim2 = Screen::Height + 32; // アニメy位置2 最終y位置 画面の高さ+32で画面外へ
      
      float yDeadAnim; // やられた際にはyの位置をアニメさせる
      // 直線的な比例アニメの式(t=時刻) 位置1×(終時刻 - t) / アニメの長さ + 位置2×(t - 始時刻) / アニメの長さ
      if (deadAnimeTime < 40) // やられアニメが始まって40フレーム目までは yAnim0 ~ yAnim1
        yDeadAnim = yAnim0 * (40 - deadAnimeTime) / 40 + yAnim1 * (deadAnimeTime - 0) / 40;
      else if (deadAnimeTime < 60) // やられアニメが始まって60フレーム目までは yAnim1 ~ yAnim2
        yDeadAnim = yAnim1 * (60 - deadAnimeTime) / 20 + yAnim2 * (deadAnimeTime - 40) / 20;
      else
      {
        yDeadAnim = yAnim2; // アニメ終了 到着 y位置をセット
        deadAnimeTime = 0; // 次にやられたときのアニメのため0リセットしておく
        
        // 再開位置からゲーム再開(各数値もリセット)
        isDead = false;
        x = xStart, y = yStart, vx = 0, vy = 0;
        isGround = false, isFlying = false;
        vx_idx = 0;
      }
      
      int deadAnimeStep = deadAnimeTime / 3; // 3で割って[チップのパラパラの]アニメの速度は弱める
      int chipAnime = deadAnimeStep % 3; // % 3で余りを求めれば余りはチップのパラパラ番号は0~2の間に収まる
      DrawRotaGraphF(x - camera_x + imgSizeX / 2, yDeadAnim, 1.0f, 10.0f * deadAnimeStep / 360.0f * 2 * DX_PI, playerChips[chipAnime], TRUE);
      
      ++deadAnimeTime; // アニメ時間を進める
    }
    else
    {
      if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f,-60.0f / 360.0f * 2 * DX_PI, playerChips[4], TRUE);
      else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 60.0f / 360.0f * 2 * DX_PI, playerChips[7], TRUE);
      else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 0.0f, playerChips[5 - animeStep], TRUE);
      else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 0.0f, playerChips[6 + animeStep], TRUE);
    }
    
    float rotaShiftX = CellSize / 2, rotaShiftY = CellSize / 2; // DrawRotaGraphF関数は画像の真ん中が起点なので1/2ずらした位置計算
    // 地形マップを[ 行のfor文×列のfor文 ]の2重ループで描く
    for (int cellY = 0, ySize = mapData.size(); cellY < ySize; ++cellY)
    {
      float chip_y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
      for (int cellX = 0, xSize = mapData[cellY].size(); cellX < xSize; ++cellX)
      {
        float chip_x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
        int id = mapData[cellY][cellX]; // [行][列] の位置のcsvの地形番号(チップ番号)を得る
        if (id < 0) continue; // -1のときは描かない
        
        // マップチップを描く
        auto mapXY = std::make_pair(cellX, cellY);
        if (blockAnime.count(mapXY) == 0)

          DrawRotaGraphF(chip_x - camera_x + rotaShiftX, chip_y - camera_y + rotaShiftY, 1.0f, 0.0f, mapChips[id], TRUE);
        else if ((int)BlockBit::Bounce & blockBits[id])
        {
          float yBase = chip_y - camera_y;
          float yAnim0 = yBase - 10, yAnim1 = yBase; // yAnim0 から yAnim1の位置までアニメする
          int blockTime = blockAnime[mapXY];
          float yAnim = yAnim0 * (15 - blockTime) / 15 + yAnim1 * blockTime / 15;
          DrawRotaGraphF(chip_x - camera_x + rotaShiftX, yAnim + rotaShiftY, 1.0f, 0.0f, mapChips[id], TRUE);
          ++blockAnime[mapXY]; // アニメ時間を進める
          if (blockTime > 15) // アニメ終了時間を15フレーム目とするとき
            blockAnime.erase(mapXY); // 辞書からアニメをerase関数で取り除く
        }

        // [デバッグ]用にcsvのidの数字を描く
        DrawString(chip_x - camera_x, chip_y - camera_y, std::to_string(id).c_str(), GetColor(255, 255, 255));
      }
    }
    
    // [デバッグ] 当たり判定の四隅を赤枠で描画
    DrawBox(getLeft(x) - camera_x, getTop(y) - camera_y, getRight(x) - camera_x, getBottom(y) - camera_y, GetColor(255, 0, 0), FALSE);
    // [デバッグ] キャラ原点(x,y)を緑丸で描画
    DrawCircle(x - camera_x, y - camera_y, 2, GetColor(0, 255, 0), TRUE);
    // [デバッグ] 当たった際の行き過ぎの めりこみ座標(debug_x,debug_y) を青線で描画
    if (debug_x != -1)
      DrawLine(debug_x - camera_x, debug_x_yTop - camera_y, debug_x - camera_x, debug_x_yBottom - camera_y, GetColor(0, 0, 255));
    if (debug_y != -1)
      DrawLine(debug_y_xLeft - camera_x, debug_y - camera_y, debug_y_xRight - camera_x, debug_y - camera_y, GetColor(0, 0, 255));
    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}




いかがでしょう、下からブロックを叩いたらブロックが上に少しバウンスするようになりましたか?


注意点として、enumでは { } の外でも名前をかぶらせることができないので、
その使いづらさを解消するために、C++ではenumのかわりにenum classがあります。
例.  enum ●● { Normal, Fire, Water.. }; と一回Normalという名前を定義したあと、enum ▲▲ { Normal };とするとNormalがかぶってエラーになってしまいます
C++ではenum classを使ったほうが無難そうですね。
(ただし、(int)型にキャストで変換しながら使う)


もう1点のポイントはstd::unordered_map 高速辞書配列の辞書のキーとできるのは
int,float,char,short,long,std::stringなどデフォルトのよく使う型のみ
なので、
今回はxとyの2つの数字のペアをキーとして使うために、
辞書にデータを突っ込む際にどの辞書のページ( = 辞書内配列の箱)に突っ込むかを決めているハッシュ関数を別途定義しなければならないです。

ハッシュ関数や暗号化という言葉を聞いたことがありますか?
https://ja.wikipedia.org/wiki/暗号学的ハッシュ関数
暗号も ハッシュ関数も 共通の性質として、
ある値(たとえばパスワード)に対して、
ある計算式をかませると、1対1の対になる計算結果がでる性質が必要です。
(暗号化するときに2つ以上答えが出ちゃう数式だと使えないですものね)
1対1の計算結果なら色んな式がありえます。
(ある数字のパスワードが来たら2倍した数字で暗号化するとか..のバレバレ数式でも1対1の対にはなるわけで..)
どうせ暗号化するならできる限り解析されにくい数式がないか数学的に考えるのが暗号学なわけですね。
そして暗号化するときに暗号化したものの桁数が何桁におさまるかも重要な式の決め方の観点です。
暗号化した際の桁数が多くなりすぎたり、桁数が不安定だと、暗号化されたデータをやり取りする際に不安定ですよね。
実際に現代社会で使われているSHA-256(NSA米国家安全保障局の考案した暗号学的ハッシュ関数)も256ビット(32バイト)の値(=ハッシュ値)を出力します

さて、なんか全然辞書と暗号がつながらないじゃんと思われたでしょうが、
辞書のデータをどのページに書き込むか決めるときにstd::mapのようにキーが順番にならんでいるなら「あ」ならこのページに書き込むと確定できますが、
キーを順番に並べない高速辞書std::unordered_mapの場合ハッシュ関数で出てきた値でデータを入れる箱の位置を決めて放り込むイメージです。
(「あみだくじで決めるようなノリ」で辞書のどのページにデータを放り込むか決めるイメージであみだくじの代わりにハッシュ関数でピシッとデータを入れる箱の位置を決める感じ)
雑に例えると、例えばたくさんロッカーがあってカバンを適当にハッシュ関数で決めたロッカーに投げ込めば、
いちいち順番が決まったロッカー番号を順に確かめながらカバンをしまう手間暇より高速にほぼ1発で投げ込んでしまえますよね。
(ただ、適当に投げ込んだ先に、すでにカバンが入っていた場合には再ハッシュなどの工夫が必要になるぶんちょっと遅くなるので、めったにかぶらないくらいの十分な格納ロッカーの数(量)も必要です。
ロッカーがかぶりにくいようなうまく結果がばらける あみだ≒ハッシュルール数式 を使うこと(質)と十分なロッカーの数(量)の観点で高速辞書格納が実現されてます)

今回のコードではxとyのペアを高速辞書のキーにするために適度に結果がばらけると研究され信じられていそうな式
seed ^ (std::hash<TypeT>()(value) + 0x9e3779b9 + (seed << 6) + (seed >> 2));
をHashCombine()関数として定義して使用しています。(16進数 0x9e3779b9やシフトなど難しい式ですが、使う側は意味を考える必要まではないです)

そしてそのHashCombine関数を高速辞書std::unordered_map内部であみだ用途で使用させるためにoperator()呼び出しもセットで準備しています。
( std::pair<int,int> a;a()など()をつけたときに呼び出されるoperator()の定義 )
( std::unordered_map<std::pair<int,int>,int> b;b[a] = 4;など辞書にデータを登録する際に暗黙でa()が呼び出されデータがあみだ的に格納される )
ハッシュのシード値やreturnされるハッシュ値はint型ではなくsize_t型で受けています(size_tならば32bitマシンなら=4バイト、64bitマシンなら=8バイトにビルドする環境で合わせてくれて安心)

(以上略)...
#include <unordered_map> // std::unordered_map 高速辞書配列に必要(チップ番号から壁などを表すビットへ変換する辞書)
#include <utility> // std::pair(データのペア型)を使うのに必要
// [ペアを高速辞書のキーにするためハッシュ関数が必要] https://qiita.com/hamamu/items/4d081751b69aa3bb3557
template<class TypeT> size_t HashCombine(const size_t seed, const TypeT& value)
{
  return seed ^ (std::hash<TypeT>()(value) + 0x9e3779b9 + (seed << 6) + (seed >> 2));
}

// pair型に()をつけたときに呼び出される処理をoperator()で定義してreturnでハッシュ関数を返す
template<class TypeT, class TypeS>
struct std::hash<std::pair<TypeT, TypeS>>
{
  size_t operator() (const std::pair<TypeT, TypeS>& keyval) const noexcept
  {   // ()をつけたときにreturnでハッシュ関数を返すようにすることで高速辞書のキーとして使えるようになる
    return HashCombine(std::hash<TypeT>()(keyval.first), keyval.second);
  };
}

(以下略)...


以上のoperator()の定義があれば
std::pair<int,int> mapXY = std::make_pair(mapX, mapY);
のように XとYのペアmapXY を 高速辞書型blockAnime のキー にしてblockAnime[mapXY] = 0; のように気軽に代入でデータを入れ込むことができるようになります。

(以上略)...
      int mapX = 0, mapY = 0; // ぶつかったらそのブロックのCSVのセルの行mapYと列mapXを得る
      if ( (int)HitPart::Top
          & hitcheck(BlockBit::Bounce, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        std::pair<int,int> mapXY = std::make_pair(mapX, mapY); // ぶつかったブロックのセルの行mapYと列mapXのペア
        if (blockAnime.count(mapXY) == 0) // 辞書にキーがあるかcountで数えて未登録==0のとき
          blockAnime[mapXY] = 0; // ブロックのアニメ時間を辞書に登録(時間が辞書にあるものだけアニメが走る)
      }

(以下略)...


辞書型のよいところは、
int data[] = { -1, -1, 2, -1, -1, -1, ....}; のようにデータを不必要な-1で埋めるようなことをせずとも、
変化があった箇所だけを 辞書[キー] = 2; のように登録できるので、配列型と違って無駄なデータのデフォルト埋めが必要ない使い方ができることです。
ブロックのうちアニメーションさせたいのは「今」や「ついさっき」叩いた、一部のブロックだけでよいですものね。
たとえば、タイルのデータが 縦 × 横 で1万 × 1万 = 1億マスあったとしましょう。
2次元配列だと たった1つのマス が 2 に 変化したことを表現するのに 9万9千999個 の無駄な デフォルトの-1 が必要になりますよね。
辞書型で 変化のあったマスだけ の「XとYのペア」をキーにして 辞書[キー] = 2; を登録するなら 9万もの -1 は必要なくなります
辞書型を使いこなせるだけで「プログラミングの機動力や柔軟性のアイデア」が格段に向上します。
なぜなら、何かと何かを辞書型なら結び付けられるので、
宝箱の位置 → 中身のアイテム、宝箱の位置 → すでに開けたかどうか、などアイデア次第で何にだって応用できてしまいます。
使い方での注意点は if( 辞書.count(キー) == 0 ) の辞書に キーが未登録かどうかを毎度きちんと判定しないと
何も入っていない 辞書[キー] にアクセスした瞬間に「デフォルトで 勝手に0 が入っちゃう大問題」があります。
(x,y) = (10,5)のマス目に宝箱がないのに
if( 辞書[(10,5)] ) などの判定式で使うだけで、宝箱がないマスなのに 辞書[(10,5)] = 0が勝手に入って 開いた宝箱が表示されちゃうバグが起こりえます。
だから必ず予防策として if( 辞書.count(キー) == 0 ) や if( 辞書.count(キー) > 0 ) などの判定とセットで使うと覚えましょう。if(辞書.find(キー) != 辞書.end())でもよいが

プログラマは ただ動くコードを書く のを目的に作業しているのではなく
「イメージ通りのデータの構造を実現するためにコードを利用する」ことができてきたときに、デジタル空間の創造主に近づけます。
プログラミングとは「データの構造と流れをコードで積み上げる作業」です。
マインクラフトでブロックを積み上げる代わりに、
いろんな構造をもった[変数]や[配列]を積み上げて、+や-などの演算子やif、for文でデータをつなぎ合わせているイメージです。
  辞書[キー] = 2;
たった1行ですが「配列型と辞書型の違い」の知識があれば、9万データが数個のデータに減らせる魔法がたった1行に凝縮されていることが理解できます。
std::から始まる色んなデータの構造や関数をきちんと勉強しておくことで、#include < ●● > してきてうまく組み合わせて応用できるようになるわけです。




ブロックidを辞書で変化させて叩いたらidが3 → 2 → -1 に変化してブロックを壊せるプログラム




さてつぎは【ブロックidの変化を辞書blockChangeで管理して叩いたらidが3 → 2 → -1 に変化してブロックを壊せるサンプル】でブロックを壊せるようにしてみましょう。

main.cppを編集します:

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

#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列std::stringに必要
#include <fstream> // ファイル読み出しstd::ifstreamに必要
#include <sstream> // 文字列ストリームに必要 ss >> num;で文字列から数字に変換できる
#include <unordered_map> // std::unordered_map 高速辞書配列に必要(チップ番号から壁などを表すビットへ変換する辞書)
#include <utility> // std::pair(データのペア型)を使うのに必要
// [ペアを高速辞書のキーにするためハッシュ関数が必要] https://qiita.com/hamamu/items/4d081751b69aa3bb3557
template<class TypeT> size_t HashCombine(const size_t seed, const TypeT& value)
{
  return seed ^ (std::hash<TypeT>()(value) + 0x9e3779b9 + (seed << 6) + (seed >> 2));
}

// pair型に()をつけたときに呼び出される処理をoperator()で定義してreturnでハッシュ関数を返す
template<class TypeT, class TypeS>
struct std::hash<std::pair<TypeT, TypeS>>
{
  size_t operator() (const std::pair<TypeT, TypeS>& keyval) const noexcept
  {   // returnでハッシュ関数を返すことで高速辞書のキーとして認め許されるようになる
    return HashCombine(std::hash<TypeT>()(keyval.first), keyval.second);
  }
};

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerChips[12]; // キャラのチップのグリッドの分割数に応じて3×4 = 12のぶんだけ配列を事前に確保
  LoadDivGraph("Image/pikkoron.png", 12, 3, 4, 32, 32, playerChips); // キャラのチップのサイズは32×32
  assert(playerChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  // 効果音ファイルを事前にメモリに読み込む https://dxlib.xsrv.jp/function/dxfunc_sound.html#R8N4
  int jumpSE[4]; // 4種類のジャンプ音を読み込む
  jumpSE[0] = LoadSoundMem("SE/Jump0.wav");
  jumpSE[1] = LoadSoundMem("SE/Jump1.wav");
  jumpSE[2] = LoadSoundMem("SE/Jump2.wav");
  jumpSE[3] = LoadSoundMem("SE/Jump3.wav");
  assert(jumpSE[0] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[1] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[2] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[3] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  
  int CellSize = 32; // マップの1マスのピクセル数
  int mapChips[64]; // マップのチップのグリッドの分割数に応じて8×8 = 64など配列で確保
  LoadDivGraph("Map/mapchip.png", 64, 8, 8, CellSize, CellSize, mapChips); // マップのチップのサイズは32×32(=CellSize)
  assert(mapChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  std::vector<std::vector<int>> mapData;// タイルマップデータ
  std::ifstream ifs_csv_file("Map/stage.csv"); // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
  
  int Width = 0, Height = 0; // マップデータの横(Width) 縦(Height)のマスの数
  int MaxWidth = 0; // 1行の数字の最大個数
  std::string line; // 1行単位ごとにcsvファイルから文字列をstd::getlineで読み込んで受け取る
  while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
  {
    std::vector<int> valuelist; // 1行ぶんの数字リスト配列
    std::istringstream linestream( line ); // 各行の文字列ストリーム
    std::string comma_part; // カンマで分割された文字列
    int widthCount = 0; // カンマで区切られた行の数字の数(各行の幅)をカウント
    while (std::getline(linestream, comma_part, { ',' })) // 1行をgetlineでカンマで区切って列を得る
    {
      std::istringstream ss( comma_part ); // カンマで区切られた文字をssを通して>>で数字に
      int num; // 数字単体
      ss >> num; // 文字列ストリーム>>で数字へ変換
      valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
      ++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
    }
    // 1行の数字の数がMAX記録を更新するかチェック
    if (widthCount > MaxWidth)
      MaxWidth = widthCount; // 暫定Maxの列の幅を更新
    
    // 1行ぶんの数字の配列をとりまとめてmapDataに1行ずつ追加
    if (valuelist.size() != 0) // 配列に数字が1つでもあるなら.size() != 0
      mapData.emplace_back(valuelist); // mapDataに1行ずつ数字配列valuelistを追加
    
    ++Height; //マップの高さをカウントアップ
  }
  Width = MaxWidth; // マップの幅Widthは一番数字の個数の多かった行に合わせる
  // ↓読込んだCSVの幅と高さをチェック
  assert(Width > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  assert(Height > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  
  
  float xStart = 200, yStart = Screen::Height - 100; // キャラの初期スタート(ミス後の再開位置)
  float x = xStart, y = yStart; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  enum class Direction { Left, Right }; // enumでキャラの向いてる方向をDirection型として2種類(LeftとRight)定義
  Direction direction = Direction::Right; // キャラの向いてる方向
  int animeTime = 0; // アニメ時間
  
  bool isDead = false; // キャラがやられたかどうか
  int deadAnimeTime = 0; // やられたときのアニメ時間
  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16, 4 }, vyDownSpeedMax[] = { 16, 16, 16, 8 }; // ダッシュ段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f,0.1f }, vyGravity[] = { 0.8f,0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  bool isFlying = false; // 飛行モード
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(playerChips[0], &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  int hitOffsetLeft = 2; // 当たり判定の左端のオフセット
  int hitOffsetRight = 2; // 当たり判定の右端のオフセット
  int hitOffsetTop = 1; // 当たり判定の上端のオフセット
  int hitOffsetBottom = 0; // 当たり判定の下端のオフセット
  
  // 当たり判定のはしっこを計算するための一時関数
  auto getLeft = [&hitOffsetLeft](float worldX) { return worldX + hitOffsetLeft; };
  auto getTop = [&hitOffsetTop](float worldY) { return worldY + hitOffsetTop; };
  auto getRight = [&hitOffsetRight, &imgSizeX](float worldX) { return worldX + imgSizeX - hitOffsetRight; };
  auto getBottom = [&hitOffsetBottom, &imgSizeY](float worldY) { return worldY + imgSizeY - hitOffsetBottom; };
  
  enum class BlockBit // ブロックの特性を32桁のビットで表現
  {
    None = 0,
    Wall   = 0b0000'0000'0000'0000'0000'0000'0000'0001,
    Damage = 0b0000'0000'0000'0000'0000'0000'0000'0010,
    Bounce = 0b0000'0000'0000'0000'0000'0000'0000'0100, // 叩くと弾むブロック
    Change = 0b0000'0000'0000'0000'0000'0000'0000'1000, // 叩くとブロックの種類が変わる
  };
  
  std::unordered_map<int, unsigned int> blockBits = {
    {-1,(int)BlockBit::None},
    { 0,(int)BlockBit::Wall},
    { 1,(int)BlockBit::Damage},
    { 2,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Change}, // 壁 | はずむ | ブロック変化 (|ビット論理和)
    { 3,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Change},
  };
  
  // ブロックをバウンドさせるための 高速辞書<ペア<cellX,cellY>, intアニメ時間>
  std::unordered_map<std::pair<int, int>, int> blockAnime;
  
  std::unordered_map<int, int> blockChange = {
    { 2, 3 }, // 叩くと 2 → 3 にかわる
    { 3, -1 }, // 叩くと 3 → -1 にかわる
  };

  
  // 指定された座標(worldX,worldY)の地形が 壁 や ダメージブロックかなどをチェックする一時関数
  auto checkID = [&blockBits, &CellSize](BlockBit checkBit, std::vector<std::vector<int>>& terrain, float worldX, float worldY,
                                int* pMapX = nullptr, int* pMapY = nullptr)
    {
      int terrainID = -1; // 指定された座標の地形のID
      
      if (worldX < 0 || worldY < 0) return false; // 負の座標が指定された場合
      
      // マップ座標系(二次元配列の行と列)に変換する
      int mapX = (int)(worldX / CellSize), mapY = (int)(worldY / CellSize);
      // 二次元配列の範囲内か判定
      if (mapY < terrain.size() && mapX < terrain[mapY].size())
        terrainID = terrain[mapY][mapX]; // 二次元配列から地形IDを取り出す
      
      bool isCheckBit = (((int)checkBit) & blockBits[terrainID]) != 0; // ビット論理積 & で地形のchekckBitを確かめる
      if (isCheckBit && pMapX != nullptr && pMapY != nullptr)
        *pMapX = mapX, *pMapY = mapY; // ブロックのcsvのセル番号をポインタに記録(nullptrの場合は記録しない)
      
      return isCheckBit;
    };
  
  enum class HitPart // 当たった箇所を表すビット
  {
    None    = 0,
    Left    = 0b0000'0000'0000'0000'0000'0000'0000'0001,
    Right   = 0b0000'0000'0000'0000'0000'0000'0000'0010,
    Top     = 0b0000'0000'0000'0000'0000'0000'0000'0100,
    Bottom  = 0b0000'0000'0000'0000'0000'0000'0000'1000,
    XMiddle = 0b0000'0000'0000'0000'0000'0000'0001'0000,
    YMiddle = 0b0000'0000'0000'0000'0000'0000'0010'0000,
  };
  
  // [x方向左右]地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheckX = [&checkID](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      // 左端がめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yTop, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Top; // 左上がめりこんでいる
      if (checkID(checkBit, terrain, xLeft, yMiddle, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::YMiddle; // 左中がめりこんでいる
      if (checkID(checkBit, terrain, xLeft, yBottom, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Bottom; // 左下がめりこんでいる
      // 右端がめりこんでいるか?
      if (checkID(checkBit, terrain, xRight, yTop, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Top; // 右上がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yMiddle, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::YMiddle; // 右中がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yBottom, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Bottom; // 右下がめりこんでいる
      
      return (int)HitPart::None;
    };
  
  // [y方向上下]地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheckY = [&checkID](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      // 頭の部分がめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yTop, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Top; // 左上がめりこんでいる
      if (checkID(checkBit, terrain, xMiddle, yTop, pMapX, pMapY))
        return (int)HitPart::XMiddle | (int)HitPart::Top; // 中上がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yTop, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Top; // 右上がめりこんでいる
      // 足元が壁にめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yBottom, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Bottom; // 左下がめりこんでいる
      if (checkID(checkBit, terrain, xMiddle, yBottom, pMapX, pMapY))
        return (int)HitPart::XMiddle | (int)HitPart::Bottom; // 中下がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yBottom, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Bottom; // 右下がめりこんでいる
      
      return (int)HitPart::None;
    };
  
  // 地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheck = [&hitcheckX, &hitcheckY](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      return hitcheckX(checkBit,terrain,xLeft,xMiddle,xRight,yTop,yMiddle,yBottom,pMapX,pMapY)
        | hitcheckY(checkBit,terrain,xLeft,xMiddle,xRight,yTop,yMiddle,yBottom,pMapX,pMapY);
    };
  
  float camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
  if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ
  float camera_y = y - Screen::Height / 2; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
  if (camera_y <= 0) camera_y = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Height * CellSize - Screen::Height <= camera_y) camera_y = Height * CellSize - Screen::Height; // ステージのはしっこ
  
  
  float debug_x = -1, debug_y = -1; // [デバッグ] めり込みすぎのラインを記録しておく変数
  float debug_x_yTop = -1, debug_x_yBottom = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の上下端も記録
  float debug_y_xLeft = -1, debug_y_xRight = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の左右端も記録
  
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      direction = Direction::Left;
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      direction = Direction::Right;
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      vy = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
      // 加速段階に応じて違うジャンプ音を鳴らす
      PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs > 9) {
          isFlying = true; // 飛行モードにする
        }
        
        // 加速段階に応じて違うジャンプ音を鳴らす
        PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    if (!isDead) // やられたときは移動を停止
      x += vx; // 速度のぶん現在の位置から移動
    if (!isDead) // やられたときは当たり判定も停止
    {
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      if(hitcheck(BlockBit::Damage, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに
      
      // 左端が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yMiddle) || // 左中が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) ) // 左下が壁か?
      {
        debug_x = xLeft; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        // std::fmodで小数点の余りを計算 めり込みすぎの余り = std::fmod(xLeft, CellSize)
        float wallRight = xLeft + CellSize - (float)std::fmod(xLeft, CellSize); // 壁の右端
        x = wallRight - hitOffsetLeft; // 左端を壁の右端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      } // ↓右端が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xRight, yTop) || // 右上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yMiddle) || // 右中が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_x = xRight; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        float wallLeft = xRight - (float)std::fmod(xRight, CellSize); // 壁の左端
        x = wallLeft + hitOffsetRight - imgSizeX; // 右端を壁の左端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      }
    }
    
    if (!isDead) // やられたときは移動を停止
      y += vy;
    if (!isDead) // やられたときは当たり判定も停止
    {
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      if (hitcheck(BlockBit::Damage, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに
      
      int mapX = 0, mapY = 0; // ぶつかったらそのブロックのCSVのセルの行mapYと列mapXを得る
      if ( (int)HitPart::Top
          & hitcheck(BlockBit::Bounce, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        std::pair<int,int> mapXY = std::make_pair(mapX, mapY); // ぶつかったブロックのセルの行mapYと列mapXのペア
        if (blockAnime.count(mapXY) == 0) // 辞書にキーがあるかcountで数えて未登録==0のとき
          blockAnime[mapXY] = 0; // ブロックのアニメ時間を辞書に登録(時間が辞書にあるものだけアニメが走る)
      }
      
      bool isBlockChanging = false; // ブロック変化フラグ(idはあとから変化させないとWallの押し戻しの跳ね返りvy = 0がなくなる)
      if ( (int)HitPart::Top
          & hitcheck(BlockBit::Change, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        isBlockChanging = true; // ここではフラグをtrueにするだけ(めり込み押し戻しvy=0するまえにid=-1にすると壊した瞬間にすり抜けちゃう)
      }

      
      // 頭の部分が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yTop) || // 中上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yTop) ) // 右上が壁か?
      {
        debug_y = yTop; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallBottom = yTop + CellSize - (float)std::fmod(yTop, CellSize); // 壁の下側の境界
        y = wallBottom - hitOffsetTop; // 頭を壁の下端に沿わす(めり込んだぶんを押し戻す)
        vy = 0; // 壁に当たったら速度をストップ
      } // ↓足元が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) || // 左下が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yBottom) || // 中下が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_y = yBottom; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallTop = yBottom - (float)std::fmod(yBottom, CellSize); // 壁の上側の境界
        y = wallTop + hitOffsetBottom - imgSizeX; // 足元を壁の上端に沿わす(めり込んだぶんを押し戻す)
        
        isGround = true;
        isFlying = false;
        vx_idx = 0;
        
        vy = 0; // 床に当たったら速度をストップ
      }
      else
        isGround = false; // ブロックに当たってないからfalseにして落下するように
      
      if (isBlockChanging) // ブロック変化フラグ==true(すでに↑めりこみ判定でvy=0で勢いを止め反射が効くからid=-1にしてもすりぬけずにすむ)
      {
        int id = mapData[mapY][mapX]; // 行mapY 列mapXの地形idを得る
        if (blockChange.count(id) > 0) // blockChangeの辞書に登録のあるidのときは
        {
          int changeID = blockChange[id]; // 変化先のID
          std::pair<int, int> mapXY = std::make_pair(mapX, mapY); // ぶつかったブロックのセルの行mapYと列mapXのペア
          if (!(blockBits[changeID] & (int)BlockBit::Bounce)) // 変化先ブロックがバウンスするならアニメ時間をeraseせずに辞書に残す
            if (blockAnime.count(mapXY) > 0)
              blockAnime.erase(mapXY); // ブロックの種類変える前にアニメをリセットしておかないと辞書に残り続ける
          
          mapData[mapY][mapX] = changeID; // idを入れ替える
        }
      }

    }
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    
    camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
    if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ
    camera_y = y - Screen::Height / 2; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
    if (camera_y <= 0) camera_y = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Height * CellSize - Screen::Height <= camera_y) camera_y = Height * CellSize - Screen::Height; // ステージのはしっこ
    
    
    // ダッシュ段階に応じてキャラクタのアニメ時間を進める
    float vx_dash = (vx < 0) ? -vx : vx; // 絶対値でプラスだけにする
    if (vx_dash > 0.5f) animeTime += 1; // 時間 +1 ( 合計 1 )
    if (vx_dash > 6) animeTime += 1; // 時間 +1 (計 1+1=2 ) 1フレームごとに2アニメ時間が進む
    if (vx_dash > 9) animeTime += 2; // 最終段階ではアニメ時間をさらに +2 (計 1+1+2=4 )
    
    int animeStep = animeTime / 10; // 10を5に変えたらアニメ速度が上がる(10ごとに1コマアニメが進む)
    if (animeStep >= 3) // アニメのステップ数が0,1,2の3段階までなら3を超えたら0へループ
    {
      animeTime = 0; // 0へ戻り再びアニメ時間を再ループ
      animeStep = 0;
    }
    
    
    // 描画処理
    if (isDead) // やられたときの描画処理
    {
      float yMiss = y - camera_y + imgSizeY / 2; // ミスったy起点
      float yAnim0 = yMiss; // アニメy位置0 始まりのy位置
      float yAnim1 = yMiss - 10; // アニメy位置1 アニメの途中少し上方向に 10 浮く
      float yAnim2 = Screen::Height + 32; // アニメy位置2 最終y位置 画面の高さ+32で画面外へ
      
      float yDeadAnim; // やられた際にはyの位置をアニメさせる
      // 直線的な比例アニメの式(t=時刻) 位置1×(終時刻 - t) / アニメの長さ + 位置2×(t - 始時刻) / アニメの長さ
      if (deadAnimeTime < 40) // やられアニメが始まって40フレーム目までは yAnim0 ~ yAnim1
        yDeadAnim = yAnim0 * (40 - deadAnimeTime) / 40 + yAnim1 * (deadAnimeTime - 0) / 40;
      else if (deadAnimeTime < 60) // やられアニメが始まって60フレーム目までは yAnim1 ~ yAnim2
        yDeadAnim = yAnim1 * (60 - deadAnimeTime) / 20 + yAnim2 * (deadAnimeTime - 40) / 20;
      else
      {
        yDeadAnim = yAnim2; // アニメ終了 到着 y位置をセット
        deadAnimeTime = 0; // 次にやられたときのアニメのため0リセットしておく
        
        // 再開位置からゲーム再開(各数値もリセット)
        isDead = false;
        x = xStart, y = yStart, vx = 0, vy = 0;
        isGround = false, isFlying = false;
        vx_idx = 0;
      }
      
      int deadAnimeStep = deadAnimeTime / 3; // 3で割って[チップのパラパラの]アニメの速度は弱める
      int chipAnime = deadAnimeStep % 3; // % 3で余りを求めれば余りはチップのパラパラ番号は0~2の間に収まる
      DrawRotaGraphF(x - camera_x + imgSizeX / 2, yDeadAnim, 1.0f, 10.0f * deadAnimeStep / 360.0f * 2 * DX_PI, playerChips[chipAnime], TRUE);
      
      ++deadAnimeTime; // アニメ時間を進める
    }
    else
    {
      if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f,-60.0f / 360.0f * 2 * DX_PI, playerChips[4], TRUE);
      else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 60.0f / 360.0f * 2 * DX_PI, playerChips[7], TRUE);
      else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 0.0f, playerChips[5 - animeStep], TRUE);
      else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 0.0f, playerChips[6 + animeStep], TRUE);
    }
    
    float rotaShiftX = CellSize / 2, rotaShiftY = CellSize / 2; // DrawRotaGraphF関数は画像の真ん中が起点なので1/2ずらした位置計算
    // 地形マップを[ 行のfor文×列のfor文 ]の2重ループで描く
    for (int cellY = 0, ySize = mapData.size(); cellY < ySize; ++cellY)
    {
      float chip_y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
      for (int cellX = 0, xSize = mapData[cellY].size(); cellX < xSize; ++cellX)
      {
        float chip_x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
        int id = mapData[cellY][cellX]; // [行][列] の位置のcsvの地形番号(チップ番号)を得る
        if (id < 0) continue; // -1のときは描かない
        
        // マップチップを描く
        auto mapXY = std::make_pair(cellX, cellY);
        if (blockAnime.count(mapXY) == 0)
          DrawRotaGraphF(chip_x - camera_x + rotaShiftX, chip_y - camera_y + rotaShiftY, 1.0f, 0.0f, mapChips[id], TRUE);
        else if ((int)BlockBit::Bounce & blockBits[id])
        {
          float yBase = chip_y - camera_y;
          float yAnim0 = yBase - 10, yAnim1 = yBase; // yAnim0 から yAnim1の位置までアニメする
          int blockTime = blockAnime[mapXY];
          float yAnim = yAnim0 * (15 - blockTime) / 15 + yAnim1 * blockTime / 15;
          DrawRotaGraphF(chip_x - camera_x + rotaShiftX, yAnim + rotaShiftY, 1.0f, 0.0f, mapChips[id], TRUE);
          ++blockAnime[mapXY]; // アニメ時間を進める
          if (blockTime > 15) // アニメ終了時間を15フレーム目とするとき
            blockAnime.erase(mapXY); // 辞書からアニメをerase関数で取り除く
        }
        // [デバッグ]用にcsvのidの数字を描く
        DrawString(chip_x - camera_x, chip_y - camera_y, std::to_string(id).c_str(), GetColor(255, 255, 255));
      }
    }
    
    // [デバッグ] 当たり判定の四隅を赤枠で描画
    DrawBox(getLeft(x) - camera_x, getTop(y) - camera_y, getRight(x) - camera_x, getBottom(y) - camera_y, GetColor(255, 0, 0), FALSE);
    // [デバッグ] キャラ原点(x,y)を緑丸で描画
    DrawCircle(x - camera_x, y - camera_y, 2, GetColor(0, 255, 0), TRUE);
    // [デバッグ] 当たった際の行き過ぎの めりこみ座標(debug_x,debug_y) を青線で描画
    if (debug_x != -1)
      DrawLine(debug_x - camera_x, debug_x_yTop - camera_y, debug_x - camera_x, debug_x_yBottom - camera_y, GetColor(0, 0, 255));
    if (debug_y != -1)
      DrawLine(debug_y_xLeft - camera_x, debug_y - camera_y, debug_y_xRight - camera_x, debug_y - camera_y, GetColor(0, 0, 255));
    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}



いかがでしょう、ブロックを下から叩くとブロックのidが変化して、ブロックが壊せるようになりましたか?
オリジナルのタイルを自分で描いて、blockChange辞書に登録して、壊せるブロックを自作で増やしてみてくださいね。





for文で複数ブロックidを辞書で変化させてON OFFブロックを叩いたらidが12⇔13、14⇔15にスイッチできるプログラム




さてつぎは【for文で複数のブロックidを辞書blockChangeその変化させる対象をblockSwitchで管理してON OFFブロックを叩いたらidが12⇔13、14⇔15にスイッチできるサンプル】でブロックをスイッチできるようにしてみましょう。

main.cppを編集します:

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

#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列std::stringに必要
#include <fstream> // ファイル読み出しstd::ifstreamに必要
#include <sstream> // 文字列ストリームに必要 ss >> num;で文字列から数字に変換できる
#include <unordered_map> // std::unordered_map 高速辞書配列に必要(チップ番号から壁などを表すビットへ変換する辞書)
#include <utility> // std::pair(データのペア型)を使うのに必要
// [ペアを高速辞書のキーにするためハッシュ関数が必要] https://qiita.com/hamamu/items/4d081751b69aa3bb3557
template<class TypeT> size_t HashCombine(const size_t seed, const TypeT& value)
{
  return seed ^ (std::hash<TypeT>()(value) + 0x9e3779b9 + (seed << 6) + (seed >> 2));
}

// pair型に()をつけたときに呼び出される処理をoperator()で定義してreturnでハッシュ関数を返す
template<class TypeT, class TypeS>
struct std::hash<std::pair<TypeT, TypeS>>
{
  size_t operator() (const std::pair<TypeT, TypeS>& keyval) const noexcept
  {   // returnでハッシュ関数を返すことで高速辞書のキーとして認め許されるようになる
    return HashCombine(std::hash<TypeT>()(keyval.first), keyval.second);
  }
};

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  int playerChips[12]; // キャラのチップのグリッドの分割数に応じて3×4 = 12のぶんだけ配列を事前に確保
  LoadDivGraph("Image/pikkoron.png", 12, 3, 4, 32, 32, playerChips); // キャラのチップのサイズは32×32
  assert(playerChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  // 効果音ファイルを事前にメモリに読み込む https://dxlib.xsrv.jp/function/dxfunc_sound.html#R8N4
  int jumpSE[4]; // 4種類のジャンプ音を読み込む
  jumpSE[0] = LoadSoundMem("SE/Jump0.wav");
  jumpSE[1] = LoadSoundMem("SE/Jump1.wav");
  jumpSE[2] = LoadSoundMem("SE/Jump2.wav");
  jumpSE[3] = LoadSoundMem("SE/Jump3.wav");
  assert(jumpSE[0] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[1] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[2] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[3] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  
  int CellSize = 32; // マップの1マスのピクセル数
  int mapChips[64]; // マップのチップのグリッドの分割数に応じて8×8 = 64など配列で確保
  LoadDivGraph("Map/mapchip.png", 64, 8, 8, CellSize, CellSize, mapChips); // マップのチップのサイズは32×32(=CellSize)
  assert(mapChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
  
  std::vector<std::vector<int>> mapData;// タイルマップデータ
  std::ifstream ifs_csv_file("Map/stage.csv"); // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
  
  int Width = 0, Height = 0; // マップデータの横(Width) 縦(Height)のマスの数
  int MaxWidth = 0; // 1行の数字の最大個数
  std::string line; // 1行単位ごとにcsvファイルから文字列をstd::getlineで読み込んで受け取る
  while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
  {
    std::vector<int> valuelist; // 1行ぶんの数字リスト配列
    std::istringstream linestream( line ); // 各行の文字列ストリーム
    std::string comma_part; // カンマで分割された文字列
    int widthCount = 0; // カンマで区切られた行の数字の数(各行の幅)をカウント
    while (std::getline(linestream, comma_part, { ',' })) // 1行をgetlineでカンマで区切って列を得る
    {
      std::istringstream ss( comma_part ); // カンマで区切られた文字をssを通して>>で数字に
      int num; // 数字単体
      ss >> num; // 文字列ストリーム>>で数字へ変換
      valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
      ++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
    }
    // 1行の数字の数がMAX記録を更新するかチェック
    if (widthCount > MaxWidth)
      MaxWidth = widthCount; // 暫定Maxの列の幅を更新
    
    // 1行ぶんの数字の配列をとりまとめてmapDataに1行ずつ追加
    if (valuelist.size() != 0) // 配列に数字が1つでもあるなら.size() != 0
      mapData.emplace_back(valuelist); // mapDataに1行ずつ数字配列valuelistを追加
    
    ++Height; //マップの高さをカウントアップ
  }
  Width = MaxWidth; // マップの幅Widthは一番数字の個数の多かった行に合わせる
  // ↓読込んだCSVの幅と高さをチェック
  assert(Width > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  assert(Height > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  
  
  float xStart = 200, yStart = Screen::Height - 100; // キャラの初期スタート(ミス後の再開位置)
  float x = xStart, y = yStart; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  enum class Direction { Left, Right }; // enumでキャラの向いてる方向をDirection型として2種類(LeftとRight)定義
  Direction direction = Direction::Right; // キャラの向いてる方向
  int animeTime = 0; // アニメ時間
  
  bool isDead = false; // キャラがやられたかどうか
  int deadAnimeTime = 0; // やられたときのアニメ時間
  
  bool isSwitchOn = false; // ステージ全体のスイッチのオン、オフ状態(オンのときはオンを押せないようにする)
  int id_On = 10, id_Off = 11; // オンのときにオンを押したりオフのときにオフを押せないように判定する

  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16, 4 }, vyDownSpeedMax[] = { 16, 16, 16, 8 }; // ダッシュ段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f,0.1f }, vyGravity[] = { 0.8f,0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  bool isFlying = false; // 飛行モード
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(playerChips[0], &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  int hitOffsetLeft = 2; // 当たり判定の左端のオフセット
  int hitOffsetRight = 2; // 当たり判定の右端のオフセット
  int hitOffsetTop = 1; // 当たり判定の上端のオフセット
  int hitOffsetBottom = 0; // 当たり判定の下端のオフセット
  
  // 当たり判定のはしっこを計算するための一時関数
  auto getLeft = [&hitOffsetLeft](float worldX) { return worldX + hitOffsetLeft; };
  auto getTop = [&hitOffsetTop](float worldY) { return worldY + hitOffsetTop; };
  auto getRight = [&hitOffsetRight, &imgSizeX](float worldX) { return worldX + imgSizeX - hitOffsetRight; };
  auto getBottom = [&hitOffsetBottom, &imgSizeY](float worldY) { return worldY + imgSizeY - hitOffsetBottom; };
  
  enum class BlockBit // ブロックの特性を32桁のビットで表現
  {
    None = 0,
    Wall   = 0b0000'0000'0000'0000'0000'0000'0000'0001,
    Damage = 0b0000'0000'0000'0000'0000'0000'0000'0010,
    Bounce = 0b0000'0000'0000'0000'0000'0000'0000'0100, // 叩くと弾むブロック
    Change = 0b0000'0000'0000'0000'0000'0000'0000'1000, // 叩くとブロックの種類が変わる
    Switch = 0b0000'0000'0000'0000'0000'0000'0001'0000, // スイッチを叩くと赤ブロックや青ブロックの無効⇔有効が切替
  };
  
  std::unordered_map<int, unsigned int> blockBits = {
    {-1,(int)BlockBit::None},
    { 0,(int)BlockBit::Wall},
    { 1,(int)BlockBit::Damage},
    { 2,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Change}, // 壁 | はずむ | ブロック変化 (|ビット論理和)
    { 3,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Change},
    {10,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Switch}, // 赤Onスイッチ
    {11,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Switch}, // 青Offスイッチ
    {12,(int)BlockBit::Wall}, // On に連動するブロック
    {14,(int)BlockBit::Wall}, // Offに連動するブロック
  };
  
  // ブロックをバウンドさせるための 高速辞書<ペア<cellX,cellY>, intアニメ時間>
  std::unordered_map<std::pair<int, int>, int> blockAnime;
  
  std::unordered_map<int, int> blockChange = {
    { 2, 3 }, // 叩くと 2 → 3 にかわる
    { 3, -1 }, // 叩くと 3 → -1 にかわる
    { 12, 13 }, // 11(OFF) を押すと 12→13
    { 13, 12 }, // 10(ON) を押すと 13→12
    { 14, 15 }, // 10(ON) を押すと 14→15
    { 15, 14 }, // 11(OFF) を押すと 15→14
  };
  
  std::unordered_map<int, std::vector<int>> blockSwitch = {
    { 10, {12,13,14,15} }, // 叩くと12,13,14,15がblockChangeの設定内容に応じて変化する
    { 11, {12,13,14,15} },
  };

  
  // 指定された座標(worldX,worldY)の地形が 壁 や ダメージブロックかなどをチェックする一時関数
  auto checkID = [&blockBits, &CellSize](BlockBit checkBit, std::vector<std::vector<int>>& terrain, float worldX, float worldY,
                                int* pMapX = nullptr, int* pMapY = nullptr)
    {
      int terrainID = -1; // 指定された座標の地形のID
      
      if (worldX < 0 || worldY < 0) return false; // 負の座標が指定された場合
      
      // マップ座標系(二次元配列の行と列)に変換する
      int mapX = (int)(worldX / CellSize), mapY = (int)(worldY / CellSize);
      // 二次元配列の範囲内か判定
      if (mapY < terrain.size() && mapX < terrain[mapY].size())
        terrainID = terrain[mapY][mapX]; // 二次元配列から地形IDを取り出す
      
      bool isCheckBit = (((int)checkBit) & blockBits[terrainID]) != 0; // ビット論理積 & で地形のchekckBitを確かめる
      if (isCheckBit && pMapX != nullptr && pMapY != nullptr)
        *pMapX = mapX, *pMapY = mapY; // ブロックのcsvのセル番号をポインタに記録(nullptrの場合は記録しない)
      
      return isCheckBit;
    };
  
  enum class HitPart // 当たった箇所を表すビット
  {
    None    = 0,
    Left    = 0b0000'0000'0000'0000'0000'0000'0000'0001,
    Right   = 0b0000'0000'0000'0000'0000'0000'0000'0010,
    Top     = 0b0000'0000'0000'0000'0000'0000'0000'0100,
    Bottom  = 0b0000'0000'0000'0000'0000'0000'0000'1000,
    XMiddle = 0b0000'0000'0000'0000'0000'0000'0001'0000,
    YMiddle = 0b0000'0000'0000'0000'0000'0000'0010'0000,
  };
  
  // [x方向左右]地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheckX = [&checkID](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      // 左端がめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yTop, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Top; // 左上がめりこんでいる
      if (checkID(checkBit, terrain, xLeft, yMiddle, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::YMiddle; // 左中がめりこんでいる
      if (checkID(checkBit, terrain, xLeft, yBottom, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Bottom; // 左下がめりこんでいる
      // 右端がめりこんでいるか?
      if (checkID(checkBit, terrain, xRight, yTop, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Top; // 右上がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yMiddle, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::YMiddle; // 右中がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yBottom, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Bottom; // 右下がめりこんでいる
      
      return (int)HitPart::None;
    };
  
  // [y方向上下]地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheckY = [&checkID](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      // 頭の部分がめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yTop, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Top; // 左上がめりこんでいる
      if (checkID(checkBit, terrain, xMiddle, yTop, pMapX, pMapY))
        return (int)HitPart::XMiddle | (int)HitPart::Top; // 中上がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yTop, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Top; // 右上がめりこんでいる
      // 足元が壁にめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yBottom, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Bottom; // 左下がめりこんでいる
      if (checkID(checkBit, terrain, xMiddle, yBottom, pMapX, pMapY))
        return (int)HitPart::XMiddle | (int)HitPart::Bottom; // 中下がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yBottom, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Bottom; // 右下がめりこんでいる
      
      return (int)HitPart::None;
    };
  
  // 地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheck = [&hitcheckX, &hitcheckY](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      return hitcheckX(checkBit,terrain,xLeft,xMiddle,xRight,yTop,yMiddle,yBottom,pMapX,pMapY)
        | hitcheckY(checkBit,terrain,xLeft,xMiddle,xRight,yTop,yMiddle,yBottom,pMapX,pMapY);
    };
  
  float camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
  if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ
  float camera_y = y - Screen::Height / 2; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
  if (camera_y <= 0) camera_y = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Height * CellSize - Screen::Height <= camera_y) camera_y = Height * CellSize - Screen::Height; // ステージのはしっこ
  
  
  float debug_x = -1, debug_y = -1; // [デバッグ] めり込みすぎのラインを記録しておく変数
  float debug_x_yTop = -1, debug_x_yBottom = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の上下端も記録
  float debug_y_xLeft = -1, debug_y_xRight = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の左右端も記録
  
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      direction = Direction::Left;
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      direction = Direction::Right;
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      vy = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
      // 加速段階に応じて違うジャンプ音を鳴らす
      PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs > 9) {
          isFlying = true; // 飛行モードにする
        }
        
        // 加速段階に応じて違うジャンプ音を鳴らす
        PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    if (!isDead) // やられたときは移動を停止
      x += vx; // 速度のぶん現在の位置から移動
    if (!isDead) // やられたときは当たり判定も停止
    {
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      if(hitcheck(BlockBit::Damage, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに
      
      // 左端が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yMiddle) || // 左中が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) ) // 左下が壁か?
      {
        debug_x = xLeft; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        // std::fmodで小数点の余りを計算 めり込みすぎの余り = std::fmod(xLeft, CellSize)
        float wallRight = xLeft + CellSize - (float)std::fmod(xLeft, CellSize); // 壁の右端
        x = wallRight - hitOffsetLeft; // 左端を壁の右端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      } // ↓右端が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xRight, yTop) || // 右上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yMiddle) || // 右中が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_x = xRight; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        float wallLeft = xRight - (float)std::fmod(xRight, CellSize); // 壁の左端
        x = wallLeft + hitOffsetRight - imgSizeX; // 右端を壁の左端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      }
    }
    
    if (!isDead) // やられたときは移動を停止
      y += vy;
    if (!isDead) // やられたときは当たり判定も停止
    {
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      if (hitcheck(BlockBit::Damage, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに
      
      int mapX = 0, mapY = 0; // ぶつかったらそのブロックのCSVのセルの行mapYと列mapXを得る
      if ( (int)HitPart::Top
          & hitcheck(BlockBit::Bounce, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        std::pair<int,int> mapXY = std::make_pair(mapX, mapY); // ぶつかったブロックのセルの行mapYと列mapXのペア
        if (blockAnime.count(mapXY) == 0) // 辞書にキーがあるかcountで数えて未登録==0のとき
          blockAnime[mapXY] = 0; // ブロックのアニメ時間を辞書に登録(時間が辞書にあるものだけアニメが走る)
      }
      
      if ((int)HitPart::Top
          & hitcheck(BlockBit::Switch, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        int id = mapData[mapY][mapX]; // 行mapY 列mapXの地形idを得る
        if ((isSwitchOn && id == id_Off || !isSwitchOn && id == id_On) && blockSwitch.count(id) > 0)
        {
          isSwitchOn = !isSwitchOn; // trueとfalseを!で反転
          for (int cellY = 0; cellY < Height; ++cellY) // 行をすべて調べる
            for (int cellX = 0; cellX < Width; ++cellX) // 列をすべて調べる
              for (auto switchID : blockSwitch[id]) // スイッチに反応するブロックのIDすべてを調べる
                if (mapData[cellY][cellX] == switchID)
                {
                  mapData[cellY][cellX] = blockChange[switchID]; // 変化先のIDへ書き換える
                  break; // 次の列のループへ移る(3重for文の一番内側のループをbreakして1つ外のループへ)
                }
        }
      }

      
      bool isBlockChanging = false; // ブロック変化フラグ(idはあとから変化させないとWallの押し戻しの跳ね返りvy = 0がなくなる)
      if ( (int)HitPart::Top
          & hitcheck(BlockBit::Change, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        isBlockChanging = true; // ここではフラグをtrueにするだけ(めり込み押し戻しvy=0するまえにid=-1にすると壊した瞬間にすり抜けちゃう)
      }
      
      // 頭の部分が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yTop) || // 中上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yTop) ) // 右上が壁か?
      {
        debug_y = yTop; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallBottom = yTop + CellSize - (float)std::fmod(yTop, CellSize); // 壁の下側の境界
        y = wallBottom - hitOffsetTop; // 頭を壁の下端に沿わす(めり込んだぶんを押し戻す)
        vy = 0; // 壁に当たったら速度をストップ
      } // ↓足元が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) || // 左下が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yBottom) || // 中下が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_y = yBottom; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallTop = yBottom - (float)std::fmod(yBottom, CellSize); // 壁の上側の境界
        y = wallTop + hitOffsetBottom - imgSizeX; // 足元を壁の上端に沿わす(めり込んだぶんを押し戻す)
        
        isGround = true;
        isFlying = false;
        vx_idx = 0;
        
        vy = 0; // 床に当たったら速度をストップ
      }
      else
        isGround = false; // ブロックに当たってないからfalseにして落下するように
      
      if (isBlockChanging) // ブロック変化フラグ==true(すでに↑めりこみ判定でvy=0で勢いを止め反射が効くからid=-1にしてもすりぬけずにすむ)
      {
        int id = mapData[mapY][mapX]; // 行mapY 列mapXの地形idを得る
        if (blockChange.count(id) > 0) // blockChangeの辞書に登録のあるidのときは
        {
          int changeID = blockChange[id]; // 変化先のID
          std::pair<int, int> mapXY = std::make_pair(mapX, mapY); // ぶつかったブロックのセルの行mapYと列mapXのペア
          if (!(blockBits[changeID] & (int)BlockBit::Bounce)) // 変化先ブロックがバウンスするならアニメ時間をeraseせずに辞書に残す
            if (blockAnime.count(mapXY) > 0)
              blockAnime.erase(mapXY); // ブロックの種類変える前にアニメをリセットしておかないと辞書に残り続ける
          
          mapData[mapY][mapX] = changeID; // idを入れ替える
        }
      }
    }
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    
    camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
    if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ
    camera_y = y - Screen::Height / 2; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
    if (camera_y <= 0) camera_y = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Height * CellSize - Screen::Height <= camera_y) camera_y = Height * CellSize - Screen::Height; // ステージのはしっこ
    
    
    // ダッシュ段階に応じてキャラクタのアニメ時間を進める
    float vx_dash = (vx < 0) ? -vx : vx; // 絶対値でプラスだけにする
    if (vx_dash > 0.5f) animeTime += 1; // 時間 +1 ( 合計 1 )
    if (vx_dash > 6) animeTime += 1; // 時間 +1 (計 1+1=2 ) 1フレームごとに2アニメ時間が進む
    if (vx_dash > 9) animeTime += 2; // 最終段階ではアニメ時間をさらに +2 (計 1+1+2=4 )
    
    int animeStep = animeTime / 10; // 10を5に変えたらアニメ速度が上がる(10ごとに1コマアニメが進む)
    if (animeStep >= 3) // アニメのステップ数が0,1,2の3段階までなら3を超えたら0へループ
    {
      animeTime = 0; // 0へ戻り再びアニメ時間を再ループ
      animeStep = 0;
    }
    
    
    // 描画処理
    if (isDead) // やられたときの描画処理
    {
      float yMiss = y - camera_y + imgSizeY / 2; // ミスったy起点
      float yAnim0 = yMiss; // アニメy位置0 始まりのy位置
      float yAnim1 = yMiss - 10; // アニメy位置1 アニメの途中少し上方向に 10 浮く
      float yAnim2 = Screen::Height + 32; // アニメy位置2 最終y位置 画面の高さ+32で画面外へ
      
      float yDeadAnim; // やられた際にはyの位置をアニメさせる
      // 直線的な比例アニメの式(t=時刻) 位置1×(終時刻 - t) / アニメの長さ + 位置2×(t - 始時刻) / アニメの長さ
      if (deadAnimeTime < 40) // やられアニメが始まって40フレーム目までは yAnim0 ~ yAnim1
        yDeadAnim = yAnim0 * (40 - deadAnimeTime) / 40 + yAnim1 * (deadAnimeTime - 0) / 40;
      else if (deadAnimeTime < 60) // やられアニメが始まって60フレーム目までは yAnim1 ~ yAnim2
        yDeadAnim = yAnim1 * (60 - deadAnimeTime) / 20 + yAnim2 * (deadAnimeTime - 40) / 20;
      else
      {
        yDeadAnim = yAnim2; // アニメ終了 到着 y位置をセット
        deadAnimeTime = 0; // 次にやられたときのアニメのため0リセットしておく
        
        // 再開位置からゲーム再開(各数値もリセット)
        isDead = false;
        x = xStart, y = yStart, vx = 0, vy = 0;
        isGround = false, isFlying = false;
        vx_idx = 0;
      }
      
      int deadAnimeStep = deadAnimeTime / 3; // 3で割って[チップのパラパラの]アニメの速度は弱める
      int chipAnime = deadAnimeStep % 3; // % 3で余りを求めれば余りはチップのパラパラ番号は0~2の間に収まる
      DrawRotaGraphF(x - camera_x + imgSizeX / 2, yDeadAnim, 1.0f, 10.0f * deadAnimeStep / 360.0f * 2 * DX_PI, playerChips[chipAnime], TRUE);
      
      ++deadAnimeTime; // アニメ時間を進める
    }
    else
    {
      if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f,-60.0f / 360.0f * 2 * DX_PI, playerChips[4], TRUE);
      else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 60.0f / 360.0f * 2 * DX_PI, playerChips[7], TRUE);
      else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 0.0f, playerChips[5 - animeStep], TRUE);
      else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 0.0f, playerChips[6 + animeStep], TRUE);
    }
    
    float rotaShiftX = CellSize / 2, rotaShiftY = CellSize / 2; // DrawRotaGraphF関数は画像の真ん中が起点なので1/2ずらした位置計算
    // 地形マップを[ 行のfor文×列のfor文 ]の2重ループで描く
    for (int cellY = 0, ySize = mapData.size(); cellY < ySize; ++cellY)
    {
      float chip_y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
      for (int cellX = 0, xSize = mapData[cellY].size(); cellX < xSize; ++cellX)
      {
        float chip_x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
        int id = mapData[cellY][cellX]; // [行][列] の位置のcsvの地形番号(チップ番号)を得る
        if (id < 0) continue; // -1のときは描かない
        
        // マップチップを描く
        auto mapXY = std::make_pair(cellX, cellY);
        if (blockAnime.count(mapXY) == 0)
          DrawRotaGraphF(chip_x - camera_x + rotaShiftX, chip_y - camera_y + rotaShiftY, 1.0f, 0.0f, mapChips[id], TRUE);
        else if ((int)BlockBit::Bounce & blockBits[id])
        {
          float yBase = chip_y - camera_y;
          float yAnim0 = yBase - 10, yAnim1 = yBase; // yAnim0 から yAnim1の位置までアニメする
          int blockTime = blockAnime[mapXY];
          float yAnim = yAnim0 * (15 - blockTime) / 15 + yAnim1 * blockTime / 15;
          DrawRotaGraphF(chip_x - camera_x + rotaShiftX, yAnim + rotaShiftY, 1.0f, 0.0f, mapChips[id], TRUE);
          ++blockAnime[mapXY]; // アニメ時間を進める
          if (blockTime > 15) // アニメ終了時間を15フレーム目とするとき
            blockAnime.erase(mapXY); // 辞書からアニメをerase関数で取り除く
        }
        // [デバッグ]用にcsvのidの数字を描く
        DrawString(chip_x - camera_x, chip_y - camera_y, std::to_string(id).c_str(), GetColor(255, 255, 255));
      }
    }
    
    // [デバッグ] 当たり判定の四隅を赤枠で描画
    DrawBox(getLeft(x) - camera_x, getTop(y) - camera_y, getRight(x) - camera_x, getBottom(y) - camera_y, GetColor(255, 0, 0), FALSE);
    // [デバッグ] キャラ原点(x,y)を緑丸で描画
    DrawCircle(x - camera_x, y - camera_y, 2, GetColor(0, 255, 0), TRUE);
    // [デバッグ] 当たった際の行き過ぎの めりこみ座標(debug_x,debug_y) を青線で描画
    if (debug_x != -1)
      DrawLine(debug_x - camera_x, debug_x_yTop - camera_y, debug_x - camera_x, debug_x_yBottom - camera_y, GetColor(0, 0, 255));
    if (debug_y != -1)
      DrawLine(debug_y_xLeft - camera_x, debug_y - camera_y, debug_y_xRight - camera_x, debug_y - camera_y, GetColor(0, 0, 255));
    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}



いかがでしょう、ON OFF スイッチを押したら赤⇔赤点線、青⇔青点線ブロックに変化するようになりましたか?
スイッチのギミックがあるだけで、ゲームのパズル要素が一気に強くなるはずです。
これである程度は面白くできなかったら「自分のゲーム制作のセンスが足りていないかも」ということになりますね(汗)。

[課題] 自作スイッチで自分のステージにパズル性を持たせて面白くしてください。





Imageのロードをclass分けして画像のロード処理をファイル分けするプログラム


DivImage.hを新規作成して【DivImage型】分割された画像を読込みその分割のグリッドもパラメータとして保持できるようにしましょう。

※シューティングのDivImageと違ってnew int[AllNum]の動的配列をint* HandleArrayで受ける形ではなくstd::vector<int> HandleArrayを使った。

#ifndef DIVIMAGE_H_
#define DIVIMAGE_H_

#include <assert.h> // 画像の2のべき乗チェックに使う
#include <vector> // 可変長配列std::vectorを使う

// 分割画像を読込むためのデータ構造(画像数や画像ハンドル配列、3Dに使う場合の2のべき乗チェック機能をもつ)
class DivImage
{  // 2Dの分割画像や3Dに分割Div画像を使うと画像全体が使われてしまうのを回避するための情報
public:
  int XNum = 0; // X方向分割画像数
  int YNum = 0; // Y方向分割画像数
  int XSize = 0; // 画像 横 幅XSize(ピクセルドット数)
  int YSize = 0; // 画像 縦 高さYSize(ピクセルドット数)
  int AllNum = 0; // トータルの分割画像数
  bool is3DTexture = false; // 3Dテクスチャ分割画像かtrueだと2のべき乗チェックが入る
  std::vector<int> HandleArray; // DXのLoadDivGraphでロードした画像ハンドル番号の配列

// 初期化 : X方向分割画像数 XNum, Y方向分割画像数 YNum, 画像 横 幅XSizeピクセル, 画像 縦 高さYSizeピクセル
  DivImage(int XNum, int YNum, int XSize, int YSize, bool is3DTexture=false)
    : XNum{ XNum }, YNum{ YNum }, XSize{ XSize }, YSize{ YSize }, is3DTexture{ is3DTexture }
  {  // 初期化コンストラクタ
    AllNum = XNum * YNum; // トータルの分割画像数
    std::vector<int>(AllNum, -1).swap(this->HandleArray); // 配列をAllNumぶん確保し-1で初期化
#ifdef _DEBUG // デバッグ機能(デバッグの時だけ _DEBUG が定義)
    if(is3DTexture==true) ImagePowCheck((*this)); // *(this)でthisポインタの示す変数内容を表す
#endif
  };

  ~DivImage() {}; // デストラクタでメモリを解放

  //【勉強 []添え字演算子を独自に定義】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型】でキャラの分割された画像と普通の単一画像を読込んでみましょう。

#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 playerImage; //プレイヤ画像のハンドラ(読込画像番号)
  static DivImage playerChips; // [分割画像]プレイヤパラパラ画像
  static DivImage mapChips; // [分割画像]マップチップ画像

private:

};
#endif


次に対となるcppファイルも作成します。
Image.cppを新規作成して内容を次のようにします:

#include "Image.h"

int Image::playerImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
// ★分割画像は初期化のときに{X方向画像数, Y方向画像数, 画像横幅XSize,画像縦幅YSize}を指定
DivImage Image::playerChips{ 3, 4, 32, 32 }; // 画像分割サイズ32×32の横3つ×縦4つの分割画像
DivImage Image::mapChips{ 8, 8, 32, 32 }; // マップのグリッドの分割数に応じて8×8 = 64など確保

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

  Image::LoadDivGraph("Image/pikkoron.png", playerChips); // キャラのチップのサイズは32×32
  for (int i = 0; i < playerChips.AllNum; ++i)
    if (playerChips[i] == -1) assert("分割画像読込失敗" == ""); // 画像読込失敗

  Image::LoadDivGraph("Map/mapchip.png", mapChips); // マップのチップの分割画像のロード
  for (int i = 0; i < mapChips.AllNum; ++i)
    if (mapChips[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.data()); //.data()でvector内部配列の先頭ポインタを渡す
};

main.cppを編集して画像のロードの処理をImageクラスを使ってするようにします:

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

#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列std::stringに必要
#include <fstream> // ファイル読み出しstd::ifstreamに必要
#include <sstream> // 文字列ストリームに必要 ss >> num;で文字列から数字に変換できる
#include <unordered_map> // std::unordered_map 高速辞書配列に必要(チップ番号から壁などを表すビットへ変換する辞書)
#include <utility> // std::pair(データのペア型)を使うのに必要
// [ペアを高速辞書のキーにするためハッシュ関数が必要] https://qiita.com/hamamu/items/4d081751b69aa3bb3557
template<class TypeT> size_t HashCombine(const size_t seed, const TypeT& value)
{
  return seed ^ (std::hash<TypeT>()(value) + 0x9e3779b9 + (seed << 6) + (seed >> 2));
}

// pair型に()をつけたときに呼び出される処理をoperator()で定義してreturnでハッシュ関数を返す
template<class TypeT, class TypeS>
struct std::hash<std::pair<TypeT, TypeS>>
{
  size_t operator() (const std::pair<TypeT, TypeS>& keyval) const noexcept
  {   // returnでハッシュ関数を返すことで高速辞書のキーとして認め許されるようになる
    return HashCombine(std::hash<TypeT>()(keyval.first), keyval.second);
  }
};

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  Image::Load(); // 画像ファイルを事前読み込み
  int playerChips[12]; // キャラのチップのグリッドの分割数に応じて3×4 = 12のぶんだけ配列を事前に確保
  LoadDivGraph("Image/pikkoron.png", 12, 3, 4, 32, 32, playerChips); // キャラのチップのサイズは32×32
  assert(playerChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  
  // 効果音ファイルを事前にメモリに読み込む https://dxlib.xsrv.jp/function/dxfunc_sound.html#R8N4
  int jumpSE[4]; // 4種類のジャンプ音を読み込む
  jumpSE[0] = LoadSoundMem("SE/Jump0.wav");
  jumpSE[1] = LoadSoundMem("SE/Jump1.wav");
  jumpSE[2] = LoadSoundMem("SE/Jump2.wav");
  jumpSE[3] = LoadSoundMem("SE/Jump3.wav");
  assert(jumpSE[0] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[1] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[2] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[3] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  
  int CellSize = 32; // マップの1マスのピクセル数
  int mapChips[64]; // マップのチップのグリッドの分割数に応じて8×8 = 64など配列で確保
  LoadDivGraph("Map/mapchip.png", 64, 8, 8, CellSize, CellSize, mapChips); // マップのチップのサイズは32×32(=CellSize)
  assert(mapChips[0] != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる

  
  std::vector<std::vector<int>> mapData;// タイルマップデータ
  std::ifstream ifs_csv_file("Map/stage.csv"); // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
  
  int Width = 0, Height = 0; // マップデータの横(Width) 縦(Height)のマスの数
  int MaxWidth = 0; // 1行の数字の最大個数
  std::string line; // 1行単位ごとにcsvファイルから文字列をstd::getlineで読み込んで受け取る
  while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
  {
    std::vector<int> valuelist; // 1行ぶんの数字リスト配列
    std::istringstream linestream( line ); // 各行の文字列ストリーム
    std::string comma_part; // カンマで分割された文字列
    int widthCount = 0; // カンマで区切られた行の数字の数(各行の幅)をカウント
    while (std::getline(linestream, comma_part, { ',' })) // 1行をgetlineでカンマで区切って列を得る
    {
      std::istringstream ss( comma_part ); // カンマで区切られた文字をssを通して>>で数字に
      int num; // 数字単体
      ss >> num; // 文字列ストリーム>>で数字へ変換
      valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
      ++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
    }
    // 1行の数字の数がMAX記録を更新するかチェック
    if (widthCount > MaxWidth)
      MaxWidth = widthCount; // 暫定Maxの列の幅を更新
    
    // 1行ぶんの数字の配列をとりまとめてmapDataに1行ずつ追加
    if (valuelist.size() != 0) // 配列に数字が1つでもあるなら.size() != 0
      mapData.emplace_back(valuelist); // mapDataに1行ずつ数字配列valuelistを追加
    
    ++Height; //マップの高さをカウントアップ
  }
  Width = MaxWidth; // マップの幅Widthは一番数字の個数の多かった行に合わせる
  // ↓読込んだCSVの幅と高さをチェック
  assert(Width > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  assert(Height > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  
  
  float xStart = 200, yStart = Screen::Height - 100; // キャラの初期スタート(ミス後の再開位置)
  float x = xStart, y = yStart; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  enum class Direction { Left, Right }; // enumでキャラの向いてる方向をDirection型として2種類(LeftとRight)定義
  Direction direction = Direction::Right; // キャラの向いてる方向
  int animeTime = 0; // アニメ時間
  
  bool isDead = false; // キャラがやられたかどうか
  int deadAnimeTime = 0; // やられたときのアニメ時間
  
  bool isSwitchOn = false; // ステージ全体のスイッチのオン、オフ状態(オンのときはオンを押せないようにする)
  int id_On = 10, id_Off = 11; // オンのときにオンを押したりオフのときにオフを押せないように判定する
  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16, 4 }, vyDownSpeedMax[] = { 16, 16, 16, 8 }; // ダッシュ段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f,0.1f }, vyGravity[] = { 0.8f,0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  bool isFlying = false; // 飛行モード
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(Image::playerChips[0], &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  int hitOffsetLeft = 2; // 当たり判定の左端のオフセット
  int hitOffsetRight = 2; // 当たり判定の右端のオフセット
  int hitOffsetTop = 1; // 当たり判定の上端のオフセット
  int hitOffsetBottom = 0; // 当たり判定の下端のオフセット
  
  // 当たり判定のはしっこを計算するための一時関数
  auto getLeft = [&hitOffsetLeft](float worldX) { return worldX + hitOffsetLeft; };
  auto getTop = [&hitOffsetTop](float worldY) { return worldY + hitOffsetTop; };
  auto getRight = [&hitOffsetRight, &imgSizeX](float worldX) { return worldX + imgSizeX - hitOffsetRight; };
  auto getBottom = [&hitOffsetBottom, &imgSizeY](float worldY) { return worldY + imgSizeY - hitOffsetBottom; };
  
  enum class BlockBit // ブロックの特性を32桁のビットで表現
  {
    None = 0,
    Wall   = 0b0000'0000'0000'0000'0000'0000'0000'0001,
    Damage = 0b0000'0000'0000'0000'0000'0000'0000'0010,
    Bounce = 0b0000'0000'0000'0000'0000'0000'0000'0100, // 叩くと弾むブロック
    Change = 0b0000'0000'0000'0000'0000'0000'0000'1000, // 叩くとブロックの種類が変わる
    Switch = 0b0000'0000'0000'0000'0000'0000'0001'0000, // スイッチを叩くと赤ブロックや青ブロックの無効⇔有効が切替
  };
  
  std::unordered_map<int, unsigned int> blockBits = {
    {-1,(int)BlockBit::None},
    { 0,(int)BlockBit::Wall},
    { 1,(int)BlockBit::Damage},
    { 2,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Change}, // 壁 | はずむ | ブロック変化 (|ビット論理和)
    { 3,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Change},
    {10,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Switch}, // 赤Onスイッチ
    {11,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Switch}, // 青Offスイッチ
    {12,(int)BlockBit::Wall}, // On に連動するブロック
    {14,(int)BlockBit::Wall}, // Offに連動するブロック
  };
  
  // ブロックをバウンドさせるための 高速辞書<ペア<cellX,cellY>, intアニメ時間>
  std::unordered_map<std::pair<int, int>, int> blockAnime;
  
  std::unordered_map<int, int> blockChange = {
    { 2, 3 }, // 叩くと 2 → 3 にかわる
    { 3, -1 }, // 叩くと 3 → -1 にかわる
    { 12, 13 }, // 11(OFF) を押すと 12→13
    { 13, 12 }, // 10(ON) を押すと 13→12
    { 14, 15 }, // 10(ON) を押すと 14→15
    { 15, 14 }, // 11(OFF) を押すと 15→14
  };
  
  std::unordered_map<int, std::vector<int>> blockSwitch = {
    { 10, {12,13,14,15} }, // 叩くと12,13,14,15がblockChangeの設定内容に応じて変化する
    { 11, {12,13,14,15} },
  };
  
  // 指定された座標(worldX,worldY)の地形が 壁 や ダメージブロックかなどをチェックする一時関数
  auto checkID = [&blockBits, &CellSize](BlockBit checkBit, std::vector<std::vector<int>>& terrain, float worldX, float worldY,
                                int* pMapX = nullptr, int* pMapY = nullptr)
    {
      int terrainID = -1; // 指定された座標の地形のID
      
      if (worldX < 0 || worldY < 0) return false; // 負の座標が指定された場合
      
      // マップ座標系(二次元配列の行と列)に変換する
      int mapX = (int)(worldX / CellSize), mapY = (int)(worldY / CellSize);
      // 二次元配列の範囲内か判定
      if (mapY < terrain.size() && mapX < terrain[mapY].size())
        terrainID = terrain[mapY][mapX]; // 二次元配列から地形IDを取り出す
      
      bool isCheckBit = (((int)checkBit) & blockBits[terrainID]) != 0; // ビット論理積 & で地形のchekckBitを確かめる
      if (isCheckBit && pMapX != nullptr && pMapY != nullptr)
        *pMapX = mapX, *pMapY = mapY; // ブロックのcsvのセル番号をポインタに記録(nullptrの場合は記録しない)
      
      return isCheckBit;
    };
  
  enum class HitPart // 当たった箇所を表すビット
  {
    None    = 0,
    Left    = 0b0000'0000'0000'0000'0000'0000'0000'0001,
    Right   = 0b0000'0000'0000'0000'0000'0000'0000'0010,
    Top     = 0b0000'0000'0000'0000'0000'0000'0000'0100,
    Bottom  = 0b0000'0000'0000'0000'0000'0000'0000'1000,
    XMiddle = 0b0000'0000'0000'0000'0000'0000'0001'0000,
    YMiddle = 0b0000'0000'0000'0000'0000'0000'0010'0000,
  };
  
  // [x方向左右]地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheckX = [&checkID](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      // 左端がめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yTop, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Top; // 左上がめりこんでいる
      if (checkID(checkBit, terrain, xLeft, yMiddle, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::YMiddle; // 左中がめりこんでいる
      if (checkID(checkBit, terrain, xLeft, yBottom, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Bottom; // 左下がめりこんでいる
      // 右端がめりこんでいるか?
      if (checkID(checkBit, terrain, xRight, yTop, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Top; // 右上がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yMiddle, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::YMiddle; // 右中がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yBottom, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Bottom; // 右下がめりこんでいる
      
      return (int)HitPart::None;
    };
  
  // [y方向上下]地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheckY = [&checkID](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      // 頭の部分がめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yTop, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Top; // 左上がめりこんでいる
      if (checkID(checkBit, terrain, xMiddle, yTop, pMapX, pMapY))
        return (int)HitPart::XMiddle | (int)HitPart::Top; // 中上がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yTop, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Top; // 右上がめりこんでいる
      // 足元が壁にめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yBottom, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Bottom; // 左下がめりこんでいる
      if (checkID(checkBit, terrain, xMiddle, yBottom, pMapX, pMapY))
        return (int)HitPart::XMiddle | (int)HitPart::Bottom; // 中下がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yBottom, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Bottom; // 右下がめりこんでいる
      
      return (int)HitPart::None;
    };
  
  // 地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheck = [&hitcheckX, &hitcheckY](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      return hitcheckX(checkBit,terrain,xLeft,xMiddle,xRight,yTop,yMiddle,yBottom,pMapX,pMapY)
        | hitcheckY(checkBit,terrain,xLeft,xMiddle,xRight,yTop,yMiddle,yBottom,pMapX,pMapY);
    };
  
  float camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
  if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ
  float camera_y = y - Screen::Height / 2; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
  if (camera_y <= 0) camera_y = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Height * CellSize - Screen::Height <= camera_y) camera_y = Height * CellSize - Screen::Height; // ステージのはしっこ
  
  
  float debug_x = -1, debug_y = -1; // [デバッグ] めり込みすぎのラインを記録しておく変数
  float debug_x_yTop = -1, debug_x_yBottom = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の上下端も記録
  float debug_y_xLeft = -1, debug_y_xRight = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の左右端も記録
  
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      direction = Direction::Left;
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      direction = Direction::Right;
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      vy = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
      // 加速段階に応じて違うジャンプ音を鳴らす
      PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs > 9) {
          isFlying = true; // 飛行モードにする
        }
        
        // 加速段階に応じて違うジャンプ音を鳴らす
        PlaySoundMem(jumpSE[vx_idx], DX_PLAYTYPE_BACK);
        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    if (!isDead) // やられたときは移動を停止
      x += vx; // 速度のぶん現在の位置から移動
    if (!isDead) // やられたときは当たり判定も停止
    {
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      if(hitcheck(BlockBit::Damage, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに
      
      // 左端が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yMiddle) || // 左中が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) ) // 左下が壁か?
      {
        debug_x = xLeft; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        // std::fmodで小数点の余りを計算 めり込みすぎの余り = std::fmod(xLeft, CellSize)
        float wallRight = xLeft + CellSize - (float)std::fmod(xLeft, CellSize); // 壁の右端
        x = wallRight - hitOffsetLeft; // 左端を壁の右端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      } // ↓右端が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xRight, yTop) || // 右上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yMiddle) || // 右中が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_x = xRight; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        float wallLeft = xRight - (float)std::fmod(xRight, CellSize); // 壁の左端
        x = wallLeft + hitOffsetRight - imgSizeX; // 右端を壁の左端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      }
    }
    
    if (!isDead) // やられたときは移動を停止
      y += vy;
    if (!isDead) // やられたときは当たり判定も停止
    {
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      if (hitcheck(BlockBit::Damage, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに
      
      int mapX = 0, mapY = 0; // ぶつかったらそのブロックのCSVのセルの行mapYと列mapXを得る
      if ( (int)HitPart::Top
          & hitcheck(BlockBit::Bounce, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        std::pair<int,int> mapXY = std::make_pair(mapX, mapY); // ぶつかったブロックのセルの行mapYと列mapXのペア
        if (blockAnime.count(mapXY) == 0) // 辞書にキーがあるかcountで数えて未登録==0のとき
          blockAnime[mapXY] = 0; // ブロックのアニメ時間を辞書に登録(時間が辞書にあるものだけアニメが走る)
      }
      
      if ((int)HitPart::Top
          & hitcheck(BlockBit::Switch, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        int id = mapData[mapY][mapX]; // 行mapY 列mapXの地形idを得る
        if ((isSwitchOn && id == id_Off || !isSwitchOn && id == id_On) && blockSwitch.count(id) > 0)
        {
          isSwitchOn = !isSwitchOn; // trueとfalseを!で反転
          for (int cellY = 0; cellY < Height; ++cellY) // 行をすべて調べる
            for (int cellX = 0; cellX < Width; ++cellX) // 列をすべて調べる
              for (auto switchID : blockSwitch[id]) // スイッチに反応するブロックのIDすべてを調べる
                if (mapData[cellY][cellX] == switchID)
                {
                  mapData[cellY][cellX] = blockChange[switchID]; // 変化先のIDへ書き換える
                  break; // 次の列のループへ移る(3重for文の一番内側のループをbreakして1つ外のループへ)
                }
        }
      }
      
      bool isBlockChanging = false; // ブロック変化フラグ(idはあとから変化させないとWallの押し戻しの跳ね返りvy = 0がなくなる)
      if ( (int)HitPart::Top
          & hitcheck(BlockBit::Change, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        isBlockChanging = true; // ここではフラグをtrueにするだけ(めり込み押し戻しvy=0するまえにid=-1にすると壊した瞬間にすり抜けちゃう)
      }
      
      // 頭の部分が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yTop) || // 中上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yTop) ) // 右上が壁か?
      {
        debug_y = yTop; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallBottom = yTop + CellSize - (float)std::fmod(yTop, CellSize); // 壁の下側の境界
        y = wallBottom - hitOffsetTop; // 頭を壁の下端に沿わす(めり込んだぶんを押し戻す)
        vy = 0; // 壁に当たったら速度をストップ
      } // ↓足元が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) || // 左下が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yBottom) || // 中下が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_y = yBottom; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallTop = yBottom - (float)std::fmod(yBottom, CellSize); // 壁の上側の境界
        y = wallTop + hitOffsetBottom - imgSizeX; // 足元を壁の上端に沿わす(めり込んだぶんを押し戻す)
        
        isGround = true;
        isFlying = false;
        vx_idx = 0;
        
        vy = 0; // 床に当たったら速度をストップ
      }
      else
        isGround = false; // ブロックに当たってないからfalseにして落下するように
      
      if (isBlockChanging) // ブロック変化フラグ==true(すでに↑めりこみ判定でvy=0で勢いを止め反射が効くからid=-1にしてもすりぬけずにすむ)
      {
        int id = mapData[mapY][mapX]; // 行mapY 列mapXの地形idを得る
        if (blockChange.count(id) > 0) // blockChangeの辞書に登録のあるidのときは
        {
          int changeID = blockChange[id]; // 変化先のID
          std::pair<int, int> mapXY = std::make_pair(mapX, mapY); // ぶつかったブロックのセルの行mapYと列mapXのペア
          if (!(blockBits[changeID] & (int)BlockBit::Bounce)) // 変化先ブロックがバウンスするならアニメ時間をeraseせずに辞書に残す
            if (blockAnime.count(mapXY) > 0)
              blockAnime.erase(mapXY); // ブロックの種類変える前にアニメをリセットしておかないと辞書に残り続ける
          
          mapData[mapY][mapX] = changeID; // idを入れ替える
        }
      }
    }
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    
    camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
    if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ
    camera_y = y - Screen::Height / 2; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
    if (camera_y <= 0) camera_y = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Height * CellSize - Screen::Height <= camera_y) camera_y = Height * CellSize - Screen::Height; // ステージのはしっこ
    
    
    // ダッシュ段階に応じてキャラクタのアニメ時間を進める
    float vx_dash = (vx < 0) ? -vx : vx; // 絶対値でプラスだけにする
    if (vx_dash > 0.5f) animeTime += 1; // 時間 +1 ( 合計 1 )
    if (vx_dash > 6) animeTime += 1; // 時間 +1 (計 1+1=2 ) 1フレームごとに2アニメ時間が進む
    if (vx_dash > 9) animeTime += 2; // 最終段階ではアニメ時間をさらに +2 (計 1+1+2=4 )
    
    int animeStep = animeTime / 10; // 10を5に変えたらアニメ速度が上がる(10ごとに1コマアニメが進む)
    if (animeStep >= 3) // アニメのステップ数が0,1,2の3段階までなら3を超えたら0へループ
    {
      animeTime = 0; // 0へ戻り再びアニメ時間を再ループ
      animeStep = 0;
    }
    
    
    // 描画処理
    if (isDead) // やられたときの描画処理
    {
      float yMiss = y - camera_y + imgSizeY / 2; // ミスったy起点
      float yAnim0 = yMiss; // アニメy位置0 始まりのy位置
      float yAnim1 = yMiss - 10; // アニメy位置1 アニメの途中少し上方向に 10 浮く
      float yAnim2 = Screen::Height + 32; // アニメy位置2 最終y位置 画面の高さ+32で画面外へ
      
      float yDeadAnim; // やられた際にはyの位置をアニメさせる
      // 直線的な比例アニメの式(t=時刻) 位置1×(終時刻 - t) / アニメの長さ + 位置2×(t - 始時刻) / アニメの長さ
      if (deadAnimeTime < 40) // やられアニメが始まって40フレーム目までは yAnim0 ~ yAnim1
        yDeadAnim = yAnim0 * (40 - deadAnimeTime) / 40 + yAnim1 * (deadAnimeTime - 0) / 40;
      else if (deadAnimeTime < 60) // やられアニメが始まって60フレーム目までは yAnim1 ~ yAnim2
        yDeadAnim = yAnim1 * (60 - deadAnimeTime) / 20 + yAnim2 * (deadAnimeTime - 40) / 20;
      else
      {
        yDeadAnim = yAnim2; // アニメ終了 到着 y位置をセット
        deadAnimeTime = 0; // 次にやられたときのアニメのため0リセットしておく
        
        // 再開位置からゲーム再開(各数値もリセット)
        isDead = false;
        x = xStart, y = yStart, vx = 0, vy = 0;
        isGround = false, isFlying = false;
        vx_idx = 0;
      }
      
      int deadAnimeStep = deadAnimeTime / 3; // 3で割って[チップのパラパラの]アニメの速度は弱める
      int chipAnime = deadAnimeStep % 3; // % 3で余りを求めれば余りはチップのパラパラ番号は0~2の間に収まる
      DrawRotaGraphF(x - camera_x + imgSizeX / 2, yDeadAnim, 1.0f, 10.0f * deadAnimeStep / 360.0f * 2 * DX_PI, Image::playerChips[chipAnime], TRUE);
      
      ++deadAnimeTime; // アニメ時間を進める
    }
    else
    {
      if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f,-60.0f / 360.0f * 2 * DX_PI, Image::playerChips[4], TRUE);
      else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 60.0f / 360.0f * 2 * DX_PI, Image::playerChips[7], TRUE);
      else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 0.0f, Image::playerChips[5 - animeStep], TRUE);
      else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 0.0f, Image::playerChips[6 + animeStep], TRUE);
    }
    
    float rotaShiftX = CellSize / 2, rotaShiftY = CellSize / 2; // DrawRotaGraphF関数は画像の真ん中が起点なので1/2ずらした位置計算
    // 地形マップを[ 行のfor文×列のfor文 ]の2重ループで描く
    for (int cellY = 0, ySize = mapData.size(); cellY < ySize; ++cellY)
    {
      float chip_y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
      for (int cellX = 0, xSize = mapData[cellY].size(); cellX < xSize; ++cellX)
      {
        float chip_x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
        int id = mapData[cellY][cellX]; // [行][列] の位置のcsvの地形番号(チップ番号)を得る
        if (id < 0) continue; // -1のときは描かない
        
        // マップチップを描く
        auto mapXY = std::make_pair(cellX, cellY);
        if (blockAnime.count(mapXY) == 0)
          DrawRotaGraphF(chip_x - camera_x + rotaShiftX, chip_y - camera_y + rotaShiftY, 1.0f, 0.0f, Image::mapChips[id], TRUE);
        else if ((int)BlockBit::Bounce & blockBits[id])
        {
          float yBase = chip_y - camera_y;
          float yAnim0 = yBase - 10, yAnim1 = yBase; // yAnim0 から yAnim1の位置までアニメする
          int blockTime = blockAnime[mapXY];
          float yAnim = yAnim0 * (15 - blockTime) / 15 + yAnim1 * blockTime / 15;
          DrawRotaGraphF(chip_x - camera_x + rotaShiftX, yAnim + rotaShiftY, 1.0f, 0.0f, Image::mapChips[id], TRUE);
          ++blockAnime[mapXY]; // アニメ時間を進める
          if (blockTime > 15) // アニメ終了時間を15フレーム目とするとき
            blockAnime.erase(mapXY); // 辞書からアニメをerase関数で取り除く
        }
        // [デバッグ]用にcsvのidの数字を描く
        DrawString(chip_x - camera_x, chip_y - camera_y, std::to_string(id).c_str(), GetColor(255, 255, 255));
      }
    }
    
    // [デバッグ] 当たり判定の四隅を赤枠で描画
    DrawBox(getLeft(x) - camera_x, getTop(y) - camera_y, getRight(x) - camera_x, getBottom(y) - camera_y, GetColor(255, 0, 0), FALSE);
    // [デバッグ] キャラ原点(x,y)を緑丸で描画
    DrawCircle(x - camera_x, y - camera_y, 2, GetColor(0, 255, 0), TRUE);
    // [デバッグ] 当たった際の行き過ぎの めりこみ座標(debug_x,debug_y) を青線で描画
    if (debug_x != -1)
      DrawLine(debug_x - camera_x, debug_x_yTop - camera_y, debug_x - camera_x, debug_x_yBottom - camera_y, GetColor(0, 0, 255));
    if (debug_y != -1)
      DrawLine(debug_y_xLeft - camera_x, debug_y - camera_y, debug_y_xRight - camera_x, debug_y - camera_y, GetColor(0, 0, 255));
    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}



いかがでしょう、動作は変わりませんが、きちんとImageクラスを通して画像ロード処理をできるようになりましたか?




Soundのロードをclass分けして音声のロード処理をファイル分けするプログラム


まずは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 void Load();
    // メモリから音声を再生
    static void Play(int handle, int PlayType = DX_PLAYTYPE_BACK);
    
    // メモリ再生音源管理番号
    static int jumpSE[4]; // 4種類のジャンプ音を読み込む
    
private:

};
#endif

Sound.cppも新規作成します。

#include "Sound.h"

// メモリ再生音源管理番号
int Sound::jumpSE[4]; // 4種類のジャンプ音を読み込む


void Sound::Load()
{
    jumpSE[0] = LoadSoundMem("SE/Jump0.wav");
    jumpSE[1] = LoadSoundMem("SE/Jump1.wav");
    jumpSE[2] = LoadSoundMem("SE/Jump2.wav");
    jumpSE[3] = LoadSoundMem("SE/Jump3.wav");
    assert(jumpSE[0] != -1); // 読込失敗、ファイル名かフォルダ名が間違ってる
    assert(jumpSE[1] != -1); // 読込失敗、ファイル名かフォルダ名が間違ってる
    assert(jumpSE[2] != -1); // 読込失敗、ファイル名かフォルダ名が間違ってる
    assert(jumpSE[3] != -1); // 読込失敗、ファイル名かフォルダ名が間違ってる
   
}

// メモリ上の音声を再生する。
void Sound::Play(int handle, int PlayType)
{   
    // メモリ上から音声再生
    PlaySoundMem(handle, PlayType);
}

main.cppを編集して音声のロードの処理をSoundクラスを使ってするようにします:

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

#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列std::stringに必要
#include <fstream> // ファイル読み出しstd::ifstreamに必要
#include <sstream> // 文字列ストリームに必要 ss >> num;で文字列から数字に変換できる
#include <unordered_map> // std::unordered_map 高速辞書配列に必要(チップ番号から壁などを表すビットへ変換する辞書)
#include <utility> // std::pair(データのペア型)を使うのに必要
// [ペアを高速辞書のキーにするためハッシュ関数が必要] https://qiita.com/hamamu/items/4d081751b69aa3bb3557
template<class TypeT> size_t HashCombine(const size_t seed, const TypeT& value)
{
  return seed ^ (std::hash<TypeT>()(value) + 0x9e3779b9 + (seed << 6) + (seed >> 2));
}

// pair型に()をつけたときに呼び出される処理をoperator()で定義してreturnでハッシュ関数を返す
template<class TypeT, class TypeS>
struct std::hash<std::pair<TypeT, TypeS>>
{
  size_t operator() (const std::pair<TypeT, TypeS>& keyval) const noexcept
  {   // returnでハッシュ関数を返すことで高速辞書のキーとして認め許されるようになる
    return HashCombine(std::hash<TypeT>()(keyval.first), keyval.second);
  }
};

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  Image::Load(); // 画像ファイルを事前読み込み
  
  // 効果音ファイルを事前にメモリに読み込む https://dxlib.xsrv.jp/function/dxfunc_sound.html#R8N4
  Sound::Load(); // 音声ファイルを事前読み込み
  int jumpSE[4]; // 4種類のジャンプ音を読み込む
  jumpSE[0] = LoadSoundMem("SE/Jump0.wav");
  jumpSE[1] = LoadSoundMem("SE/Jump1.wav");
  jumpSE[2] = LoadSoundMem("SE/Jump2.wav");
  jumpSE[3] = LoadSoundMem("SE/Jump3.wav");
  assert(jumpSE[0] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[1] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[2] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる
  assert(jumpSE[3] != -1); // SE読込失敗、ファイル名かフォルダ名が間違ってる

  
  int CellSize = 32; // マップの1マスのピクセル数
  
  std::vector<std::vector<int>> mapData;// タイルマップデータ
  std::ifstream ifs_csv_file("Map/stage.csv"); // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
  
  int Width = 0, Height = 0; // マップデータの横(Width) 縦(Height)のマスの数
  int MaxWidth = 0; // 1行の数字の最大個数
  std::string line; // 1行単位ごとにcsvファイルから文字列をstd::getlineで読み込んで受け取る
  while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
  {
    std::vector<int> valuelist; // 1行ぶんの数字リスト配列
    std::istringstream linestream( line ); // 各行の文字列ストリーム
    std::string comma_part; // カンマで分割された文字列
    int widthCount = 0; // カンマで区切られた行の数字の数(各行の幅)をカウント
    while (std::getline(linestream, comma_part, { ',' })) // 1行をgetlineでカンマで区切って列を得る
    {
      std::istringstream ss( comma_part ); // カンマで区切られた文字をssを通して>>で数字に
      int num; // 数字単体
      ss >> num; // 文字列ストリーム>>で数字へ変換
      valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
      ++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
    }
    // 1行の数字の数がMAX記録を更新するかチェック
    if (widthCount > MaxWidth)
      MaxWidth = widthCount; // 暫定Maxの列の幅を更新
    
    // 1行ぶんの数字の配列をとりまとめてmapDataに1行ずつ追加
    if (valuelist.size() != 0) // 配列に数字が1つでもあるなら.size() != 0
      mapData.emplace_back(valuelist); // mapDataに1行ずつ数字配列valuelistを追加
    
    ++Height; //マップの高さをカウントアップ
  }
  Width = MaxWidth; // マップの幅Widthは一番数字の個数の多かった行に合わせる
  // ↓読込んだCSVの幅と高さをチェック
  assert(Width > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  assert(Height > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  
  
  float xStart = 200, yStart = Screen::Height - 100; // キャラの初期スタート(ミス後の再開位置)
  float x = xStart, y = yStart; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  enum class Direction { Left, Right }; // enumでキャラの向いてる方向をDirection型として2種類(LeftとRight)定義
  Direction direction = Direction::Right; // キャラの向いてる方向
  int animeTime = 0; // アニメ時間
  
  bool isDead = false; // キャラがやられたかどうか
  int deadAnimeTime = 0; // やられたときのアニメ時間
  
  bool isSwitchOn = false; // ステージ全体のスイッチのオン、オフ状態(オンのときはオンを押せないようにする)
  int id_On = 10, id_Off = 11; // オンのときにオンを押したりオフのときにオフを押せないように判定する
  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16, 4 }, vyDownSpeedMax[] = { 16, 16, 16, 8 }; // ダッシュ段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f,0.1f }, vyGravity[] = { 0.8f,0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  bool isFlying = false; // 飛行モード
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(Image::playerChips[0], &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  int hitOffsetLeft = 2; // 当たり判定の左端のオフセット
  int hitOffsetRight = 2; // 当たり判定の右端のオフセット
  int hitOffsetTop = 1; // 当たり判定の上端のオフセット
  int hitOffsetBottom = 0; // 当たり判定の下端のオフセット
  
  // 当たり判定のはしっこを計算するための一時関数
  auto getLeft = [&hitOffsetLeft](float worldX) { return worldX + hitOffsetLeft; };
  auto getTop = [&hitOffsetTop](float worldY) { return worldY + hitOffsetTop; };
  auto getRight = [&hitOffsetRight, &imgSizeX](float worldX) { return worldX + imgSizeX - hitOffsetRight; };
  auto getBottom = [&hitOffsetBottom, &imgSizeY](float worldY) { return worldY + imgSizeY - hitOffsetBottom; };
  
  enum class BlockBit // ブロックの特性を32桁のビットで表現
  {
    None = 0,
    Wall   = 0b0000'0000'0000'0000'0000'0000'0000'0001,
    Damage = 0b0000'0000'0000'0000'0000'0000'0000'0010,
    Bounce = 0b0000'0000'0000'0000'0000'0000'0000'0100, // 叩くと弾むブロック
    Change = 0b0000'0000'0000'0000'0000'0000'0000'1000, // 叩くとブロックの種類が変わる
    Switch = 0b0000'0000'0000'0000'0000'0000'0001'0000, // スイッチを叩くと赤ブロックや青ブロックの無効⇔有効が切替
  };
  
  std::unordered_map<int, unsigned int> blockBits = {
    {-1,(int)BlockBit::None},
    { 0,(int)BlockBit::Wall},
    { 1,(int)BlockBit::Damage},
    { 2,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Change}, // 壁 | はずむ | ブロック変化 (|ビット論理和)
    { 3,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Change},
    {10,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Switch}, // 赤Onスイッチ
    {11,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Switch}, // 青Offスイッチ
    {12,(int)BlockBit::Wall}, // On に連動するブロック
    {14,(int)BlockBit::Wall}, // Offに連動するブロック
  };
  
  // ブロックをバウンドさせるための 高速辞書<ペア<cellX,cellY>, intアニメ時間>
  std::unordered_map<std::pair<int, int>, int> blockAnime;
  
  std::unordered_map<int, int> blockChange = {
    { 2, 3 }, // 叩くと 2 → 3 にかわる
    { 3, -1 }, // 叩くと 3 → -1 にかわる
    { 12, 13 }, // 11(OFF) を押すと 12→13
    { 13, 12 }, // 10(ON) を押すと 13→12
    { 14, 15 }, // 10(ON) を押すと 14→15
    { 15, 14 }, // 11(OFF) を押すと 15→14
  };
  
  std::unordered_map<int, std::vector<int>> blockSwitch = {
    { 10, {12,13,14,15} }, // 叩くと12,13,14,15がblockChangeの設定内容に応じて変化する
    { 11, {12,13,14,15} },
  };
  
  // 指定された座標(worldX,worldY)の地形が 壁 や ダメージブロックかなどをチェックする一時関数
  auto checkID = [&blockBits, &CellSize](BlockBit checkBit, std::vector<std::vector<int>>& terrain, float worldX, float worldY,
                                int* pMapX = nullptr, int* pMapY = nullptr)
    {
      int terrainID = -1; // 指定された座標の地形のID
      
      if (worldX < 0 || worldY < 0) return false; // 負の座標が指定された場合
      
      // マップ座標系(二次元配列の行と列)に変換する
      int mapX = (int)(worldX / CellSize), mapY = (int)(worldY / CellSize);
      // 二次元配列の範囲内か判定
      if (mapY < terrain.size() && mapX < terrain[mapY].size())
        terrainID = terrain[mapY][mapX]; // 二次元配列から地形IDを取り出す
      
      bool isCheckBit = (((int)checkBit) & blockBits[terrainID]) != 0; // ビット論理積 & で地形のchekckBitを確かめる
      if (isCheckBit && pMapX != nullptr && pMapY != nullptr)
        *pMapX = mapX, *pMapY = mapY; // ブロックのcsvのセル番号をポインタに記録(nullptrの場合は記録しない)
      
      return isCheckBit;
    };
  
  enum class HitPart // 当たった箇所を表すビット
  {
    None    = 0,
    Left    = 0b0000'0000'0000'0000'0000'0000'0000'0001,
    Right   = 0b0000'0000'0000'0000'0000'0000'0000'0010,
    Top     = 0b0000'0000'0000'0000'0000'0000'0000'0100,
    Bottom  = 0b0000'0000'0000'0000'0000'0000'0000'1000,
    XMiddle = 0b0000'0000'0000'0000'0000'0000'0001'0000,
    YMiddle = 0b0000'0000'0000'0000'0000'0000'0010'0000,
  };
  
  // [x方向左右]地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheckX = [&checkID](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      // 左端がめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yTop, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Top; // 左上がめりこんでいる
      if (checkID(checkBit, terrain, xLeft, yMiddle, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::YMiddle; // 左中がめりこんでいる
      if (checkID(checkBit, terrain, xLeft, yBottom, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Bottom; // 左下がめりこんでいる
      // 右端がめりこんでいるか?
      if (checkID(checkBit, terrain, xRight, yTop, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Top; // 右上がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yMiddle, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::YMiddle; // 右中がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yBottom, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Bottom; // 右下がめりこんでいる
      
      return (int)HitPart::None;
    };
  
  // [y方向上下]地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheckY = [&checkID](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      // 頭の部分がめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yTop, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Top; // 左上がめりこんでいる
      if (checkID(checkBit, terrain, xMiddle, yTop, pMapX, pMapY))
        return (int)HitPart::XMiddle | (int)HitPart::Top; // 中上がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yTop, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Top; // 右上がめりこんでいる
      // 足元が壁にめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yBottom, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Bottom; // 左下がめりこんでいる
      if (checkID(checkBit, terrain, xMiddle, yBottom, pMapX, pMapY))
        return (int)HitPart::XMiddle | (int)HitPart::Bottom; // 中下がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yBottom, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Bottom; // 右下がめりこんでいる
      
      return (int)HitPart::None;
    };
  
  // 地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheck = [&hitcheckX, &hitcheckY](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      return hitcheckX(checkBit,terrain,xLeft,xMiddle,xRight,yTop,yMiddle,yBottom,pMapX,pMapY)
        | hitcheckY(checkBit,terrain,xLeft,xMiddle,xRight,yTop,yMiddle,yBottom,pMapX,pMapY);
    };
  
  float camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
  if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ
  float camera_y = y - Screen::Height / 2; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
  if (camera_y <= 0) camera_y = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Height * CellSize - Screen::Height <= camera_y) camera_y = Height * CellSize - Screen::Height; // ステージのはしっこ
  
  
  float debug_x = -1, debug_y = -1; // [デバッグ] めり込みすぎのラインを記録しておく変数
  float debug_x_yTop = -1, debug_x_yBottom = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の上下端も記録
  float debug_y_xLeft = -1, debug_y_xRight = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の左右端も記録
  
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      direction = Direction::Left;
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      direction = Direction::Right;
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      vy = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
      // 加速段階に応じて違うジャンプ音を鳴らす
      PlaySoundMem(Sound::jumpSE[vx_idx], DX_PLAYTYPE_BACK);
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs > 9) {
          isFlying = true; // 飛行モードにする
        }
        
        // 加速段階に応じて違うジャンプ音を鳴らす
        PlaySoundMem(Sound::jumpSE[vx_idx], DX_PLAYTYPE_BACK);
        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    if (!isDead) // やられたときは移動を停止
      x += vx; // 速度のぶん現在の位置から移動
    if (!isDead) // やられたときは当たり判定も停止
    {
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      if(hitcheck(BlockBit::Damage, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに
      
      // 左端が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yMiddle) || // 左中が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) ) // 左下が壁か?
      {
        debug_x = xLeft; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        // std::fmodで小数点の余りを計算 めり込みすぎの余り = std::fmod(xLeft, CellSize)
        float wallRight = xLeft + CellSize - (float)std::fmod(xLeft, CellSize); // 壁の右端
        x = wallRight - hitOffsetLeft; // 左端を壁の右端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      } // ↓右端が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xRight, yTop) || // 右上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yMiddle) || // 右中が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_x = xRight; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        float wallLeft = xRight - (float)std::fmod(xRight, CellSize); // 壁の左端
        x = wallLeft + hitOffsetRight - imgSizeX; // 右端を壁の左端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      }
    }
    
    if (!isDead) // やられたときは移動を停止
      y += vy;
    if (!isDead) // やられたときは当たり判定も停止
    {
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      if (hitcheck(BlockBit::Damage, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに
      
      int mapX = 0, mapY = 0; // ぶつかったらそのブロックのCSVのセルの行mapYと列mapXを得る
      if ( (int)HitPart::Top
          & hitcheck(BlockBit::Bounce, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        std::pair<int,int> mapXY = std::make_pair(mapX, mapY); // ぶつかったブロックのセルの行mapYと列mapXのペア
        if (blockAnime.count(mapXY) == 0) // 辞書にキーがあるかcountで数えて未登録==0のとき
          blockAnime[mapXY] = 0; // ブロックのアニメ時間を辞書に登録(時間が辞書にあるものだけアニメが走る)
      }
      
      if ((int)HitPart::Top
          & hitcheck(BlockBit::Switch, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        int id = mapData[mapY][mapX]; // 行mapY 列mapXの地形idを得る
        if ((isSwitchOn && id == id_Off || !isSwitchOn && id == id_On) && blockSwitch.count(id) > 0)
        {
          isSwitchOn = !isSwitchOn; // trueとfalseを!で反転
          for (int cellY = 0; cellY < Height; ++cellY) // 行をすべて調べる
            for (int cellX = 0; cellX < Width; ++cellX) // 列をすべて調べる
              for (auto switchID : blockSwitch[id]) // スイッチに反応するブロックのIDすべてを調べる
                if (mapData[cellY][cellX] == switchID)
                {
                  mapData[cellY][cellX] = blockChange[switchID]; // 変化先のIDへ書き換える
                  break; // 次の列のループへ移る(3重for文の一番内側のループをbreakして1つ外のループへ)
                }
        }
      }
      
      bool isBlockChanging = false; // ブロック変化フラグ(idはあとから変化させないとWallの押し戻しの跳ね返りvy = 0がなくなる)
      if ( (int)HitPart::Top
          & hitcheck(BlockBit::Change, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        isBlockChanging = true; // ここではフラグをtrueにするだけ(めり込み押し戻しvy=0するまえにid=-1にすると壊した瞬間にすり抜けちゃう)
      }
      
      // 頭の部分が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yTop) || // 中上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yTop) ) // 右上が壁か?
      {
        debug_y = yTop; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallBottom = yTop + CellSize - (float)std::fmod(yTop, CellSize); // 壁の下側の境界
        y = wallBottom - hitOffsetTop; // 頭を壁の下端に沿わす(めり込んだぶんを押し戻す)
        vy = 0; // 壁に当たったら速度をストップ
      } // ↓足元が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) || // 左下が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yBottom) || // 中下が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_y = yBottom; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallTop = yBottom - (float)std::fmod(yBottom, CellSize); // 壁の上側の境界
        y = wallTop + hitOffsetBottom - imgSizeX; // 足元を壁の上端に沿わす(めり込んだぶんを押し戻す)
        
        isGround = true;
        isFlying = false;
        vx_idx = 0;
        
        vy = 0; // 床に当たったら速度をストップ
      }
      else
        isGround = false; // ブロックに当たってないからfalseにして落下するように
      
      if (isBlockChanging) // ブロック変化フラグ==true(すでに↑めりこみ判定でvy=0で勢いを止め反射が効くからid=-1にしてもすりぬけずにすむ)
      {
        int id = mapData[mapY][mapX]; // 行mapY 列mapXの地形idを得る
        if (blockChange.count(id) > 0) // blockChangeの辞書に登録のあるidのときは
        {
          int changeID = blockChange[id]; // 変化先のID
          std::pair<int, int> mapXY = std::make_pair(mapX, mapY); // ぶつかったブロックのセルの行mapYと列mapXのペア
          if (!(blockBits[changeID] & (int)BlockBit::Bounce)) // 変化先ブロックがバウンスするならアニメ時間をeraseせずに辞書に残す
            if (blockAnime.count(mapXY) > 0)
              blockAnime.erase(mapXY); // ブロックの種類変える前にアニメをリセットしておかないと辞書に残り続ける
          
          mapData[mapY][mapX] = changeID; // idを入れ替える
        }
      }
    }
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    
    camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
    if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ
    camera_y = y - Screen::Height / 2; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
    if (camera_y <= 0) camera_y = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Height * CellSize - Screen::Height <= camera_y) camera_y = Height * CellSize - Screen::Height; // ステージのはしっこ
    
    
    // ダッシュ段階に応じてキャラクタのアニメ時間を進める
    float vx_dash = (vx < 0) ? -vx : vx; // 絶対値でプラスだけにする
    if (vx_dash > 0.5f) animeTime += 1; // 時間 +1 ( 合計 1 )
    if (vx_dash > 6) animeTime += 1; // 時間 +1 (計 1+1=2 ) 1フレームごとに2アニメ時間が進む
    if (vx_dash > 9) animeTime += 2; // 最終段階ではアニメ時間をさらに +2 (計 1+1+2=4 )
    
    int animeStep = animeTime / 10; // 10を5に変えたらアニメ速度が上がる(10ごとに1コマアニメが進む)
    if (animeStep >= 3) // アニメのステップ数が0,1,2の3段階までなら3を超えたら0へループ
    {
      animeTime = 0; // 0へ戻り再びアニメ時間を再ループ
      animeStep = 0;
    }
    
    
    // 描画処理
    if (isDead) // やられたときの描画処理
    {
      float yMiss = y - camera_y + imgSizeY / 2; // ミスったy起点
      float yAnim0 = yMiss; // アニメy位置0 始まりのy位置
      float yAnim1 = yMiss - 10; // アニメy位置1 アニメの途中少し上方向に 10 浮く
      float yAnim2 = Screen::Height + 32; // アニメy位置2 最終y位置 画面の高さ+32で画面外へ
      
      float yDeadAnim; // やられた際にはyの位置をアニメさせる
      // 直線的な比例アニメの式(t=時刻) 位置1×(終時刻 - t) / アニメの長さ + 位置2×(t - 始時刻) / アニメの長さ
      if (deadAnimeTime < 40) // やられアニメが始まって40フレーム目までは yAnim0 ~ yAnim1
        yDeadAnim = yAnim0 * (40 - deadAnimeTime) / 40 + yAnim1 * (deadAnimeTime - 0) / 40;
      else if (deadAnimeTime < 60) // やられアニメが始まって60フレーム目までは yAnim1 ~ yAnim2
        yDeadAnim = yAnim1 * (60 - deadAnimeTime) / 20 + yAnim2 * (deadAnimeTime - 40) / 20;
      else
      {
        yDeadAnim = yAnim2; // アニメ終了 到着 y位置をセット
        deadAnimeTime = 0; // 次にやられたときのアニメのため0リセットしておく
        
        // 再開位置からゲーム再開(各数値もリセット)
        isDead = false;
        x = xStart, y = yStart, vx = 0, vy = 0;
        isGround = false, isFlying = false;
        vx_idx = 0;
      }
      
      int deadAnimeStep = deadAnimeTime / 3; // 3で割って[チップのパラパラの]アニメの速度は弱める
      int chipAnime = deadAnimeStep % 3; // % 3で余りを求めれば余りはチップのパラパラ番号は0~2の間に収まる
      DrawRotaGraphF(x - camera_x + imgSizeX / 2, yDeadAnim, 1.0f, 10.0f * deadAnimeStep / 360.0f * 2 * DX_PI, Image::playerChips[chipAnime], TRUE);
      
      ++deadAnimeTime; // アニメ時間を進める
    }
    else
    {
      if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f,-60.0f / 360.0f * 2 * DX_PI, Image::playerChips[4], TRUE);
      else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 60.0f / 360.0f * 2 * DX_PI, Image::playerChips[7], TRUE);
      else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 0.0f, Image::playerChips[5 - animeStep], TRUE);
      else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
        DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 0.0f, Image::playerChips[6 + animeStep], TRUE);
    }
    
    float rotaShiftX = CellSize / 2, rotaShiftY = CellSize / 2; // DrawRotaGraphF関数は画像の真ん中が起点なので1/2ずらした位置計算
    // 地形マップを[ 行のfor文×列のfor文 ]の2重ループで描く
    for (int cellY = 0, ySize = mapData.size(); cellY < ySize; ++cellY)
    {
      float chip_y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
      for (int cellX = 0, xSize = mapData[cellY].size(); cellX < xSize; ++cellX)
      {
        float chip_x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
        int id = mapData[cellY][cellX]; // [行][列] の位置のcsvの地形番号(チップ番号)を得る
        if (id < 0) continue; // -1のときは描かない
        
        // マップチップを描く
        auto mapXY = std::make_pair(cellX, cellY);
        if (blockAnime.count(mapXY) == 0)
          DrawRotaGraphF(chip_x - camera_x + rotaShiftX, chip_y - camera_y + rotaShiftY, 1.0f, 0.0f, Image::mapChips[id], TRUE);
        else if ((int)BlockBit::Bounce & blockBits[id])
        {
          float yBase = chip_y - camera_y;
          float yAnim0 = yBase - 10, yAnim1 = yBase; // yAnim0 から yAnim1の位置までアニメする
          int blockTime = blockAnime[mapXY];
          float yAnim = yAnim0 * (15 - blockTime) / 15 + yAnim1 * blockTime / 15;
          DrawRotaGraphF(chip_x - camera_x + rotaShiftX, yAnim + rotaShiftY, 1.0f, 0.0f, Image::mapChips[id], TRUE);
          ++blockAnime[mapXY]; // アニメ時間を進める
          if (blockTime > 15) // アニメ終了時間を15フレーム目とするとき
            blockAnime.erase(mapXY); // 辞書からアニメをerase関数で取り除く
        }
        // [デバッグ]用にcsvのidの数字を描く
        DrawString(chip_x - camera_x, chip_y - camera_y, std::to_string(id).c_str(), GetColor(255, 255, 255));
      }
    }
    
    // [デバッグ] 当たり判定の四隅を赤枠で描画
    DrawBox(getLeft(x) - camera_x, getTop(y) - camera_y, getRight(x) - camera_x, getBottom(y) - camera_y, GetColor(255, 0, 0), FALSE);
    // [デバッグ] キャラ原点(x,y)を緑丸で描画
    DrawCircle(x - camera_x, y - camera_y, 2, GetColor(0, 255, 0), TRUE);
    // [デバッグ] 当たった際の行き過ぎの めりこみ座標(debug_x,debug_y) を青線で描画
    if (debug_x != -1)
      DrawLine(debug_x - camera_x, debug_x_yTop - camera_y, debug_x - camera_x, debug_x_yBottom - camera_y, GetColor(0, 0, 255));
    if (debug_y != -1)
      DrawLine(debug_y_xLeft - camera_x, debug_y - camera_y, debug_y_xRight - camera_x, debug_y - camera_y, GetColor(0, 0, 255));
    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}



いかがでしょう、動作は変わりませんが、きちんとSoundクラスを通して音声ロード処理をできるようになりましたか?




Cameraの描画ずらし処理をclass分けしてファイル分けしてさらにズーム機能を付加するプログラム



まずはカメラのxとy座標をまとめて扱うためにVector2クラスを準備しましょう。

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

#ifndef VECTOR2_H_
#define VECTOR2_H_

#include <array> // 有限個数配列。生配列[]より便利な機能がある(.size()関数とかが使える)
#include <cmath>
#include <limits> // 無限大∞をstd::numeric_limits<float>::infinity()のように使う

//【注意】std::vectorとは全く違う。数学で習う【ベクトル】です。ごっちゃにしないで。

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

// 2変数x,yを持つ構造体。2つの数字をまとめて扱うならXY座標以外にも使える。x,y別々に足したり引いたり定義したり面倒でしょう。
struct Vector2
{
  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;
    float y;
  }; //[匿名共用体とは] https://code.i-harness.com/ja-jp/q/4d437c
  std::array<float, 2> xy; // float xy[2];と同じ意味 float 2個ぶんのデータサイズでx,y 2個ぶんと一致するので★unionで共用
  }; // unionは異なる複数のものをメモリ上の同一の番地に割り当てられる⇒x,y分けて記述するの面倒なとき配列xy[2]をfor文i=0~2で回せる
  
  Vector2() = default; // デフォルト初期化コンストラクタ
  
  // 初期化コンストラクタ
  Vector2(float x, float y = 0)
  {
    this->xy = { x,y };// たった1行で書ける
    //this->x = x;
    //this->y = y; 2行が上のxyの様にたった1行で書けるようになる
  }
  
  // Unityのstatic変数を参考に https://docs.unity3d.com/ja/current/ScriptReference/Vector3.html
  static const Vector2 zero;// = { 0.f, 0.f };
  static const Vector2 one; // = { 1.f, 1.f };
  static const Vector2 up; // = { 0.f, 1.f };
  static const Vector2 down;// = { 0.f, -1.f };
  static const Vector2 left; // = { -1.f, 0.f };
  static const Vector2 right;// = { 1.f, 0.f };
  //[負の値の最小値] https://stackoverflow.com/questions/20016600/negative-infinity
  static const Vector2 negativeInfinity;// = { -std::numeric_limits<float>::infinity(), .. };
  //[極大値]https://cpprefjp.github.io/reference/limits/numeric_limits/lowest.html
  static const Vector2 positiveInfinity;// = { std::numeric_limits<float>::infinity(), .. };
  
  
  // 2つのベクトルの内積
  static float dot(const Vector2& leftVec, const Vector2& rightVec)
  {
    return leftVec.x * rightVec.x + leftVec.y * rightVec.y; //普通にx同士,y同士掛けるだけ
  }
  
  // 2つのベクトルの外積
  static float cross(const Vector2& leftVec, const Vector2& rightVec)
  {
    return leftVec.x * rightVec.y - leftVec.y * rightVec.x;// 外積のたすき掛け
  }
  
  // [static版]ベクトルの長さの2乗(0から点(x,y)までの距離)(平方根しないぶん計算速い)
  static float _sqrMagnitude(const Vector2& _vec)
  {
    return _vec.x * _vec.x + _vec.y * _vec.y; // x*x + y*y
  }
  
  // [static版]ベクトルの長さ(0から点(x,y)までの距離)
  static float _magnitude(const Vector2& _vec)
  {
    return std::sqrt(_sqrMagnitude(_vec)); // √x*x + y*y
  }
  
  // destArrayにデータを保管しておく
  Vector2& copyToArray(std::array<float, 2>& destArray)
  {
    destArray = this->xy;//配列としてコピー
    return *this;
  }
  
  // sourceArray配列からVector3データを生成
  Vector2 fromArray(std::array<float, 2>& sourceArray)
  {
    return Vector2{ sourceArray[0], sourceArray[1] }; //配列から初期化
  }
  
  // ベクトルの長さの2乗(0から点(x,y)までの距離)(平方根しないぶん計算速い)
  float sqrMagnitude()
  {
    return x * x + y * y; // x*x + y*y
  }
  
  // ベクトルの長さ(0から点(x,y)までの距離)
  float magnitude()
  {
    return std::sqrt(x * x + y * y); // √(x*x + y*y)
  }
  
  // 正規化したベクトルを返す
  Vector2 normalized()
  {
    float mag = magnitude();
    if (mag < 0.00001f) // ほぼ0ベクトルか?
      return *this;
    else
      return Vector2{ x / mag, y / mag }; // x / |x|, y / |y|
  }
  
  
  //----- 演算子オーバーロード -----
  
  // 逆ベクトル
  Vector2 operator -()
  {
    return Vector2{ -this->x, -this->y };
  }
  
  // Vector同士の足し算 x,y個別に足し合わせる
  Vector2 operator + (const Vector2 add_v2) const
  {
    Vector2 v2; //★*thisじゃダメな理由 a = vec1 + vec2のとき vec1の数値が書き変わったらあかんから
    v2.x = this->x + add_v2.x; //[コレはダメ] this->x = this->x + add_v3.x;
    v2.y = this->y + add_v2.y;
    
    return v2;
  }
  
  // Vector同士の引き算 x,y個別に引き合わせる
  Vector2 operator - (const Vector2 minus_v2) const
  {
    Vector2 v2;
    v2.x = this->x - minus_v2.x;
    v2.y = this->y - minus_v2.y;
    
    return v2;
  }
  
  // Vector同士の割り算 x,y個別に割り合わせる(0.fでは割れないようにしてある)
  Vector2 operator / (float divide_num) const
  {
    if (divide_num == 0.0f)
      return *this; // Vector2 v2;と定義せずreturnしたほうが無駄な定義処理がかからないぶん速い
    
    Vector2 v2;
    v2.x = this->x / divide_num;
    v2.y = this->y / divide_num;
    
    return v2;
  }
  
  Vector2& operator += (const Vector2 add_v2)
  {
    this->x += add_v2.x;
    this->y += add_v2.y;
    
    return *this; //*thisを返すことで v1 + v2 + v3みたく数珠繋ぎできる
  }
  
  Vector2& operator -= (const Vector2 minus_v2)
  {
    this->x -= minus_v2.x;
    this->y -= minus_v2.y;
    
    return *this;
  }
  
  // 代入演算子 x,yを全部 change_numに変える
  Vector2& operator = (float change_num)
  {
    this->x = change_num;
    this->y = change_num;
    
    return *this;
  }
  
  // 一致演算子 x,yが全部一致するか 一つでも違えばfalse
  bool operator == (const Vector2& v2_other)
  {
    if (this->x != v2_other.x) return false;
    if (this->y != v2_other.y) return false;
    return true;
  }
  
  // 不一致演算子 x,yが全部不一致か 一つでも同じならfalse
  bool operator != (const Vector2& v2_other)
  {
    if (this->x == v2_other.x) return false;
    if (this->y == v2_other.y) return false;
    return true;
  }
  
  //---- 以下 DXライブラリ向けにVector3とDX版Vectorの変換方法を用意 ---------------
  
// DXlibを#includeしていればDX_LIB_Hも定義済みなので下記関数も定義される VECTORはDXライブラリ版のVector3
#ifdef DX_LIB_H
  // Vector3からDxLibのVECTORへの変換
  VECTOR Vec2ToVec()
  {
    VECTOR Result;
    Result.x = this->x;
    Result.y = this->y;
    Result.z = 0.f;
    return Result;
  }
  
  // 左辺値がDXのVECTOR型のときの=代入対応
  inline operator VECTOR() const
  {
    VECTOR Result;
    Result.x = this->x;
    Result.y = this->y;
    Result.z = 0.f;
    return Result;
  }
  
  // 右辺値がDXのVECTOR型のときの=代入対応
  Vector2 operator=(const VECTOR& other)
  {
    this->x = other.x;
    this->y = other.y;
    return *this;
  }
#endif
  
};

// [inlineを利用することで.cppではなく.hにstatic関数を定義] https://theolizer.com/cpp-school1/cpp-school1-28/
//↓スカラー倍や割り算については他のようにメンバ関数とすると右側からしか掛け算できなくなるので以下のようにメンバ外で関数化する

// Vectorとスカラの掛け算(スカラー×ベクトル) x,y個別に掛け合わせる
inline Vector2 operator * (float left, const Vector2& right)
{
  Vector2 v2;
  v2.x = left * right.x;
  v2.y = left * right.y;
  return v2;
}

// (右側から)Vectorとスカラの掛け算(ベクトル×スカラー) x,y個別に掛け合わせる
inline Vector2 operator * (const Vector2& left, float right)
{
  Vector2 v2;
  v2.x = left.x * right;
  v2.y = left.y * right;
  return v2;
}

// (左側から)Vectorとスカラの割り算(スカラー/ベクトル) x,y個別に割る
inline Vector2 operator / (float lhs, const Vector2& rhs)
{
  Vector2 v2;
  v2.x = (rhs.x == 0.0f) ? lhs / rhs.x
                         : (rhs.x > 0) ? std::numeric_limits<float>::infinity() : -std::numeric_limits<float>::infinity();
  v2.y = (rhs.y == 0.0f) ? lhs / rhs.y
                         : (rhs.y > 0) ? std::numeric_limits<float>::infinity() : -std::numeric_limits<float>::infinity();
  return v2;
}

// (右側から)Vectorとスカラの割り算(ベクトル/スカラー) x,y個別に割る
inline Vector2 operator / (const Vector2& lhs, float rhs)
{
  Vector2 v2;
  if (rhs == 0.0f)
  { // 0で割ったら∞になる
    v2.x = (lhs.x > 0) ? std::numeric_limits<float>::infinity() : -std::numeric_limits<float>::infinity();
    v2.y = (lhs.y > 0) ? std::numeric_limits<float>::infinity() : -std::numeric_limits<float>::infinity();
  }
  else
  {
    v2.x = lhs.x / rhs;
    v2.y = lhs.y / rhs;
  }
  return v2;
}

#endif

Vector2.cppも新規作成します。

#include "Vector2.h"

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

上記Vector2クラスを使って次はカメラのクラスをつくります。

つぎにCameraクラスを作ってみましょう。

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

#ifndef CAMERA_H_
#define CAMERA_H_

#include "DxLib.h"
#include "Screen.h"
#include "Vector2.h"

#include <unordered_map> // 高速辞書配列(複数のカメラを管理するために使う)
#include <memory> // 回し読みポインタstd::shared_ptrを使う
#include <string>

// [カメラ]画面のスクロール量を管理して描くときにそのスクロールぶんずらして描くクラス
class Camera
{
public:
  Vector2 pos{ 0, 0 }; // カメラの位置 画面左上のワールド座標を表す
  Vector2 leftTop{ 0, 0 }; // 描くときのの起点位置
  float zoomRatio = 1.0f; // 拡大率(2.0fにするとカメラのDraw系関数を通したものを2.0f倍で描く)
  
  std::string tag{ "" }; // カメラを管理するタグ(プレイヤカメラ1など名前をつけてタグ検索できるように)
  
  // [static変数] Camera::list["プレイヤ1"]などカメラ辞書配列にタグをつけてリンクを保持
  static std::unordered_map<std::string, std::shared_ptr<Camera>> list;
  
  // [static関数]カメラを管理する辞書をクリア
  static void clear() { std::unordered_map<std::string, std::shared_ptr<Camera>>().swap(list); }
  
  // [static関数]カメラを辞書から1つ消す(tagと辞書のタグが一致するものを辞書から消す、共有ポインタなのでカメラの実態も道連れにメモリから消える)
  static void erase(std::string& tag) { list.erase(tag); }
  
  // 初期化コンストラクタ
  Camera(const std::string& tag, Vector2 pos, float zoomRatio = 1.0f)
    : pos{ pos }, zoomRatio{ zoomRatio }, tag{ tag } {}
    
  virtual ~Camera() {} // 破棄デストラクタ
  
  // [関数]指定されたワールド座標が画面の中心に来るように、カメラの位置を変更する
  void LookAt(Vector2 pos) { LookAt(pos.x,pos.y); }
  // [関数]指定されたワールド座標が画面の中心に来るように、カメラの位置を変更する
  void LookAt(float targetX, float targetY = Screen::Height / 2);
  
  // [関数]画像を描画(回転と個別ズームも可能)
  inline int DrawRotaGraphF(float worldX, float worldY, double ExRate, double Angle, int GrHandle, int TransFlag = TRUE)
  {   // カメラのx,yぶんだけマイナスして描く起点をずらして描くことでスクロールさせる(ズームにあわせ左上leftTopのx,yぶんもずらす)
    if (zoomRatio == 1.0f)
      return DxLib::DrawRotaGraphF(worldX - pos.x, worldY - pos.y, ExRate, Angle, GrHandle, TRUE);
    else
    return DxLib::DrawRotaGraphF((worldX - pos.x) * zoomRatio + leftTop.x, (worldY - pos.y) * zoomRatio + leftTop.y,
                                  ExRate * zoomRatio, Angle, GrHandle, TRUE);
  }
  
  // [関数]文字列を描画
  inline int DrawString(float worldX, float worldY, const TCHAR* String, unsigned int Color, unsigned int EdgeColor DEFAULTPARAM(= 0))
  {   // カメラのx,yぶんだけマイナスして描く起点をずらして描くことでスクロールさせる(ズームにあわせ左上leftTopのx,yぶんもずらす)
    if (zoomRatio == 1.0f)
      return DxLib::DrawString((int)(worldX - pos.x), (int)(worldY - pos.y), String, Color);
    else
      return DxLib::DrawExtendString((int)(worldX - pos.x) * zoomRatio + leftTop.x, (int)(worldY - pos.y) * zoomRatio + leftTop.y,
                                    zoomRatio, zoomRatio, String, Color, EdgeColor);
  }
  
  // [関数]四角形をカメラの(pox.x,pos.y)ぶんずらして描画する
  inline int DrawBox(float xLeft, float yTop, float xRight, float yBottom, unsigned int Color, int FillFlag)
  {   // カメラの(pox.x,pos.y)ぶんずらして描画(ズームにあわせ左上leftTopのx,yぶんもずらす)
    if (zoomRatio == 1.0f)
      return DxLib::DrawBox((int)(xLeft - pos.x + 0.5f), (int)(yTop - pos.y + 0.5f),
                          (int)(xRight - pos.x + 0.5f), (int)(yBottom - pos.y + 0.5f), Color, FillFlag);
    else
      return DxLib::DrawBox((int)(xLeft - pos.x + 0.5f) * zoomRatio + leftTop.x, (int)(yTop - pos.y + 0.5f) * zoomRatio + leftTop.y,
                          (int)(xRight - pos.x + 0.5f) * zoomRatio + leftTop.x, (int)(yBottom - pos.y + 0.5f) * zoomRatio + leftTop.y, Color, FillFlag);
  }
  
  // [関数]円形をカメラの(pox.x,pos.y)ぶんずらして描画する
  inline int DrawCircle(int x, int y, int r, unsigned int Color, int FillFlag DEFAULTPARAM(= TRUE), int LineThickness DEFAULTPARAM(= 1))
  {   // カメラの(pox.x,pos.y)ぶんずらして描画(ズームにあわせ左上leftTopのx,yぶんもずらす)
    if (zoomRatio == 1.0f)
      return DxLib::DrawCircle(x - pos.x, y - pos.y, r, Color, FillFlag, LineThickness);
    else
      return DxLib::DrawCircle((x - pos.x) * zoomRatio + leftTop.x, (y - pos.y) * zoomRatio + leftTop.y,
                              r * zoomRatio, Color, FillFlag, LineThickness);
  }
  
  // [関数]直線をカメラの(pox.x,pos.y)ぶんずらして描画する
  inline int DrawLine(int xStart, int yStart, int xEnd, int yEnd, unsigned int Color, int Thickness DEFAULTPARAM(= 1))
  {   // カメラの(pox.x,pos.y)ぶんずらして描画(ズームにあわせ左上leftTopのx,yぶんもずらす)
    if (zoomRatio == 1.0f)
      return DxLib::DrawLine(xStart - pos.x, yStart - pos.y, xEnd - pos.x, yEnd - pos.y, Color, Thickness);
    else
      return DxLib::DrawLine((xStart - pos.x) * zoomRatio + leftTop.x, (yStart - pos.y) * zoomRatio + leftTop.y,
                               (xEnd - pos.x) * zoomRatio + leftTop.x, (yEnd - pos.y) * zoomRatio + leftTop.y, Color, Thickness);
  }
};
#endif

Camera.cppも新規作成して[static変数]を.cppファイルに クラスの{}の外側に 定義 (int型以外のstatic変数では必須)します。

#include "Camera.h"

std::unordered_map<std::string, std::shared_ptr<Camera>> Camera::list; // カメラの辞書<タグ,カメラの共有ポインタ>

void Camera::LookAt(float targetX, float targetY)
{  // 描画起点をずらす量がここに計算される
  float halfWidth = (float)Screen::Width / 2.0f;
  float halfHeight = (float)Screen::Height / 2.0f;
  pos.x = targetX - halfWidth; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
  pos.y = targetY - halfHeight; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
  if (zoomRatio != 1.0f)
  {
    leftTop.x = halfWidth - halfWidth * zoomRatio;
    leftTop.y = halfHeight - halfHeight * zoomRatio;
  }
  if (pos.x <= 0) pos.x = 0; // マップの端ではカメラスクロール止める
  if (pos.y <= 0) pos.y = 0; // マップの端ではカメラスクロール止める
}

main.cppを編集してカメラで描く起点をずらす処理(とズーム処理)をCameraクラスを使ってするようにします:

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

#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列std::stringに必要
#include <fstream> // ファイル読み出しstd::ifstreamに必要
#include <sstream> // 文字列ストリームに必要 ss >> num;で文字列から数字に変換できる
#include <unordered_map> // std::unordered_map 高速辞書配列に必要(チップ番号から壁などを表すビットへ変換する辞書)
#include <utility> // std::pair(データのペア型)を使うのに必要
// [ペアを高速辞書のキーにするためハッシュ関数が必要] https://qiita.com/hamamu/items/4d081751b69aa3bb3557
template<class TypeT> size_t HashCombine(const size_t seed, const TypeT& value)
{
  return seed ^ (std::hash<TypeT>()(value) + 0x9e3779b9 + (seed << 6) + (seed >> 2));
}

// pair型に()をつけたときに呼び出される処理をoperator()で定義してreturnでハッシュ関数を返す
template<class TypeT, class TypeS>
struct std::hash<std::pair<TypeT, TypeS>>
{
  size_t operator() (const std::pair<TypeT, TypeS>& keyval) const noexcept
  {   // returnでハッシュ関数を返すことで高速辞書のキーとして認め許されるようになる
    return HashCombine(std::hash<TypeT>()(keyval.first), keyval.second);
  }
};

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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  Image::Load(); // 画像ファイルを事前読み込み
  
  Sound::Load(); // 音声ファイルを事前読み込み
  
  int CellSize = 32; // マップの1マスのピクセル数
  
  std::vector<std::vector<int>> mapData;// タイルマップデータ
  std::ifstream ifs_csv_file("Map/stage.csv"); // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
  
  int Width = 0, Height = 0; // マップデータの横(Width) 縦(Height)のマスの数
  int MaxWidth = 0; // 1行の数字の最大個数
  std::string line; // 1行単位ごとにcsvファイルから文字列をstd::getlineで読み込んで受け取る
  while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
  {
    std::vector<int> valuelist; // 1行ぶんの数字リスト配列
    std::istringstream linestream( line ); // 各行の文字列ストリーム
    std::string comma_part; // カンマで分割された文字列
    int widthCount = 0; // カンマで区切られた行の数字の数(各行の幅)をカウント
    while (std::getline(linestream, comma_part, { ',' })) // 1行をgetlineでカンマで区切って列を得る
    {
      std::istringstream ss( comma_part ); // カンマで区切られた文字をssを通して>>で数字に
      int num; // 数字単体
      ss >> num; // 文字列ストリーム>>で数字へ変換
      valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
      ++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
    }
    // 1行の数字の数がMAX記録を更新するかチェック
    if (widthCount > MaxWidth)
      MaxWidth = widthCount; // 暫定Maxの列の幅を更新
    
    // 1行ぶんの数字の配列をとりまとめてmapDataに1行ずつ追加
    if (valuelist.size() != 0) // 配列に数字が1つでもあるなら.size() != 0
      mapData.emplace_back(valuelist); // mapDataに1行ずつ数字配列valuelistを追加
    
    ++Height; //マップの高さをカウントアップ
  }
  Width = MaxWidth; // マップの幅Widthは一番数字の個数の多かった行に合わせる
  // ↓読込んだCSVの幅と高さをチェック
  assert(Width > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  assert(Height > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  
  
  float xStart = 200, yStart = Screen::Height - 100; // キャラの初期スタート(ミス後の再開位置)
  float x = xStart, y = yStart; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  enum class Direction { Left, Right }; // enumでキャラの向いてる方向をDirection型として2種類(LeftとRight)定義
  Direction direction = Direction::Right; // キャラの向いてる方向
  int animeTime = 0; // アニメ時間
  
  bool isDead = false; // キャラがやられたかどうか
  int deadAnimeTime = 0; // やられたときのアニメ時間
  
  bool isSwitchOn = false; // ステージ全体のスイッチのオン、オフ状態(オンのときはオンを押せないようにする)
  int id_On = 10, id_Off = 11; // オンのときにオンを押したりオフのときにオフを押せないように判定する
  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16, 4 }, vyDownSpeedMax[] = { 16, 16, 16, 8 }; // ダッシュ段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f,0.1f }, vyGravity[] = { 0.8f,0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  bool isFlying = false; // 飛行モード
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(Image::playerChips[0], &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  int hitOffsetLeft = 2; // 当たり判定の左端のオフセット
  int hitOffsetRight = 2; // 当たり判定の右端のオフセット
  int hitOffsetTop = 1; // 当たり判定の上端のオフセット
  int hitOffsetBottom = 0; // 当たり判定の下端のオフセット
  
  // 当たり判定のはしっこを計算するための一時関数
  auto getLeft = [&hitOffsetLeft](float worldX) { return worldX + hitOffsetLeft; };
  auto getTop = [&hitOffsetTop](float worldY) { return worldY + hitOffsetTop; };
  auto getRight = [&hitOffsetRight, &imgSizeX](float worldX) { return worldX + imgSizeX - hitOffsetRight; };
  auto getBottom = [&hitOffsetBottom, &imgSizeY](float worldY) { return worldY + imgSizeY - hitOffsetBottom; };
  
  enum class BlockBit // ブロックの特性を32桁のビットで表現
  {
    None = 0,
    Wall   = 0b0000'0000'0000'0000'0000'0000'0000'0001,
    Damage = 0b0000'0000'0000'0000'0000'0000'0000'0010,
    Bounce = 0b0000'0000'0000'0000'0000'0000'0000'0100, // 叩くと弾むブロック
    Change = 0b0000'0000'0000'0000'0000'0000'0000'1000, // 叩くとブロックの種類が変わる
    Switch = 0b0000'0000'0000'0000'0000'0000'0001'0000, // スイッチを叩くと赤ブロックや青ブロックの無効⇔有効が切替
  };
  
  std::unordered_map<int, unsigned int> blockBits = {
    {-1,(int)BlockBit::None},
    { 0,(int)BlockBit::Wall},
    { 1,(int)BlockBit::Damage},
    { 2,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Change}, // 壁 | はずむ | ブロック変化 (|ビット論理和)
    { 3,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Change},
    {10,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Switch}, // 赤Onスイッチ
    {11,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Switch}, // 青Offスイッチ
    {12,(int)BlockBit::Wall}, // On に連動するブロック
    {14,(int)BlockBit::Wall}, // Offに連動するブロック
  };
  
  // ブロックをバウンドさせるための 高速辞書<ペア<cellX,cellY>, intアニメ時間>
  std::unordered_map<std::pair<int, int>, int> blockAnime;
  
  std::unordered_map<int, int> blockChange = {
    { 2, 3 }, // 叩くと 2 → 3 にかわる
    { 3, -1 }, // 叩くと 3 → -1 にかわる
    { 12, 13 }, // 11(OFF) を押すと 12→13
    { 13, 12 }, // 10(ON) を押すと 13→12
    { 14, 15 }, // 10(ON) を押すと 14→15
    { 15, 14 }, // 11(OFF) を押すと 15→14
  };
  
  std::unordered_map<int, std::vector<int>> blockSwitch = {
    { 10, {12,13,14,15} }, // 叩くと12,13,14,15がblockChangeの設定内容に応じて変化する
    { 11, {12,13,14,15} },
  };
  
  // 指定された座標(worldX,worldY)の地形が 壁 や ダメージブロックかなどをチェックする一時関数
  auto checkID = [&blockBits, &CellSize](BlockBit checkBit, std::vector<std::vector<int>>& terrain, float worldX, float worldY,
                                int* pMapX = nullptr, int* pMapY = nullptr)
    {
      int terrainID = -1; // 指定された座標の地形のID
      
      if (worldX < 0 || worldY < 0) return false; // 負の座標が指定された場合
      
      // マップ座標系(二次元配列の行と列)に変換する
      int mapX = (int)(worldX / CellSize), mapY = (int)(worldY / CellSize);
      // 二次元配列の範囲内か判定
      if (mapY < terrain.size() && mapX < terrain[mapY].size())
        terrainID = terrain[mapY][mapX]; // 二次元配列から地形IDを取り出す
      
      bool isCheckBit = (((int)checkBit) & blockBits[terrainID]) != 0; // ビット論理積 & で地形のchekckBitを確かめる
      if (isCheckBit && pMapX != nullptr && pMapY != nullptr)
        *pMapX = mapX, *pMapY = mapY; // ブロックのcsvのセル番号をポインタに記録(nullptrの場合は記録しない)
      
      return isCheckBit;
    };
  
  enum class HitPart // 当たった箇所を表すビット
  {
    None    = 0,
    Left    = 0b0000'0000'0000'0000'0000'0000'0000'0001,
    Right   = 0b0000'0000'0000'0000'0000'0000'0000'0010,
    Top     = 0b0000'0000'0000'0000'0000'0000'0000'0100,
    Bottom  = 0b0000'0000'0000'0000'0000'0000'0000'1000,
    XMiddle = 0b0000'0000'0000'0000'0000'0000'0001'0000,
    YMiddle = 0b0000'0000'0000'0000'0000'0000'0010'0000,
  };
  
  // [x方向左右]地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheckX = [&checkID](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      // 左端がめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yTop, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Top; // 左上がめりこんでいる
      if (checkID(checkBit, terrain, xLeft, yMiddle, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::YMiddle; // 左中がめりこんでいる
      if (checkID(checkBit, terrain, xLeft, yBottom, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Bottom; // 左下がめりこんでいる
      // 右端がめりこんでいるか?
      if (checkID(checkBit, terrain, xRight, yTop, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Top; // 右上がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yMiddle, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::YMiddle; // 右中がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yBottom, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Bottom; // 右下がめりこんでいる
      
      return (int)HitPart::None;
    };
  
  // [y方向上下]地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheckY = [&checkID](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      // 頭の部分がめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yTop, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Top; // 左上がめりこんでいる
      if (checkID(checkBit, terrain, xMiddle, yTop, pMapX, pMapY))
        return (int)HitPart::XMiddle | (int)HitPart::Top; // 中上がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yTop, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Top; // 右上がめりこんでいる
      // 足元が壁にめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yBottom, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Bottom; // 左下がめりこんでいる
      if (checkID(checkBit, terrain, xMiddle, yBottom, pMapX, pMapY))
        return (int)HitPart::XMiddle | (int)HitPart::Bottom; // 中下がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yBottom, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Bottom; // 右下がめりこんでいる
      
      return (int)HitPart::None;
    };
  
  // 地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheck = [&hitcheckX, &hitcheckY](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      return hitcheckX(checkBit,terrain,xLeft,xMiddle,xRight,yTop,yMiddle,yBottom,pMapX,pMapY)
        | hitcheckY(checkBit,terrain,xLeft,xMiddle,xRight,yTop,yMiddle,yBottom,pMapX,pMapY);
    };
  
  // カメラをmake_sharedで共有生成して Camera::list 辞書配列 に"プレイヤ0"というタグをつけて保持
  Camera::list["プレイヤ0"] = std::make_shared<Camera>("プレイヤ0", Vector2(x,y), 1.0f); // ズーム率初期値1.0f
  auto pCamera = Camera::list["プレイヤ0"]; // タイルをスクロールして描くカメラへの共有リンクを得る
  pCamera->LookAt(x, y); // プレイヤのキャラ(x,y)位置を中心に追跡スクロール
  int zoomAnimCount = 0; // ジャンプした時のズームアウトアニメのカウンタ

  float camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
  if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ
  float camera_y = y - Screen::Height / 2; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
  if (camera_y <= 0) camera_y = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
  else if (Height * CellSize - Screen::Height <= camera_y) camera_y = Height * CellSize - Screen::Height; // ステージのはしっこ

  
  
  float debug_x = -1, debug_y = -1; // [デバッグ] めり込みすぎのラインを記録しておく変数
  float debug_x_yTop = -1, debug_x_yBottom = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の上下端も記録
  float debug_y_xLeft = -1, debug_y_xRight = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の左右端も記録
  
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      direction = Direction::Left;
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      direction = Direction::Right;
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      vy = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
      // 加速段階に応じて違うジャンプ音を鳴らす
      PlaySoundMem(Sound::jumpSE[vx_idx], DX_PLAYTYPE_BACK);
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs > 9) {
          isFlying = true; // 飛行モードにする
        }
        
        // 加速段階に応じて違うジャンプ音を鳴らす
        PlaySoundMem(Sound::jumpSE[vx_idx], DX_PLAYTYPE_BACK);
        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    if (!isDead) // やられたときは移動を停止
      x += vx; // 速度のぶん現在の位置から移動
    if (!isDead) // やられたときは当たり判定も停止
    {
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      if(hitcheck(BlockBit::Damage, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに
      
      // 左端が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yMiddle) || // 左中が壁か?
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) ) // 左下が壁か?
      {
        debug_x = xLeft; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        // std::fmodで小数点の余りを計算 めり込みすぎの余り = std::fmod(xLeft, CellSize)
        float wallRight = xLeft + CellSize - (float)std::fmod(xLeft, CellSize); // 壁の右端
        x = wallRight - hitOffsetLeft; // 左端を壁の右端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      } // ↓右端が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xRight, yTop) || // 右上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yMiddle) || // 右中が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_x = xRight; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        float wallLeft = xRight - (float)std::fmod(xRight, CellSize); // 壁の左端
        x = wallLeft + hitOffsetRight - imgSizeX; // 右端を壁の左端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      }
    }
    
    if (!isDead) // やられたときは移動を停止
      y += vy;
    if (!isDead) // やられたときは当たり判定も停止
    {
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      if (hitcheck(BlockBit::Damage, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに
      
      int mapX = 0, mapY = 0; // ぶつかったらそのブロックのCSVのセルの行mapYと列mapXを得る
      if ( (int)HitPart::Top
          & hitcheck(BlockBit::Bounce, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        std::pair<int,int> mapXY = std::make_pair(mapX, mapY); // ぶつかったブロックのセルの行mapYと列mapXのペア
        if (blockAnime.count(mapXY) == 0) // 辞書にキーがあるかcountで数えて未登録==0のとき
          blockAnime[mapXY] = 0; // ブロックのアニメ時間を辞書に登録(時間が辞書にあるものだけアニメが走る)
      }
      
      if ((int)HitPart::Top
          & hitcheck(BlockBit::Switch, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        int id = mapData[mapY][mapX]; // 行mapY 列mapXの地形idを得る
        if ((isSwitchOn && id == id_Off || !isSwitchOn && id == id_On) && blockSwitch.count(id) > 0)
        {
          isSwitchOn = !isSwitchOn; // trueとfalseを!で反転
          for (int cellY = 0; cellY < Height; ++cellY) // 行をすべて調べる
            for (int cellX = 0; cellX < Width; ++cellX) // 列をすべて調べる
              for (auto switchID : blockSwitch[id]) // スイッチに反応するブロックのIDすべてを調べる
                if (mapData[cellY][cellX] == switchID)
                {
                  mapData[cellY][cellX] = blockChange[switchID]; // 変化先のIDへ書き換える
                  break; // 次の列のループへ移る(3重for文の一番内側のループをbreakして1つ外のループへ)
                }
        }
      }
      
      bool isBlockChanging = false; // ブロック変化フラグ(idはあとから変化させないとWallの押し戻しの跳ね返りvy = 0がなくなる)
      if ( (int)HitPart::Top
          & hitcheck(BlockBit::Change, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        isBlockChanging = true; // ここではフラグをtrueにするだけ(めり込み押し戻しvy=0するまえにid=-1にすると壊した瞬間にすり抜けちゃう)
      }
      
      // 頭の部分が壁にめりこんでいるか?
      if (checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yTop) || // 中上が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yTop) ) // 右上が壁か?
      {
        debug_y = yTop; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallBottom = yTop + CellSize - (float)std::fmod(yTop, CellSize); // 壁の下側の境界
        y = wallBottom - hitOffsetTop; // 頭を壁の下端に沿わす(めり込んだぶんを押し戻す)
        vy = 0; // 壁に当たったら速度をストップ
      } // ↓足元が壁にめりこんでいるか?
      else if (
          checkID(BlockBit::Wall, mapData, xLeft, yBottom) || // 左下が壁か?
          checkID(BlockBit::Wall, mapData, xMiddle, yBottom) || // 中下が壁か?
          checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_y = yBottom; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallTop = yBottom - (float)std::fmod(yBottom, CellSize); // 壁の上側の境界
        y = wallTop + hitOffsetBottom - imgSizeX; // 足元を壁の上端に沿わす(めり込んだぶんを押し戻す)
        
        isGround = true;
        isFlying = false;
        vx_idx = 0;
        
        vy = 0; // 床に当たったら速度をストップ
      }
      else
        isGround = false; // ブロックに当たってないからfalseにして落下するように
      
      if (isBlockChanging) // ブロック変化フラグ==true(すでに↑めりこみ判定でvy=0で勢いを止め反射が効くからid=-1にしてもすりぬけずにすむ)
      {
        int id = mapData[mapY][mapX]; // 行mapY 列mapXの地形idを得る
        if (blockChange.count(id) > 0) // blockChangeの辞書に登録のあるidのときは
        {
          int changeID = blockChange[id]; // 変化先のID
          std::pair<int, int> mapXY = std::make_pair(mapX, mapY); // ぶつかったブロックのセルの行mapYと列mapXのペア
          if (!(blockBits[changeID] & (int)BlockBit::Bounce)) // 変化先ブロックがバウンスするならアニメ時間をeraseせずに辞書に残す
            if (blockAnime.count(mapXY) > 0)
              blockAnime.erase(mapXY); // ブロックの種類変える前にアニメをリセットしておかないと辞書に残り続ける
          
          mapData[mapY][mapX] = changeID; // idを入れ替える
        }
      }
    }
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    
    if ( (isDead || vy >= 0.0f) && zoomAnimCount > 0)
      --zoomAnimCount;
    else if (zoomAnimCount < 60) // 上方向の加速中は最大60までカウントしてズームアウト(広域を映す)
      ++zoomAnimCount;
    // 最大(1-0.25)=0.75倍ズームアウト
    pCamera->zoomRatio = (zoomAnimCount < 2) ? 1.0f : 1.0f - 0.25f * zoomAnimCount / 60.0f;
    
    pCamera->LookAt(x, y); // プレイヤのキャラ(x,y)位置を中心に追跡スクロール

    camera_x = x - Screen::Width / 2; // 指定されたx座標が画面の中心に来るようにScreenの幅 / 2を引く
    if (camera_x <= 0) camera_x = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Width * CellSize - Screen::Width <= camera_x) camera_x = Width * CellSize - Screen::Width; // ステージのはしっこ
    camera_y = y - Screen::Height / 2; // 指定されたy座標が画面の中心に来るようにScreenの高さ / 2を引く
    if (camera_y <= 0) camera_y = 0; // ステージのはしっこではカメラの追随を止める(csvファイルのマイナスエリアを描かないように)
    else if (Height * CellSize - Screen::Height <= camera_y) camera_y = Height * CellSize - Screen::Height; // ステージのはしっこ

    
    
    // ダッシュ段階に応じてキャラクタのアニメ時間を進める
    float vx_dash = (vx < 0) ? -vx : vx; // 絶対値でプラスだけにする
    if (vx_dash > 0.5f) animeTime += 1; // 時間 +1 ( 合計 1 )
    if (vx_dash > 6) animeTime += 1; // 時間 +1 (計 1+1=2 ) 1フレームごとに2アニメ時間が進む
    if (vx_dash > 9) animeTime += 2; // 最終段階ではアニメ時間をさらに +2 (計 1+1+2=4 )
    
    int animeStep = animeTime / 10; // 10を5に変えたらアニメ速度が上がる(10ごとに1コマアニメが進む)
    if (animeStep >= 3) // アニメのステップ数が0,1,2の3段階までなら3を超えたら0へループ
    {
      animeTime = 0; // 0へ戻り再びアニメ時間を再ループ
      animeStep = 0;
    }
    
    
    // 描画処理
    if (isDead) // やられたときの描画処理
    {
      float yMiss = y - camera_y + imgSizeY / 2; // ミスったy起点
      float yAnim0 = yMiss; // アニメy位置0 始まりのy位置
      float yAnim1 = yMiss - 10; // アニメy位置1 アニメの途中少し上方向に 10 浮く
      float yAnim2 = Screen::Height + 32; // アニメy位置2 最終y位置 画面の高さ+32で画面外へ
      
      float yDeadAnim; // やられた際にはyの位置をアニメさせる
      // 直線的な比例アニメの式(t=時刻) 位置1×(終時刻 - t) / アニメの長さ + 位置2×(t - 始時刻) / アニメの長さ
      if (deadAnimeTime < 40) // やられアニメが始まって40フレーム目までは yAnim0 ~ yAnim1
        yDeadAnim = yAnim0 * (40 - deadAnimeTime) / 40 + yAnim1 * (deadAnimeTime - 0) / 40;
      else if (deadAnimeTime < 60) // やられアニメが始まって60フレーム目までは yAnim1 ~ yAnim2
        yDeadAnim = yAnim1 * (60 - deadAnimeTime) / 20 + yAnim2 * (deadAnimeTime - 40) / 20;
      else
      {
        yDeadAnim = yAnim2; // アニメ終了 到着 y位置をセット
        deadAnimeTime = 0; // 次にやられたときのアニメのため0リセットしておく
        
        // 再開位置からゲーム再開(各数値もリセット)
        isDead = false;
        x = xStart, y = yStart, vx = 0, vy = 0;
        isGround = false, isFlying = false;
        vx_idx = 0;
      }
      
      int deadAnimeStep = deadAnimeTime / 3; // 3で割って[チップのパラパラの]アニメの速度は弱める
      int chipAnime = deadAnimeStep % 3; // % 3で余りを求めれば余りはチップのパラパラ番号は0~2の間に収まる
      pCamera->DrawRotaGraphF(x - camera_x + imgSizeX / 2, yDeadAnim, 1.0f, 10.0f * deadAnimeStep / 360.0f * 2 * DX_PI, Image::playerChips[chipAnime], TRUE);
      
      ++deadAnimeTime; // アニメ時間を進める
    }
    else
    {
      if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
        pCamera->DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f,-60.0f / 360.0f * 2 * DX_PI, Image::playerChips[4], TRUE);
      else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
        pCamera->DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 60.0f / 360.0f * 2 * DX_PI, Image::playerChips[7], TRUE);
      else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
        pCamera->DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 0.0f, Image::playerChips[5 - animeStep], TRUE);
      else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
        pCamera->DrawRotaGraphF(x - camera_x + imgSizeX / 2, y - camera_y + imgSizeY / 2, 1.0f, 0.0f, Image::playerChips[6 + animeStep], TRUE);
    }
    
    float rotaShiftX = CellSize / 2, rotaShiftY = CellSize / 2; // DrawRotaGraphF関数は画像の真ん中が起点なので1/2ずらした位置計算
    // 地形マップを[ 行のfor文×列のfor文 ]の2重ループで描く
    for (int cellY = 0, ySize = mapData.size(); cellY < ySize; ++cellY)
    {
      float chip_y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
      for (int cellX = 0, xSize = mapData[cellY].size(); cellX < xSize; ++cellX)
      {
        float chip_x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
        int id = mapData[cellY][cellX]; // [行][列] の位置のcsvの地形番号(チップ番号)を得る
        if (id < 0) continue; // -1のときは描かない
        
        // マップチップを描く
        auto mapXY = std::make_pair(cellX, cellY);
        if (blockAnime.count(mapXY) == 0)
          pCamera->DrawRotaGraphF(chip_x - camera_x + rotaShiftX, chip_y - camera_y + rotaShiftY, 1.0f, 0.0f, Image::mapChips[id], TRUE);
        else if ((int)BlockBit::Bounce & blockBits[id])
        {
          float yBase = chip_y - camera_y;
          float yAnim0 = yBase - 10, yAnim1 = yBase; // yAnim0 から yAnim1の位置までアニメする
          int blockTime = blockAnime[mapXY];
          float yAnim = yAnim0 * (15 - blockTime) / 15 + yAnim1 * blockTime / 15;
          pCamera->DrawRotaGraphF(chip_x - camera_x + rotaShiftX, yAnim + rotaShiftY, 1.0f, 0.0f, Image::mapChips[id], TRUE);
          ++blockAnime[mapXY]; // アニメ時間を進める
          if (blockTime > 15) // アニメ終了時間を15フレーム目とするとき
            blockAnime.erase(mapXY); // 辞書からアニメをerase関数で取り除く
        }
        // [デバッグ]用にcsvのidの数字を描く
        pCamera->DrawString(chip_x - camera_x, chip_y - camera_y, std::to_string(id).c_str(), GetColor(255, 255, 255));
      }
    }
    
    // [デバッグ] 当たり判定の四隅を赤枠で描画
    pCamera->DrawBox(getLeft(x) - camera_x, getTop(y) - camera_y, getRight(x) - camera_x, getBottom(y) - camera_y, GetColor(255, 0, 0), FALSE);
    // [デバッグ] キャラ原点(x,y)を緑丸で描画
    pCamera->DrawCircle(x - camera_x, y - camera_y, 2, GetColor(0, 255, 0), TRUE);
    // [デバッグ] 当たった際の行き過ぎの めりこみ座標(debug_x,debug_y) を青線で描画
    if (debug_x != -1)
      pCamera->DrawLine(debug_x - camera_x, debug_x_yTop - camera_y, debug_x - camera_x, debug_x_yBottom - camera_y, GetColor(0, 0, 255));
    if (debug_y != -1)
      pCamera->DrawLine(debug_y_xLeft - camera_x, debug_y - camera_y, debug_y_xRight - camera_x, debug_y - camera_y, GetColor(0, 0, 255));
    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}



いかがでしょう、Cameraクラスを通してゲームを正しく描けましたか?ジャンプするときズームアウトするようになりましたか?
ズームインやズームアウトは目立たず地味ですが、これがなければスマブラのぶっとび演出が表現できませんよね。
数式もうまく工夫しないと急なズームアウトも不自然ですし、カメラワークは難しいです。
(カメラの存在をユーザーが意識しなくて地味であればあるほど、それは自然なカメラワークということになりますから)

キャラの移動の速度があがればあがるほど、ズームアウトして移動先をみせなければ、フィールドの先に何があるのかをユーザーが予見できないようになります。
空を飛ぶのであれば空とフィールドの広さをズームアウトで表現できれば、操作自体は変わらずともユーザーに広い視野で自由な移動感覚を演出できます。
ゲームに限らずとも「スケール感覚とは相対的なもの」です。
ドラゴンボールでフリーザが惑星を破壊する何年も前に、すでに「アラレちゃんは地球を割っていました

つまり「カメラをどこに置くか」「大きいものと小さいものをどのように相対的に対比させるか」などの観点でアイデアを出してはじめて「スケールを表現手段」にできるのです。
さらにもう1点、Google Mapのピンチイン、ピンチアウトはユーザーが気軽にスケールを直感的に変えられるという意味で発明的なUIだと感じませんか。
ユーザーの「操作をどう直感的にズームなどのスケールと連動させるか」という観点も非常にセンスが問われます。
逆にいうなら、FPSやホラーなど潜む系のゲームではあえてユーザーの視野を狭くしたり広くしたり「視野の観点がない」レベルデザインは単調さの要因となりえます。

ひとつひとつの「自分の中のあたりまえを批判的に整理」することでしか「自分にしかできないこだわり」にたどりつくことはできません。
じゃないと、プロの作ったあたりまえの気持ちいいゲーム性の上で転がされるただの消費者ユーザーの域を出ず、既存のゲーム超えるきっかけがつかめないことになりかねません。

ズームアウトひとつとっても「全然あたりまえじゃない」んです。
プレイヤーや他の制作者を出し抜くためには「目の前のあたりまえの表現をこそ疑問を持ちうたぐってかかり」
「あたりまえ → 新しい観点の発掘」か「あたりまえ → モチーフの置換」など思考を巡らせ「計画可能な手数で」新しさの尻尾を自分自身の表現利益のために掴み取らねば
なりません



地形をBlock(アニメ機能付き)とTile(データ読込とブロック描画)という概念に切り分けたプログラム




まずは地形という概念について哲学してみましょう。
この2Dゲーム世界の地形の構成要素はブロックです。マインクラフトならボクセルです。
そしてブロックがタイル状に並ぶことによって、マリオのような地形が形作られています。
そういう意味では、マインクラフトをはじめて見たとき「3Dボクセル版マリオだな」と感じました。

初代マリオの面白さは1つ1つのブロックのバリエーションにあり「子供でもステージを自作したい」と刺激されるような「レゴブロック感」にあると初見で感じました。
3D版マリオよりもマインクラフトのほうが、初代2Dマリオをはじめて見た時の「自分でも作ってみたい(作れそう)」という気持ちが刺激されるような気がします。

シンプルな構成要素は子供でも組み合わせをイメージできるような発展性があるということではないでしょうか。


さて、BlockブロックとTileタイルの概念に戻ります。
地形を単純におおざっぱにステージと呼んでしまうことは楽ですが、つきつめて考えて単一物(ブロック)と複合物(タイル)という概念を哲学して分別して考えられることが大事です。
ゲーム世界の最小構成要素(原子のようなもの)からじっくり考え抜いてイメージすることこそが、オリジナルのゲームやプログラムを自力でくみ上げるための第1歩となります。
(最終的にはその構成要素同士が連動したり、配置されたりして絡み合うことで「ゲーム性」が生まれるわけですから)
世界の定義そのものが新しいゲーム性を生み出す設定アイデアだってありうるのです。



さてまずはブロックのアニメ(位置バウンスや画像切替など)のデータを扱うためにキーフレーム(アニメ時間と位置とオプションで画像ハンドルやループ時間を持つ構造体)を定義します。

KeyFrame.hを新規作成してブロックのアニメの時間と位置と画像ハンドル番号(チップのパラパラアニメ)やtNext(ループで戻す時刻)を持つ構造体を作成します。

#ifndef KEYFRAME_H_
#define KEYFRAME_H_

#include "Vector2.h"

struct KeyFrame // アニメの時間と位置(とオプションで画像ハンドル)を持つ構造体
{
  Vector2 pos{ 0,0 }; // ある時間での位置
  int time = 0; // 時刻(経過フレーム数)
  int handle = -1; // 画像ハンドル(途中で画像を変えるなら)
  int tNext = -1; // このキーフレームのあとtime=tNextの時刻に(0にすればループに使える)
  bool isStop = false; // キーフレームの時間経過をストップするか
  KeyFrame& SetStop(bool isStop) { this->isStop = isStop; return *this; }
  
  KeyFrame(int time, Vector2 pos, int handle = -1, int tNext = -1, bool isStop = false)
      : time{ time }, pos{ pos }, handle{ handle }, tNext{ tNext }, isStop{ isStop } {}
  virtual ~KeyFrame() {}
};

#endif

Block.hを新規作成してタイルマップの1つ1つのブロックの行と列番号やアニメや描画する機能を管理するクラスを作成します。

#ifndef BLOCK_H_
#define BLOCK_H_

#include "DxLib.h"
#include "Vector2.h"
#include "KeyFrame.h"

#include <vector> // アニメのキーフレームを2次元vector配列に格納
#include <string> // 文字列std::stringを使う

#include <unordered_map> // 高速辞書配列std::unordered_mapを使う
#include <utility> // std::pair(データのペア型)を使うのに必要
// [ペアを高速辞書のキーにするためハッシュ関数が必要] https://qiita.com/hamamu/items/4d081751b69aa3bb3557
template<class TypeT> size_t HashCombine(const size_t seed, const TypeT& value)
{
  return seed ^ (std::hash<TypeT>()(value) + 0x9e3779b9 + (seed << 6) + (seed >> 2));
}

// pair型に()をつけたときに呼び出される処理をoperator()で定義してreturnでハッシュ関数を返す
template<class TypeT, class TypeS> struct std::hash<std::pair<TypeT, TypeS>>
{
  size_t operator() (const std::pair<TypeT, TypeS>& keyval) const noexcept
  {   // returnでハッシュ関数を返すことで高速辞書のキーとして認め許されるようになる
    return HashCombine(std::hash<TypeT>()(keyval.first), keyval.second);
  }
};

class Camera; // 循環回避の前方宣言(ポインタ使用のみなら.hで#inludeせずともエラー出なくなる)
class Tile; // 循環回避の前方宣言(ポインタ使用のみ)

class Block // 1つ1つのブロック(タイルマップのチップ)を表すクラス
{
public:
  int CellSize = 32; // マップの1マスのピクセル数
  int cellX = -1, cellY = -1; // マス目番号(csvの列と行の番号)
  Vector2 pos{ 0,0 }; // ブロックのワールド座標位置
  int* pID = nullptr; // マップチップ画像のIDへのリンク
  int* pHandle = nullptr; // 描くチップの画像ハンドル番号へのリンク
  Tile* pTile = nullptr; // このブロックが配置されているタイルへのリンク
  Block& SetTile(Tile* pTile) { this->pTile = pTile; return *this; }
  
  // 初期化コンストラクタ
  Block(int cellX, int cellY, int cellSize = 32, int* pHandle = nullptr, int* pID = nullptr)
      : cellX{ cellX }, cellY{ cellY }, CellSize{ cellSize }, pHandle{ pHandle }, pID{ pID }
  {
    pos.y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
    pos.x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
  }
  
  virtual ~Block() {} // 破棄デストラクタ
  
  virtual void Update(); // 更新処理
  
  virtual void Draw(Camera* pCamera); // 描画処理
};
  
  


class AnimBlock : public Block // ベースとなるBlock型を継承してアニメするブロックを定義する
{
public:
  Vector2 animPos{ 0,0 }; // アニメで(pos.x + animPos.x, pos.y + animPos.y)にずらして描く
  int time = 0; // ブロック単品で時刻をカウントするときに使う(pTileのタイル辞書時刻を使うときはそちら優先)
  AnimBlock& SetTime(int time) { this->time = time; }
  
  std::vector<KeyFrame> anim; // 一連のアニメの時間と位置を持つキーレームの配列
  
  // 初期化コンストラクタ
  AnimBlock(std::vector<KeyFrame>& anim, int cellX, int cellY, int cellSize = 32, int* pHandle = nullptr, int* pID = nullptr)
      : anim{ anim }, Block(cellX, cellY, cellSize, pHandle, pID) {}
  
  virtual void Update() override; // 更新処理
  
  virtual void Draw(Camera* pCamera) override; // 描画処理
};
#endif

Block.cppを新規作成してBlock.hの処理やCamera.hを#includeしてカメラぶんずらしてブロックを描いたりアニメする処理を作成します。

#include "Block.h"

#include "Camera.h" // CameraでもBlockやTileを#includeする可能性を考えて循環回避(.cppで#include)
#include "Tile.h" // 循環回避のため.cppで相互に#includeしあう


// 更新処理
void Block::Update()
{
  pos.y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
  pos.x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
}

// 描画処理
void Block::Draw(Camera* pCamera)
{
  if (pCamera == nullptr) return; // カメラのリンク切れなら描けない
  
  // ポインタからidを読みだす
  int id = (pID != nullptr) ? *pID : -1;
  int handle = (pHandle != nullptr) ? *pHandle : -1;
  
  // マップチップを描く
  float rotaShiftX = CellSize / 2, rotaShiftY = CellSize / 2;
  pCamera->DrawRotaGraphF(pos.x + rotaShiftX, pos.y + rotaShiftY, 1.0f, 0.0f, handle, TRUE);
  
  // デバッグ用にcsvのidの数字を描く
  if (id != -1)
    pCamera->DrawString(pos.x, pos.y, std::to_string(id).c_str(), GetColor(255, 255, 255));
}

// 更新処理
void AnimBlock::Update()
{
  pos.y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
  pos.x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
  
  auto mapXY = std::make_pair(cellX, cellY);
  int* pBlockTime = &time; // タイル時刻辞書へのリンク切れの場合は内部変数timeへリンク
  if (pTile != nullptr && pTile->cellTime.count(mapXY) > 0) // タイル時刻辞書へのリンクが有効なら
    pBlockTime = &(pTile->cellTime[mapXY]); // タイル辞書の時刻へのリンクを張る
  
  size_t animSize = anim.size(); // anim配列の数(.size()関数を毎度呼ぶだけでも少し計算のムダ:1度目に変数に保持)
  if (animSize == 0) // アニメがない場合は(0,0)の固定位置
    animPos = Vector2{ 0,0 };
  else if (animSize >= 1)
  {
    size_t i = 0;
    for (size_t j = 1; j < animSize; ++j)
      if (anim[j-1].time <= *pBlockTime && *pBlockTime <= anim[j].time)
      {
        i = j - 1; // 時刻に対応するキーフレーム番号区間を見つけた
        break;
      }
    
    Vector2 startPos{ anim[i].pos.x, anim[i].pos.y };
    float nowTime = *pBlockTime - anim[i].time;
    if (animSize < 2) // キーフレームが1つ以下なら
      animPos = startPos; // アニメせずスタート位置のまま固定
    else
    {
      Vector2 endPos{ anim[i + 1].pos.x, anim[i + 1].pos.y };
      
      //数値例.float yAnim0 = yBase - 10, yAnim1 = yBase; yAnim0 から yAnim1の位置までアニメする
      float time = anim[i + 1].time - anim[i].time;
      
      //数値例.float yAnim = yAnim0 * (15 - blockTime) / 15 + yAnim1 * blockTime / 15;
      animPos = startPos * ((time - nowTime) / time) + endPos * (nowTime / time);
    }
    
    // 画像をパラパラ画像アニメするならキーフレームの画像ハンドル番号へリンクする
    pHandle = &anim[i].handle; // iが変わってhandle番号が変われば画像が変わりパラパラアニメする
    
    if (anim[i].isStop != true)
      ++(*pBlockTime); // アニメ時間を進める
    
    int iEnd = animSize - 1; // 最後の配列番号
    if (*pBlockTime > anim[iEnd].time) // アニメ終了時間をこえたとき
      if (anim[iEnd].tNext >= 0)
        *pBlockTime = anim[iEnd].tNext; // アニメ時刻をtNextに飛ばして辞書時刻を書き換え
      else if (pTile != nullptr && pTile->cellTime.count(mapXY) > 0) // タイル時刻辞書リンクが有効なら
        pTile->cellTime.erase(mapXY); // 辞書からアニメをerase関数で取り除く
  }
}

// 描画処理
void AnimBlock::Draw(Camera* pCamera)
{
  if (pCamera == nullptr) return;
  
  // ポインタからidを読みだす
  int id = (pID != nullptr) ? *pID : -1;
  int handle = (pHandle != nullptr) ? *pHandle : -1;
  
  // マップチップを描く
  float rotaShiftX = CellSize / 2, rotaShiftY = CellSize / 2;
  pCamera->DrawRotaGraphF(pos.x + animPos.x + rotaShiftX, pos.y + animPos.y + rotaShiftY, 1.0f, 0.0f, handle, TRUE);
  
  // デバッグ用にcsvのidの数字を描く
  if (id != -1)
    pCamera->DrawString(pos.x, pos.y, std::to_string(id).c_str(), GetColor(255, 255, 255));
}

Tile.hを新規作成してCSVからマップの1つ1つのブロックidを読み出して(x,y)に対応するマス目(cellX,cellY)のidに応じてブロックをまとめて描くクラスを作成します。

#ifndef TILE_H_
#define TILE_H_

#include "DxLib.h"
#include "DataCsv.h"
#include "Block.h"

#include <assert.h> // 読込み失敗表示用
#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列std::stringを使う
#include <unordered_map> // 高速辞書配列std::unordered_mapを使う

class Camera; // 循環回避の前方宣言(ポインタ使用のみなら.hで#inludeせずともエラー出なくなる)


enum class BlockBit // ブロックの特性を32桁のビットで表現
{
  None = 0,
  Wall   = 0b0000'0000'0000'0000'0000'0000'0000'0001,
  Damage = 0b0000'0000'0000'0000'0000'0000'0000'0010,
  Bounce = 0b0000'0000'0000'0000'0000'0000'0000'0100, // 叩くと弾むブロック
  Change = 0b0000'0000'0000'0000'0000'0000'0000'1000, // 叩くとブロックの種類が変わる
  Switch = 0b0000'0000'0000'0000'0000'0000'0001'0000, // スイッチを叩くと赤ブロックや青ブロックの無効⇔有効が切替
};


enum class HitPart // 当たった箇所を表すビット
{
  None    = 0,
  Left    = 0b0000'0000'0000'0000'0000'0000'0000'0001,
  Right   = 0b0000'0000'0000'0000'0000'0000'0000'0010,
  Top     = 0b0000'0000'0000'0000'0000'0000'0000'0100,
  Bottom  = 0b0000'0000'0000'0000'0000'0000'0000'1000,
  XMiddle = 0b0000'0000'0000'0000'0000'0000'0001'0000,
  YMiddle = 0b0000'0000'0000'0000'0000'0000'0010'0000,
};


class Tile // タイルマップのデータクラス
{
public:
  int CellSize = 32; // マップの1マスのピクセル数
  std::unordered_map<int, int> blockBits; // ブロックのビット辞書
  
  // ブロックをアニメ(バウンドなど)させるための 高速辞書<ペア<cellX,cellY>, intアニメ時間>
  std::unordered_map<std::pair<int, int>, int> cellTime;
  
  bool isSwitchOn = false; // ステージ全体のスイッチのオン、オフ状態(オンのときはオンを押せないようにする)
  int id_On = 10, id_Off = 11; // オンのときにオンを押したりオフのときにオフを押せないように判定する
  
  std::unordered_map<int, std::vector<int>> blockSwitch; // 登録されたidを叩くとblockChangeの設定に応じて変化
  
  std::unordered_map<int, int> blockChange; // ブロックの<変化前int→変化先id int>の辞書
  
  DataCsv Data;// csvデータ
  int& Width = Data.Width; // マップデータの横のマス数への&参照
  int& Height = Data.Height; // マップデータの縦のマス数への&参照
  
  // ★スムーズに[][]でアクセスできるように[]演算子を独自定義する
  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と被らせることで使う側は2次元vectorのままのコードで使える
    return Data.size();
  }
  
  // 初期化コンストラクタ
  Tile(std::string filePath = "", int cellSize = 32) : CellSize { cellSize }
  {
    Load(filePath); // csvファイルをロード
  }
  
  // 破棄デストラクタ
  virtual ~Tile()
  {
    Data.clear(); // csvデータをメモリからクリアする
  }
  
  // csvファイルの読み込み
  void Load(std::string& filePath)
  {
    Data.Load(filePath);
  }
  
  // 指定された座標(worldX,worldY)の地形が 壁 や ダメージブロックかなどをチェックする一時関数
  bool checkID(BlockBit checkBit, float worldX, float worldY,
      int* pMapX = nullptr, int* pMapY = nullptr, int* pTerrainID = nullptr);
  
  // [X方向左右]地形の ダメージブロックなどにぶつかったかをチェックする関数
  int hitcheckX(BlockBit checkBit,
      float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
      int* pMapX = nullptr, int* pMapY = nullptr);
  
  // [Y方向上下]地形の ダメージブロックなどにぶつかったかをチェックする関数
  int hitcheckY(BlockBit checkBit,
      float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
      int* pMapX = nullptr, int* pMapY = nullptr);
  
  // 地形の ダメージブロックなどにぶつかったかをチェックする関数
  int hitcheck(BlockBit checkBit,
      float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
      int* pMapX = nullptr, int* pMapY = nullptr);
  
  // 描画処理
  void Draw(Camera* pCamera);
};
#endif

Tile.cppを新規作成してタイルマップの1つ1つのブロックの行と列番号やアニメや描画する機能を管理するクラスを作成します。

#include "Tile.h"

#include "Image.h"
#include "Camera.h" // CameraでもBlockやTileを#includeする可能性を考えて循環回避(.cppで#include)

// 指定された座標(worldX,worldY)の地形が 壁 や ダメージブロックかなどをチェックする関数
bool Tile::checkID(BlockBit checkBit, float worldX, float worldY,
    int* pMapX, int* pMapY, int* pTerrainID)
{
  int terrainID; // 指定された座標の地形のID
  
  if (worldX < 0 || worldY < 0) return false; // 負の座標(データ配列外)が指定された場合
  
  // マップ座標系(二次元配列の行と列)に変換する
  int mapX = (int)(worldX / CellSize), mapY = (int)(worldY / CellSize);
  
  // 二次元配列の範囲内か判定
  if (mapY < Data.size() && mapX < Data[mapY].size())
    terrainID = Data[mapY][mapX]; // 二次元配列から地形IDを取り出す
  else
    terrainID = -1; // 配列の外のデータは-1扱いにする
  
  if (pTerrainID != nullptr)
    *pTerrainID = terrainID;
  
  bool isCheckBit = (((int)checkBit) & blockBits[terrainID]) != 0; // ビット論理積 & で地形のchekckBitを確かめる
  if (isCheckBit && pMapX != nullptr && pMapY != nullptr)
    *pMapX = mapX, * pMapY = mapY; // ブロックのcsvのセル番号をポインタに記録(nullptrの場合は記録しない)
  
  return isCheckBit;
}

// [X方向左右]地形の ダメージブロックなどにぶつかったかをチェックする関数
int Tile::hitcheckX(BlockBit checkBit,
    float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
    int* pMapX, int* pMapY) // pMapX,pMapYにはぶつかったらブロックのマス目番号が代入され返却される
{
  // 左端がめりこんでいるか?
  if (checkID(checkBit, xLeft, yTop, pMapX, pMapY))
    return (int)HitPart::Left | (int)HitPart::Top; // 左上がめりこんでいる
  if (checkID(checkBit, xLeft, yMiddle, pMapX, pMapY))
    return (int)HitPart::Left | (int)HitPart::YMiddle; // 左中がめりこんでいる
  if (checkID(checkBit, xLeft, yBottom, pMapX, pMapY))
    return (int)HitPart::Left | (int)HitPart::Bottom; // 左下がめりこんでいる
  // 右端がめりこんでいるか?
  if (checkID(checkBit, xRight, yTop, pMapX, pMapY))
    return (int)HitPart::Right | (int)HitPart::Top; // 右上がめりこんでいる
  if (checkID(checkBit, xRight, yMiddle, pMapX, pMapY))
    return (int)HitPart::Right | (int)HitPart::YMiddle; // 右中がめりこんでいる
  if (checkID(checkBit, xRight, yBottom, pMapX, pMapY))
    return (int)HitPart::Right | (int)HitPart::Bottom; // 右下がめりこんでいる
  
  return (int)HitPart::None;
}

// [Y方向上下]地形の ダメージブロックなどにぶつかったかをチェックする関数
int Tile::hitcheckY(BlockBit checkBit,
    float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
    int* pMapX, int* pMapY) // pMapX,pMapYにはぶつかったらブロックのマス目番号が代入され返却される
{
  // 頭の部分がめりこんでいるか?
  if (checkID(checkBit, xLeft, yTop, pMapX, pMapY))
    return (int)HitPart::Left | (int)HitPart::Top; // 左上がめりこんでいる
  if (checkID(checkBit, xMiddle, yTop, pMapX, pMapY))
    return (int)HitPart::XMiddle | (int)HitPart::Top; // 中上がめりこんでいる
  if (checkID(checkBit, xRight, yTop, pMapX, pMapY))
    return (int)HitPart::Right | (int)HitPart::Top; // 右上がめりこんでいる
  // 足元が壁にめりこんでいるか?
  if (checkID(checkBit, xLeft, yBottom, pMapX, pMapY))
    return (int)HitPart::Left | (int)HitPart::Bottom; // 左下がめりこんでいる
  if (checkID(checkBit, xMiddle, yBottom, pMapX, pMapY))
    return (int)HitPart::XMiddle | (int)HitPart::Bottom; // 中下がめりこんでいる
  if (checkID(checkBit, xRight, yBottom, pMapX, pMapY))
    return (int)HitPart::Right | (int)HitPart::Bottom; // 右下がめりこんでいる
  
  return (int)HitPart::None;
}

// 地形の ダメージブロックなどにぶつかったかをチェックする関数
int Tile::hitcheck(BlockBit checkBit,
    float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
    int* pMapX, int* pMapY) // pMapX,pMapYにはぶつかったらブロックのマス目番号が代入され返却される
{
  return hitcheckX(checkBit, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, pMapX, pMapY)
       | hitcheckY(checkBit, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, pMapX, pMapY);
}

// 描画処理
void Tile::Draw(Camera* pCamera)
{
  float rotaShiftX = CellSize / 2, rotaShiftY = CellSize / 2;
  for (int cellY = 0; cellY < Height; ++cellY)
  {
    float chip_y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
    for (int cellX = 0; cellX < Width; ++cellX)
    {
      float chip_x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
      int id = -1;
      if (cellY < Data.size() && cellX < Data[cellY].size())
        id = Data[cellY][cellX]; // [行][列]
      if (id < 0) continue; // -1のときは描かない
      
      // マップチップを描く
      auto mapXY = std::make_pair(cellX, cellY);
      if (cellTime.count(mapXY) == 0)
      { // 普通のBlock型(マス目位置XY ↑ に対応したアニメ時刻が辞書に未登録count=0のとき)
        Block block{ cellX, cellY, CellSize, &Image::mapChips[id], &id };
        block.Update();
        block.Draw(pCamera); // カメラのリンクを渡してブロックを描く
      }
      else if ((int)BlockBit::Bounce & blockBits[id])
      { //[アニメ]とりあえずローカル{}内で変数定義するが、コマ数(データ量)が増えたらループ負荷の少ない所に移動が無難か
        std::vector<KeyFrame> anim = { {0, Vector2(0,-10), Image::mapChips[id]},
                                       {15, Vector2(0,0), Image::mapChips[id]} };
        AnimBlock block{ anim, cellX, cellY, CellSize};
        block.SetTile(this); // ブロックにタイルへのリンクを張る(アニメ時刻辞書cellTimeにアクセスできるように)
        block.Update(); // アニメ位置を更新してから
        block.Draw(pCamera); // カメラのリンクを渡してブロックを描く
      }
    }
  }
}

main.cppを編集してマップとの当たり判定やタイルの描画をTileやBlockクラスを使ってするようにします:

#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"
#include "Camera.h"
#include "Tile.h"

#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列std::stringに必要
#include <fstream> // ファイル読み出しstd::ifstreamに必要
#include <sstream> // 文字列ストリームに必要 ss >> num;で文字列から数字に変換できる
#include <unordered_map> // std::unordered_map 高速辞書配列に必要(チップ番号から壁などを表すビットへ変換する辞書)
#include <utility> // std::pair(データのペア型)を使うのに必要
// [ペアを高速辞書のキーにするためハッシュ関数が必要] https://qiita.com/hamamu/items/4d081751b69aa3bb3557
template<class TypeT> size_t HashCombine(const size_t seed, const TypeT& value)
{
  return seed ^ (std::hash<TypeT>()(value) + 0x9e3779b9 + (seed << 6) + (seed >> 2));
}

// pair型に()をつけたときに呼び出される処理をoperator()で定義してreturnでハッシュ関数を返す
template<class TypeT, class TypeS>
struct std::hash<std::pair<TypeT, TypeS>>
{
  size_t operator() (const std::pair<TypeT, TypeS>& keyval) const noexcept
  {   // returnでハッシュ関数を返すことで高速辞書のキーとして認め許されるようになる
    return HashCombine(std::hash<TypeT>()(keyval.first), keyval.second);
  }
};


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

  // DXライブラリの初期化
  if (DxLib_Init() < 0)
  {
    // DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
    return -1;
  }
  
  
  // ゲームのwhileループを開始する前の初期化処理
  Image::Load(); // 画像ファイルを事前読み込み
  
  Sound::Load(); // 音声ファイルを事前読み込み
  
  Tile mapData{ "Map/stage.csv" , 32 }; // マップの1マスのピクセル数32
  int CellSize = 32; // マップの1マスのピクセル数
  
  std::vector<std::vector<int>> mapData;// タイルマップデータ
  std::ifstream ifs_csv_file("Map/stage.csv"); // 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
  
  int Width = 0, Height = 0; // マップデータの横(Width) 縦(Height)のマスの数
  int MaxWidth = 0; // 1行の数字の最大個数
  std::string line; // 1行単位ごとにcsvファイルから文字列をstd::getlineで読み込んで受け取る
  while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
  {
    std::vector<int> valuelist; // 1行ぶんの数字リスト配列
    std::istringstream linestream( line ); // 各行の文字列ストリーム
    std::string comma_part; // カンマで分割された文字列
    int widthCount = 0; // カンマで区切られた行の数字の数(各行の幅)をカウント
    while (std::getline(linestream, comma_part, { ',' })) // 1行をgetlineでカンマで区切って列を得る
    {
      std::istringstream ss( comma_part ); // カンマで区切られた文字をssを通して>>で数字に
      int num; // 数字単体
      ss >> num; // 文字列ストリーム>>で数字へ変換
      valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
      ++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
    }
    // 1行の数字の数がMAX記録を更新するかチェック
    if (widthCount > MaxWidth)
      MaxWidth = widthCount; // 暫定Maxの列の幅を更新
    
    // 1行ぶんの数字の配列をとりまとめてmapDataに1行ずつ追加
    if (valuelist.size() != 0) // 配列に数字が1つでもあるなら.size() != 0
      mapData.emplace_back(valuelist); // mapDataに1行ずつ数字配列valuelistを追加
    
    ++Height; //マップの高さをカウントアップ
  }
  Width = MaxWidth; // マップの幅Widthは一番数字の個数の多かった行に合わせる
  // ↓読込んだCSVの幅と高さをチェック
  assert(Width > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  assert(Height > 0 && "マップ読込み失敗ファイル名間違い、もしくはMapフォルダにファイルがないのでは?" != "");
  

  
  float xStart = 200, yStart = Screen::Height - 100; // キャラの初期スタート(ミス後の再開位置)
  float x = xStart, y = yStart; // 初期位置
  float yJumpStart = 0; // ジャンプした瞬間のyの位置
  
  float vx = 0, vy = 0; // プレイヤの速度
  enum class Direction { Left, Right }; // enumでキャラの向いてる方向をDirection型として2種類(LeftとRight)定義
  Direction direction = Direction::Right; // キャラの向いてる方向
  int animeTime = 0; // アニメ時間
  
  bool isDead = false; // キャラがやられたかどうか
  int deadAnimeTime = 0; // やられたときのアニメ時間
  
  bool isSwitchOn = false; // ステージ全体のスイッチのオン、オフ状態(オンのときはオンを押せないようにする)
  int id_On = 10, id_Off = 11; // オンのときにオンを押したりオフのときにオフを押せないように判定する

  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  float vyJumpSpeed[] = { 14, 15, 16, 4 }, vyDownSpeedMax[] = { 16, 16, 16, 8 }; // ダッシュ段階に応じた複数のジャンプの上昇と降下スピードのリミット
  float vyForce = 0;
  float vyForceJump[] = { 0.5f,0.4f,0.3f,0.1f }, vyGravity[] = { 0.8f,0.8f,0.8f,0.8f }; // ダッシュ段階に応じた複数のパラメータを持つように
  bool isGround = true; // 着地しているか
  bool isFlying = false; // 飛行モード
  
  int imgSizeX, imgSizeY; // 画像の縦横サイズ
  GetGraphSize(Image::playerChips[0], &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
  
  int hitOffsetLeft = 2; // 当たり判定の左端のオフセット
  int hitOffsetRight = 2; // 当たり判定の右端のオフセット
  int hitOffsetTop = 1; // 当たり判定の上端のオフセット
  int hitOffsetBottom = 0; // 当たり判定の下端のオフセット
  
  // 当たり判定のはしっこを計算するための一時関数
  auto getLeft = [&hitOffsetLeft](float worldX) { return worldX + hitOffsetLeft; };
  auto getTop = [&hitOffsetTop](float worldY) { return worldY + hitOffsetTop; };
  auto getRight = [&hitOffsetRight, &imgSizeX](float worldX) { return worldX + imgSizeX - hitOffsetRight; };
  auto getBottom = [&hitOffsetBottom, &imgSizeY](float worldY) { return worldY + imgSizeY - hitOffsetBottom; };
  
  enum class BlockBit // ブロックの特性を32桁のビットで表現
  {
    None = 0,
    Wall   = 0b0000'0000'0000'0000'0000'0000'0000'0001,
    Damage = 0b0000'0000'0000'0000'0000'0000'0000'0010,
    Bounce = 0b0000'0000'0000'0000'0000'0000'0000'0100, // 叩くと弾むブロック
    Change = 0b0000'0000'0000'0000'0000'0000'0000'1000, // 叩くとブロックの種類が変わる
    Switch = 0b0000'0000'0000'0000'0000'0000'0001'0000, // スイッチを叩くと赤ブロックや青ブロックの無効⇔有効が切替
  };

  
  mapData.blockBits = {
    {-1,(int)BlockBit::None},
    { 0,(int)BlockBit::Wall},
    { 1,(int)BlockBit::Damage},
    { 2,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Change}, // 壁 | はずむ | ブロック変化 (|ビット論理和)
    { 3,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Change},
    {10,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Switch}, // 赤Onスイッチ
    {11,(int)BlockBit::Wall | (int)BlockBit::Bounce | (int)BlockBit::Switch}, // 青Offスイッチ
    {12,(int)BlockBit::Wall}, // On に連動するブロック
    {14,(int)BlockBit::Wall}, // Offに連動するブロック
  };
  
  // ブロックをバウンドさせるための 高速辞書<ペア<cellX,cellY>, intアニメ時間>
  std::unordered_map<std::pair<int, int>, int> blockAnime;

  
  mapData.blockChange = {
    { 2, 3 }, // 叩くと 2 → 3 にかわる
    { 3, -1 }, // 叩くと 3 → -1 にかわる
    { 12, 13 }, // 11(OFF) を押すと 12→13
    { 13, 12 }, // 10(ON) を押すと 13→12
    { 14, 15 }, // 10(ON) を押すと 14→15
    { 15, 14 }, // 11(OFF) を押すと 15→14
  };
  
  mapData.blockSwitch = {
    { 10, {12,13,14,15} }, // 叩くと12,13,14,15がblockChangeの設定内容に応じて変化する
    { 11, {12,13,14,15} },
  };
  
  // 指定された座標(worldX,worldY)の地形が 壁 や ダメージブロックかなどをチェックする一時関数
  auto checkID = [&blockBits, &CellSize](BlockBit checkBit, std::vector<std::vector<int>>& terrain, float worldX, float worldY,
                                int* pMapX = nullptr, int* pMapY = nullptr)
    {
      int terrainID = -1; // 指定された座標の地形のID
      
      if (worldX < 0 || worldY < 0) return false; // 負の座標が指定された場合
      
      // マップ座標系(二次元配列の行と列)に変換する
      int mapX = (int)(worldX / CellSize), mapY = (int)(worldY / CellSize);
      // 二次元配列の範囲内か判定
      if (mapY < terrain.size() && mapX < terrain[mapY].size())
        terrainID = terrain[mapY][mapX]; // 二次元配列から地形IDを取り出す
      
      bool isCheckBit = (((int)checkBit) & blockBits[terrainID]) != 0; // ビット論理積 & で地形のchekckBitを確かめる
      if (isCheckBit && pMapX != nullptr && pMapY != nullptr)
        *pMapX = mapX, *pMapY = mapY; // ブロックのcsvのセル番号をポインタに記録(nullptrの場合は記録しない)
      
      return isCheckBit;
    };
  
  enum class HitPart // 当たった箇所を表すビット
  {
    None    = 0,
    Left    = 0b0000'0000'0000'0000'0000'0000'0000'0001,
    Right   = 0b0000'0000'0000'0000'0000'0000'0000'0010,
    Top     = 0b0000'0000'0000'0000'0000'0000'0000'0100,
    Bottom  = 0b0000'0000'0000'0000'0000'0000'0000'1000,
    XMiddle = 0b0000'0000'0000'0000'0000'0000'0001'0000,
    YMiddle = 0b0000'0000'0000'0000'0000'0000'0010'0000,
  };
  
  // [x方向左右]地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheckX = [&checkID](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      // 左端がめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yTop, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Top; // 左上がめりこんでいる
      if (checkID(checkBit, terrain, xLeft, yMiddle, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::YMiddle; // 左中がめりこんでいる
      if (checkID(checkBit, terrain, xLeft, yBottom, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Bottom; // 左下がめりこんでいる
      // 右端がめりこんでいるか?
      if (checkID(checkBit, terrain, xRight, yTop, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Top; // 右上がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yMiddle, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::YMiddle; // 右中がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yBottom, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Bottom; // 右下がめりこんでいる
      
      return (int)HitPart::None;
    };
  
  // [y方向上下]地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheckY = [&checkID](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      // 頭の部分がめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yTop, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Top; // 左上がめりこんでいる
      if (checkID(checkBit, terrain, xMiddle, yTop, pMapX, pMapY))
        return (int)HitPart::XMiddle | (int)HitPart::Top; // 中上がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yTop, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Top; // 右上がめりこんでいる
      // 足元が壁にめりこんでいるか?
      if (checkID(checkBit, terrain, xLeft, yBottom, pMapX, pMapY))
        return (int)HitPart::Left | (int)HitPart::Bottom; // 左下がめりこんでいる
      if (checkID(checkBit, terrain, xMiddle, yBottom, pMapX, pMapY))
        return (int)HitPart::XMiddle | (int)HitPart::Bottom; // 中下がめりこんでいる
      if (checkID(checkBit, terrain, xRight, yBottom, pMapX, pMapY))
        return (int)HitPart::Right | (int)HitPart::Bottom; // 右下がめりこんでいる
      
      return (int)HitPart::None;
    };
  
  // 地形の ダメージブロックなどにぶつかったかをチェックする一時関数
  auto hitcheck = [&hitcheckX, &hitcheckY](BlockBit checkBit, std::vector<std::vector<int>>& terrain,
                                  float xLeft, float xMiddle, float xRight, float yTop, float yMiddle, float yBottom,
                                  int* pMapX = nullptr, int* pMapY = nullptr)
    {
      return hitcheckX(checkBit,terrain,xLeft,xMiddle,xRight,yTop,yMiddle,yBottom,pMapX,pMapY)
        | hitcheckY(checkBit,terrain,xLeft,xMiddle,xRight,yTop,yMiddle,yBottom,pMapX,pMapY);
    };

  
  // カメラをmake_sharedで共有生成して Camera::list 辞書配列 に"プレイヤ0"というタグをつけて保持
  Camera::list["プレイヤ0"] = std::make_shared<Camera>("プレイヤ0", Vector2(x,y), 1.0f); // ズーム率初期値1.0f
  auto pCamera = Camera::list["プレイヤ0"]; // タイルをスクロールして描くカメラへの共有リンクを得る
  pCamera->LookAt(x, y); // プレイヤのキャラ(x,y)位置を中心に追跡スクロール
  int zoomAnimCount = 0; // ジャンプした時のズームアウトアニメのカウンタ
  
  
  float debug_x = -1, debug_y = -1; // [デバッグ] めり込みすぎのラインを記録しておく変数
  float debug_x_yTop = -1, debug_x_yBottom = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の上下端も記録
  float debug_y_xLeft = -1, debug_y_xRight = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の左右端も記録
  
  
  Input::Init(); // Inputクラスの初期化
  
  ScreenFlip();
  
  // アニメーション(パラパラ漫画)するにはWhile文
  while (ProcessMessage() == 0)
  {  // ProcessMessage() == 0になるのは×ボタン押したときなど
    ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
    
    Input::Update(); // Inputの更新(これを毎ターンやらないとキーの状態が変わらず反応しない)
    
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      direction = Direction::Left;
      if (vx > 0) vx = 0; // 急ブレーキ
      if (vx >= -vxSpeedMax)
        vx += -0.35f;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      direction = Direction::Right;
      if (vx < 0) vx *= 0.9; // すべりながら減衰ブレーキ表現(vxは9/10ずつ減少する)
      if (vx <= vxSpeedMax)
        vx += 0.35f;
    }
    
    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      vy = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
      // 加速段階に応じて違うジャンプ音を鳴らす
      PlaySoundMem(Sound::jumpSE[vx_idx], DX_PLAYTYPE_BACK);
    }
    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (vx < 0) ? -vx : vx; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs > 9) {
          isFlying = true; // 飛行モードにする
        }
        
        // 加速段階に応じて違うジャンプ音を鳴らす
        PlaySoundMem(Sound::jumpSE[vx_idx], DX_PLAYTYPE_BACK);
        
        vy = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx]; // 上昇フェーズ中の下方向へ引っ張られる力(この行がないと際限なくジャンプ)
        yJumpStart = y; // ジャンプ開始する瞬間のy位置を保管する
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (vy >= 0) // 下落開始(vy < 0のときはまだジャンプ上昇の勢いありvy==0を境に勢いは逆方向に)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy < 0のとき)にボタンを離したら早めに重力をかけ始める(小ジャンプ)
    }
    
    if (!isDead) // やられたときは移動を停止
      x += vx; // 速度のぶん現在の位置から移動
    if (!isDead) // やられたときは当たり判定も停止
    {
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      if (mapData.hitcheck(BlockBit::Damage, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに
      
      // 左端が壁にめりこんでいるか?
      if (mapData.checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          mapData.checkID(BlockBit::Wall, mapData, xLeft, yMiddle) || // 左中が壁か?
          mapData.checkID(BlockBit::Wall, mapData, xLeft, yBottom) ) // 左下が壁か?
      {
        debug_x = xLeft; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        // std::fmodで小数点の余りを計算 めり込みすぎの余り = std::fmod(xLeft, CellSize)
        float wallRight = xLeft + mapData.CellSize - (float)std::fmod(xLeft, mapData.CellSize); // 壁の右端
        x = wallRight - hitOffsetLeft; // 左端を壁の右端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      } // ↓右端が壁にめりこんでいるか?
      else if (
          mapData.checkID(BlockBit::Wall, mapData, xRight, yTop) || // 右上が壁か?
          mapData.checkID(BlockBit::Wall, mapData, xRight, yMiddle) || // 右中が壁か?
          mapData.checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_x = xRight; // [デバック] めり込んだラインを記録しておく
        debug_x_yTop = y - Screen::Height / 2, debug_x_yBottom = y + Screen::Height / 2;
        float wallLeft = xRight - (float)std::fmod(xRight, mapData.CellSize); // 壁の左端
        x = wallLeft + hitOffsetRight - imgSizeX; // 右端を壁の左端に沿わす(めり込んだぶんを押し戻す)
        vx = 0; // 壁に当たったら速度をストップ
      }
    }
    
    if (!isDead) // やられたときは移動を停止
      y += vy;
    if (!isDead) // やられたときは当たり判定も停止
    {
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(x) + 0.01f, xRight = getRight(x) - 0.01f;
      float yTop = getTop(y) + 0.01f, yBottom = getBottom(y) - 0.01f;
      float xMiddle = xLeft + (getRight(x) - getLeft(x)) / 2;
      float yMiddle = yTop + (getBottom(y) - getTop(y)) / 2;
      
      if (mapData.hitcheck(BlockBit::Damage, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに
      
      int mapX = 0, mapY = 0; // ぶつかったらそのブロックのCSVのセルの行mapYと列mapXを得る
      if ( (int)HitPart::Top
          & mapData.hitcheck(BlockBit::Bounce, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        std::pair<int,int> mapXY = std::make_pair(mapX, mapY); // ぶつかったブロックのセルの行mapYと列mapXのペア
        if (mapData.cellTime.count(mapXY) == 0) // 辞書にキーがあるかcountで数えて未登録==0のとき
          mapData.cellTime[mapXY] = 0; // ブロックのアニメ時間を辞書に登録(時間が辞書にあるものだけアニメが走る)
      }
      
      if ((int)HitPart::Top
          & mapData.hitcheck(BlockBit::Switch, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        int id = mapData[mapY][mapX]; // 行mapY 列mapXの地形idを得る
        if ((mapData.isSwitchOn && id == mapData.id_Off || !mapData.isSwitchOn && id == mapData.id_On)
            && mapData.blockSwitch.count(id) > 0)
        {
          mapData.isSwitchOn = !mapData.isSwitchOn; // trueとfalseを!で反転
          for (int cellY = 0; cellY < Height; ++cellY) // 行をすべて調べる
            for (int cellX = 0; cellX < Width; ++cellX) // 列をすべて調べる
              for (auto switchID : mapData.blockSwitch[id]) // スイッチに反応するブロックのIDすべてを調べる
                if (mapData[cellY][cellX] == switchID)
                {
                  mapData[cellY][cellX] = mapData.blockChange[switchID]; // 変化先のIDへ書き換える
                  break; // 次の列のループへ移る(3重for文の一番内側のループをbreakして1つ外のループへ)
                }
        }
      }
      
      bool isBlockChanging = false; // ブロック変化フラグ(idはあとから変化させないとWallの押し戻しの跳ね返りvy = 0がなくなる)
      if ( (int)HitPart::Top
          & mapData.hitcheck(BlockBit::Change, mapData, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        isBlockChanging = true; // ここではフラグをtrueにするだけ(めり込み押し戻しvy=0するまえにid=-1にすると壊した瞬間にすり抜けちゃう)
      }
      
      // 頭の部分が壁にめりこんでいるか?
      if (mapData.checkID(BlockBit::Wall, mapData, xLeft, yTop) || // 左上が壁か?
          mapData.checkID(BlockBit::Wall, mapData, xMiddle, yTop) || // 中上が壁か?
          mapData.checkID(BlockBit::Wall, mapData, xRight, yTop) ) // 右上が壁か?
      {
        debug_y = yTop; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallBottom = yTop + mapData.CellSize - (float)std::fmod(yTop, mapData.CellSize); // 壁の下側の境界
        y = wallBottom - hitOffsetTop; // 頭を壁の下端に沿わす(めり込んだぶんを押し戻す)
        vy = 0; // 壁に当たったら速度をストップ
      } // ↓足元が壁にめりこんでいるか?
      else if (
          mapData.checkID(BlockBit::Wall, mapData, xLeft, yBottom) || // 左下が壁か?
          mapData.checkID(BlockBit::Wall, mapData, xMiddle, yBottom) || // 中下が壁か?
          mapData.checkID(BlockBit::Wall, mapData, xRight, yBottom) ) // 右下が壁か?
      {
        debug_y = yBottom; // [デバック] めり込んだラインを記録しておく
        debug_y_xLeft = x - Screen::Width / 2, debug_y_xRight = x + Screen::Width / 2;
        float wallTop = yBottom - (float)std::fmod(yBottom, mapData.CellSize); // 壁の上側の境界
        y = wallTop + hitOffsetBottom - imgSizeX; // 足元を壁の上端に沿わす(めり込んだぶんを押し戻す)
        
        isGround = true;
        isFlying = false;
        vx_idx = 0;
        
        vy = 0; // 床に当たったら速度をストップ
      }
      else
        isGround = false; // ブロックに当たってないからfalseにして落下するように
      
      if (isBlockChanging) // ブロック変化フラグ==true(すでに↑めりこみ判定でvy=0で勢いを止め反射が効くからid=-1にしてもすりぬけずにすむ)
      {
        int id = mapData[mapY][mapX]; // 行mapY 列mapXの地形idを得る
        if (mapData.blockChange.count(id) > 0) // blockChangeの辞書に登録のあるidのときは
        {
          int changeID = mapData.blockChange[id]; // 変化先のID
          std::pair<int, int> mapXY = std::make_pair(mapX, mapY); // ぶつかったブロックのセルの行mapYと列mapXのペア
          if (!(mapData.blockBits[changeID] & (int)BlockBit::Bounce)) // 変化先ブロックがバウンスするならアニメ時間をeraseせずに辞書に残す
            if (mapData.cellTime.count(mapXY) > 0)
              mapData.cellTime.erase(mapXY); // ブロックの種類変える前にアニメをリセットしておかないと辞書に残り続ける
          
          mapData[mapY][mapX] = changeID; // idを入れ替える
        }
      }
    }
    
    vx *= 0.97f; // 減速率
    vy += vyForce; // 勢い(力・フォースを加算)
    
    if (vy >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      vy = vyDownSpeedMax[vx_idx];
    
    
    if ( (isDead || vy >= 0.0f) && zoomAnimCount > 0)
      --zoomAnimCount;
    else if (zoomAnimCount < 60) // 上方向の加速中は最大60までカウントしてズームアウト(広域を映す)
      ++zoomAnimCount;
    // 最大(1-0.25)=0.75倍ズームアウト
    pCamera->zoomRatio = (zoomAnimCount < 2) ? 1.0f : 1.0f - 0.25f * zoomAnimCount / 60.0f;
    
    pCamera->LookAt(x, y); // プレイヤのキャラ(x,y)位置を中心に追跡スクロール
    
    
    // ダッシュ段階に応じてキャラクタのアニメ時間を進める
    float vx_dash = (vx < 0) ? -vx : vx; // 絶対値でプラスだけにする
    if (vx_dash > 0.5f) animeTime += 1; // 時間 +1 ( 合計 1 )
    if (vx_dash > 6) animeTime += 1; // 時間 +1 (計 1+1=2 ) 1フレームごとに2アニメ時間が進む
    if (vx_dash > 9) animeTime += 2; // 最終段階ではアニメ時間をさらに +2 (計 1+1+2=4 )
    
    int animeStep = animeTime / 10; // 10を5に変えたらアニメ速度が上がる(10ごとに1コマアニメが進む)
    if (animeStep >= 3) // アニメのステップ数が0,1,2の3段階までなら3を超えたら0へループ
    {
      animeTime = 0; // 0へ戻り再びアニメ時間を再ループ
      animeStep = 0;
    }
    
    
    // 描画処理
    if (isDead) // やられたときの描画処理
    {
      float yMiss = y + imgSizeY / 2; // ミスったy起点
      float yAnim0 = yMiss; // アニメy位置0 始まりのy位置
      float yAnim1 = yMiss - 10; // アニメy位置1 アニメの途中少し上方向に 10 浮く
      float yAnim2 = Screen::Height + 32; // アニメy位置2 最終y位置 画面の高さ+32で画面外へ
      
      float yDeadAnim; // やられた際にはyの位置をアニメさせる
      // 直線的な比例アニメの式(t=時刻) 位置1×(終時刻 - t) / アニメの長さ + 位置2×(t - 始時刻) / アニメの長さ
      if (deadAnimeTime < 40) // やられアニメが始まって40フレーム目までは yAnim0 ~ yAnim1
        yDeadAnim = yAnim0 * (40 - deadAnimeTime) / 40 + yAnim1 * (deadAnimeTime - 0) / 40;
      else if (deadAnimeTime < 60) // やられアニメが始まって60フレーム目までは yAnim1 ~ yAnim2
        yDeadAnim = yAnim1 * (60 - deadAnimeTime) / 20 + yAnim2 * (deadAnimeTime - 40) / 20;
      else
      {
        yDeadAnim = yAnim2; // アニメ終了 到着 y位置をセット
        deadAnimeTime = 0; // 次にやられたときのアニメのため0リセットしておく
        
        // 再開位置からゲーム再開(各数値もリセット)
        isDead = false;
        x = xStart, y = yStart, vx = 0, vy = 0;
        isGround = false, isFlying = false;
        vx_idx = 0;
      }
      
      int deadAnimeStep = deadAnimeTime / 3; // 3で割って[チップのパラパラの]アニメの速度は弱める
      int chipAnime = deadAnimeStep % 3; // % 3で余りを求めれば余りはチップのパラパラ番号は0~2の間に収まる
      pCamera->DrawRotaGraphF(x + imgSizeX / 2, yDeadAnim, 1.0f, 10.0f * deadAnimeStep / 360.0f * 2 * DX_PI, Image::playerChips[chipAnime], TRUE);
      
      ++deadAnimeTime; // アニメ時間を進める
    }
    else
    {
      if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
        pCamera->DrawRotaGraphF(x + imgSizeX / 2, y + imgSizeY / 2, 1.0f,-60.0f / 360.0f * 2 * DX_PI, Image::playerChips[4], TRUE);
      else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
        pCamera->DrawRotaGraphF(x + imgSizeX / 2, y + imgSizeY / 2, 1.0f, 60.0f / 360.0f * 2 * DX_PI, Image::playerChips[7], TRUE);
      else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
        pCamera->DrawRotaGraphF(x + imgSizeX / 2, y + imgSizeY / 2, 1.0f, 0.0f, Image::playerChips[5 - animeStep], TRUE);
      else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
        pCamera->DrawRotaGraphF(x + imgSizeX / 2, y + imgSizeY / 2, 1.0f, 0.0f, Image::playerChips[6 + animeStep], TRUE);
    }
    
    mapData.Draw(pCamera.get()); // タイルマップ地形を描く
    float rotaShiftX = CellSize / 2, rotaShiftY = CellSize / 2; // DrawRotaGraphF関数は画像の真ん中が起点なので1/2ずらした位置計算
    // 地形マップを[ 行のfor文×列のfor文 ]の2重ループで描く
    for (int cellY = 0, ySize = mapData.size(); cellY < ySize; ++cellY)
    {
      float chip_y = (float)(cellY * CellSize); // マス目Y(行)番号×マス目サイズで y座標に戻す
      for (int cellX = 0, xSize = mapData[cellY].size(); cellX < xSize; ++cellX)
      {
        float chip_x = (float)(cellX * CellSize); // マス目X(列)番号×マス目サイズで x座標に戻す
        int id = mapData[cellY][cellX]; // [行][列] の位置のcsvの地形番号(チップ番号)を得る
        if (id < 0) continue; // -1のときは描かない
        
        // マップチップを描く
        auto mapXY = std::make_pair(cellX, cellY);
        if (blockAnime.count(mapXY) == 0)
          pCamera->DrawRotaGraphF(chip_x + rotaShiftX, chip_y + rotaShiftY, 1.0f, 0.0f, Image::mapChips[id], TRUE);
        else if ((int)BlockBit::Bounce & blockBits[id])
        {
          float yBase = chip_y;
          float yAnim0 = yBase - 10, yAnim1 = yBase; // yAnim0 から yAnim1の位置までアニメする
          int blockTime = blockAnime[mapXY];
          float yAnim = yAnim0 * (15 - blockTime) / 15 + yAnim1 * blockTime / 15;
          pCamera->DrawRotaGraphF(chip_x + rotaShiftX, yAnim + rotaShiftY, 1.0f, 0.0f, Image::mapChips[id], TRUE);
          ++blockAnime[mapXY]; // アニメ時間を進める
          if (blockTime > 15) // アニメ終了時間を15フレーム目とするとき
            blockAnime.erase(mapXY); // 辞書からアニメをerase関数で取り除く
        }
        // [デバッグ]用にcsvのidの数字を描く
        pCamera->DrawString(chip_x, chip_y, std::to_string(id).c_str(), GetColor(255, 255, 255));
      }
    }

    
    // [デバッグ] 当たり判定の四隅を赤枠で描画
    pCamera->DrawBox(getLeft(x), getTop(y), getRight(x), getBottom(y), GetColor(255, 0, 0), FALSE);
    // [デバッグ] キャラ原点(x,y)を緑丸で描画
    pCamera->DrawCircle(x, y, 2, GetColor(0, 255, 0), TRUE);
    // [デバッグ] 当たった際の行き過ぎの めりこみ座標(debug_x,debug_y) を青線で描画
    if (debug_x != -1)
      pCamera->DrawLine(debug_x, debug_x_yTop, debug_x, debug_x_yBottom, GetColor(0, 0, 255));
    if (debug_y != -1)
      pCamera->DrawLine(debug_y_xLeft, debug_y, debug_y_xRight, debug_y, GetColor(0, 0, 255));
    
    // 速度などをデバッグ表示
    if(vy != 0)
      printfDx("y:%.1f vy:%.2f idx:%d\n", y, vy, vx_idx);
    
    ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
  }   
  // キー入力待ちをする
  WaitKey();
  // DXライブラリの後始末
  DxLib_End();
  // ソフトの終了
  return 0;
}



いかがでしょう、きちんとマップ地形は表示されていますか?
TileクラスとBlockクラスにファイル分けしたことでmain.cppも少しコード量が減って見通しが(ほんの少し)よくなってきました。
ごちゃごちゃした処理の「どの部分が地形に関連する処理か」を、main.cppを書き直しながら1つ1つ実感して見抜けるようになることが大事です。
classクラスやstruct構造体が内部にごちゃごちゃした変数や処理をパッケージングしてうまく抱え込んでいるという実感を手を動かしながら感じ取りましょう。




PlayerクラスをGameObjectクラスを継承して作成し、実行時型識別(RTTI)もできるようにするプログラム


まずはベースとなるGameObject.hを新規作成します。

#ifndef GAMEOBJECT_H_
#define GAMEOBJECT_H_

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

// ゲーム上に表示される物体の基底クラス。
// プレイヤーや敵、アイテムなどはこのクラスを継承して作る。
class GameObject
{
public:
  Vector3 pos {0,0}; // xy座標
  Vector3 v{0,0}; //xy方向の速度
  
  bool isDead = false; // 死んだ(削除対象)フラグ
  
  std::string tag = ""; // Zako0やBossやPlayerなど小カテゴリ
  std::string typeTag = ""; // EnemyやPlayerなど大カテゴリ
  std::string statusTag = ""; // 爆発状態やアイテムゲット状態など自由に使えばよい

  // コンストラクタ
  GameObject(Vector3 pos) : pos{ pos }
  {
  
  }
  
  //★仮想デストラクタ【忘れるとメモリがヤバいダメ絶対】
  //【注意!】基底では必ず定義しないとstd::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

Player.hを新規作成して共通のベースとなるGameObjectを継承するようにします。

#ifndef PLAYER_H_
#define PLAYER_H_

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

#include "GameObject.h"

// プレイヤのクラス
class Player : public GameObject
{ // 継承は↑ : public ~にする。publicつけ忘れるとGameObjectのposなどがすべてprivate継承されちゃうので注意
public:
  // コンストラクタ
  Player(Vector3 pos) : GameObject( pos ) //【忘れると】GameObjectの初期化が飛ばされpos.xなどが初期化されない
  {
    this->tag = "Player"; // 文字列タグでクラスの種類を判別する方式
    this->typeTag = "Player"; // 文字列タグでクラスの種類を判別する方式
  }
  
  virtual ~Player() {}; // 仮想デストラクタ
  
  // 更新処理の関数定義
  virtual void Update() override { };
  
  // 描画処理の関数定義
  virtual void Draw() override { };

};

#endif



さて、このままでも if (object->tag == "Player") のように文字列タグでクラスの種類を判別することはできます。
しかし、例えばプレイヤではなく大量の1万個の弾を1つ1つ文字列比較判定すると処理速度が遅くなる可能性があります。
実際の開発現場では文字列の一致判定はバグレベルで遅いというのが一般的な評判です。
プログラムの実行時にクラスの型を判別することを「実行時型識別(RTTI=Run-Time Type Identification)」と呼びます。
ではどうやってクラスの型を実行時に判別すれば高速となるか?
その1つのやり方としては、型をマクロ関数#define TO_STRING( x ) #x で#をつけ文字を返し再生ボタンを押してビルドする瞬間に文字列に変換して、
クラスの型の名前をTO_STRING(Player)で"Player"という文字に変換したものを、
ハッシュの仕組みを使って、std::hash<std::string>()( TO_STRING(Player) ) で64bitなど一定の桁数におさまるハッシュ数字IDに変換しておくことで、
時間のかかる文字列の比較ではなく、数字ハッシュIDの比較という計算時間の少ない比較方式に置き換えるという手段が有効です。

 [文字列比較(遅い)] 例. if(object->tag == "Player")   文字列の==の一致判定を1万個のGameObjectに対してやると低速

 [数字比較(高速)] 例. if(object->hashID == 234567....)   数字IDの==の一致判定なら高速なのでハッシュ関数で"Player"という文字を数字IDに変換してやる。

ベースとなるGameObject.hに実行時に型を識別(RTTI)するコードを追加変更します。

#ifndef GAMEOBJECT_H_
#define GAMEOBJECT_H_

#include "DxLib.h"
#include "Vector2.h"
#include <string> //文字タグのため
#include <memory> //回し読みポインタの定義のため
#include <functional> //ハッシュstd::hashを使う

// C#なら is でできる実行時型識別(RTTI)がC++では難しいので#define TO_STRINGとhashを駆使して実現
// https://stackoverflow.com/questions/44105058/implementing-component-system-from-unity-in-c


// クラスの型名(Player型など)を #x で★ビルド時に文字に変える最重要手法
#define TO_STRING( x ) #x


// クラス名の文字列"Player"などからハッシュで数値に変換して if ( obj->isClass( TO_HASH("Player") ) )でobjのクラスを判別
#define TO_HASH( classnameStr ) std::hash<std::string>()( classnameStr )




// ゲーム上に表示される物体の基底クラス。
// プレイヤーや敵、アイテムなどはこのクラスを継承して作る。
class GameObject
{
public:
  static inline const std::size_t classID() // クラス名をかぶらないハッシュ数字IDに変える
  { // 文字列をhashでハッシュ数字IDへ変換 (TO_STRINGで再生ビルド時にクラス名を文字列に変換)
    static const std::size_t _classID_ = std::hash<std::string>()( TO_STRING(GameObject) );
  }

  
  virtual bool isClass(const std::size_t class_id) const // クラスの種類を一致判定する
  {
    
    if (class_id == classID())
      return true; // クラスの型が一致(ハッシュIDが一致)
    return false;
  }

  
  Vector3 pos {0,0}; // xy座標
  Vector3 v{0,0}; //xy方向の速度
  
  bool isDead = false; // 死んだ(削除対象)フラグ
  
  std::string tag = ""; // Zako0やBossやPlayerなど小カテゴリ
  std::string typeTag = ""; // EnemyやPlayerなど大カテゴリ
  std::string statusTag = ""; // 爆発状態やアイテムゲット状態など自由に使えばよい


  // コンストラクタ
  GameObject(Vector3 pos) : pos{ pos }
  {
  
  }
  
  //★仮想デストラクタ【忘れるとメモリがヤバいダメ絶対】
  //【注意!】基底では必ず定義しないとstd::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

Player.hを新規作成して共通のベースとなるGameObjectを継承するようにします。

#ifndef PLAYER_H_
#define PLAYER_H_

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

#include "GameObject.h"

// プレイヤのクラス
class Player : public GameObject
{ // 継承は↑ : public ~にする。publicつけ忘れるとGameObjectのposなどがすべてprivate継承されちゃうので注意
public:
  static inline const std::size_t classID() // クラス名をかぶらないハッシュ数字IDに変える
  { // 文字列をhashでハッシュ数字IDへ変換 (TO_STRINGで再生ビルド時にクラス名を文字列に変換)
    static const std::size_t _classID_ = std::hash<std::string>()( TO_STRING(Player) );
    return _classID_; // staticで1度目のみHash値を計算してinlineで各コード箇所に数値で埋め込む
  }

  
  virtual bool isClass(const std::size_t class_id) const // クラスの種類を一致判定する
  
    if (class_id == classID())
      return true; // クラスの型が一致(ハッシュIDが一致)
    return GameObject::isClass(class_id); // 親クラスもたどって一致するか(子Playerも親GameObjectの一種)
  }

  
  // コンストラクタ
  Player(Vector3 pos) : GameObject( pos ) //【忘れると】GameObjectの初期化が飛ばされpos.xなどが初期化されない
  {
    this->tag = "Player"; // 文字列タグでクラスの種類を判別する方式
    this->typeTag = "Player"; // 文字列タグでクラスの種類を判別する方式

  }
  
  virtual ~Player() {}; // 仮想デストラクタ
  
  // 更新処理の関数定義
  virtual void Update() override { };
  
  // 描画処理の関数定義
  virtual void Draw() override { };

};

#endif



これで、if ( pObject->isClass( Player::classID() ) )どの種類のクラスかあいまいなpObjectもisClass関数を通してPlayerクラスか判定できるようになります。
しかし、新しい自作のZako0やZako1などを定義する際にいちいちclassID関数やisClass関数を定義する形だと、面倒ですよね。
#defineマクロを使って、classID関数やisClass関数のテンプレを再生ビルドする際に楽にコード埋め込みされるように改修してみましょう。

ベースとなるGameObject.hに関数定義マクロを追加してハッシュ関数と親子関係をたどって型一致を判定する関数を簡単に定義できるよう準備します。

#ifndef GAMEOBJECT_H_
#define GAMEOBJECT_H_

#include "DxLib.h"
#include "Vector2.h"
#include <string> //文字タグのため
#include <memory> //回し読みポインタの定義のため
#include <functional> //ハッシュstd::hashを使う

// C#なら is でできる実行時型識別(RTTI)がC++では難しいので#define TO_STRINGとhashを駆使して実現
// https://stackoverflow.com/questions/44105058/implementing-component-system-from-unity-in-c

// クラスの型名(Player型など)を #x で★ビルド時に文字に変える最重要手法
#define TO_STRING( x ) #x

// クラス名の文字列"Player"などからハッシュで数値に変換して if ( obj->isClass( TO_HASH("Player") ) )でobjのクラスを判別
#define TO_HASH( classnameStr ) std::hash<std::string>()( classnameStr )

// クラスの実行時型識別(RTTI)のハッシュ関数と親子関係をたどって型一致を判定する関数を定義するマクロ
#define CLASS_RTTI_DEF( childclass, parentclass ) \
  static inline const std::size_t classID() \
  { \
    static const std::size_t _classID_ = std::hash<std::string>()( TO_STRING(childclass) ); \
    return _classID_; \
  } \
  virtual bool isClass(const std::size_t class_id) const \
  { \
    if (class_id == classID()) \
      return true; \
    return parentclass::isClass(class_id); \
  } \



// ゲーム上に表示される物体の基底クラス。
// プレイヤーや敵、アイテムなどはこのクラスを継承して作る。
class GameObject
{
public:
  static inline const std::size_t classID() // クラス名をかぶらないハッシュ数字IDに変える
  { // 文字列をhashでハッシュ数字IDへ変換 (TO_STRINGで再生ビルド時にクラス名を文字列に変換)
    static const std::size_t _classID_ = std::hash<std::string>()( TO_STRING(GameObject) );
  }
  
  virtual bool isClass(const std::size_t class_id) const // クラスの種類を一致判定する
  {
    
    if (class_id == classID())
      return true; // クラスの型が一致(ハッシュIDが一致)
    return false;
  }
  
  Vector3 pos {0,0}; // xy座標
  Vector3 v{0,0}; //xy方向の速度
  
  bool isDead = false; // 死んだ(削除対象)フラグ
  

  // コンストラクタ
  GameObject(Vector3 pos) : pos{ pos }
  {
  
  }
  
  //★仮想デストラクタ【忘れるとメモリがヤバいダメ絶対】
  //【注意!】基底では必ず定義しないとstd::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

Player.hの実行時型識別(RTTI)関連の長い定義をCLASS_RTTI_DEF( 子クラス型, 親クラス型 )のようにシンプルに簡単に書きかえます。

#ifndef PLAYER_H_
#define PLAYER_H_

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

#include "GameObject.h"

// プレイヤのクラス
class Player : public GameObject
{ // 継承は↑ : public ~にする。publicつけ忘れるとGameObjectのposなどがすべてprivate継承されちゃうので注意
public:
  CLASS_RTTI_DEF( Player, GameObject )
  static inline const std::size_t classID() // クラス名をかぶらないハッシュ数字IDに変える
  { // 文字列をhashでハッシュ数字IDへ変換 (TO_STRINGで再生ビルド時にクラス名を文字列に変換)
    static const std::size_t _classID_ = std::hash<std::string>()( TO_STRING(Player) );
    return _classID_; // staticで1度目のみHash値を計算してinlineで各コード箇所に数値で埋め込む
  }
  
  virtual bool isClass(const std::size_t class_id) const // クラスの種類を一致判定する
  
    if (class_id == classID())
      return true; // クラスの型が一致(ハッシュIDが一致)
    return GameObject::isClass(class_id); // 親クラスもたどって一致するか(子Playerも親GameObjectの一種)
  }

  
  // コンストラクタ
  Player(Vector3 pos) : GameObject( pos ) //【忘れると】GameObjectの初期化が飛ばされpos.xなどが初期化されない
  {
  }
  
  virtual ~Player() {}; // 仮想デストラクタ
  
  // 更新処理の関数定義
  virtual void Update() override { };
  
  // 描画処理の関数定義
  virtual void Draw() override { };

};

#endif



さて、とりあえずは最もシンプルにGameObject.hとPlayer.hのひな形を作ってお膳立ての準備をしました。
(main.cppではまだPlayer.hをインクルードしていないので、エラーも動作の変化もまだありえません)。
次の項では、main.cppの大量のプレイヤに関するコードをPlayer.hへと移植します。



PlayerやGameObjectクラスにmain.cppのプレイヤ関連の処理を移植したプログラム



まずはベースとなるGameObject.hにプレイヤ以外も使うような共通の変数を移植してGameObjectを継承したPlayerが当たり判定のはしっこなどを計算できるよう準備します。

#ifndef GAMEOBJECT_H_
#define GAMEOBJECT_H_

#include "DxLib.h"
#include "Vector2.h"
#include <string> //文字タグのため
#include <memory> //回し読みポインタの定義のため
#include <functional> //ハッシュstd::hashを使う

// C#なら is でできる実行時型識別(RTTI)がC++では難しいので#define TO_STRINGとhashを駆使して実現
// https://stackoverflow.com/questions/44105058/implementing-component-system-from-unity-in-c

// クラスの型名(Player型など)を #x で★ビルド時に文字に変える最重要手法
#define TO_STRING( x ) #x

// クラス名の文字列"Player"などからハッシュで数値に変換して if ( obj->isClass( TO_HASH("Player") ) )でobjのクラスを判別
#define TO_HASH( classnameStr ) std::hash<std::string>()( classnameStr )

// クラスの実行時型識別(RTTI)のハッシュ関数と親子関係をたどって型一致を判定する関数を定義するマクロ
#define CLASS_RTTI_DEF( childclass, parentclass ) \
  static inline const std::size_t classID() \
  { \
    static const std::size_t _classID_ = std::hash<std::string>()( TO_STRING(childclass) ); \
    return _classID_; \
  } \
  virtual bool isClass(const std::size_t class_id) const \
  { \
    if (class_id == classID()) \
      return true; \
    return parentclass::isClass(class_id); \
  } \


// ゲーム上に表示される物体の基底クラス。
// プレイヤーや敵、アイテムなどはこのクラスを継承して作る。
class GameObject
{
public:
  static inline const std::size_t classID() // クラス名をかぶらないハッシュ数字IDに変える
  { // 文字列をhashでハッシュ数字IDへ変換 (TO_STRINGで再生ビルド時にクラス名を文字列に変換)
    static const std::size_t _classID_ = std::hash<std::string>()( TO_STRING(GameObject) );
  }
  
  virtual bool isClass(const std::size_t class_id) const // クラスの種類を一致判定する
  {
    
    if (class_id == classID())
      return true; // クラスの型が一致(ハッシュIDが一致)
    return false;
  }
  
  Vector3 pos {0,0}; // xy座標
  Vector3 v{0,0}; //xy方向の速度
  
  int imgSizeX = 32, imgSizeY = 32; // 画像の縦横サイズ
  
  int hitOffsetLeft = 0; // 当たり判定の左端のオフセット
  int hitOffsetRight = 0; // 当たり判定の右端のオフセット
  int hitOffsetTop = 0; // 当たり判定の上端のオフセット
  int hitOffsetBottom = 0; // 当たり判定の下端のオフセット
  
  // 当たり判定のはしっこを求めるための関数
  float getLeft(float worldX) { return worldX + hitOffsetLeft; };
  float getTop(float worldY) { return worldY + hitOffsetTop; };
  float getRight(float worldX) { return worldX + imgSizeX - hitOffsetRight; };
  float getBottom(float worldY) { return worldY + imgSizeY - hitOffsetBottom; };

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

  // コンストラクタ
  GameObject(Vector3 pos) : pos{ pos }
  {
  
  }
  
  //★仮想デストラクタ【忘れるとメモリがヤバいダメ絶対】
  //【注意!】基底では必ず定義しないとstd::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

Player.hにmain.cppからプレイヤに関する変数をごっそりと移植します。

#ifndef PLAYER_H_
#define PLAYER_H_

#include <vector> //可変長配列std::vectorを使う

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

#include "GameObject.h"

// プレイヤのクラス
class Player : public GameObject
{ // 継承は↑ : public ~にする。publicつけ忘れるとGameObjectのposなどがすべてprivate継承されちゃうので注意
public:
  CLASS_RTTI_DEF( Player, GameObject )
  static inline const std::size_t classID() // クラス名をかぶらないハッシュ数字IDに変える
  { // 文字列をhashでハッシュ数字IDへ変換 (TO_STRINGで再生ビルド時にクラス名を文字列に変換)
    static const std::size_t _classID_ = std::hash<std::string>()( TO_STRING(Player) );
    return _classID_; // staticで1度目のみHash値を計算してinlineで各コード箇所に数値で埋め込む
  }
  
  virtual bool isClass(const std::size_t class_id) const // クラスの種類を一致判定する
  
    if (class_id == classID())
      return true; // クラスの型が一致(ハッシュIDが一致)
    return GameObject::isClass(class_id); // 親クラスもたどって一致するか(子Playerも親GameObjectの一種)
  }
  
  enum class Direction { Left, Right }; // enumでキャラの向いてる方向をDirection型として2種類(LeftとRight)定義
  Direction direction = Direction::Right; // キャラの向いてる方向

  
  int animeTime = 0; // アニメ時間
  int deadAnimeTime = 0; // やられたときのアニメ時間

  
  Vector2 posStart{0,0}; // キャラの初期スタート(ミス後の再開位置)
  float yJumpStart = 0; // ジャンプした瞬間のyの位置

  
  int vx_idx = 0; // 横方向の加速の段階
  float vxSpeedMax = 15; // X左右方向の限界速度
  std::vector<float> vyJumpSpeed = { 14, 15, 16, 4 }, vyDownSpeedMax = { 16, 16, 16, 8 }; // ジャンプの上昇と降下スピードのリミット
  float vyForce = 0; // y方向にかかる力・加速の勢い
  std::vector<float> vyForceJump = { 0.5f,0.4f,0.3f,0.1f }, vyGravity = { 0.8f,0.8f,0.8f,0.8f };
  bool isGround = false; // 着地しているか
  bool isFlying = false; // 飛行モード

  
  float debug_x = -1, debug_y = -1;
  float debug_x_yTop = -1, debug_x_yBottom = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の上下端も記録
  float debug_y_xLeft = -1, debug_y_xRight = -1; // [デバッグ] カメラが動くのでめり込んだ瞬間の左右端も記録

  
  std::shared_ptr<Camera> pCamera = Camera::list["プレイヤ1"]; // タイルマップをスクロールして描くカメラ
  int zoomAnimCount = 0; // カメラのズームアウトのアニメのカウンタ

  
  Tile* pMapData{nullptr}; // マップへのリンク
  
  // コンストラクタ
  Player(Vector3 pos, Tile* pMapData)
     :pMapData{ pMapData }, posStart{pos}, GameObject( pos ) //【忘れると】GameObjectの初期化が飛ばされpos.xなどが初期化されない
  {
    GetGraphSize(Image::playerChips[0], &imgSizeX, &imgSizeY); // GetGraphSize関数で画像の縦横を得られる
    
    this->hitOffsetLeft = 2; // 当たり判定の左端のオフセット
    this->hitOffsetRight = 2; // 当たり判定の右端のオフセット
    this->hitOffsetTop = 1; // 当たり判定の上端のオフセット
    this->hitOffsetBottom = 0; // 当たり判定の下端のオフセット
  }
  
  virtual ~Player() {}; // 仮想デストラクタ
  
  // 更新処理の関数定義
  virtual void Update() override
  {
    if (Input::GetButton(PAD_INPUT_LEFT))
    {
      direction = Direction::Left;
      if (v.x > 0) v.x = 0; // 急ブレーキ
      if (v.x >= -vxSpeedMax)
        v.x += -0.35;
    }
    if (Input::GetButton(PAD_INPUT_RIGHT))
    {
      direction = Direction::Right;
      if (v.x < 0) v.x = 0; // 急ブレーキ
      if (v.x <= vxSpeedMax)
        v.x += 0.35;
    }

    
    // 飛行モード中にZキーを押した瞬間
    if (isFlying && !isDead && Input::GetButtonDown(PAD_INPUT_A))
    {
      vx_idx = 3; // [3]番には飛行中のパラメータが入ってる
      v.y = -vyJumpSpeed[vx_idx]; // 押すたび少し羽ばたく
      vyForce = vyForceJump[vx_idx];
      // 加速段階に応じて違うジャンプ音を鳴らす
      PlaySoundMem(Sound::jumpSE[vx_idx], DX_PLAYTYPE_BACK);
    }

    
    if (isGround)
    {
      if (Input::GetButtonDown(PAD_INPUT_A)) // Zキーを押した瞬間
      {
        vx_idx = 0; // 加速の段階をリセット
        float vx_abs = (v.x < 0) ? -v.x : v.x; // absはvxの絶対値(マイナスなら-をつけてプラスに直す)
        if (vx_abs > 6) ++vx_idx; // 一定値を超えるごとに加速段階を+1
        if (vx_abs > 9) ++vx_idx; // 最大0~2段階まで +1ずつされる
        
        if (vx_abs < 9) {
          isFlying = true; // 飛行モードにする
        }
        
        // 加速段階に応じて違うジャンプ音を鳴らす
        PlaySoundMem(Sound::jumpSE[vx_idx], DX_PLAYTYPE_BACK);
        
        v.y = -vyJumpSpeed[vx_idx]; // ジャンプ方向はY上方向(マイナス方向)
        vyForce = vyForceJump[vx_idx];
        yJumpStart = pos.y;
        isGround = false; // ジャンプ中はisGroundフラグがfalse
      }
    }
    else if (v.y >= 0) // 下落開始(vyが0未満のときはまだジャンプ中)
    {
      vyForce = vyGravity[vx_idx]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
    }
    else if (Input::GetButtonUp(PAD_INPUT_A)) // Zキーを離した瞬間
    {
      if (yJumpStart - pos.y >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
        vyForce = vyGravity[vx_idx]; // ジャンプ上昇中(vy未満のとき)にボタンを離したら早めに重力をかけ始める
    }

    
    if (!isDead) // やられたときは移動を停止
      pos.x += v.x; // 速度のぶん現在の位置から移動
    if (!isDead && pMapData != nullptr)
    {
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(pos.x) + 0.01f, xRight = getRight(pos.x) - 0.01f;
      float yTop = getTop(pos.y) + 0.01f, yBottom = getBottom(pos.y) - 0.01f;
      float yMiddle = yTop + (getBottom(pos.y) - getTop(pos.y)) / 2;
      float xMiddle = xLeft + (getRight(pos.x) - getLeft(pos.x)) / 2;
      if (pMapData->hitcheck(BlockBit::Damage, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに
      
      // 左端が壁にめりこんでいるか?
      if (pMapData->checkID(BlockBit::Wall, xLeft, yTop) || // 左上が壁か?
          pMapData->checkID(BlockBit::Wall, xLeft, yMiddle) || // 左中が壁か?
          pMapData->checkID(BlockBit::Wall, xLeft, yBottom)) // 左下が壁か?
      {
        debug_x = xLeft;
        debug_x_yTop = pos.y - Screen::Height / 2, debug_x_yBottom = pos.y + Screen::Height / 2;
        // std::fmodで小数点の余りを計算 めり込みすぎの余り = std::fmod(xLeft, CellSize)
        float wallRight = xLeft + pMapData->CellSize - (float)std::fmod(xLeft, pMapData->CellSize); // 壁の右端
        pos.x = wallRight - hitOffsetLeft; // 左端を壁の右端に沿わす
        v.x = 0;
      } // 右端が壁にめりこんでいるか?
      else if (
        pMapData->checkID(BlockBit::Wall, xRight, yTop) || // 右上が壁か?
        pMapData->checkID(BlockBit::Wall, xRight, yMiddle) || // 右中が壁か?
        pMapData->checkID(BlockBit::Wall, xRight, yBottom)) // 右下が壁か?
      {
        debug_x = xRight;
        debug_x_yTop = v.y - Screen::Height / 2, debug_x_yBottom = v.y + Screen::Height / 2;
        float wallLeft = xRight - (float)std::fmod(xRight, pMapData->CellSize); // 壁の左端
        pos.x = wallLeft + hitOffsetRight - imgSizeX; // 右端を壁の左端に沿わす
        v.x = 0;
      }
    }

    
    if (!isDead)
      pos.y += v.y;
    if (!isDead)
    {
      // 当たり判定の四隅の座標(±0.01fで1ピクセル隣)を取得
      float xLeft = getLeft(pos.x) + 0.01f, xRight = getRight(pos.x) - 0.01f;
      float yTop = getTop(pos.y) + 0.01f, yBottom = getBottom(pos.y) - 0.01f;
      float xMiddle = xLeft + (getRight(pos.x) - getLeft(pos.x)) / 2;
      float yMiddle = yTop + (getBottom(pos.y) - getTop(pos.y)) / 2;
      
      if (pMapData->hitcheck(BlockBit::Damage, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom))
        isDead = true; // やられたフラグをtrueに
      
      int mapX = 0, mapY = 0; // ぶつかったらそのブロックのCSVのセルの行mapYと列mapXを得る
      if ((int)HitPart::Top
          & pMapData->hitcheck(BlockBit::Bounce, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        std::pair<int, int> mapXY = std::make_pair(mapX, mapY); // ぶつかったブロックのセルの行mapYと列mapXのペア
        if (pMapData->cellTime.count(mapXY) == 0) // 辞書にキーがあるかcountで数えて未登録==0のとき
          pMapData->cellTime[mapXY] = 0; // ブロックのアニメ時間を辞書に登録(時間が辞書にあるものだけアニメが走る)
      }
      
      if ((int)HitPart::Top
          & pMapData->hitcheck(BlockBit::Switch, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        int id = (*pMapData)[mapY][mapX]; // 行mapY 列mapXの地形idを得る
        if ((pMapData->isSwitchOn && id == pMapData->id_Off || !pMapData->isSwitchOn && id == pMapData->id_On)
            && pMapData->blockSwitch.count(id) > 0)
        {
          pMapData->isSwitchOn = !pMapData->isSwitchOn; // trueとfalseを!で反転
          for (int cellY = 0; cellY < pMapData->Height; ++cellY) // 行をすべて調べる
            for (int cellX = 0; cellX < pMapData->Width; ++cellX) // 列をすべて調べる
              for (auto switchID : pMapData->blockSwitch[id]) // スイッチに反応するブロックのIDすべてを調べる
                if ((*pMapData)[cellY][cellX] == switchID)
                {
                  (*pMapData)[cellY][cellX] = (*pMapData).blockChange[switchID]; // 変化先のIDへ書き換える
                  break; // 次の列のループへ移る(3重for文の一番内側のループをbreakして1つ外のループへ)
                }
        }
      }
      
      bool isBlockChanging = false; // ブロック変化フラグ(あとから変化させないとWallの押し戻しの跳ね返りがなくなる)
      if ((int)HitPart::Top
          & pMapData->hitcheck(BlockBit::Change, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, &mapX, &mapY))
      {
        isBlockChanging = true;
      }
      
      // 頭の部分が壁にめりこんでいるか?
      if (pMapData->checkID(BlockBit::Wall, xLeft, yTop) || // 左上が壁か?
          pMapData->checkID(BlockBit::Wall, xMiddle, yTop) || // 中上が壁か?
          pMapData->checkID(BlockBit::Wall, xRight, yTop)) // 右上が壁か?
      {
        debug_y = yTop;
        debug_y_xLeft = pos.x - Screen::Width / 2, debug_y_xRight = pos.x + Screen::Width / 2;
        float wallBottom = yTop + pMapData->CellSize - (float)std::fmod(yTop, pMapData->CellSize); // 壁の下側の境界
        pos.y = wallBottom - hitOffsetTop;
        v.y = 0;
      } // 足元が壁にめりこんでいるか?
      else if (
          pMapData->checkID(BlockBit::Wall, xLeft, yBottom) || // 壁か?
          pMapData->checkID(BlockBit::Wall, xMiddle, yBottom) || // 壁か?
          pMapData->checkID(BlockBit::Wall, xRight, yBottom)) // 壁か?
      {
        debug_y = yBottom;
        debug_y_xLeft = pos.x - Screen::Width / 2, debug_y_xRight = pos.x + Screen::Width / 2;
        float wallTop = yBottom - (float)std::fmod(yBottom, pMapData->CellSize); // 壁の上側の境界
        pos.y = wallTop + hitOffsetBottom - imgSizeX;
        
        isGround = true;
        isFlying = false;
        
        v.y = 0;
        vx_idx = 0;
      }
      else
        isGround = false;
      
      if (isBlockChanging)
      {
        int id = (*pMapData)[mapY][mapX]; // 行mapY 列mapXの地形idを得る
        if (pMapData->blockChange.count(id) > 0) // blockChangeの辞書に登録のあるidのときは
        {
          int changeID = pMapData->blockChange[id]; // 変化先のID
          std::pair<int, int> mapXY = std::make_pair(mapX, mapY); // ぶつかったブロックのセルの行mapYと列mapXのペア
          if (!(pMapData->blockBits[changeID] & (int)BlockBit::Bounce)) // 変化先ブロックがバウンスするならアニメを続けたい
            if (pMapData->cellTime.count(mapXY) > 0)
              pMapData->cellTime.erase(mapXY); // ブロックの種類変える前にアニメをリセットしておかないと辞書に残り続ける
          
          (*pMapData)[mapY][mapX] = changeID; // idを入れ替える
        }
      }
    }

    
    v.x *= 0.97f; // 減速率
    v.y += vyForce; // 勢い(力・フォースを加算

    
    if (v.y >= vyDownSpeedMax[vx_idx]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
      v.y = vyDownSpeedMax[vx_idx];

    
    if (v.y >= 0.0f && zoomAnimCount > 0)
      --zoomAnimCount;
    else if (zoomAnimCount < 60) // 上方向の加速中は最大60までカウントしてズームアウト(広域を映す)
      ++zoomAnimCount;
    // 最大(1-0.25)倍ズーム
    pCamera->zoomRatio = (zoomAnimCount < 2) ? 1.0f : 1.0f - 0.25f * zoomAnimCount / 60.0f;

    
    pCamera->LookAt(pos.x, pos.y); // プレイヤのキャラ(x,y)位置を中心に追跡スクロール
    
    // ダッシュ段階に応じてキャラクタのアニメ時間を進める
    float vx_dash = (v.x < 0) ? -v.x : v.x; // 絶対値でプラスだけにする
    if (vx_dash > 0.5f) animeTime += 1; // 時間 +1 ( 合計 1 )
    if (vx_dash > 6) animeTime += 1; // 時間 +1 (計 1+1=2 ) 1フレームごとに2アニメ時間が進む
    if (vx_dash > 9) animeTime += 2; // 最終段階ではアニメ時間をさらに +2 (計 1+1+2=4 )

    
  };
  
  // 描画処理の関数定義
  virtual void Draw() override
  {
    int animeStep = animeTime / 10; // 10を5に変えたらアニメ速度が上がる(10ごとに1コマアニメが進む)
    if (animeStep >= 3) // アニメのステップ数が0,1,2の3段階までなら3を超えたら0へループ
    {
      animeTime = 0; // 0へ戻り再びアニメ時間を再ループ
      animeStep = 0;
    }

    
    // 描画処理
    if (isDead)
    {
      float yMiss = pos.y + imgSizeY / 2;
      float yAnim0 = yMiss;
      float yAnim1 = yMiss - 10;
      float yAnim2 = Screen::Height + 32;
      float yDeadAnim; // やられた際にはyの位置をアニメさせる
      
      // 直線的な比例アニメの式(t=時刻) 位置1×(終時刻 - t) / 終時刻 + 位置2×(t - 始時刻) / 終時刻
      if (deadAnimeTime < 40)
        yDeadAnim = yAnim0 * (40 - deadAnimeTime) / 40 + yAnim1 * (deadAnimeTime - 0) / 40;
      else if (deadAnimeTime < 60)
        yDeadAnim = yAnim1 * (60 - deadAnimeTime) / 20 + yAnim2 * (deadAnimeTime - 40) / 20;
      else
      {
        yDeadAnim = yAnim2;
        deadAnimeTime = 0;
        
        // 再開位置からゲーム再開(各数値もリセット)
        isDead = false;
        
        pos = posStart, v.x = 0, v.y = 0;
        isGround = false, isFlying = false;
        vx_idx = 0;
      }
      
      int deadAnimeStep = deadAnimeTime / 3;
      int chipAnime = deadAnimeStep % 3; // % 3で余りを求めれば余りは0~2の間に収まる
      pCamera->DrawRotaGraphF(v.x + imgSizeX / 2, yDeadAnim, 1.0f, 60.0f * deadAnimeStep / 5.0f / 360.0f * 2 * DX_PI, Image::playerChips[chipAnime], TRUE);
      
      ++deadAnimeTime; // アニメ時間を進める
    }
    else
    {
      if (isFlying && direction == Direction::Left) // 飛ぶと-60度傾けて表示 キャラチップは[4]で固定
        pCamera->DrawRotaGraphF(pos.x + imgSizeX / 2, pos.y + imgSizeY / 2, 1.0f, -60.0f / 360.0f * 2 * DX_PI, Image::playerChips[4], TRUE);
      else if (isFlying && direction == Direction::Right) // 60度回転させて表示 キャラチップは[7]で固定
        pCamera->DrawRotaGraphF(pos.x + imgSizeX / 2, pos.y + imgSizeY / 2, 1.0f, 60.0f / 360.0f * 2 * DX_PI, Image::playerChips[7], TRUE);
      else if (direction == Direction::Left) // キャラのチップの[5][4][3]を[5 - animeStep]で使う(animeStep=0,1,2)
        pCamera->DrawRotaGraphF(pos.x + imgSizeX / 2, pos.y + imgSizeY / 2, 1.0f, 0.0f, Image::playerChips[5 - animeStep], TRUE);
      else if (direction == Direction::Right) // キャラのチップの[6][7][8]を[6 + animeStep]で使う(animeStep=0,1,2)
        pCamera->DrawRotaGraphF(pos.x + imgSizeX / 2, pos.y + imgSizeY / 2, 1.0f, 0.0f, Image::playerChips[6 + animeStep], TRUE);
    }

    
    // [デバッグ] 当たり判定の四隅を赤枠で描画
    pCamera->DrawBox(getLeft(pos.x), getTop(pos.y), getRight(pos.x), getBottom(pos.y), GetColor(255, 0, 0), FALSE);
    // [デバッグ] キャラ原点(x,y)を緑丸で描画
    pCamera->DrawCircle(pos.x, pos.y, 2, GetColor(0, 255, 0), TRUE);
    // [デバッグ] 当たった際の行き過ぎの めりこみ座標(debug_x,debug_y) を青線で描画
    if (debug_x != -1) pCamera->DrawLine(debug_x, debug_x_yTop, debug_x, debug_x_yBottom, GetColor(0, 0, 255));
    if (debug_y != -1) pCamera->DrawLine(debug_y_xLeft, debug_y, debug_y_xRight, debug_y, GetColor(0, 0, 255));

    
  };

};

#endif