[C++]アクションゲームで基本練習
以下の【キャラを出すだけの最小限サンプル】でキャラクターが出るかを確認しましょう
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;
}
では次は【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;
}
今度は【キャラを左右キーで加速させる】ように改造してみましょう
以下の【キャラを左右キーで加速させるだけの最小限サンプル】でキャラクターを加速させられるか挑戦しましょう
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;
}
以下の【キャラを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;
}
以下の【初代マリオ風の処理(小数の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;
}
以下の【ダッシュの加速段階によってパラメータを切り替えるサンプル】でダッシュの段階に応じた高さでジャンプさせられるか挑戦しましょう
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;
}
以下の【ダッシュで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;
}
以下の【キャラチップ配列のダッシュ左[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;
}
// 描画処理
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);
あとは以下の【効果音を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;
}
さてまずは【地形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;
}
さてつぎは【地形のマップチップ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;
}
さてつぎは【キャラの四隅が壁のマップチップか調べて、めり込みの余りのぶんだけ押し戻すサンプル】で地形のブロックの当たり判定でブロックに乗れるようにしましょう。
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;
}
さてつぎは【キャラの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;
}
さてつぎは【キャラの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;
}
さてつぎは【トゲトゲに当たると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;
}
さてつぎは【ブロックにどの方向からめりこんだかを得られるようにして叩かれたブロックのマス目番号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;
}
(以上略)...
#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);
};
}
(以下略)...
(以上略)...
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; // ブロックのアニメ時間を辞書に登録(時間が辞書にあるものだけアニメが走る)
}
(以下略)...
さてつぎは【ブロック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;
}
さてつぎは【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;
}
DivImage.h
を新規作成して【DivImage型】分割された画像を読込みその分割のグリッドもパラメータとして保持できるようにしましょう。
#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;
}
まずは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;
}
まずはカメラの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());
つぎに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;
}
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;
}
まずはベースとなる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
ベースとなる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
ベースとなる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にプレイヤ以外も使うような共通の変数を移植
して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