[C++]シューティングゲーム 1
シューティングではなくアクションゲームで基礎を練習するなら ↓
https://yonezawashin.github.io/yonezawashin.github.io-2020c/practice.html
数学のベクトルの基礎を練習するなら ↓
https://yonezawashin.github.io/yonezawashin.github.io-2020c/vector.html
ポリゴンと3D描画とライティングの基礎を練習するなら(3Dエディタの作成) ↓
https://yonezawashin.github.io/yonezawashin.github.io-2020c/polygon3d/index.html
複数分割画面で複数プレイヤで遊ぶ3Dのタイルボックスゲームを制作するなら ↓
https://yonezawashin.github.io/yonezawashin.github.io-2020c/tile3d/index.html
テキスト2もこのファイル内にまとめた。
[C++]シューティングゲーム 2このHTMLのWEBテキストはBlueGriffonという無料のソフトで書いた
https://forest.watch.impress.co.jp/library/software/bluegriffon/
githubで自分のサイトを無料公開できるので自作サイトにも挑戦しては
シューティングの制作をC++で行うことで、以下を作業を通じて【体得しよう】
最終的には、C++で制作したゲームの完成を目標にしましょう。
なお、C++は下手にお金出して本を買うくらいなら以下のサイトが教科書代わりになりそうです。
・https://atcoder.jp/contests/apg4b
・https://zenn.dev/reputeless/books/standard-cpp-for-competitive-programming/viewer/string
・https://ezoeryou.github.io/cpp-intro/
新しいプロジェクトの作成
をクリックC++ 空のプロジェクト
を選択して次へ
今回のプロジェクト名は【1】「自分の作るゲーム名」か【2】毎回C++のプロジェクトを作るのが面倒な人は今回作るプロジェクトをフォルダごと量産するとして「DXLibGameBase」(ゲームの基礎ベース、コピペで量産用プロジェクト)にしましょう。
プロジェクトが作成されたら、まず【main.cpp】作りましょう。
少なくとも一つ【~.cpp】作らないと出てこない次にやる設定↓で【出てこない項目】があります(【C/C++全般】が出てこない!)
プロパティ
をクリック
マルチバイト文字セットを使用
に変更
DXライブラリのフォルダ位置(PATH)
を設定
DXライブラリのフォルダ位置(PATH)
を設定
Windows(SUBSYSTEM:WINDOWS)
に設定
以下の【ウィンドウを出すだけの最小限サンプル】を動かして設定を確認しましょう
main.cpp【ウィンドウを出すだけの最小限サンプル】
を編集します:
#include "DxLib.h"
// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// 画面モードの設定
SetGraphMode( 960 , 540 , 32 ) ; // 画面サイズ960×540のカラービット数32ビットで起動
SetWindowSize(960, 540);// ウィンドウサイズ960×540(こことSetGraphModeのサイズが異なると画像がゆがむ)
ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
SetMainWindowText("ゲームのウィンドウ名を変えるときはここ");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも
//↑ここまでの設定は↓下のDXライブラリ初期化より先にやらないとDxLib_Init()中は画面がフルスクリーンになって終わってからウィンドウサイズが変更になり見苦しい
// DXライブラリの初期化
if (DxLib_Init() < 0)
{
// DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
return -1;
}
// この位置にウィンドウの中に画像を描く処理などを書く
// DXライブラリ公式の色々なサンプルを試してみよう!
// https://dxlib.xsrv.jp/dxfunc.html
// キー入力待ちをする
WaitKey();
// DXライブラリの後始末
DxLib_End();
// ソフトの終了
return 0;
}
【C++で】画面解像度を定義するクラスを作ってみましょう。ソリューションエクスプローラーのプロジェクトを右クリックし、追加>ヘッダファイル(.h)
Screen.hという名前でヘッダファイルを生成し、そのファイルにクラスを記述します。
Screen.h
を編集します:
#ifndef _SCREEN_H
#define _SCREEN_H
// #ifndefは「_SCREEN_H」がifもしもn(not) def(定義)されていないなら#endifに挟まれたコードまでを有効化する(だから2度目以降は無効になる)
// 二重定義を防止するために#から始まる【プリプロセッサ】を使っている
// 【プリプロセッサ】はビルドする段階でコード自体に【前処理】として影響を与える
// たとえば
// ・ファイルの読み込み (including) →#includeがこれ
// ・マクロの展開(シンボルを、あらかじめ定義された規則に従って置換する)
// ・コンパイル条件によるソースコードの部分的選択→【#ifndef ~ #endifに囲まれた部分】は一度(#defineで)定義されると無効化
// ・コメントの削除 【 // 】のコメントの削除も【プリプロセス=前段階で行われる】
// 画面解像度
class Screen
{
public: //publicはC#と違いpublic:以下にまとめて書かれるスタイル
static const int Width = 960; // 幅
static const int Height = 540; // 高さ C#と違いstaticクラスではなく個々の【すべての変数にstaticをつけて】staticなクラスとする
};// C#と違ってクラスの定義も;セミコロンで終わる
#endif
main.cpp
を編集してScreen.hを読み込ませます:
#include "DxLib.h"
#include "Screen.h"
// ↑#includeで別ファイルのヘッダファイル.hをこのコード内に読み込む
// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// 画面モードの設定
SetGraphMode( Screen::Width, Screen::Height, 32 ) ; // 画面サイズWidth×Heightのカラービット数32ビットで起動
SetWindowSize(Screen::Width, Screen::Height);// ウィンドウサイズWidth×Height(こことSetGraphModeのサイズが異なると画像がゆがむ)
ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
SetMainWindowText("ゲームのウィンドウ名を変えるときはここ");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも
// DXライブラリの初期化
if (DxLib_Init() < 0)
{
// DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
return -1;
}
// (中略).....................................
// ソフトの終了
return 0;
}
次は画像ファイルをプロジェクトに追加します。フォルダは【ウィンドウズ上のエクスプローラで作成】します。
GIMPをダウンロードして画像をつくってみましょう。
https://forest.watch.impress.co.jp/library/software/gimp/
ダウンロードしたら起動して【ファイル】→【新しい画像】
キャンバスのサイズはとりあえず64×64でつくってみます(自由につくってもよいがサイズをおぼえておいて、あとで自分のプログラムで読み込むときに合わせないといけないよ)
透明のレイヤーをつくって背景が透けるようにしておく
消しゴムで消して透明にしておく
新しいレイヤをつくって、そこのレイヤに自分の画像をえがいていく
マウスで長押しして楕円選択をえらんで円形にえがくエリアをえらぶ
ぬりつぶしの色を決めて、えらんでいるエリアを円形にぬりつぶす
テキストのボタンをおして円のうちがわに自機という文字をうちこむ
画像を保存するフォルダをつくっておく
つくったフォルダにエクスポートする
自分でboss1.png、boss2.png、boss3.pngを180×180など別のサイズでつくってみたり、
webでゲーム素材、フリーなどのワードで検索して、素材をさがしてみよう(著作権や利用規約をよく読んで注意しないとお金とられて財布が空になっちゃうので注意!!)
https://pipoya.net/sozai/
https://qiita.com/TD12734/items/5dc8068732f94452efe2
えがいた画像をべつのフォルダに保存した場合は、下のアニメようにウィンドウズでImageフォルダーにコピーして貼り付けしてください。
main.cpp
にプログラムを追加し、画像が正しく表示できるかテストします。
#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
#include "Screen.h"
// #includeで別ファイルのヘッダファイル.hをこのコード内に読み込む
// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// 画面モードの設定
SetGraphMode(Screen::Width, Screen::Height, 32 ) ; // 画面サイズWidth×Heightのカラービット数32ビットで起動
SetWindowSize(Screen::Width, Screen::Height);// ウィンドウサイズWidth×Height(こことSetGraphModeのサイズが異なると画像がゆがむ)
ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
SetMainWindowText("ゲームのウィンドウ名を変えるときはここ");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも
// DXライブラリの初期化
if (DxLib_Init() < 0)
{
// DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
return -1;
}
//表示しているスクリーンの後ろで隠れて次に描く画像を先に描くモード
// これとペアでScreenFlip();でつぎのページと入れ替えでちらつきを防ぐ
SetDrawScreen(DX_SCREEN_BACK);
// ゲームのwhileループを開始する前の初期化処理
int bossImage = -1;
bossImage = LoadGraph("Image/boss1.png");
assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
int x = 100; // Xの初期位置
int vx = 10;
ScreenFlip();
// アニメーション(パラパラ漫画)するにはWhile文
while (ProcessMessage() == 0)
{// ProcessMessage() == 0になるのは×ボタン押したときなど
ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
// 1フレームごとの更新処理
if (x < 0)
{
vx = 10; //速度の変更
}
else if (x > Screen::Width)
{
vx = -10;
}
x += vx; // X位置の更新
// 描画処理(位置x, y, 拡大率, 回転, 画像ID, TRUEなら透過有効)
DrawRotaGraphF(x, 200, 1.0f, 0, bossImage, TRUE);
ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
}
// キー入力待ちをする
WaitKey();
// DXライブラリの後始末
DxLib_End();
// ソフトの終了
return 0;
}
Imageクラスを作成しましょう。Imageクラスの役割は次の2つです。
ソリューションエクスプローラーからプロジェクトを右クリックし、追加>ヘッダファイル
「Image」という名前でヘッダファイルを作り、クラスを定義します。
Image.h
の内容を次のようにします:
#ifndef IMAGE_H_
#define IMAGE_H_
#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
class Image
{
public:
Image() {}; // 初期化コンストラクタ:定義と{}空の処理
~Image() {}; // 破棄する処理デストラクタ:定義と{}空の処理
static void Load();
static int bossImage; //ボス画像のハンドラ(読込画像番号)
private:
};
#endif
次に対となるcppファイルを作成します。
ソリューションエクスプローラーからプロジェクトを右クリックし、追加>cppファイル
「Image.cpp」という名前でcppファイルを作り、クラスを実装します。
Image.cpp
の内容を次のようにします:
#include "Image.h"
int Image::bossImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
void Image::Load()
{
bossImage = LoadGraph("Image/boss1.png");
assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
}
main.cpp
のプログラムを変更し、画像が正しく表示できるかテストします。
#include "DxLib.h"
#include "Screen.h"
#include "Image.h" // 画像読み込みクラス
// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// 画面モードの設定
SetGraphMode(Screen::Width, Screen::Height, 32 ) ; // 画面サイズWidth×Heightのカラービット数32ビットで起動
SetWindowSize(Screen::Width, Screen::Height);// ウィンドウサイズWidth×Height(こことSetGraphModeのサイズが異なると画像がゆがむ)
ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
SetMainWindowText("ゲームのウィンドウ名を変えるときはここ");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも
// DXライブラリの初期化
if (DxLib_Init() < 0)
{
// DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
return -1;
}
//表示しているスクリーンの後ろで隠れて次に描く画像を先に描くモード
// これとペアでScreenFlip();でつぎのページと入れ替えでちらつきを防ぐ
SetDrawScreen(DX_SCREEN_BACK);
// ゲームのwhileループを開始する前の初期化処理
Image::Load();
int bossImage = -1;
bossImage = LoadGraph("Image/boss1.png");
assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
int x = 100; // Xの初期位置
int vx = 10;
ScreenFlip();
// アニメーション(パラパラ漫画)するにはWhile文
while (ProcessMessage() == 0)
{// ProcessMessage() == 0になるのは×ボタン押したときなど
ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
// 1フレームごとの更新処理
if (x < 0)
{
vx = 10; //速度の変更
}
else if (x > Screen::Width)
{
vx = -10;
}
x += vx; // X位置の更新
// 描画処理(位置x, y, 拡大率, 回転, 画像ID, TRUEなら透過有効)
DrawRotaGraphF(x, 200, 1.0f, 0, Image::bossImage, TRUE);
ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
}
// キー入力待ちをする
WaitKey();
// DXライブラリの後始末
DxLib_End();
// ソフトの終了
return 0;
}
Gameクラスを作成しましょう。Gameクラスの重要な役割は次の3つ。
ソリューションエクスプローラーからプロジェクトを[右クリック]し、[追加] → [新しい項目] → Game.hというファイル名を入力
「Game.h」という名前でヘッダファイルを作り、クラスを定義します。
Game.h
の内容を次のようにします:
#ifndef GAME_H_
#define GAME_H_
#include "Image.h"
#include "Screen.h"
class Game
{
public :
/* ↓{ }を.hに書くことでcppに分けて書くはずの処理ぶぶんを.hに書いてもよい */
Game() { }; // 初期化コンストラクタ
~Game() { }; // 破棄処理デストラクタ
void Init(); // Init処理(定義だけ)
void Update(); // 更新処理(定義だけ)
void Draw();// 描画処理(定義だけ)
int x = 0; //staticじゃないintはここで=0で初期化できる
int vx = 0;
};
#endif
次に対となるcppファイルを作成します。
ソリューションエクスプローラーからプロジェクトを[右クリック]し、[追加] → [新しい項目] → Game.cppというファイル名を入力
「Game.cpp」という名前でcppファイルを作り、クラスの処理を実装します。
Game.cpp
の内容を次のようにします:
#include "Game.h"
void Game::Init()
{// Init処理
Image::Load(); //画像の読込み
x = 100; // Xの初期位置
vx = 10; // ボスの初期速度
}
void Game::Update()
{// 更新処理
if (x < 0)
{
vx = 10; //速度の変更
}
else if (x > Screen::Width)
{
vx = -10; //画面端で移動方向反転
}
x += vx; // ボス画像のX位置の更新
}
void Game::Draw()
{// 描画処理
DrawRotaGraphF(x, 200, 1.0f, 0, Image::bossImage, TRUE);
}
main.cpp
のプログラムを変更し、画像が正しくGameクラスで表示できるかテストします。
#include "DxLib.h"
#include "Screen.h"
#include "Image.h" // 画像読み込みクラス
#include "Game.h"
// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// 画面モードの設定
SetGraphMode(Screen::Width, Screen::Height, 32 ) ; // 画面サイズWidth×Heightのカラービット数32ビットで起動
SetWindowSize(Screen::Width, Screen::Height);// ウィンドウサイズWidth×Height(こことSetGraphModeのサイズが異なると画像がゆがむ)
ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
SetMainWindowText("ゲームのウィンドウ名を変えるときはここ");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも
// DXライブラリの初期化
if (DxLib_Init() < 0)
{
// DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
return -1;
}
//表示しているスクリーンの後ろで隠れて次に描く画像を先に描くモード
// これとペアでScreenFlip();でつぎのページと入れ替えでちらつきを防ぐ
SetDrawScreen(DX_SCREEN_BACK);
// ゲームのwhileループを開始する前の初期化処理
Image::Load();
int x = 100; // Xの初期位置
int vx = 10;
// Init初期化処理
Game game; //Gameの定義(定義と同時に初期化コンストラクタが実行される)
game.Init(); // gameのInit準備
ScreenFlip();
// アニメーション(パラパラ漫画)するにはWhile文
while (ProcessMessage() == 0)
{// ProcessMessage() == 0になるのは×ボタン押したときなど
ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
// Update更新処理
game.Update(); // gameの更新処理
// Draw描画処理
game.Draw();
ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
}
// キー入力待ちをする
WaitKey();
// DXライブラリの後始末
DxLib_End();
// ソフトの終了
return 0;
}
Inputクラスを作成しましょう。
ソリューションエクスプローラーからプロジェクトを[右クリック]し、[追加] → [新しい項目] → Game.hというファイル名を入力
「Input.h」という名前でヘッダファイルを作り、クラスを定義します。
Input.hを新規作成して
以下を実装
#ifndef INPUT_H_
#define INPUT_H_
#include "DxLib.h"
// 入力クラス
class Input
{
public:
static int prevState; // 1フレーム前の状態
static int currentState; // 現在の状態
// 初期化。最初に1回だけ呼んでください。
static void Init()
{
prevState = 0;
currentState = 0;
}
// 最新の入力状況に更新する処理。
// 毎フレームの最初に(ゲームの処理より先に)呼んでください。
static void Update()
{
prevState = currentState;
currentState = GetJoypadInputState(DX_INPUT_KEY_PAD1);
}
// ボタンが押されているか?
static bool GetButton(int buttonId)
{
// 今ボタンが押されているかどうかを返却
return (currentState & buttonId) != 0;
}
// ボタンが押された瞬間か?
static bool GetButtonDown(int buttonId)
{
// 今は押されていて、かつ1フレーム前は押されていない場合はtrueを返却
return ((currentState & buttonId) & ~(prevState & buttonId)) != 0;
}
// ボタンが離された瞬間か?
static bool GetButtonUp(int buttonId)
{
// 1フレーム前は押されていて、かつ今は押されていない場合はtrueを返却
return ((prevState & buttonId) & ~(currentState & buttonId)) != 0;
}
}; //←【注意】クラス定義の終わりにはコロンが必要だよ!
#endif //ここでエラー出た人は↑【注意】の;コロン忘れ
次に対となるcppファイルを作成します。
ソリューションエクスプローラーからプロジェクトを[右クリック]し、[追加] → [新しい項目] → Input.cppというファイル名を入力
「Input.cpp」という名前でcppファイルを作ります(たった2行の初期化static int変数の初期化のために必須)。
Input.cppを新規作成して
、その内容を次のようにします:
#include "Input.h"
int Input::prevState { 0 }; // 1フレーム前の状態
int Input::currentState { 0 }; // 現在の状態
// ↑static intのときはこの行書かないとシンボリックエラーになる件(このためだけにcpp書かなきゃならん)
Playerクラスを作成する前に、忘れずにプレイヤ画像はDXのプロジェクトのImageフォルダにコピー貼り付けしましょう。
Imageフォルダにコピーしたらプロジェクトで
次は画像ファイルをプロジェクトに追加します。画像は【プロジェクトを右クリックで「追加」→「既存の項目」で画像を追加】します。
Image.h
の内容を次のようにします:
#ifndef IMAGE_H_
#define IMAGE_H_
#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
class Image
{
public:
Image() {}; // 初期化コンストラクタ:定義と{}空の処理
~Image() {}; // 破棄する処理デストラクタ:定義と{}空の処理
static void Load();
static int bossImage; //ボス画像のハンドラ(読込画像番号)
static int player; //プレイヤ画像のハンドラ
private:
};
#endif
次に対となるcppファイルも変更します。
Image.cpp
の内容を次のようにします:
#include "Image.h"
int Image::bossImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{-1};
void Image::Load()
{
bossImage = LoadGraph("Image/boss1.png");
assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
player= LoadGraph("Image/player.png");
assert(player!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
}
Playerクラスを作成しましょう。
Playerクラスをつくるだけで安心すると【落とし穴】があります。Game.cppでInput.Update();を忘れるとプレイヤが動きません。
Update()やDraw()やInput.Update()を忘れないように注意して進めましょう。
ソリューションエクスプローラーからプロジェクトを[右クリック]し、[追加] → [新しい項目] → Player.hというファイル名を入力
「Player.h」という名前でヘッダファイルを作り、クラスを定義します。
Player.h
の内容を次のようにします:
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
class Player
{
public:
const float MoveSpeed = 6; // 移動速度
float x; // x座標
float y; // y座標
// コンストラクタ
// x : 初期位置x
// y : 初期位置y
Player(float x, float y)
{
this->x = x;
this->y = y;
}
// 更新処理
void Update()
{
float vx = 0; // x方向移動速度
float vy = 0; // y方向移動速度
if (Input::GetButton(PAD_INPUT_LEFT))
{
vx = -MoveSpeed; // 左
}
else if (Input::GetButton(PAD_INPUT_RIGHT))
{
vx = MoveSpeed; // 右
}
if (Input::GetButton(PAD_INPUT_UP))
{
vy = -MoveSpeed; // 上
}
else if (Input::GetButton(PAD_INPUT_DOWN))
{
vy = MoveSpeed; // 下
}
// 実際に位置を動かす
x += vx;
y += vy;
}
// 描画処理
void Draw()
{
DrawRotaGraphF(x, y, 1.0f, 0, Image::player, TRUE);
}
};
#endif
Game.h
にPlayerを追加します:
#ifndef GAME_H_
#define GAME_H_
#include "Image.h"
#include "Screen.h"
#include "Player.h"
class Game
{
public :
/* ↓{ }を.hに書くことでcppに分けて書くはずの処理ぶぶんを.hに書いてもよい */
Game() { }; // 初期化コンストラクタ
~Game() { }; // 破棄処理デストラクタ
void Init(); // Init処理(定義だけ)
void Update(); // 更新処理(定義だけ)
void Draw();// 描画処理(定義だけ)
int x = 0; //staticじゃないintはここで=0で初期化できる
int vx = 0;
Player player{ 100, Screen::Height / 2 }; // 自機の初期化
};
#endif
次にGame.cppファイルにUpdateとDraw処理を追加してプレイヤの動作を確認しましょう。
Game.cpp
の内容を次のようにします:
#include "Game.h"
void Game::Init()
{// Init処理
Image::Load(); //画像の読込み
x = 100; // Xの初期位置
vx = 10; // ボスの初期速度
}
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
if (x < 0)
{
vx = 10; //速度の変更
}
else if (x > Screen::Width)
{
vx = -10; //画面端で移動方向反転
}
x += vx; // ボス画像のX位置の更新
player.Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
}
void Game::Draw()
{// 描画処理
DrawRotaGraphF(x, 200, 1.0f, 0, Image::bossImage, TRUE);
player.Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
}
さて、無事にプレイヤをキー入力(↑↓←→)で動かせるようになりましたか???。
【C++でも】斜め方向の移動速度を√2で割って補正するために、自分用数学定義クラスを作ってみましょう。ソリューションエクスプローラーのプロジェクトを右クリックし、追加>ヘッダファイル(.h)
MyMath.hという名前でヘッダファイルを生成し、そのファイルにクラスを記述します。
MyMath.hを新規作成して
その内容を編集します:
#ifndef _MYMATH_H
#define _MYMATH_H
// 数学関連クラス
class MyMath
{
public:
// C++標準では、静的static定数は【整数型】または【列挙型】のみを.hのクラス内で初期化できます。
// これが、intやenumの初期化はエラーにならないのに他のstatic floatなどがエラーになる理由です。
// ルート2(変数名の定義だけ)
static const float Sqrt2; // = 1.41421356237f;(.hではfloat小数型の初期化はできない int型はできるのに)
}; // 【注意】セミコロン抜けで【宣言が必要ですエラー】
#endif // 【宣言が必要ですエラーは上のセミコロン抜け】
MyMath.cppを新規作成して
その内容を編集して変数名にSqrt2 = √2 = 1.41421356237f を 割り当てます。
#include "MyMath.h"
const float MyMath::Sqrt2 = 1.41421356237f;//(floatは有効桁は実質7桁まで正確だがそれ以降は環境によって誤差出る)
定義だけじゃなく、実際にプレイヤを動かしているPlayer.hも編集しなきゃ意味がないよ!お忘れなく!!
Player.h
の内容を次のようにします:
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"
class Player
{
public:
const float MoveSpeed = 6; // 移動速度
float x; // x座標
float y; // y座標
// コンストラクタ
// x : 初期位置x
// y : 初期位置y
Player(float x, float y)
{
this->x = x;
this->y = y;
}
// 更新処理
void Update()
{
float vx = 0; // x方向移動速度
float vy = 0; // y方向移動速度
if (Input::GetButton(PAD_INPUT_LEFT))
{
vx = -MoveSpeed; // 左
}
else if (Input::GetButton(PAD_INPUT_RIGHT))
{
vx = MoveSpeed; // 右
}
if (Input::GetButton(PAD_INPUT_UP))
{
vy = -MoveSpeed; // 上
}
else if (Input::GetButton(PAD_INPUT_DOWN))
{
vy = MoveSpeed; // 下
}
// 斜め移動も同じ速度になるように調整
if (vx != 0 && vy != 0)
{
vx /= MyMath::Sqrt2;
vy /= MyMath::Sqrt2;
}
// 実際に位置を動かす
x += vx;
y += vy;
}
// 描画処理
void Draw()
{
DrawRotaGraphF(x, y, 1, 0, Image::player, TRUE);
}
};
#endif
ソリューションエクスプローラーからプロジェクトを[右クリック]し、[追加] → [新しい項目] → PlayerBullet.hというファイル名を入力
「PlayerBullet.h」という名前でヘッダファイルを作り、クラスを定義します。
PlayerBullet.h
の内容を次のようにします:
#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_
#include "DxLib.h"
#include "Image.h"
// 自機弾クラス
class PlayerBullet
{
public:
const float Speed = 25; // 移動速度
float x; // x座標
float y; // y座標
// コンストラクタ
PlayerBullet(float x, float y)
{
this->x = x;
this->y = y;
}
// 更新処理
void Update()
{
// 移動
x += Speed;
}
// 描画処理
void Draw()
{
DrawRotaGraphF(x, y, 1, 0, Image::playerBullet,TRUE);
}
};
#endif
Image.h
に自機弾PlayerBullet画像定義を追加します:
#ifndef IMAGE_H_
#define IMAGE_H_
#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
class Image
{
public:
Image() {}; // 初期化コンストラクタ:定義と{}空の処理
~Image() {}; // 破棄する処理デストラクタ:定義と{}空の処理
static void Load();
static int bossImage; //ボス画像のハンドラ(読込画像番号)
static int player; //プレイヤ画像のハンドラ
static int playerBullet; //プレイヤの弾画像のハンドラ
private:
};
#endif
次に対となるcppファイルも変更します。
Image.cpp
の内容を次のようにします:
#include "Image.h"
int Image::bossImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{-1};
int Image::playerBullet{-1};
void Image::Load()
{
bossImage = LoadGraph("Image/boss1.png");
assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
player= LoadGraph("Image/player.png");
assert(player!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
playerBullet = LoadGraph("Image/player_bullet.png");
assert(playerBullet!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
}
Game.h
にPlayerBulletを追加します:
#ifndef GAME_H_
#define GAME_H_
#include <vector>// 配列std::vectorを使う
#include <memory>//スマートなポインタを使うのに必要(shared_ptrなど)
#include "Image.h"
#include "Screen.h"
#include "Player.h"
//前方宣言でPlayerBulletという【ポインタの型】があることだけを宣言すれば#includeせずにstd::vectorの中身にPlayerBulletのリンクだけなら入れられる
class PlayerBullet;
class Game
{
public :
Game() {}; // 初期化コンストラクタ
~Game() {}; // 破棄処理デストラクタ
void Init(); // Init処理(定義だけ)
void Update(); // 更新処理(定義だけ)
void Draw();// 描画処理(定義だけ)
int x = 0;
int vx = 0;
Player player{ 100, Screen::Height / 2 }; // 自機の初期化
std::shared_ptr<Player> player{ std::make_shared<Player>(100, Screen::Height / 2, this) }; // 自機の初期化
//↑プレイヤを回し読みポインタ型に。std::make_shared<Player>でメモリ上に実体を生成
std::vector<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
//可変長配列vector ↑【shared_ptr:共有ポインタ】
// ★【shared_ptr:共有ポインタ】とは【データの回し読み(読まれなくなったら廃棄)】
// 「例えば、兄弟で週刊少年ジャンプを【回し読みするときは】
// おかんに【全員読み終わるまではメモリから捨てられないように】する
// 【読むつもりの人のカウンタ.use_count()】がゼロになると【おかんに捨てられる(リンク切れはメモリから廃棄する)】」
};
#endif
次にGame.cppファイルにUpdateとDrawに自機弾更新と描画の処理を追加しましょう。
Game.cpp
の内容を次のようにします:
#include "Game.h"
#include "PlayerBullet.h" // ★Update(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
Image::Load(); //画像の読込み
x = 100; // Xの初期位置
vx = 10;
};
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
if (x < 0)
{
vx = 10; //速度の変更
}
else if (x > Screen::Width)
{
vx = -10;
}
x += vx; // X位置の更新
player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
//ポインタ↑になると.から->でのアクセスになる
// 自機弾の更新処理
// for文で全自機弾をループで回してUpdateで更新
for (const auto& b : playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Update();
}
//【放置厳禁!!!このままここに弾の削除処理書かないとメモリがどんどん減ってくよ(気づかないうちにどんどん使用メモリが増える..怖)】
};
void Game::Draw()
{// 描画処理
DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);
player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
//ポインタ↑になると.から->でのアクセスになる
// 自機弾の描画処理
// for文で全自機弾をループで回してUpdateで更新
for (const auto& b : playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Draw();
}
};
Player.h
にPlayerBulletへの参照を得るためGameへのポインタ参照を追加します:
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"
class Game; //←【エラー地獄に注意!】ここにGameクラスがあるものとして書くことで【循環相互インクルード問題】を回避
//↑Gameクラス内にPlayer変数を持っているからPlayerの定義が終わらないとGameの定義が完了しない(未定義エラー)
//【なのに】Playerクラス内にGame変数を持っているからPlayerの定義も終わらない!!(未定義エラー)
//【つまりPlayerとGameが互いに定義が終わるのを待つ】【循環相互インクルードどうぞどうぞ状態】になる
//【解決するために】とりあえずclass Game;とだけここに書いてGameは定義完了済と見せかけているわけです!
//[エラー:Playerは未定義です]とか[エラー:Gameは未定義です]とか定義してるはずのものが未定義の場合は大体これ!!
class Player
{
public:
const float MoveSpeed = 6; // 移動速度
float x; // x座標
float y; // y座標
Game* game { nullptr }; // Gameのインスタンスの参照
//↑ * ポインタ型です!プレイヤの【内部に】ゲームの情報を【メモリ実体として持つ】と
//そのプレイヤを親のゲームがもつと【延々とループで終わりなきメモリ爆発ループ】になるから
//メモリに対しての参照=【レコードの針をGameに指して】【Gameの住所アドレスだけをプレイヤ内部に持つ】
// コンストラクタ
// x : 初期位置x
// y : 初期位置y
Player(float x, float y, Game* game)
{
this->x = x;
this->y = y;
this->game = game;
}
// 更新処理
void Update();//【★←Player.cppを新規作成してそちらに処理を書き移す】
// 描画処理
void Draw();//【★←Player.cppを新規作成してそちらに処理を書き移す】
};
#endif
↑Player.hにあった上記処理部分を↓Player.cppを新規作成して書き移します
Player.cpp
にPlayerBulletの発射処理を追加します:
#include "Player.h"
#include "Game.h" // ★game->playerBulletsなどClass Game;でカバーできない【定義の内部のplayerBulletsにアクセスするのでインクルード必要】
#include "PlayerBullet.h" // ★弾を生成して【使う処理があるのでインクルード必要】
// 更新処理【書き移す際にはPlayer::という形で::所属が必要になります】
void Player::Update()
{
float vx = 0; // x方向移動速度
float vy = 0; // y方向移動速度
if (Input::GetButton(PAD_INPUT_LEFT))
{
vx = -MoveSpeed; // 左
}
else if (Input::GetButton(PAD_INPUT_RIGHT))
{
vx = MoveSpeed; // 右
}
if (Input::GetButton(PAD_INPUT_UP))
{
vy = -MoveSpeed; // 上
}
else if (Input::GetButton(PAD_INPUT_DOWN))
{
vy = MoveSpeed; // 下
}
// 斜め移動も同じ速度になるように調整
if (vx != 0 && vy != 0)
{
vx /= MyMath::Sqrt2;
vy /= MyMath::Sqrt2;
}
// 実際に位置を動かす
x += vx;
y += vy;
// ボタン押下で自機弾を発射
if (Input::GetButtonDown(PAD_INPUT_1))
{
// emplace_backで配列に追加。make_shared↓でプレイヤ弾を新しく生成
game->playerBullets.emplace_back(std::make_shared<PlayerBullet>(x, y));
}
}
// 描画処理
void Player::Draw()
{
DrawRotaGraphF(x, y, 1, 0, Image::player, TRUE);
}
【確認してみましょう】ここまでで一旦弾が打てるかチェックしてみましょう!【メモリは削除されないバージョンなので要注意!!】
【さてここで大量のエラーで詰んだ人も多いのでは】おそらくは【Player.hに弾を弾を撃つ処理を書いたのでは】
それにしても【なぜヘッダh.ファイルとcppファイルに書く違いだけで大量のエラー】が出てしまうのでしょう?
原因である【循環参照・循環インクルード】について説明します。
実はインクルードしたPlayer.hやGame.hは【⇒ビルド】した際に【Player.hの中身が#include "Player.h"へとコピペ】されるのです
そして問題は相互に【Player.hで#include "Game.h"】⇔【Game.hで#include "Player.h"】すると
互いへの【コピペがループする】ため【延々とGameかPlayerの定義が完成せず】
結果、邪悪なことに【全然関係ないPlayerBulletが未定義などと、とばっちりエラーが起こる】のです
さてこの【循環参照・循環インクルード】への対策をどうするかが問題です。
【1】 ヘッダ.hでのインクルードを避け.cppファイルでインクルードするのが基本的には有効です(ヘッダ.h同士が相互にコピペしあわないから)
【2】 しかし根本的には【★相互に参照しあう構造の設計自体すっきりさせたい】です。
今はPlayer⇔Gameの1対1の関係でまだ把握可能ですが、ザコや敵の弾などが他に増えてくると
恐怖の三角関係や四角関係など【どこかで一周回ると無限インクルード発生】が起こってきます。
まだ【1対1の循環なら見抜ける】けど【★三角ループや四角ループは発見が難しすぎ】です。
そう、相互に行き来する【★矢印を一つへ集中させる】のです。
こういったパターンを【★仲介者(Meditator:メディエーター)パターン】あるいは【★窓口(Facade:ファサード)パターン】と呼びます。
このパターンは【循環参照問題以外にも色んな利点】があり多用されます。
一点に集中させれば【メモリの状況もここに集中します】し
ここを経由すれば【どこからでもどこへでも連絡でき指令が出せます】
そしていちいち初期化の時に【参照の連絡先の交換が必要なくなります】
ついでにもう一つ【★唯一物(シングルトン)パターン】も取り入れておきます。
こういった先人の生み出したパターンを【デザインパターン】といいます。
有用なデザインパターンは23ほどあり、状況に合わせてうまく取り入れることが上級者への道で
変な構造で作っていると起こる面倒な時間のロスが減り【残業時間が減ります】
案外【ブラックな現場は構造のデザインそのものが悪かった】ことも多いです(なんかいちいちやりづらい環境)
こういう【構造自体に疑問を持てる人が開発のリーダーシップを取れる】と心地よく制作が進むので
時間とスキルに余裕ができたら将来のリーダーに向けて【デザイン構造パターン】を勉強するとよいかもしれません。
さて、ではまずSingleton.h
を追加しましょう。
コード中身は難しい文法が多いですが、
基本の要点は
【1】「代入」「コピー」「実体ポインタへの直接アクセス」を★private:として【外部から禁止】
【2】「実体へは★GetInstance()関数経由でしかアクセスできない」よう窓口を絞り
【3】「テンプレ化」★template<class T>して何でも唯一化できるようになっています
難しいコードなので導入するだけでも良いですが、
C++の「代入」「コピー」「テンプレ化」は基礎として大事なので頑張って6割ほどは理解できるようになりたいですね。
2,3年後上級者となって読み返して9割ほど理解できるようになるよう成長してゆきましょう。
Singleton.h
#ifndef SINGLETON_H_
#define SINGLETON_H_
// テンプレートT型シングルトンSingleton<~> ~になんでも指定して唯一物にできる
// ★Singleton<T>型は【必ず唯一で複製不可】(使い方:どこからでもアクセスし変数を共有するクラス向き)
// ★ゲーム内の【どこからアクセスしても統一性が保たれる】(唯一だから)「クラス名と実体が一致」
template<class T>
class Singleton
{
public:
// GetInstanceを通して【しか】T型クラスをつくれないうえに、
// ★作ったものは【かならず唯一で複製不可】
// それが【シングルトン型】!
// ゲーム中で唯一で複製不可にしたい【ゲームを一律管理するクラス】などに最適!
// https://teratail.com/questions/17416
// http://rudora7.blog81.fc2.com/blog-entry-393.html
// ②↓同じ関数の定義はプログラム全体で1つだけしか許されません。(static関数名は被らない)
static inline T& GetInstance()
{ //①↓関数の中で定義されたstatic変数は、関数の定義毎に1つ領域が確保されます。
static T instance; //メモリ上にstatic変数確保(静かに常に待機するinstance)
return instance; //①=(常駐)②=(唯一) ①と②両方満たすinstanceは【メモリ上に唯一】
}
//★【{}内のローカルstaticの特性:ラベル(上の例ではinstance)がリセットされずに常駐】https://monozukuri-c.com/langc-static-memory/
//つまり、ゲームマネージャなどを★リセットされず常駐状態にできるわけです
protected:
Singleton() {} // 外部でのインスタンス作成は禁止(protected:内部公開、外部禁止)
virtual ~Singleton() {} // 仮想デストラクタ(シングルトンを【継承するためには忘れちゃダメ】)
private: //代入やコピーをprivateにして外部クラスからアクセスできないように【縛る】
void operator=(const Singleton& obj) {} // 代入演算子禁止
Singleton(const Singleton& obj) {} // コピーコンストラクタ禁止(コピーできない【唯一】)
static T* instance; // private:だから外部アクセスできない【GetInstance()経由しかアクセスできないように縛る】
};
// いつもcppで書いたstatic変数初期化も★【Tテンプレートクラスだからすべて.hヘッダにまとめて書いた】
template< class T >
T* Singleton< T >::instance = 0; //cppでの定義を.h内に書いた(static変数はclass内で初期化できないから)
#endif
続いて仲介の役割を担当するGameManager.h
を作成します。
#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_
#include <memory>
#include <vector>
#include "Singleton.h"
class Player; // クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class PlayerBullet;
class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可
std::shared_ptr<Player> player{ nullptr }; // 自機の初期化
std::vector<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
// 複数リスト↑vector ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる
protected:
GameManager() {}; // 外部からのインスタンス作成は禁止
virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};
#endif
Game.h
からGameManager.hに移行した処理を消して代わりにGameMangerへの連絡先gmを確保します。
#ifndef GAME_H_
#define GAME_H_
#include <vector>//配列 std::vectorを使う
#include <memory>//スマートなポインタを使うのに必要(shared_ptrなど)
#include "Image.h"
#include "Screen.h"
#include "Player.h"
//前方宣言でPlayerBulletという【ポインタの型】があることだけを宣言すれば#includeせずにstd::vectorの中身にPlayerBulletのリンクだけなら入れられる
class PlayerBullet;
#include "GameManager.h"
class Game
{
public :
Game() {}; // 初期化コンストラクタ
~Game() {}; // 破棄処理デストラクタ
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
void Init(); // Init処理(定義だけ)
void Update(); // 更新処理(定義だけ)
void Draw();// 描画処理(定義だけ)
int x = 0;
int vx = 0;
std::shared_ptr<Player> player{ std::make_shared<Player>(100, Screen::Height / 2, this) }; // 自機の初期化
//↑プレイヤを回し読みポインタ型に。std::make_shared<Player>でメモリ上に実体を生成
std::vector<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
};
#endif
次にGame.cppファイルのPlayerやPlayerBulletのアクセスをGamaManagerのgm経由に書き換えましょう。
Game.cpp
の内容を次のように変更します:
#include "Game.h"
#include "Player.h" // ★初期化やUpdate(),Draw()など【実際の処理の関数を呼出すのでインクルード必要】
#include "PlayerBullet.h" // ★Update(),Draw()など【実際の処理の関数を呼出すのでインクルード必要】
void Game::Init()
{// Init処理
Image::Load(); //画像の読込み
x = 100; // Xの初期位置
vx = 10;
gm.player = std::make_shared<Player>((float)100, (float)(Screen::Height / 2)); // 自機の初期化
};
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
if (x < 0)
{
vx = 10; //速度の変更
}
else if (x > Screen::Width)
{
vx = -10;
}
x += vx; // X位置の更新
gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
//ポインタ↑になると.から->でのアクセスになる
// 自機弾の更新処理
// C#のforeach文と同じく全自機弾を更新できる
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Update();
}
//【放置厳禁!!!このままここに弾削除処理書かないとメモリがどんどん減ってくよ(気づかないうちにね..怖)】
};
void Game::Draw()
{// 描画処理
DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);
gm.player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
//ポインタ↑になると.から->でのアクセスになる
// 自機弾の描画処理
// C#のforeach文と同じく全自機弾を更新できる
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Draw();
}
};
Player.h
もGameManagerのgm経由でのアクセスにするので#include "Game.h"やGame*のポインタはなくなります。
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"
class Game;//←【エラー地獄に注意!】ここにGameクラスがあるものとして書くことで【循環相互インクルード問題】を回避
#include "GameManager.h"
class Player
{
public:
const float MoveSpeed = 6; // 移動速度
float x; // x座標
float y; // y座標
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
Game* game { nullptr }; // Gameのインスタンスの参照
//↑ * ポインタ型です!プレイヤの【内部に】ゲームの情報を【メモリ実体として持つ】と
//そのプレイヤを親のゲームがもつと【延々とループで終わりなきメモリ爆発ループ】になるから
//メモリに対しての参照=【レコードの針をGameに指して】【Gameの住所アドレスだけをプレイヤ内部に持つ】
// コンストラクタ
// x : 初期位置x
// y : 初期位置y
Player(float x, float y, Game* game)
{
this->x = x;
this->y = y;
this->game = game;
}
// 更新処理の関数定義
void Update();
// 描画処理の関数定義
void Draw();
};
#endif
Player.cpp
もGameManagerのgm経由でのアクセスに書き換えます
#include "Player.h"
#include "Game.h"
#include "PlayerBullet.h" // ★弾を生成して【使う処理があるのでインクルード必要】
// 更新処理【書き移す際にはPlayer::という形で::所属が必要になります】
void Player::Update()
{
float vx = 0; // x方向移動速度
float vy = 0; // y方向移動速度
if (Input::GetButton(PAD_INPUT_LEFT))
{
vx = -MoveSpeed; // 左
}
else if (Input::GetButton(PAD_INPUT_RIGHT))
{
vx = MoveSpeed; // 右
}
if (Input::GetButton(PAD_INPUT_UP))
{
vy = -MoveSpeed; // 上
}
else if (Input::GetButton(PAD_INPUT_DOWN))
{
vy = MoveSpeed; // 下
}
// 斜め移動も同じ速度になるように調整
if (vx != 0 && vy != 0)
{
vx /= MyMath::Sqrt2;
vy /= MyMath::Sqrt2;
}
// 実際に位置を動かす
x += vx;
y += vy;
// ボタン押下で自機弾を発射
if (Input::GetButtonDown(PAD_INPUT_1))
{
// emplace_backで配列に追加。make_shared↓でプレイヤ弾を新しく生成
gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(x, y));
}
}
// 描画処理
void Player::Draw()
{
DrawRotaGraphF(x, y, 1, 0, Image::player, TRUE);
}
PlayerBulletクラスを改造しましょう。
PlayerBullet.h
の内容を次のように【改造】します:
#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_
#include "DxLib.h"
#include "Image.h"
#include "Screen.h"
// 自機弾クラス
class PlayerBullet
{
public:
const float Speed = 25; // 移動速度
const int VisibleRadius = 16; // 見た目の半径
// ↓【使用要注意!(テスト用!!)】メモリ消費弾(4MB)撃つたびに4MB消費(下の行のコメント外すと)
//【使用注意】int memMUDA[1000000]; // 4バイト×1000000= 4MBの無駄メモリ
float x; // x座標
float y; // y座標
bool isDead = false; // 死亡フラグ
// コンストラクタ
PlayerBullet(float x, float y)
{
this->x = x;
this->y = y;
}
// 更新処理
void Update()
{
// 移動
x += Speed;
// 画面外に出たら、死亡フラグを立てる
if (x - VisibleRadius > Screen::Width)
{
isDead = true;
}
}
// 描画処理
void Draw()
{
DrawRotaGraphF(x, y, 1, 0, Image::playerBullet,TRUE);
}
};
#endif
次にGame.cppファイルに自機弾PlayerBulletの【削除処理】を追加しましょう。
Game.cpp
の内容を次のようにします:
#include "Game.h"
#include "Player.h" // ★初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // ★Update(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
Image::Load(); //画像の読込み
x = 100; // Xの初期位置
vx = 10;
gm.player = std::make_shared<Player>((float)100, (float)Screen::Height / 2); // 自機の初期化
};
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
if (x < 0)
{
vx = 10; //速度の変更
}
else if (x > Screen::Width)
{
vx = -10;
}
x += vx; // X位置の更新
gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
// 自機弾の更新処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Update();
}
//return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
// 自機弾のリストから死んでるものを除去する
// 非常に複雑な式に見えるがこれでisDeadのものを削除
// https://stackoverflow.com/questions/42723205/remove-if-from-a-stdvector-of-shared-pointers-with-a-member-function
gm.playerBullets.erase(
std::remove_if(gm.playerBullets.begin(), gm.playerBullets.end(),
[](std::shared_ptr<PlayerBullet> &ptr) {
//int usingCount = -1;//テスト用コード(以下のコメントアウト部分は省略可)
//if (ptr->isDead) usingCount = ptr.use_count();//おかんのコレ捨てるよカウンタ
//弾を捨てるまでの残り↑カウント(1だとisDeadがtrueなら0になり削除へ
return ptr->isDead;
}),
gm.playerBullets.end()
);
};
void Game::Draw()
{// 描画処理
DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);
gm.player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
// 自機弾の描画処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Draw();
}
};
さて、削除部分の処理が非常に複雑ですね。C#と比較してみましょう。
【C#】
playerBullets.RemoveAll(pb => pb.isDead);
【C++】
gm.playerBullets.erase(
std::remove_if(gm.playerBullets.begin(), gm.playerBullets.end(),
[](std::shared_ptr<PlayerBullet> &ptr) {
//int usingCount = -1;//テスト用コード(以下のコメントアウト部分は省略可)
//if (ptr->isDead) usingCount = ptr.use_count();//おかんのコレ捨てるよカウンタ
//弾を捨てるまでの残り↑カウント(1だとisDeadがtrueなら0になり削除へ
return ptr->isDead;
}),
gm.playerBullets.end()
);
gm.playerBullets.erase(
std::remove_if(gm.playerBullets.begin(), gm.playerBullets.end(),
[](std::shared_ptr<PlayerBullet> &ptr) {
return ptr->isDead;
}),
gm.playerBullets.end()
);
gm.playerBullets.erase(
……
……
……
……,
gm.playerBullets.end()
);
[](std::shared_ptr<PlayerBullet> &ptr) { return ptr->isDead; }
しかし、なぜ【わざわざeraseとremove_ifを組合わせる必要があるのか】
それは【erase単体だけだと削除処理が遅い】からです。
vectorはメモリ上にデータが【隙間なく詰まってます】ゆえに【通常性能は速いです】
ただ引き換えに【あいだのデータを1個消す】だけで【隙間を埋める座席移動が発生】します。
100万のデータが座ってる中、中央付近のデータを1個消すだけで【50万回ぶん一つずつ隙間を埋める座席移動が発生】します。
これがeraseだけ異様に遅い理由です。
逆に映画館の座席で通路付近に座ってる人を思い浮かべてください。
映画が終わると通路付近の座席の人はすぐに立って横に出れば最初に通路への出られますよね。
ゆえに配列の【後ろから】一つ削除する【pop_back処理は速い】です。
したがって【後ろに消したいものを先に集めてしまえばよいのです】
これが【remove_if】処理です。remove_ifは条件を指定して【削除候補を後ろに並べ替えます】
ゆえに【2】remove_ifで先に後ろに並べ替えて【1】eraseでまとめて削除の【1】【2】のハイブリッドが必要なのです。
ただ、毎回この複雑なコードを書くと【コードが見にくくなる】ので【テンプレート化】に挑戦してみましょう。
GameManager.h
に共通の削除機能として【テンプレート機能】として追加してみましょう。
#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_
#include <memory>
#include <vector>
#include "Singleton.h"
class Player; // クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class PlayerBullet;
class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可
std::shared_ptr<Player> player{ nullptr }; // 自機の初期化
std::vector<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
// 複数リスト↑vector ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる
// ★削除処理を共通テンプレート関数にする
// [共通テンプレート関数]https://programming-place.net/ppp/contents/cpp/language/009.html#function_template
template <typename T, class T_if>
void EraseRemoveIf(std::vector<T> &v, T_if if_condition)
{ // 特定のタイプT↑ ↑配列v ↑条件式if_condition
v.erase(
std::remove_if(v.begin(), v.end(),if_condition),
v.end() // ↓remove_ifの位置
);//例.[生][生][死][死][死]← v.end()の位置
};
protected:
GameManager() {}; // 外部からのインスタンス作成は禁止
virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};
#endif
そしてGame.cpp
の削除処理をシンプルに書き直します
#include "Game.h"
#include "Player.h" // ★初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // ★Update(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
Image::Load(); //画像の読込み
x = 100; // Xの初期位置
vx = 10;
gm.player = std::make_shared<Player>((float)100, (float)Screen::Height / 2); // 自機の初期化
};
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
if (x < 0)
{
vx = 10; //速度の変更
}
else if (x > Screen::Width)
{
vx = -10;
}
x += vx; // X位置の更新
gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
// 自機弾の更新処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Update();
}
//return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
// 自機弾の削除処理
gm.EraseRemoveIf(gm.playerBullets,
[](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 自機弾のリストから死んでるものを除去する
// 非常に複雑な式に見えるがこれでisDeadのものを削除
// https://stackoverflow.com/questions/42723205/remove-if-from-a-stdvector-of-shared-pointers-with-a-member-function
gm.playerBullets.erase(
std::remove_if(gm.playerBullets.begin(), gm.playerBullets.end(),
[](std::shared_ptr<PlayerBullet> &ptr) {
//int usingCount = -1;//テスト用コード(以下のコメントアウト部分は省略可)
//if (ptr->isDead) usingCount = ptr.use_count();//おかんのコレ捨てるよカウンタ
//弾を捨てるまでの残り↑カウント(1だとisDeadがtrueなら0になり削除へ
return ptr->isDead;
}),
gm.playerBullets.end()
);
};
void Game::Draw()
{// 描画処理
DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);
gm.player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
// 自機弾の描画処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Draw();
}
};
さて、使う側はすっきり2行にまとまりました。
これなら削除するリストがenemiesやenemyBulletsなど増えてきても書きやすいです。
テンプレート化template <typename T>を習得することは上級者への第一歩です。
C++だけでなく【C#でもテンプレート化ができます】ぜひ練習して習得してゆきましょう。
逆の目線で言うと【実は今まではテンプレート化された使いやすい機能を使わせてもらっていた】のです。意識せずに。
使う側が意識しなくていいほど【使いやすいテンプレート】を自力で作れることが理想形です。トライしてゆきましょう。
ここまでで一応弾の削除処理は書けました。しかし本当に弾が削除されているか実感がありません。
弾1個は現状では【かなり微小なメモリしか】使用しません。
実行して【メモリのグラフを見ても増えているか減っているか見えないほど微小です】
そこで【人為的に弾1個のメモリを大きくする】実験をしてみましょう。
注意が必要です【危険な実験】です(ほったらかすと)。【実験コードは必ず元に戻してください!!】
では実験を始めてみましょう。
PlayerBullet.h
の内容を次のように【改造】します:
#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_
#include "DxLib.h"
#include "Image.h"
#include "Screen.h"
// 自機弾クラス
class PlayerBullet
{
public:
const float Speed = 25; // 移動速度
const int VisibleRadius = 16; // 見た目の半径
// ↓【使用要注意!(テスト用!!)】メモリ消費弾(4MB)撃つたびに4MB消費(下の行のコメント外すと)
int memMUDA[1000000]; // 4バイト×1000000= 4MBの無駄メモリ
float x; // x座標
float y; // y座標
bool isDead = false; // 死亡フラグ
// コンストラクタ
PlayerBullet(float x, float y)
{
this->x = x;
this->y = y;
}
// 更新処理
void Update()
{
// 移動
x += Speed;
// 画面外に出たら、死亡フラグを立てる
if (x - VisibleRadius > Screen::Width)
{
isDead = true;
}
}
// 描画処理
void Draw()
{
DrawRotaGraphF(x, y, 1, 0, Image::playerBullet,TRUE);
}
};
#endif
無駄メモリをあえて人為的に定義して弾を撃ってみましょう。
メモリの【グラフがうなぎ上りに上昇しなければ】プレイヤの弾に関してはメモリの削除と開放が成功している証拠です。
試しにGame.cpp
で削除処理をわすれた場合の実験をしてみましょう。
#include "Game.h"
#include "Player.h" // ★初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // ★Update(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
Image::Load(); //画像の読込み
x = 100; // Xの初期位置
vx = 10;
gm.player = std::make_shared<Player>((float)100, (float)Screen::Height / 2); // 自機の初期化
};
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
if (x < 0)
{
vx = 10; //速度の変更
}
else if (x > Screen::Width)
{
vx = -10;
}
x += vx; // X位置の更新
gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
// 自機弾の更新処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Update();
}
return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
// 自機弾の削除処理
//gm.EraseRemoveIf(gm.playerBullets,
// [](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
};
void Game::Draw()
{// 描画処理
DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);
player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
// 自機弾の描画処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Draw();
}
};
実験が終わったら実験コードをもとに戻しましょうPlayerBullet.h
の内容を次のようにします
#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_
#include "DxLib.h"
#include "Image.h"
#include "Screen.h"
// 自機弾クラス
class PlayerBullet
{
public:
const float Speed = 25; // 移動速度
const int VisibleRadius = 16; // 見た目の半径
// ↓【使用要注意!(テスト用!!)】メモリ消費弾(4MB)撃つたびに4MB消費(下の行のコメント外すと)
//【実験コード】int memMUDA[1000000]; // 4バイト×1000000= 4MBの無駄メモリ
float x; // x座標
float y; // y座標
bool isDead = false; // 死亡フラグ
// コンストラクタ
PlayerBullet(float x, float y)
{
this->x = x;
this->y = y;
}
// 更新処理
void Update()
{
// 移動
x += Speed;
// 画面外に出たら、死亡フラグを立てる
if (x - VisibleRadius > Screen::Width)
{
isDead = true;
}
}
// 描画処理
void Draw()
{
DrawRotaGraphF(x, y, 1, 0, Image::playerBullet,TRUE);
}
};
#endif
Game.cpp
の削除処理ももとに戻しましょう。
#include "Game.h"
#include "Player.h" // ★初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // ★Update(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
Image::Load(); //画像の読込み
x = 100; // Xの初期位置
vx = 10;
gm.player = std::make_shared<Player>((float)100, (float)Screen::Height / 2); // 自機の初期化
};
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
if (x < 0)
{
vx = 10; //速度の変更
}
else if (x > Screen::Width)
{
vx = -10;
}
x += vx; // X位置の更新
gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
// 自機弾の更新処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Update();
}
//return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
// 自機弾の削除処理
gm.EraseRemoveIf(gm.playerBullets,
[](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
};
void Game::Draw()
{// 描画処理
DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);
player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
// 自機弾の描画処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Draw();
}
};
PlayerBullet.h
に角度をつけられるようにします
#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_
#include <cmath> //sin,cosを使うのに必要
#include "DxLib.h"
#include "Image.h"
#include "Screen.h"
// 自機弾クラス
class PlayerBullet
{
public:
const float Speed = 25; // 移動速度
const int VisibleRadius = 16; // 見た目の半径
// ↓【使用要注意!(テスト用!!)】メモリ消費弾(4MB)撃つたびに4MB消費(下の行のコメント外すと)
//【実験コード】int memMUDA[1000000]; // 4バイト×1000000= 4MBの無駄メモリ
float x; // x座標
float y; // y座標
bool isDead = false; // 死亡フラグ
float vx; // x方向移動速度
float vy; // y方向移動速度
float angle; // 移動角度
// コンストラクタ
PlayerBullet(float x, float y, float angle)
{
this->x = x;
this->y = y;
this->angle = angle;
vx = (float)std::cos(angle) * Speed;
vy = (float)std::sin(angle) * Speed;
}
// 更新処理
void Update()
{
// 移動
x += vx;
y += vy;
// 画面外に出たら、死亡フラグを立てる
if (x + VisibleRadius < 0 || x - VisibleRadius > Screen::Width ||
y + VisibleRadius < 0 || y - VisibleRadius > Screen::Height)
{
isDead = true;
}
}
// 描画処理
void Draw()
{
DrawRotaGraphF(x, y, 1, angle, Image::playerBullet,TRUE);
}
};
#endif
MyMath.h
を編集します:
#ifndef _MYMATH_H
#define _MYMATH_H
// 数学関連クラス
class MyMath
{
public:
static const float Sqrt2; // = 1.41421356237f;(.hではfloat小数型の初期化はできない int型はできるのに)
static const float PI; // 円周率 3.14159265359f;
static const float Deg2Rad; // 度からラジアンに変換する定数 PI / 180f;
}; // 【注意】セミコロン抜けで【宣言が必要ですエラー】
#endif //【宣言が必要ですエラーは上のセミコロン抜け】
MyMath.cpp
を編集して変数に数値を割り当て。
#include "MyMath.h"
const float MyMath::Sqrt2 = 1.41421356237f;//(floatは有効桁は実質7桁まで正確だがそれ以降は環境によって誤差出る)
const float MyMath::PI = 3.14159265359f; // 円周率
const float MyMath::Deg2Rad = MyMath::PI / 180; // 度からラジアンに変換する定数
Player.cpp
のプレイヤの弾の生成を改修します。
#include "Player.h"
#include "MyMath.h"
(前略)...................................
// ボタン押下で自機弾を発射
if (Input::GetButtonDown(PAD_INPUT_1))
{
// C++ではAddがemplace_backに↓ newがmake_shared↓に
gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(x, y, 0));//右
gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(x, y, -15 * MyMath::Deg2Rad));//右上
gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(x, y, +15 * MyMath::Deg2Rad));//右下
}
}
(以下略)
GameManager.h
で管理しているPlayerBulletのvector配列をlist構造に変更してみましょう。
#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_
#include <memory>
#include <vector>
#include <list>
#include "Singleton.h"
class Player; // クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class PlayerBullet;
class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可
std::shared_ptr<Player> player{ nullptr }; // 自機の初期化
std::list<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
// 複数リスト↑list ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる
// ★削除処理を共通テンプレート関数にする
// [共通テンプレート関数]https://programming-place.net/ppp/contents/cpp/language/009.html#function_template
template <typename T, class T_if>
void EraseRemoveIf(std::list<T> &v, T_if if_condition)
{ // 特定のタイプT↑ ↑配列v ↑条件式if_condition
v.erase(
std::remove_if(v.begin(), v.end(),if_condition),
v.end()
);
};
protected:
GameManager() {}; // 外部からのインスタンス作成は禁止
virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};
#endif
現状ではX,Yは別々の変数として定義していますが、これを数学のベクトルを扱うVector3型を定義してそちらに置き換えましょう(std::vectorとはつづりが同じだけどまったく別のもの)
Vector3.h
を作成します(拡張も見越して3DのZ座標も導入しておきます)
#ifndef VECTOR3_H_
#define VECTOR3_H_
#include <array> //vector配列と基本同じだが【有限】の個数である違いがあるxyzなど3つ固定の個数ならstd::vectorよりstd::array型がよい
#include <cmath>
#include <limits> // 無限大などをstd::numeric_limits<float>::infinity()で取得
//★【UnityのVector演算コード】
// https://github.com/Unity-Technologies/UnityCsReference/blob/02d565cf3dd0f6b15069ba976064c75dc2705b08/Runtime/Export/Math/Vector3.cs
// 3変数x,y,zを持つ構造体。3つの数字をまとめて扱うならXYZ座標以外にも使えるよ。x,y,z別々に足したり引いたり面倒でしょう。
struct Vector3
{
// struct型はpublic:がなくてもデフォルトがpublic: private:を書けばプライベートにもできる
float x = 0; // x座標
float y = 0; // y座標
float z = 0; //【3D】z座標
// コンストラクタ 初期化 z = 0.fでzのデフォルト値があるので2Dとしても使える
Vector3(float x = 0.f, float y = 0.f, float z = 0.f)
{
this->x = x;
this->y = y;
this->z = z;
}
//仮想デストラクタ
virtual ~Vector3()
{
}
};
#endif
Vector3.h
を書き換えます。
#ifndef VECTOR3_H_
#define VECTOR3_H_
#include <array> //vector配列と基本同じだが【有限】の個数である違いがあるxyzなど3つ固定の個数ならstd::vectorよりstd::array型がよい
#include <cmath>
#include <limits> // 無限大などをstd::numeric_limits<float>::infinity()で取得
//★【UnityのVector演算コード】
// https://github.com/Unity-Technologies/UnityCsReference/blob/02d565cf3dd0f6b15069ba976064c75dc2705b08/Runtime/Export/Math/Vector3.cs
// 3変数x,y,zを持つ構造体。3つの数字をまとめて扱うならXYZ座標以外にも使えるよ。x,y,z別々に足したり引いたり面倒でしょう。
struct Vector3
{
// struct型はpublic:がなくてもデフォルトがpublic: private:を書けばプライベートにもできる
//float x = 0; // x座標
//float y = 0; // y座標
//float z = 0; // z座標 3つの定義をunionで配列xyzと共用することでxyz[1]=5.5f;とすると yも5.5となる【運命共用体】(メモリ上は同じ位置になる)
union { // ★共用体unionテクニック https://inemaru.hatenablog.com/entry/2016/03/02/005408
struct { // [参考ビットサイズ] https://marycore.jp/prog/c-lang/data-type-ranges-and-bit-byte-sizes/
float x;// x座標
float y;// y座標
float z;// z座標
}; //[匿名共用体とは] https://zenn.dev/block/articles/beda29f11f05afc147ef
std::array<float, 3> xyz; // float xyz[3];と同じ意味 float 3個ぶんのデータサイズでx,y,z 3個ぶんと一致するので★unionで共用
}; // unionは異なる複数のものをメモリ上の同一の番地に割り当てられる⇒x,y,z分けて記述するの面倒なとき配列xyz[3]をfor文i=0~3で回せる
// コンストラクタ 初期化 z = 0.fでzのデフォルト値があるので2Dとしても使える
Vector3(float x = 0.f, float y = 0.f, float z = 0.f)
{
this->xyz = { x,y,z };// たった1行で書ける
}
//仮想デストラクタ
virtual ~Vector3()
{
}
};
#endif
Vector3.h
を書き換えます。
#ifndef VECTOR3_H_
#define VECTOR3_H_
#include <array> //vector配列と基本同じだが【有限】の個数である違いがあるxyzなど3つ固定の個数ならstd::vectorよりstd::array型がよい
#include <cmath>
#include <limits> // 無限大などをstd::numeric_limits<float>::infinity()で取得
//★【UnityのVector演算コード】
// https://github.com/Unity-Technologies/UnityCsReference/blob/02d565cf3dd0f6b15069ba976064c75dc2705b08/Runtime/Export/Math/Vector3.cs
// 3変数x,y,zを持つ構造体。3つの数字をまとめて扱うならXYZ座標以外にも使えるよ。x,y,z別々に足したり引いたり面倒でしょう。
struct Vector3
{
// struct型はpublic:がなくてもデフォルトがpublic: private:を書けばプライベートにもできる
//float x = 0; // x座標
//float y = 0; // y座標
//float z = 0; // z座標 3つの定義をunionで配列xyzと共用することでxyz[1]=5.5f;とすると yも5.5となる【運命共用体】(メモリ上は同じ位置になる)
union { // ★共用体unionテクニック https://inemaru.hatenablog.com/entry/2016/03/02/005408
struct { // [参考ビットサイズ] https://marycore.jp/prog/c-lang/data-type-ranges-and-bit-byte-sizes/
float x;// x座標
float y;// y座標
float z;// z座標
}; //[匿名共用体とは] https://zenn.dev/block/articles/beda29f11f05afc147ef
std::array<float, 3> xyz; // float xyz[3];と同じ意味 float 3個ぶんのデータサイズでx,y,z 3個ぶんと一致するので★unionで共用
}; // unionは異なる複数のものをメモリ上の同一の番地に割り当てられる⇒x,y,z分けて記述するの面倒なとき配列xyz[3]をfor文i=0~3で回せる
// コンストラクタ 初期化 z = 0.fでzのデフォルト値があるので2Dとしても使える
Vector3(float x = 0.f, float y = 0.f, float z = 0.f)
{
this->xyz = { x,y,z };// たった1行で書ける
}
//仮想デストラクタ
virtual ~Vector3()
{
}
// Unityのstatic変数を参考に https://docs.unity3d.com/ja/current/ScriptReference/Vector3.html
static const Vector3 zero;// = { 0.f, 0.f, 0.f };
static const Vector3 one; // = { 1.f, 1.f, 1.f };
static const Vector3 forward;// = { 0.f, 0.f, 1.f };
static const Vector3 back; // = { 0.f, 0.f, -1.f };
static const Vector3 up; // = { 0.f, 1.f, 0.f };
static const Vector3 down;// = { 0.f, -1.f, 0.f };
static const Vector3 left; // = {-1.f, 0.f, 0.f };
static const Vector3 right;// = { 1.f, 0.f, 0.f };
//[負の値の最小値] https://stackoverflow.com/questions/20016600/negative-infinity
static const Vector3 negativeInfinity;
//[極大値]https://cpprefjp.github.io/reference/limits/numeric_limits/lowest.html
static const Vector3 positiveInfinity;
// 2つのベクトルの内積
static float dot(const Vector3& leftVec, const Vector3& rightVec)
{
return leftVec.x * rightVec.x + leftVec.y * rightVec.y + leftVec.z * rightVec.z; //普通にx同士,y同士,z同士掛けるだけ
}
// 2つのベクトルの外積
static Vector3 cross(const Vector3& leftVec, const Vector3& rightVec)
{
return Vector3{
leftVec.y * rightVec.z - leftVec.z * rightVec.y, // x = 左y * 右z - 左z * 右y
leftVec.z * rightVec.x - leftVec.x * rightVec.z, // y = 左z * 右x - 左x * 右z
leftVec.x * rightVec.y - leftVec.y * rightVec.x };// z = 左x * 右y - 左y * 右x //行列独特の特殊な掛け方
}
// ベクトルの 2 乗の長さを返します(平方根しないぶん計算速い)(スタティック版)
static float _sqrMagnitude(const Vector3& _vec)
{
return _vec.x * _vec.x + _vec.y * _vec.y + _vec.z * _vec.z; // x*x + y*y + z*z
}
// ベクトルの長さ(0から点(x,y,z)までの距離)(スタティック版)
static float _magnitude(const Vector3& _vec)
{
return std::sqrt(_sqrMagnitude(_vec)); // √x*x + y*y + z*z
}
// destArrayにデータを保管しておく
Vector3& copyToArray(std::array<float, 3>& destArray)
{
destArray = this->xyz;//配列としてコピー
return *this;
}
// sourceArray配列からVector3データを生成
Vector3 fromArray(std::array<float, 3>& sourceArray)
{
return Vector3{ sourceArray[0], sourceArray[1], sourceArray[2] }; //配列から初期化
}
// ベクトルの長さ(0から点(x,y,z)までの距離)
float magnitude()
{
return std::sqrt(x * x + y * y + z * z); // √(x*x + y*y + z*z)
};
// ベクトルの 2 乗の長さを返します(平方根しないぶん計算速い)
float sqrMagnitude()
{
return x * x + y * y + z * z; // x*x + y*y + z*z
};
// 正規化したベクトルを返す
Vector3 normalized()
{
float mag = magnitude();
if (mag < 0.00001f) // ほぼ0ベクトルか?
return *this;
else
return Vector3{ x / mag, y / mag, z / mag }; // x / |x|, y / |y|, z / |z|
}
/*----- 演算子オーバーロード -----*/
// 逆ベクトル
inline Vector3 operator - () const
{
return *this*-1;
}
// Vectorをそのまま足し合わせる
inline Vector3 operator + () const
{
return *this;
}
// Vector同士の足し算 x,y,z個別に足し合わせる
Vector3 operator + (const Vector3 add_v3) const
{
Vector3 v3; //★*thisじゃダメな理由 a = vec1 + vec2のとき vec1の数値が書き変わったらあかんから
v3.x = this->x + add_v3.x; //[コレはダメ] this->x = this->x + add_v3.x;
v3.y = this->y + add_v3.y;
v3.z = this->z + add_v3.z;
return v3;
}
// Vector同士の引き算 x,y,z個別に引き合わせる
Vector3 operator - (const Vector3 minus_v3) const
{
Vector3 v3;
v3.x = this->x - minus_v3.x;
v3.y = this->y - minus_v3.y;
v3.z = this->z - minus_v3.z;
return v3;
}
// Vector同士の掛け算 x,y,z個別に掛け合わせる
Vector3 operator * (float multiply_num) const
{
Vector3 v3;
v3.x = this->x * multiply_num;
v3.y = this->y * multiply_num;
v3.z = this->z * multiply_num;
return v3;
}
// Vector同士の割り算 x,y,z個別に割り合わせる
// 0.fでは割れないようにしてある
Vector3 operator / (float divide_num) const
{
if (divide_num == 0.0f)
return *this;
Vector3 v3;
v3.x = this->x / divide_num;
v3.y = this->y / divide_num;
v3.z = this->z / divide_num;
return v3;
}
Vector3& operator += (const Vector3 add_v3)
{
this->x += add_v3.x;
this->y += add_v3.y;
this->z += add_v3.z;
return *this; //*thisを返すことで v1 + v2 + v3見たく数珠繋ぎできる
}
Vector3& operator -= (const Vector3 minus_v3)
{
this->x -= minus_v3.x;
this->y -= minus_v3.y;
this->z -= minus_v3.z;
return *this;
}
Vector3& operator *= (float multiply_num)
{
this->x *= multiply_num;
this->y *= multiply_num;
this->z *= multiply_num;
return *this;
}
// 0.fでは割れないようにしてある
Vector3& operator /= (float divide_num)
{
if (divide_num == 0.0f)
return *this;
this->x /= divide_num;
this->y /= divide_num;
this->z /= divide_num;
return *this;
}
// 代入演算子 x,y,zを全部 change_numに変える
Vector3& operator = (float change_num)
{
this->x = change_num;
this->y = change_num;
this->z = change_num;
return *this;
}
// 一致演算子 x,y,zを全部一致するか 一つでも違えばfalse
bool operator == (const Vector3& v3_other)
{
if (this->x != v3_other.x) return false;
if (this->y != v3_other.y) return false;
if (this->z != v3_other.z) return false;
return true;
}
// 不一致演算子 x,y,zを全部不一致か 一つでも同じならfalse
bool operator != (const Vector3& v3_other)
{
if (this->x == v3_other.x) return false;
if (this->y == v3_other.y) return false;
if (this->z == v3_other.z) return false;
return true;
}
/*---- 以下 DXライブラリ向けにVector3とDX版Vectorの変換方法を用意 ---------------*/
// DXlibを#includeしていればDX_LIB_Hも定義済みなので下記関数も定義される VECTORの定義はDXライブラリ版のVector3
#ifdef DX_LIB_H
// Vector3からDxLibのVECTORへの変換
VECTOR Vec3ToVec()
{
VECTOR Result;
Result.x = this->x;
Result.y = this->y;
Result.z = this->z;
return Result;
}
// 左辺値がDXのVECTOR型のときの=代入対応
inline operator VECTOR() const
{
VECTOR Result;
Result.x = this->x;
Result.y = this->y;
Result.z = this->z;
return Result;
}
// 右辺値がDXのVECTOR型のときの=代入対応
Vector3 operator=(const VECTOR& other)
{
this->x = other.x;
this->y = other.y;
this->z = other.z;
return *this;
}
#endif
};
#endif
Vector3.cppを新規作成して
その内容を次のようにします:#include "Vector3.h"
// Unityのstatic変数を参考に https://docs.unity3d.com/ja/current/ScriptReference/Vector3.html
Vector3 const Vector3::zero = Vector3(0.f, 0.f, 0.f );
Vector3 const Vector3::one = Vector3(1.f, 1.f, 1.f );
Vector3 const Vector3::forward = Vector3(0.f, 0.f, 1.f );
Vector3 const Vector3::back = Vector3(0.f, 0.f, -1.f );
Vector3 const Vector3::up = Vector3(0.f, 1.f, 0.f );
Vector3 const Vector3::down = Vector3(0.f, -1.f, 0.f );
Vector3 const Vector3::left = Vector3(-1.f, 0.f, 0.f );
Vector3 const Vector3::right = Vector3(1.f, 0.f, 0.f );
//[負の値の最小値] https://stackoverflow.com/questions/20016600/negative-infinity
Vector3 const Vector3::negativeInfinity = Vector3(-std::numeric_limits<float>::infinity(), -std::numeric_limits<float>::infinity(), -std::numeric_limits<float>::infinity() );
//[極大値]https://cpprefjp.github.io/reference/limits/numeric_limits/lowest.html
Vector3 const Vector3::positiveInfinity = Vector3(std::numeric_limits<float>::infinity(), std::numeric_limits<float>::infinity(), std::numeric_limits<float>::infinity() );
PlayerBullet.h
のx,yをVector3型におきかえます。
#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_
#include <cmath> //sin,cosを使うのに必要
#include "DxLib.h"
#include "Image.h"
#include "Screen.h"
#include "Vector3.h"
// 自機弾クラス
class PlayerBullet
{
public:
Vector3 position; // xyz座標
Vector3 v; // xyz方向移動速度
const float Speed = 25; // 移動速度
const int VisibleRadius = 16; // 見た目の半径
float x; // x座標
float y; // y座標
bool isDead = false; // 死亡フラグ
float vx; // x方向移動速度
float vy; // y方向移動速度
float angle; // 移動角度
// コンストラクタ
PlayerBullet(Vector3 pos, float angle) : position { pos }
{
this->x = x;
this->y = y;
this->angle = angle;
this->v = Vector3((float)std::cos(angle) * Speed,(float)std::sin(angle) * Speed, 0);
vx = (float)std::cos(angle) * Speed;
vy = (float)std::sin(angle) * Speed;
}
// 更新処理
void Update()
{
// 移動
position += v;
x += vx;
y += vy;
// 画面外に出たら、死亡フラグを立てる
if (position.x + VisibleRadius < 0 || position.x - VisibleRadius > Screen::Width ||
position.y + VisibleRadius < 0 || position.y - VisibleRadius > Screen::Height)
{
isDead = true;
}
}
// 描画処理
void Draw()
{
DrawRotaGraphF(position.x, position.y, 1, angle, Image::playerBullet,TRUE);
}
};
#endif
Player.h
の内容を次のようにします:#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"
#include "Vector3.h"
#include "GameManager.h"
class Player
{
public:
Vector3 position; // xyz座標
const float MoveSpeed = 6; // 移動速度
float x; // x座標
float y; // y座標
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
// コンストラクタ
// pos : 初期位置
Player(Vector3 pos) : position { pos }
{
this->x = x;
this->y = y;
}
virtual ~Player() {}; // 仮想デストラクタ ※無いと【削除された関数を参照しようとしてますエラー】が出るかも
// 更新処理の関数定義
void Update();
// 描画処理の関数定義
void Draw();
};
#endif
Player.cpp
もVector3での処理に書き換えます
#include "Player.h"
#include "PlayerBullet.h" // ★弾を生成して【使う処理があるのでインクルード必要】
// 更新処理【書き移す際にはPlayer::という形で::所属が必要になります】
void Player::Update()
{
float vx = 0; // x方向移動速度
float vy = 0; // y方向移動速度
Vector3 v{0,0,0}; // xyz方向移動速度
if (Input::GetButton(PAD_INPUT_LEFT))
{
v.x = -MoveSpeed; // 左
}
else if (Input::GetButton(PAD_INPUT_RIGHT))
{
v.x = MoveSpeed; // 右
}
if (Input::GetButton(PAD_INPUT_UP))
{
v.y = -MoveSpeed; // 上
}
else if (Input::GetButton(PAD_INPUT_DOWN))
{
v.y = MoveSpeed; // 下
}
// 斜め移動も同じ速度になるように調整
if (v.magnitude() > 0)
{
v /= MyMath::Sqrt2;
vx /= MyMath::Sqrt2;
vy /= MyMath::Sqrt2;
}
// 実際に位置を動かす
position += v;
x += vx;
y += vy;
// ボタン押下で自機弾を発射
if (Input::GetButtonDown(PAD_INPUT_1))
{
// C++ではAddがpush_backに↓ newがmake_shared↓に
gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, 0));//右
gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, -15 * MyMath::Deg2Rad));//右上
gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, +15 * MyMath::Deg2Rad));//右下
}
}
// 描画処理
void Player::Draw()
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::player, TRUE);
}
次にGame.cppファイルのPlayerの生成処理をVector3に対応する形に書き換えましょう。
Game.cpp
の内容を次のように変更します:
#include "Game.h"
#include "Player.h" // ★初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // ★Update(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
Image::Load(); //画像の読込み
x = 100; // Xの初期位置
vx = 10;
gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
};
(中略)...
};
Enemyクラスを定義してそれを継承する形でZako0をつくることもできますが
その構造で作り進めると循環参照インクルードが起こってしまいがちです。
なぜなら、衝突判定で【PlayerBullet⇔Enemy⇔Playerの相互の矢印の参照ループが生まれやすくなります】
そこで共通の基底(ベース)クラスとなるGameObject、すなわち全てのものが継承する【ベースとなる継承元】を導入します。
これで矢印はGameObjectにまとまりオブジェクト同士の参照をせずに済みます。
GameObject.h
を作成します(拡張も見越して3DのZ座標も導入しておきます)。
#ifndef GAMEOBJECT_H_
#define GAMEOBJECT_H_
#include "DxLib.h"
#include "Vector3.h"
#include <string> //文字タグのため
#include <memory> //回し読みポインタの定義のため
// ゲーム上に表示される物体の基底クラス。
// プレイヤーや敵、アイテムなどはこのクラスを継承して作る。
class GameObject
{
public:
Vector3 position {0,0,0}; // xyz座標
Vector3 v{0,0,0}; //xyz方向の速度
bool isDead = false; // 死んだ(削除対象)フラグ
std::string tag = ""; // Zako0やBossやPlayerなど小カテゴリ
std::string typeTag = ""; // EnemyやPlayerなど大カテゴリ
std::string statusTag = ""; // 爆発状態やアイテムゲット状態など自由に使えばよい
// コンストラクタ
GameObject(Vector3 pos) : position{ pos }
{
}
//★仮想デストラクタ【忘れるとメモリがヤバいダメ絶対】
//【注意!】ベースの基底では必ず定義しないとstringなどが浪費され極悪なメモリ被害に発展
virtual ~GameObject()
{
}
//★純粋仮想関数=0は継承したZakoやPlayerなどの必須機能(継承したら絶対override必須縛り)
// 更新処理
virtual void Update() = 0; //純粋仮想関数に(継承したらoverride必須)
// 描画処理
virtual void Draw() = 0; //純粋仮想関数に(継承したらoverride必須)
//★virtual仮想関数は共通カスタマイズ機能(継承してもoverride必須ではないご自由にカスタマイズ)
// 衝突したときの関数(仮のvirtual関数←純粋仮想関数と違い継承してもoverride必須ではない)
virtual void OnCollision(std::shared_ptr<GameObject> other)
{
}
};
#endif
Enemy.h
を作成します。
#ifndef ENEMY_H_
#define ENEMY_H_
#include "GameObject.h"
#include "GameManager.h"
// 敵の基底クラス。
// 全ての敵は、このクラスを継承して作る。
// ★【勉強】abstract型はC++にない!!【純粋仮想関数】を使って作る
class Enemy : public GameObject
{
public:
//【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
// コンストラクタ
Enemy(Vector3 pos) : GameObject( pos ) // GameObjectをベースとして初期化処理を継承する
{
this->typeTag = "Enemy";
}
// 更新処理。派生クラスでオーバーライドして使う
virtual void Update() = 0;//virtual ~ = 0;で【純粋仮想関数に】これで【C#のabstract型に】
// 描画処理。派生クラスでオーバーライドして使う
virtual void Draw() = 0; //virtual ~ = 0;で【純粋仮想関数に】【関数 = 0で未定義状態を表現】
//【勉強】関数 = 0って何?と思った人は【関数もメモリ上では住所アドレスに過ぎない】ことを知ろう
// ★関数 = 0はつまり関数の住所ポインタの針が【メモリ上の何かを針で参照してない(住所不定無処理)】
// つまり【関数が未定義ってこと】=【あいまい部分があるabstractクラスってこと】
//http://etc2myday.jugem.jp/?eid=204
//http://wisdom.sakura.ne.jp/programming/c/c54.html
//逆にいうと【関数を配列】にするっていうトリッキーなこともできる!
//http://www.ced.is.utsunomiya-u.ac.jp/lecture/2012/prog/p3/kadai3/virtualfunc2.php
// 自機弾に当たったときの処理
virtual void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet)
{
}
};
#endif
Player.h
も共通のベースとなるGameObjectを継承するように書き換えます。
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"
#include "Vector3.h"
#include "GameObject.h"
#include "GameManager.h"
class Player : public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:
//【★注意!】ここに【positionの定義が残ってるとGameObjectと被る】ので
// 名前の同じ変数が2つ【パラレルワールドに存在してしまう!】
// Playerからx=13,y=5に変えたつもりでも【ベースのGameObjectはx=0,y=0のままの不思議バグで混乱!】
Vector3 position; // xyz座標
const float MoveSpeed = 6; // 移動速度
//【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
// コンストラクタ
// pos : 初期位置
Player(Vector3 pos) : GameObject( pos ) //【忘れると】GameObjectの初期化処理が飛ばされて大混乱!
{
}
virtual ~Player() {}; // 仮想デストラクタ
// 更新処理の関数定義
void Update();
// 描画処理の関数定義
void Draw();
};
#endif
PlayerBullet.h
もGameObjectを継承する形に書き換えます。
#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_
#include <cmath> //sin,cosを使うのに必要
#include "DxLib.h"
#include "Image.h"
#include "Screen.h"
#include "Vector3.h"
#include "GameObject.h"
// 自機弾クラス
class PlayerBullet: public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:
//【★注意!GameObjectと被る変数は全部消す】
Vector3 position; // xyz座標
Vector3 v; // xyz方向移動速度
const float Speed = 25; // 移動速度
const int VisibleRadius = 16; // 見た目の半径
bool isDead = false; // 死亡フラグ
float angle; // 移動角度
// コンストラクタ
PlayerBullet(Vector3 pos, float angle) : GameObject( pos )
{
this->tag = "PlayerBullet";
this->angle = angle;
this->v = Vector3((float)std::cos(angle) * Speed, (float)std::sin(angle) * Speed, 0);
}
// 更新処理
void Update()
{
(中略)....................
}
// 描画処理
void Draw()
{
DrawRotaGraphF(position.x, position.y, 1, angle, Image::playerBullet,TRUE);
}
};
#endif
Zako0.h
を作成します。
#ifndef ZAKO_0_H_
#define ZAKO_0_H_
#include "Enemy.h"
#include "Image.h"
// ザコ0クラス
class Zako0 : public Enemy
{
public:
// コンストラクタ
Zako0(Vector3 pos)
: Enemy( pos )
{
this->tag = "Zako0";
}
// 更新処理
void Update() override
{
position.x -= 1; // とりあえず左へ移動
}
// 描画処理
void Draw() override
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako0, TRUE);
}
};
#endif
Image.h
にザコ0,1,2,3と敵弾の画像定義を追加します:
#ifndef IMAGE_H_
#define IMAGE_H_
#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
class Image
{
public:
Image() {}; // 初期化コンストラクタ:定義と{}空の処理
~Image() {}; // 破棄する処理デストラクタ:定義と{}空の処理
static void Load();
static int bossImage; //ボス画像のハンドラ(読込画像番号)
static int player; //プレイヤ画像のハンドラ
static int playerBullet; //プレイヤの弾画像のハンドラ
static int enemyBullet16; //敵弾 画像のハンドラ(読込画像番号)
static int zako0; //ザコ0 画像のハンドラ(読込画像番号)
static int zako1; //ザコ1 画像のハンドラ(読込画像番号)
static int zako2; //ザコ2 画像のハンドラ(読込画像番号)
static int zako3; //ザコ3 画像のハンドラ(読込画像番号)
private:
};
#endif
次に対となるcppファイルも変更します。
Image.cpp
の内容を次のようにします:
#include "Image.h"
int Image::bossImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{-1};
int Image::playerBullet{-1};
int Image::enemyBullet16{-1};
int Image::zako0{-1};
int Image::zako1{-1};
int Image::zako2{-1};
int Image::zako3{-1};
void Image::Load()
{
bossImage = LoadGraph("Image/boss1.png");
assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
player= LoadGraph("Image/player.png");
assert(player!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
playerBullet = LoadGraph("Image/player_bullet.png");
assert(playerBullet!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
enemyBullet16 = LoadGraph("Image/enemy_bullet_16.png");
assert(enemyBullet16!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako0 = LoadGraph("Image/zako0.png");
assert(zako0!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako1 = LoadGraph("Image/zako1.png");
assert(zako1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako2 = LoadGraph("Image/zako2.png");
assert(zako2!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako3 = LoadGraph("Image/zako3.png");
assert(zako3!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
}
GameManager.h
に敵のリストを追加しましょう。
#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_
#include <memory>
#include <vector>
#include "Singleton.h"
class Player; // クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class PlayerBullet;
class Enemy;
class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可
std::shared_ptr<Player> player{ nullptr }; // 自機のポインタ
std::list<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
// 複数リスト↑list ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる
(中略)..............
protected:
GameManager() {}; // 外部からのインスタンス作成は禁止
virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};
#endif
Game.cpp
に敵の生成と更新と描画処理を追加しましょう。
#include "Game.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
Image::Load(); //画像の読込み
x = 100; // Xの初期位置
vx = 10;
gm.player = std::make_shared<Player>(Vector3((float)100, (float)Screen::Height / 2)); // 自機の初期化
// とりあえず適当に敵を生成
gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)600, (float)100)));
gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)1000, (float)500)));
};
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
if (x < 0)
{
vx = 10; //速度の変更
}
else if (x > Screen::Width)
{
vx = -10;
}
x += vx; // X位置の更新
gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
// 自機弾の更新処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Update();
}
// 敵の更新処理
for (const auto& b : gm.enemies)
{
b->Update();
}
//return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
gm.EraseRemoveIf(gm.playerBullets,
[](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
};
void Game::Draw()
{// 描画処理
DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);
// 敵の描画処理
for (const auto& b : gm.enemies)
{
b->Draw();
}
gm.player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
// 自機弾の描画処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Draw();
}
};
[C++]シューティングゲーム 2
次のテキスト3はこちら。
当たり判定の式をMyMathクラスに定義します。
MyMath.h
を編集します:
#ifndef _MYMATH_H
#define _MYMATH_H
#include "Vector3.h"
// 数学関連クラス
class MyMath
{
public:
static const float Sqrt2; // = 1.41421356237f;(.hではfloat小数型の初期化はできない int型はできるのに)
static const float PI; // 円周率 3.14159265359f;
static const float Deg2Rad; // 度からラジアンに変換する定数 PI / 180f;
/// <summary>
/// 円と円が重なっているかを調べる
/// </summary>
/// <param name="pos1">円1の中心</param>
/// <param name="radius1">円1の半径</param>
/// <param name="pos2">円2の中心</param>
/// <param name="radius2">円2の半径</param>
/// <returns>重なっていればtrue、重なっていなければfalseを返却する</returns>
static bool CircleCircleIntersection(
Vector3 pos1, float radius1,
Vector3 pos2, float radius2)
{ //[2乗のまま < 比較すると高速] https://www.stmn.tech/entry/2016/06/15/033835
return ((pos1 - pos2).sqrMagnitude()
< (radius1 + radius2) * (radius1 + radius2));
}
}; // 【注意】セミコロン抜けで【宣言が必要ですエラー】
#endif //【宣言が必要ですエラーは上のセミコロン抜け】
Enemy.h
を改修します。当たり判定の半径を表す変数と、死亡フラグ、被弾時の処理を追加します。
#ifndef ENEMY_H_
#define ENEMY_H_
#include "GameObject.h"
#include "GameManager.h"
// 敵の基底クラス。
// 全ての敵は、このクラスを継承して作る。
// ★【勉強】abstract型はC++にない!!【純粋仮想関数】を使って作る
class Enemy : public GameObject
{
public:
//【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
float collisionRadius = 32; // 当たり判定の半径
float life = 1; // 耐久力
// コンストラクタ
Enemy(Vector3 pos) : GameObject( pos ) // GameObjectをベースとして初期化処理を継承する
{
this->typeTag = "Enemy";
}
// 更新処理。派生クラスでオーバーライドして使う
virtual void Update() = 0;//virtual ~ = 0;で【純粋仮想関数に】これで【C#のabstract型に】
// 描画処理。派生クラスでオーバーライドして使う
virtual void Draw() = 0; //virtual ~ = 0;で【純粋仮想関数に】【関数 = 0で未定義状態を表現】
//【勉強】関数 = 0って何?と思った人は【関数もメモリ上では住所アドレスに過ぎない】ことを知ろう
// ★関数 = 0はつまり関数の住所ポインタの針が【メモリ上の何かを針で参照してない(住所不定無処理)】
// つまり【関数が未定義ってこと】=【あいまい部分があるabstractクラスってこと】
//http://etc2myday.jugem.jp/?eid=204
//http://wisdom.sakura.ne.jp/programming/c/c54.html
//逆にいうと【関数を配列】にするっていうトリッキーなこともできる!
//http://www.ced.is.utsunomiya-u.ac.jp/lecture/2012/prog/p3/kadai3/virtualfunc2.php
// 自機弾に当たったときの処理 関数の【★引数をGameObject型↓にすることで循環インクルード抑止】
virtual void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet)
{
life -= 1; // ライフを減らす
// ライフが無くなったら、死亡
if (life <= 0)
{
isDead = true;
}
}
};
#endif
PlayerBullet.h
を改修します。当たり判定を表す変数を追加します。
#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_
#include <cmath> //sin,cosを使うのに必要
#include "DxLib.h"
#include "Image.h"
#include "Screen.h"
#include "GameObject.h"
// 自機弾クラス
class PlayerBullet: public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:
const float Speed = 25; // 移動速度
const int VisibleRadius = 16; // 見た目の半径
float collisionRadius = 16; // 当たり判定の半径
float angle; // 移動角度
(中略)....................
// 更新処理
void Update()
{
(中略)....................
}
// 描画処理
void Draw()
{
DrawRotaGraphF(x, y, 1, angle, Image::playerBullet,TRUE);
}
// 敵にぶつかった時の処理
void OnCollisionEnemy(std::shared_ptr<GameObject> other)
{
isDead = true;
}
};
#endif
Game.cpp
を改修します。自機弾と敵が当たっているか調べる処理を追加します。
#include "Game.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
(中略)..................
};
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
(中略)..................
gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
// 自機弾の更新処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Update();
}
// 敵の更新処理
for (const auto& b : gm.enemies)
{
b->Update();
}
// 自機弾と敵の衝突判定
for(const auto& playerBullet : gm.playerBullets)
{
// 自機弾が死んでたらスキップする
if (playerBullet->isDead)
continue;
for(const auto& enemy : gm.enemies)
{
// 敵が死んでたらスキップする
if (enemy->isDead)
continue;
// 自機弾と敵が重なっているか?
if (MyMath::CircleCircleIntersection(
playerBullet->position, playerBullet->collisionRadius,
enemy->position, enemy->collisionRadius))
{
// 重なっていたら、それぞれのぶつかったときの処理を呼び出す
enemy->OnCollisionPlayerBullet(playerBullet);
playerBullet->OnCollisionEnemy(enemy);
// 衝突の結果、自機弾が死んだら、この弾のループはおしまい
if (playerBullet->isDead)
break;
}
}
}
//return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
// 自機弾のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.playerBullets,
[](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 敵のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.enemies,
[](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
};
void Game::Draw()
{// 描画処理
(以下略)............
Zako1.h
のlifeを5にすることでZako1を硬くしてみましょう。
ついでにsinの波運動も試してみましょう。
Zako1.hを新規作成
します。黄色の部分がZako0との違いです。
#ifndef ZAKO_1_H_
#define ZAKO_1_H_
#include "Enemy.h"
#include <cmath> // Sinの計算に使う
#include "MyMath.h"
#include "Image.h"
// ザコ1クラス
class Zako1 : public Enemy
{
public:
float sinMove = 0; // Sinの動きをする際の0~360度
float sinRadius = 70; // Sinの動きをするときの半径
// コンストラクタ
Zako1(Vector3 pos) : Enemy ( pos )
{
this->tag = "Zako1";//オブジェクトの種類判別タグ
life = 5;
}
// 更新処理
void Update() override
{
position.x -= 1; // 左へ移動
float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
sinMove += 2.5f; // Sinの波運動の角度を進める
float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
// (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
position.y += currentSinY - prevSinY; // 上下方向のSinの波運動
}
// 描画処理
void Draw() override
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako1, TRUE);
}
};
#endif
Game.cpp
にZako1の生成処理を追加します。
#include "Game.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
(中略)..................
// とりあえず適当に敵を生成
gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)600, (float)100)));
gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)1000, (float)500)));
gm.enemies.emplace_back(std::make_shared<Zako1>(Vector3((float)1000, (float)150)));
};
Player.h
に当たり判定半径を定義します。
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"
#include "Vector3.h"
#include "GameObject.h"
#include "GameManager.h"
class Player : public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:
const float MoveSpeed = 6; // 移動速度
float collisionRadius = 32; // 当たり判定半径
//【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
// コンストラクタ
// pos : 初期位置
Player(Vector3 pos) : GameObject( pos ) //【忘れると】GameObjectの初期化処理が飛ばされて大混乱!
{
}
virtual ~Player() {}; // 仮想デストラクタ ※無いと【削除された関数を参照しようとしてますエラー】が出るかも
// 更新処理の関数定義
void Update();
// 描画処理の関数定義
void Draw();
// 敵にぶつかった時の処理
void OnCollisionEnemy(std::shared_ptr<GameObject> other);
};
#endif
Player.cpp
に敵との当たり判定処理を追加します。
#include "Player.h"
#include "PlayerBullet.h" // 弾を生成して【使う処理があるのでインクルード必要】
// 更新処理
void Player::Update()
{
// 継承したGameObjectにvがあるのでvをfloatで定義し直さないように注意
v = Vector3(0,0,0); // xyz方向移動速度
if (Input::GetButton(PAD_INPUT_LEFT))
{
v.x = -MoveSpeed; // 左
}
else if (Input::GetButton(PAD_INPUT_RIGHT))
{
v.x = MoveSpeed; // 右
}
if (Input::GetButton(PAD_INPUT_UP))
{
v.y = -MoveSpeed; // 上
}
else if (Input::GetButton(PAD_INPUT_DOWN))
{
v.y = MoveSpeed; // 下
}
// 斜め移動も同じ速度になるように調整
if (v.magnitude() > 0)
{
v /= MyMath::Sqrt2;
}
// 実際に位置を動かす
position += v;
// ボタン押下で自機弾を発射
if (Input::GetButtonDown(PAD_INPUT_1))
{
// C++ではAddがpush_backに↓ newがmake_shared↓に
gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, 0));//右
gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, -15 * MyMath::Deg2Rad));//右上
gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, +15 * MyMath::Deg2Rad));//右下
}
}
// 描画処理
void Player::Draw()
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::player, TRUE);
}
// 敵にぶつかった時の処理
void Player::OnCollisionEnemy(std::shared_ptr<GameObject> other)
{
isDead = true;
}
Game.cpp
を改修します。自機が死んだとき消す処理と自機と敵の衝突判定を追加します。
#include "Game.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
(中略)..................
};
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
(中略)..................
if (!gm.player->isDead) // 自機が死んでいなければ
gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
// 自機弾の更新処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Update();
}
// 敵の更新処理
for (const auto& b : gm.enemies)
{
b->Update();
}
// 自機弾と敵の衝突判定
for(const auto& playerBullet : gm.playerBullets)
{
// 自機弾が死んでたらスキップする
if (playerBullet->isDead)
continue;
for(const auto& enemy : gm.enemies)
{
// 敵が死んでたらスキップする
if (enemy->isDead)
continue;
// 自機弾と敵が重なっているか?
if (MyMath::CircleCircleIntersection(
playerBullet->position, playerBullet->collisionRadius,
enemy->position, enemy->collisionRadius))
{
// 重なっていたら、それぞれのぶつかったときの処理を呼び出す
enemy->OnCollisionPlayerBullet(playerBullet);
playerBullet->OnCollisionEnemy(enemy);
// 衝突の結果、自機弾が死んだら、この弾のループはおしまい
if (playerBullet->isDead)
break;
}
}
}
// 自機と敵の衝突判定
for(const auto& enemy : gm.enemies)
{
// 自機が死んでたらこれ以上判定しない
if (gm.player->isDead)
break;
// 敵が死んでたらスキップ
if (enemy->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
gm.player->position, gm.player->collisionRadius,
enemy->position, enemy->collisionRadius))
{
gm.player->OnCollisionEnemy(enemy);
}
}
//return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
// 自機弾のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.playerBullets,
[](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 敵のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.enemies,
[](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
};
void Game::Draw()
{// 描画処理
DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);
// 敵の描画処理
for (const auto& e : gm.enemies)
{
e->Draw();
}
if (!gm.player->isDead) // 自機が死んでいなければ
gm.player->Draw(); // プレイヤの描画【忘れるとプレイヤ表示されない】
// 自機弾の描画処理
// C#のforeach文と同じく全自機弾を更新できる
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Draw();
}
};
Player.h
にライフと無敵時間を定義します。
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"
#include "GameObject.h"
#include "GameManager.h"
class Player : public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:
const float MoveSpeed = 6; // 移動速度
int MutekiJikan = 120; // 無敵時間
float collisionRadius = 32; // 当たり判定半径
int life = 3; // ライフ
int mutekiTimer = 0; // 残り無敵時間。0以下なら無敵じゃないってこと
//【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
// コンストラクタ
// pos : 初期位置
Player(Vector3 pos) : GameObject( pos ) //【忘れると】GameObjectの初期化処理が飛ばされて大混乱!
{
}
virtual ~Player() {}; // 仮想デストラクタ ※無いと【削除された関数を参照しようとしてますエラー】が出るかも
// 更新処理の関数定義
void Update();
// 描画処理の関数定義
void Draw();
// 敵にぶつかった時の処理
void OnCollisionEnemy(std::shared_ptr<GameObject> other);
// ダメージを受ける処理
void TakeDamage();
};
#endif
Player.cpp
に敵との当たり判定処理を追加します。
#include "Player.h"
#include "PlayerBullet.h" // 弾を生成して【使う処理があるのでインクルード必要】
// 更新処理
void Player::Update()
{
// 継承したGameObjectにvがあるのでvをfloatで定義し直さないように修正
v = Vector3(0,0,0); // xyz方向移動速度
if (Input::GetButton(PAD_INPUT_LEFT))
{
v.x = -MoveSpeed; // 左
}
else if (Input::GetButton(PAD_INPUT_RIGHT))
{
v.x = MoveSpeed; // 右
}
if (Input::GetButton(PAD_INPUT_UP))
{
v.y = -MoveSpeed; // 上
}
else if (Input::GetButton(PAD_INPUT_DOWN))
{
v.y = MoveSpeed; // 下
}
// 斜め移動も同じ速度になるように調整
if (v.magnitude() > 0)
{
v /= MyMath::Sqrt2;
}
// 実際に位置を動かす
position += v;
// ボタン押下で自機弾を発射
if (Input::GetButtonDown(PAD_INPUT_1))
{
// C++ではAddがpush_backに↓ newがmake_shared↓に
gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, 0));//右
gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, -15 * MyMath::Deg2Rad));//右上
gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, +15 * MyMath::Deg2Rad));//右下
}
mutekiTimer--; // 無敵タイマーは無条件で毎フレームカウントダウン
}
// 描画処理
void Player::Draw()
{
// 無敵ではない場合は描画
// 無敵中は2フレームに1回描画(点滅)
if (mutekiTimer <= 0 || mutekiTimer % 2 == 0)
DrawRotaGraphF(position.x, position.y, 1, 0, Image::player, TRUE);
}
// 敵にぶつかった時の処理
void Player::OnCollisionEnemy(std::shared_ptr<GameObject> other)
{
// 無敵じゃなければダメージを受ける
if (mutekiTimer <= 0)
{
TakeDamage();
}
}
// ダメージを受ける処理
void Player::TakeDamage()
{
life -= 1; // ライフ減少
if (life <= 0)
{
// ライフが無くなったら死亡
isDead = true;
}
else
{
// 無敵時間発動
mutekiTimer = MutekiJikan;
}
}
EnemyBullet.h
を新規作成します。
#ifndef ENEMYBULLET_H_
#define ENEMYBULLET_H_
#include <cmath> //sin,cosを使うのに必要
#include "DxLib.h"
#include "Image.h"
#include "Screen.h"
#include "Vector3.h"
#include "GameObject.h"
// 敵弾クラス
class EnemyBullet: public GameObject
{
public:
const int VisibleRadius = 8; // 見た目の半径
float angle; // 移動角度(垂直Vertical)
float angleH=0; // 移動角度(水平Horizontal)
// コンストラクタ
EnemyBullet(Vector3 pos, float angle, float speed) : GameObject( pos )
{
this->angle = angle;
// 角度からx方向の移動速度を算出
v.x = (float)std::cos(angle) * speed;
// 角度からy方向の移動速度を算出
v.y = (float)std::sin(angle) * speed;
}
// コンストラクタ
EnemyBullet(Vector3 pos, float angleV, float angleH, float speed) : GameObject( pos )
{
this->angle = angleV;
this->angleH = angleH;
// [3Dの角度は水平・垂直に分割]https://teratail.com/questions/126004
float cosAngleV = (float)std::cos(angle);
// 角度からx方向の移動速度を算出
v.x = cosAngleV * (float)std::sin(angleH) * speed;
// 角度からy方向の移動速度を算出
v.y = (float)std::sin(angle) * speed;
// 角度からz方向の移動速度を算出
v.z = cosAngleV * (float)std::cos(angleH) * speed;
}
// 更新処理
void Update()
{
// 速度の分だけ移動
position += v;
// 画面外に出たら死亡フラグを立てる
if (position.y + VisibleRadius < 0 || position.y - VisibleRadius > Screen::Height ||
position.x + VisibleRadius < 0 || position.x - VisibleRadius > Screen::Width)
{
isDead = true;
}
}
// 描画処理
void Draw()
{
DrawRotaGraphF(position.x, position.y, 1, angle, Image::enemyBullet16,TRUE);
}
// プレイヤにぶつかった時の処理
virtual void OnCollisionPlayer(std::shared_ptr<GameObject> other)
{
isDead = true;
}
};
#endif
GameManager.h
にEnemyBulletを管理するリストの定義を追加します。
#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_
#include <memory>
#include <vector>
#include "Singleton.h"
class Player; // クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class PlayerBullet;
class Enemy;
class EnemyBullet;
class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可
std::shared_ptr<Player> player{ nullptr }; // 自機のポインタ
std::list<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
std::list<std::shared_ptr<EnemyBullet>> enemyBullets; // 敵弾のリスト
// 複数リスト↑list ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる
(中略)..............
protected:
GameManager() {}; // 外部からのインスタンス作成は禁止
virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};
#endif
Game.cpp
にEnemyBulletの更新Updateと削除Remove、描画Drawの処理を追加します。
#include "Game.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "EnemyBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
(中略)..................
};
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
(中略)..................
if (!gm.player->isDead) // 自機が死んでいなければ
gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
// 自機弾の更新処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Update();
}
// 敵弾の更新処理
for (const auto& b : gm.enemyBullets)
{
b->Update();
}
// 敵の更新処理
for (const auto& b : gm.enemies)
{
b->Update();
}
// 自機弾と敵の衝突判定
for(const auto& playerBullet : gm.playerBullets)
{
// 自機弾が死んでたらスキップする
if (playerBullet->isDead)
continue;
for(const auto& enemy : gm.enemies)
{
// 敵が死んでたらスキップする
if (enemy->isDead)
continue;
// 自機弾と敵が重なっているか?
if (MyMath::CircleCircleIntersection(
playerBullet->position, playerBullet->collisionRadius,
enemy->position, enemy->collisionRadius))
{
// 重なっていたら、それぞれのぶつかったときの処理を呼び出す
enemy->OnCollisionPlayerBullet(playerBullet);
playerBullet->OnCollisionEnemy(enemy);
// 衝突の結果、自機弾が死んだら、この弾のループはおしまい
if (playerBullet->isDead)
break;
}
}
}
// 自機と敵の衝突判定
for(const auto& enemy : gm.enemies)
{
// 自機が死んでたらこれ以上判定しない
if (gm.player->isDead)
continue;
// 敵が死んでたらスキップ
if (enemy->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
gm.player->position, gm.player->collisionRadius,
enemy->position, enemy->collisionRadius))
{
gm.player->OnCollisionEnemy(enemy);
}
}
//return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
// 自機弾のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.playerBullets,
[](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 敵のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.enemies,
[](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 敵弾のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.enemyBullets,
[](std::shared_ptr<EnemyBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
};
void Game::Draw()
{// 描画処理
DrawRotaGraphF(x, 200, 0.9f, 0, Image::bossImage, TRUE);
// 敵の描画処理
for (const auto& e : gm.enemies)
{
e->Draw();
}
if (!gm.player->isDead) // 自機が死んでいなければ
gm.player->Draw(); // プレイヤの描画【忘れるとプレイヤ表示されない】
// 自機弾の描画処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Draw();
}
// 敵弾の描画処理
for (const auto& b : gm.enemyBullets)
{
b->Draw();
}
};
Zako0.h
に弾を発射間隔を開けながら撃たせます。
#ifndef ZAKO_0_H_
#define ZAKO_0_H_
#include "Enemy.h"
#include "Image.h"
#include "MyMath.h"
#include "EnemyBullet.h" // make_sharedなど【実際の弾を撃ち生成処理を行うのでインクルード必要】
// ザコ0クラス
class Zako0 : public Enemy
{
public:
int counter = 0;
// コンストラクタ
Zako0(Vector3 pos) : Enemy( pos )
{
this->tag = "Zako0";
}
// 更新処理
void Update() override
{
position.x -= 1; // とりあえず左へ移動
counter++;
if (counter % 10 == 0)
{
gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, 180 * MyMath::Deg2Rad, 8));
}
}
// 描画処理
void Draw() override
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako0, TRUE);
}
};
#endif
Zako0.h
の弾の発射間隔を開ける方法は別のやり方もあります。
#ifndef ZAKO_0_H_
#define ZAKO_0_H_
#include "Enemy.h"
#include "Image.h"
#include "MyMath.h"
#include "EnemyBullet.h" // make_sharedなど【実際の弾を撃ち生成処理を行うのでインクルード必要】
// ザコ0クラス
class Zako0 : public Enemy
{
public:
int coolTime = 0; // クールタイム(冷却時間。0になるまで次の弾が撃てない)
// コンストラクタ
Zako0(Vector3 pos) : Enemy( pos )
{
this->tag = "Zako0";
}
// 更新処理
void Update() override
{
position.x -= 1; // とりあえず左へ移動
coolTime--;
if (coolTime <= 0)
{
gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, 180 * MyMath::Deg2Rad, 8));
coolTime += 10;
}
}
// 描画処理
void Draw() override
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako0, TRUE);
}
};
#endif
弾の発射角度の方向を180度 + (-15度~+15度)にすることで散乱弾を実現します。
まず、乱数生成に必要な乱数クラスMyRandom.h
を作成します。
#ifndef MYRANDOM_H_
#define MYRANDOM_H_
#include <random>
class MyRandom
{
public:
// ゲーム中で唯一のインスタンス
static std::random_device rnd;// 非決定的な乱数生成器
static std::mt19937 random; // メルセンヌ・ツイスタの32ビット版
// 初期化(シード指定無し)
static void Init()
{ // https://cpprefjp.github.io/reference/random/random_device.html
random.seed(rnd()); // 乱数をシードにすることで毎回ランダムにする
}
// 初期化(シードを指定)
static void Init(int seed)
{
random.seed(seed);
}
// 指定した範囲の整数の乱数を取得する
static int Range(int min, int max)
{
std::uniform_int_distribution<> randRange(min, max); // [min, max] 範囲の一様乱数
return randRange(random);
}
// 指定した範囲の小数の乱数を取得する
static float Range(float min, float max)
{
std::uniform_real_distribution<> randRange(min, max); // [min<= x <=max] 範囲の一様乱数
return (float)randRange(random);
}
// 指定した確率(%)でtrueになる
static bool Percent(float probability)
{
return Range(0.0f, 1.0f) * 100 <= probability;
}
// 指定した範囲で乱数を返却する。
// 例えば1.5fを指定すると、-1.5~+1.5の範囲の値を返却する。
static float PlusMinus(float value)
{
return Range(-value, value);
}
};
#endif
staticなクラスなのでMyRandom.cpp
を作成して変数をcppで定義初期化します。
#include "MyRandom.h"
std::random_device MyRandom::rnd;// 非決定的な乱数生成器
std::mt19937 MyRandom::random; // メルセンヌ・ツイスタの32ビット版 https://qiita.com/angel_p_57/items/971d0208186f81d5d044
Zako2.h
を作成して弾の発射方向を-15度~+15度までランダムに散らすようにしてみます。
#ifndef ZAKO_2_H_
#define ZAKO_2_H_
#include "Enemy.h"
#include "Image.h"
#include "Screen.h"
#include <cmath> // Sin Cosの計算に使う
#include "MyMath.h"
#include "MyRandom.h"
#include "EnemyBullet.h" // make_sharedなど【実際の弾を撃ち生成処理を行うのでインクルード必要】
// ザコ2クラス
class Zako2 : public Enemy
{
public:
int coolTime = 0; // クールタイム(冷却時間。0になるまで次の弾が撃てない)
// コンストラクタ
Zako2(Vector3 pos) : Enemy( pos )
{
this->tag = "Zako2";
}
// 更新処理
void Update() override
{
if(position.x > Screen::Width - 200)
{ // スクリーンの端から200の位置で止まる
position.x -= 1; // とりあえず左へ移動
}
coolTime--;
if (coolTime <= 0)
{
float bulletAngle = MyRandom::Range(-15.0f,15.0f); // ±15度の乱数の角度を取得
gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, (180 + bulletAngle) * MyMath::Deg2Rad, 8));
coolTime += 10;
}
}
// 描画処理
void Draw() override
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako2, TRUE);
}
};
#endif
Game.cpp
にMyRandom乱数とザコ2の初期化を追加します。
#include "Game.h"
#include "MyRandom.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "EnemyBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako2.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
Image::Load(); //画像の読込み
MyRandom::Init(); // 乱数シードの初期化
(中略)..................
// とりあえず適当に敵を生成
gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)600, (float)100)));
gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)1000, (float)500)));
gm.enemies.emplace_back(std::make_shared<Zako1>(Vector3((float)1000, (float)150)));
gm.enemies.emplace_back(std::make_shared<Zako2>(Vector3((float)1200, (float)250)));
};
void Game::Update()
{// 更新処理
(中略)..................
};
void Game::Draw()
{// 描画処理
(中略)..................
};
どうでしょうか(-15度~+15度)の散乱弾を撃つ敵キャラを作れましたか?
まずはザコ2を試してみましょう。うまくいったら次のザコ3を試してみましょう。
こんどはZako3.h
を作成して10ダメージ受けると点滅状態(5秒=5×60=300フレーム)になり、isDeadと同時に弾を全方向に0度~360度に散らすようにしてみます。
勉強になるテクニックとして【共通処理の上書きオーバーライド】や【点滅処理の条件式】や【ド・モルガンの定理】を使っているので上級者も1度は試してみましょう。
#ifndef ZAKO_3_H_
#define ZAKO_3_H_
#include "Enemy.h"
#include "Image.h"
#include "Screen.h"
#include <cmath> // Sin Cosの計算に使う
#include "MyMath.h"
#include "Vector3.h"
#include "MyRandom.h"
#include "EnemyBullet.h" // make_sharedなど【実際の弾を撃ち生成処理を行うのでインクルード必要】
// ザコ3クラス(自爆型機雷タイプの敵キャラ)
class Zako3 : public Enemy
{
public:
int bombCounter = -1; // やられたときのisDeadまでのカウントダウン
int BombTime = 300; // 爆発までのカウント時間
int life = 10; // 敵ライフ
// コンストラクタ
Zako3(Vector3 pos) : Enemy( pos )
{
this->tag = "Zako3";
}
// 更新処理
void Update() override
{
if(position.x > Screen::Width - 350)
{ // スクリーンの端から350の位置で止まる
position.x -= 1; // とりあえず左へ移動
}
else
{ // スクリーン端から350の位置で自爆モードに入る
life = 0; // 画面中央付近で勝手にlife=0で自爆モードに入る
}
bombCounter--; // 爆発カウンタは常にカウントダウン(-1から始まるときは0を通過しないので爆発しない)
if(life <= 0 && bombCounter < 0)
bombCounter = BombTime; // 爆発モードに入る(bombCounterがプラス値になり0になると爆発)
if (bombCounter == 0) // 初期状態は-1からカウントダウン-1,-2,-3..するからここには入らずに済む
{
for(int i=0; i < 360; i = i+10) //【勉強】for文は1ずつプラスじゃなくてもよい
{
gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, (180 + i) * MyMath::Deg2Rad, 6));
}
isDead = true; // 爆発カウンタ==0で初めてisDead状態に
}
}
// 【勉強】共通処理を上書きオーバーライドで上塗りして共通処理を無効化・乗っ取るテクニック
// 自機弾との衝突処理を★【上書きオーバーライド】して死亡フラグが立たないように
virtual void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet) override
{
life -= 1; // ライフを減らす
// 死亡フラグはこのザコ3だけ特別に衝突ではONにしない
}
// 描画処理
void Draw() override
{
//【ド・モルガンの定理を学ぼう】||と&&の逆の関係性の法則
//【勉強】条件【または||】は条件【かつ&&】の(life <= 0 && bombCounter > 0)のちょうど逆条件!
if(life > 0 || bombCounter <= 0
|| (bombCounter % 8 < 4) ) //[点滅]パラパラ8枚の周期で余り4以下の時だけ描く
DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako3, TRUE);
}
};
#endif
Game.cpp
にザコ3の生成処理を追加します。
#include "Game.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "EnemyBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako2.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako3.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
(中略)..................
// とりあえず適当に敵を生成
gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)600, (float)100)));
gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)1000, (float)500)));
gm.enemies.emplace_back(std::make_shared<Zako1>(Vector3((float)1000, (float)150)));
gm.enemies.emplace_back(std::make_shared<Zako2>(Vector3((float)1200, (float)250)));
gm.enemies.emplace_back(std::make_shared<Zako3>(Vector3((float)1300, (float)400)));
};
void Game::Update()
{// 更新処理
(中略)..................
};
void Game::Draw()
{// 描画処理
(中略)..................
};
どうでしょうか爆弾っぽい自爆タイプの敵キャラが作れましたか?
[C++]シューティングゲーム 3
次のテキスト4はこちら。
自機と敵弾がぶつかったら、自機がダメージを受けるようにしましょう。
EnemyBullet.h
に当たり判定の大きさを表す変数を追加します。
#ifndef ENEMYBULLET_H_
#define ENEMYBULLET_H_
#include <cmath> //sin,cosを使うのに必要
#include "DxLib.h"
#include "Image.h"
#include "Screen.h"
#include "GameObject.h"
// 敵弾クラス
class EnemyBullet: public GameObject
{
public:
const int VisibleRadius = 8; // 見た目の半径
float collisionRadius = 8; // 当たり判定半径
(以下略)........................
Player.h
に敵弾とぶつかったときの処理の定義を追加します。
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"
#include "GameObject.h"
#include "GameManager.h"
class Player : public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:
(中略)...........
// 敵にぶつかった時の処理
void OnCollisionEnemy(std::shared_ptr<GameObject> other);
// 敵弾とぶつかった時の処理
void OnCollisionEnemyBullet(std::shared_ptr<GameObject> other);
// ダメージを受ける処理
void TakeDamage();
};
#endif
Player.cpp
に敵弾とぶつかったときの処理を追加します。
#include "Player.h"
#include "PlayerBullet.h" // 弾を生成して【使う処理があるのでインクルード必要】
// 更新処理
void Player::Update()
{
(中略).........
}
// 描画処理
void Player::Draw()
{
(中略).........
}
// 敵にぶつかった時の処理
void Player::OnCollisionEnemy(std::shared_ptr<GameObject> other)
{
// 無敵じゃなければダメージを受ける
if (mutekiTimer <= 0)
{
TakeDamage();
}
}
// 敵弾とぶつかった時の処理
void Player::OnCollisionEnemyBullet(std::shared_ptr<GameObject> other)
{
// 無敵じゃなければダメージを受ける
if (mutekiTimer <= 0)
{
TakeDamage();
}
}
// ダメージを受ける処理
void Player::TakeDamage()
{
life -= 1; // ライフ減少
if (life <= 0)
{
// ライフが無くなったら死亡
isDead = true;
}
else
{
// 無敵時間発動
mutekiTimer = MutekiJikan;
}
}
Game.cpp
に当たり判定の処理を追加します。
#include "Game.h"
#include "MyRandom.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "EnemyBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako2.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako3.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
(中略)..................
};
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
(中略)..................
if (!gm.player->isDead) // 自機が死んでいなければ
gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
// 自機弾の更新処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Update();
}
// 敵弾の更新処理
for (const auto& b : gm.enemyBullets)
{
b->Update();
}
// 自機と敵弾の衝突判定
for (const auto& enemyBullet : gm.enemyBullets)
{
// 自機が死んでたらこれ以上判定しない
if (gm.player->isDead)
break;
// 敵弾が死んでたらスキップ
if (enemyBullet->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
gm.player->position, gm.player->collisionRadius,
enemyBullet->position, enemyBullet->collisionRadius))
{
gm.player->OnCollisionEnemyBullet(enemyBullet);
}
}
// 敵の更新処理
for (const auto& e : gm.enemies)
{
e->Update();
}
// 自機弾と敵の衝突判定
for(const auto& playerBullet : gm.playerBullets)
{
// 自機弾が死んでたらスキップする
if (playerBullet->isDead)
continue;
for(const auto& enemy : gm.enemies)
{
// 敵が死んでたらスキップする
if (enemy->isDead)
continue;
// 自機弾と敵が重なっているか?
if (MyMath::CircleCircleIntersection(
playerBullet->position, playerBullet->collisionRadius,
enemy->position, enemy->collisionRadius))
{
// 重なっていたら、それぞれのぶつかったときの処理を呼び出す
enemy->OnCollisionPlayerBullet(playerBullet);
playerBullet->OnCollisionEnemy(enemy);
// 衝突の結果、自機弾が死んだら、この弾のループはおしまい
if (playerBullet->isDead)
break;
}
}
}
// 自機と敵の衝突判定
for(const auto& enemy : gm.enemies)
{
// 自機が死んでたらこれ以上判定しない
if (gm.player->isDead)
continue;
// 敵が死んでたらスキップ
if (enemy->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
gm.player->position, gm.player->collisionRadius,
enemy->position, enemy->collisionRadius))
{
gm.player->OnCollisionEnemy(enemy);
}
}
//return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
// 自機弾のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.playerBullets,
[](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 敵のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.enemies,
[](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
};
void Game::Draw()
{// 描画処理
(中略)......
};
DivImage.h
を新規作成して、カスタムしたデータタイプ【DivImage型】を定義して縦2×横8の分割された爆発画像を読込みましょう。
#ifndef DIVIMAGE_H_
#define DIVIMAGE_H_
#include "DxLib.h"
#include <assert.h> // 画像の2のべき乗チェックに使う
// 分割画像を読込むためのデータ構造(画像数や画像ハンドル配列、3Dに使う場合の2のべき乗チェック機能をもつ)
class DivImage
{ // 2Dの分割画像や3Dに分割Div画像を使うと画像全体が使われてしまうのを回避するための情報
public:
int XNum = 0;
int YNum = 0;
int XSize = 0;
int YSize = 0;
int* HandleArray = nullptr;
int AllNum = 0;
bool is3DTexture = false;
// 初期化 : X方向分割画像数 XNum, Y方向分割画像数 YNum, 画像 横 幅XSizeピクセル, 画像 縦 高さYSizeピクセル
DivImage(int XNum, int YNum, int XSize, int YSize, bool is3DTexture=false)
{ // 初期化コンストラクタ
this->XNum = XNum; // X方向分割画像数
this->YNum = YNum; // Y方向分割画像数
this->XSize = XSize; // 画像 横 幅XSize(ピクセルドット数)
this->YSize = YSize; // 画像 縦 高さYSize(ピクセルドット数)
AllNum = XNum * YNum; // トータルの分割画像数
this->is3DTexture = is3DTexture; // 3Dテクスチャ分割画像かtrueだと2のべき乗チェックが入る
// ★div分割画像読込ハンドラ保存用のint配列メモリを確保し-1で初期化
this->HandleArray = new int[AllNum]; // 配列を確保し-1で初期化
for (int i = 0; i < AllNum; i++) HandleArray[i] = -1;
//↑【演習★】初期化後の動的配列をデバッガで見てみよう[ary,256として再評価ボタン]
// http://visualstudiostudy.blog.fc2.com/blog-entry-17.html
int* ary = HandleArray;
#ifdef _DEBUG // デバッグ機能(デバッグの時だけ _DEBUG が定義)
if(is3DTexture==true) ImagePowCheck((*this)); // *(this)でthisポインタの示す変数内容を表す
#endif
};
~DivImage()
{ // デストラクタでメモリを解放
if (this->HandleArray != nullptr)
delete[] this->HandleArray;
};
//【勉強 []添え字演算子を独自に定義】https://programming.pc-note.net/cpp/operator.html
// 内部のHandleArrayへ今までの様にImage::explosion[5]とかで簡単アクセスできるように【あいだに噛ます】
int const& operator [](int index) const
{
if(index<0 || AllNum<=index) assert("画像データのアクセス番号がおかしい!" == "");
return HandleArray[index];
}
int& operator [](int index)
{
if(index<0 || AllNum<=index) assert("画像データのアクセス番号がおかしい!" == "");
return HandleArray[index];
}
#ifdef _DEBUG // デバッグ機能(デバッグの時だけ _DEBUG が定義)
bool is_pow2(unsigned int x) // 2のべき乗か計算
{ // https://programming-place.net/ppp/contents/c/rev_res/math012.html
return (x != 0) && (x & (x - 1)) == 0;
}
void ImagePowCheck(DivImage& divImage)
{ // ★3Dに使う画像は2のべき乗でなければ受け付けないようにする
// https://yttm-work.jp/gmpg/gmpg_0031.html
// https://yappy-t.hatenadiary.org/entry/20100110/1263138881
if (divImage.XSize > 0 && divImage.YSize > 0
&& is_pow2(divImage.XSize) && is_pow2(divImage.YSize)) return;
else assert("3Dに使うなら2のべき乗の画像サイズにしなきゃ" == "");
}
#endif
private: // コピーと代入をプライベートにして禁止する
// コピーコンストラクタの禁止privateオーバーロード
DivImage(const DivImage& divImage) {};
// 代入演算子の禁止privateオーバーロード
void operator=(const DivImage& divImage) {};
};
#endif
Image.h
でカスタムしたデータタイプ【DivImage型】で縦2×横8の分割された爆発画像を読込みましょう。
#ifndef IMAGE_H_
#define IMAGE_H_
#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
#include "DivImage.h" // 分割画像の読込みに使う
class Image
{
public:
Image() {}; // 初期化コンストラクタ:定義と{}空の処理
~Image() {}; // 破棄する処理デストラクタ:定義と{}空の処理
static void Load();
static int LoadDivGraph(const TCHAR* FileName, DivImage& divImage);
static int bossImage; //ボス画像のハンドラ(読込画像番号)
static int player; //プレイヤ画像のハンドラ
static int playerBullet; //プレイヤの弾画像のハンドラ
static int enemyBullet16; //敵弾 画像のハンドラ(読込画像番号)
static int zako0; //ザコ0 画像のハンドラ(読込画像番号)
static int zako1; //ザコ1 画像のハンドラ(読込画像番号)
static int zako2; //ザコ2 画像のハンドラ(読込画像番号)
static int zako3; //ザコ3 画像のハンドラ(読込画像番号)
static DivImage explosion; // [分割画像]爆発エフェクト
private:
};
#endif
次に対となるcppファイルも変更します。
Image.cpp
の内容を次のようにします:
#include "Image.h"
int Image::bossImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{-1};
int Image::playerBullet{-1};
int Image::enemyBullet16{-1};
int Image::zako0{-1};
int Image::zako1{-1};
int Image::zako2{-1};
int Image::zako3{-1};
// ★分割画像は初期化のときに{X方向画像数, Y方向画像数, 画像横幅XSize,画像縦幅YSize}を指定
DivImage Image::explosion{ 8, 2, 64, 64 };
//↑どのような画像を使うかで変わる
// 例えば https://pipoya.net/sozai/assets/effects/effect-event-1/ のイベントエフェクト.zipの
// 1800×180の縦一列のみなら DivImage Image::explosion{ 10, 1, 180, 180 };となる
void Image::Load()
{
bossImage = LoadGraph("Image/boss1.png");
assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
player= LoadGraph("Image/player.png");
assert(player!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
playerBullet = LoadGraph("Image/player_bullet.png");
assert(playerBullet!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
enemyBullet16 = LoadGraph("Image/enemy_bullet_16.png");
assert(enemyBullet16!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako0 = LoadGraph("Image/zako0.png");
assert(zako0!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako1 = LoadGraph("Image/zako1.png");
assert(zako1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako2 = LoadGraph("Image/zako2.png");
assert(zako2!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako3 = LoadGraph("Image/zako3.png");
assert(zako3!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
// div分割画像をロード
Image::LoadDivGraph("Image/explosion.png", explosion);
for (int i = 0; i < explosion.AllNum; i++)
{ // 画像読込失敗
if (explosion.HandleArray[i] == -1) assert("爆発分割画像読込失敗" == "");
}
}
// DXライブラリのLoadDivGraphを使いやすくラッピング
// ★関数に参照渡しを行う
// 関数の引数に参照渡しを行うことでdivImageの中身を書き換えることができる(値渡しでは書換不可)
// https://qiita.com/agate-pris/items/05948b7d33f3e88b8967
// https://qiita.com/RuthTaro/items/f35c3a26779c0ca1a41a
int Image::LoadDivGraph(const TCHAR* FileName, DivImage& divImage)
{
return DxLib::LoadDivGraph(FileName, divImage.XNum * divImage.YNum,
divImage.XNum, divImage.YNum, divImage.XSize, divImage.YSize, divImage.HandleArray);
};
Explosion.h
を新規作成して爆発エフェクトを表示するクラスを作ります。
#ifndef EXPLOSION_H_
#define EXPLOSION_H_
#include "Image.h"
#include "GameObject.h"
// 爆発エフェクトクラス
class Explosion : public GameObject
{
public:
int counter = 0; // 時間を計るための変数
int imageIndex = 0; // 表示すべき画像の番号
// コンストラクタ
Explosion(Vector3 pos) : GameObject( pos )
{
this->tag = "Explosion";
}
// 更新処理
void Update() override
{
counter++;
imageIndex = counter / 3; // そのままだと速すぎるので、3で割る
// 絵は16枚しかない(0~15)ので、
// 16番目を表示しようとしたら終了。
if (imageIndex >= 16)
{
isDead = true;
}
}
// 描画処理
void Draw() override
{
// 分割して読み込んだ絵のハンドルが各要素に格納されているので、
// 要素番号を指定してハンドルを取り出して使う。
DrawRotaGraphF(position.x, position.y, 1, 0, Image::explosion[imageIndex], TRUE);
}
};
#endif
GameManager.h
にExplosionを管理するリストの定義を追加します。
#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_
#include <memory>
#include <vector>
#include "Singleton.h"
class Player; // クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class PlayerBullet;
class Enemy;
class EnemyBullet;
class Explosion;
class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可
std::shared_ptr<Player> player{ nullptr }; // 自機のポインタ
std::list<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
std::list<std::shared_ptr<EnemyBullet>> enemyBullets; // 敵弾のリスト
std::list<std::shared_ptr<Explosion>> explosions; // 爆発エフェクトのリスト
// 複数リスト↑list ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる
(中略)..............
protected:
GameManager() {}; // 外部からのインスタンス作成は禁止
virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};
#endif
Game.cpp
に当たり判定の処理を追加します。
#include "Game.h"
#include "MyRandom.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "EnemyBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Explosion.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako2.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako3.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
(中略)..................
};
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
(中略)..................
// 自機と敵の衝突判定
for(const auto& enemy : gm.enemies)
{
// 自機が死んでたらこれ以上判定しない
if (gm.player->isDead)
continue;
// 敵が死んでたらスキップ
if (enemy->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
gm.player->position, gm.player->collisionRadius,
enemy->position, enemy->collisionRadius))
{
gm.player->OnCollisionEnemy(enemy);
}
}
// 爆発エフェクトの更新処理
for (const auto& e : gm.explosions)
{
e->Update();
}
//return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
// 自機弾のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.playerBullets,
[](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 敵のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.enemies,
[](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 敵弾のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.enemyBullets,
[](std::shared_ptr<EnemyBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
//爆発エフェクトのリストから死んでるものを除去する
gm.EraseRemoveIf(gm.explosions,
[](std::shared_ptr<Explosion>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
};
void Game::Draw()
{// 描画処理
(中略)......
// 敵の描画処理
for (const auto& e : gm.enemies)
{
e->Draw();
}
// 爆発エフェクトの描画処理
for (const auto& e : gm.explosions)
{
e->Draw();
}
(中略)......
};
Enemy.h
を改修して、敵が死んだときに爆発エフェクトを出すようにしましょう。
#ifndef ENEMY_H_
#define ENEMY_H_
#include "GameObject.h"
#include "GameManager.h"
#include "Explosion.h"
// 敵の基底クラス。
// 全ての敵は、このクラスを継承して作る。
// ★【勉強】abstract型はC++にない!!【純粋仮想関数】を使って作る
class Enemy : public GameObject
{
public:
//【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
float collisionRadius = 32; // 当たり判定の半径
bool isDead = false; // 死亡フラグ
float life = 1; // 耐久力
// コンストラクタ
Enemy(Vector3 pos) : GameObject( pos ) // GameObjectをベースとして初期化処理を継承する
{
this->typeTag = "Enemy";
}
// 更新処理。派生クラスでオーバーライドして使う
virtual void Update() = 0;//virtual ~ = 0;で【純粋仮想関数に】これで【C#のabstract型に】
// 描画処理。派生クラスでオーバーライドして使う
virtual void Draw() = 0; //virtual ~ = 0;で【純粋仮想関数に】【関数 = 0で未定義状態を表現】
// 自機弾に当たったときの処理
virtual void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet)
{
life -= 1; // ライフを減らす
// ライフが無くなったら、死亡
if (life <= 0)
{
isDead = true;
// 爆発を出す
gm.explosions.emplace_back(std::make_shared<Explosion>(Vector3(position.x, position.y)));
}
}
};
#endif
まずはボスの3状態の画像をImageフォルダに用意しておきましょう。
Image.h
にboss1.png、boss2.png、boss3.pngの読込み処理を追加します。
#ifndef IMAGE_H_
#define IMAGE_H_
#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
#include "DivImage.h" // 分割画像の読込みに使う
class Image
{
public:
Image() {}; // 初期化コンストラクタ:定義と{}空の処理
~Image() {}; // 破棄する処理デストラクタ:定義と{}空の処理
static void Load();
static int LoadDivGraph(const TCHAR* FileName, DivImage& divImage);
static int bossImage; //ボス画像のハンドラ(読込画像番号)
static int player; //プレイヤ画像のハンドラ
static int playerBullet; //プレイヤの弾画像のハンドラ
static int enemyBullet16; //敵弾 画像のハンドラ(読込画像番号)
static int zako0; //ザコ0 画像のハンドラ(読込画像番号)
static int zako1; //ザコ1 画像のハンドラ(読込画像番号)
static int zako2; //ザコ2 画像のハンドラ(読込画像番号)
static int zako3; //ザコ3 画像のハンドラ(読込画像番号)
static int boss1; //ボス通常 画像のハンドラ(読込画像番号)
static int boss2; //ボス2気絶 画像のハンドラ(読込画像番号)
static int boss3; //ボス3発狂 画像のハンドラ(読込画像番号)
static DivImage explosion; // [分割画像]爆発エフェクト
private:
};
#endif
次に対となるcppファイルも変更します。
Image.cpp
の内容を次のようにします:
#include "Image.h"
int Image::bossImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{-1};
int Image::playerBullet{-1};
int Image::enemyBullet16{-1};
int Image::zako0{-1};
int Image::zako1{-1};
int Image::zako2{-1};
int Image::zako3{-1};
int Image::boss1{-1};
int Image::boss2{-1};
int Image::boss3{-1};
// ★分割画像は初期化のときに{X方向画像数, Y方向画像数, 画像横幅XSize,画像縦幅YSize}を指定
DivImage Image::explosion{ 8, 2, 64, 64 };
void Image::Load()
{
bossImage = LoadGraph("Image/boss1.png");
assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
player= LoadGraph("Image/player.png");
assert(player!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
playerBullet = LoadGraph("Image/player_bullet.png");
assert(playerBullet!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
enemyBullet16 = LoadGraph("Image/enemy_bullet_16.png");
assert(enemyBullet16!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako0 = LoadGraph("Image/zako0.png");
assert(zako0!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako1 = LoadGraph("Image/zako1.png");
assert(zako1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako2 = LoadGraph("Image/zako2.png");
assert(zako2!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako3 = LoadGraph("Image/zako3.png");
assert(zako3!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
boss1 = LoadGraph("Image/boss1.png");
assert(boss1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
boss2 = LoadGraph("Image/boss2.png");
assert(boss2!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
boss3 = LoadGraph("Image/boss3.png");
assert(boss3!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
// div分割画像をロード
Image::LoadDivGraph("Image/explosion.png", explosion);
for (int i = 0; i < explosion.AllNum; i++)
{ // 画像読込失敗
if (explosion.HandleArray[i] == -1) assert("爆発分割画像読込失敗" == "");
}
}
// DXライブラリのLoadDivGraphを使いやすくラッピング
// ★関数に参照渡しを行う
// 関数の引数に参照渡しを行うことでdivImageの中身を書き換えることができる(値渡しでは書換不可)
// https://qiita.com/agate-pris/items/05948b7d33f3e88b8967
// https://qiita.com/RuthTaro/items/f35c3a26779c0ca1a41a
int Image::LoadDivGraph(const TCHAR* FileName, DivImage& divImage)
{
return DxLib::LoadDivGraph(FileName, divImage.XNum * divImage.YNum,
divImage.XNum, divImage.YNum, divImage.XSize, divImage.YSize, divImage.HandleArray);
};
Boss.h
を新規作成してif else でStateに応じて行動を遷移させてみましょう。
「登場」画面右から出てくる
↓
「通常」通常の状態。通常の攻撃を行う
↓
「気絶」気絶状態の絵になる
↓
「発狂」怒った絵になる。激しい攻撃を繰り広げてくる
↓
「死亡」少し時間をかけて死亡の演出を行う
↓
完全に消滅
Boss.hを新規作成
します。#ifndef BOSS_H_
#define BOSS_H_
#include "Enemy.h"
#include "Image.h"
// ボスクラス。Enemyを継承して作る
class Boss : public Enemy
{
public:
// ボスの状態種別
enum class State
{
Appear, // 登場
Normal, // 通常時
Swoon, // 気絶時
Angry, // 発狂モード
Dying, // 死亡
};
State state = State::Appear; // 現在の状態
int swoonTime = 120; // 残り気絶時間
int dyingTime = 180; // 死亡時のアニメーション時間
// コンストラクタ
Boss(Vector3 pos) : Enemy( pos )
{
this->tag = "Boss"; // オブジェクトの種類判別タグ
life = 100; // ライフ
collisionRadius = 70; // 当たり判定半径
}
// 更新処理
void Update() override
{
if (state == State::Appear) // 登場状態
{
position.x -= 1; // 左へ移動
if (position.x <= 750) // x座標が750以下になったら
{
state = State::Normal; // 通常状態へ移行
}
}
else if (state == State::Normal) // 通常状態
{
}
else if (state == State::Swoon) // 気絶状態
{
swoonTime--; // タイマー減少
if (swoonTime <= 0) // タイマーが0になったら
{
state = State::Angry; // 発狂モードへ
}
}
else if (state == State::Angry) // 発狂モード
{
}
else if (state == State::Dying) // 死亡中
{
dyingTime--; // タイマー減少
if (dyingTime <= 0) // タイマーが0になったら
{
isDead = true; // 完全に消滅
}
}
}
// 描画処理
void Draw() override
{
if (state == State::Appear || state == State::Normal) // 登場時と通常時
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss1, TRUE); // 普通の画像
}
else if (state == State::Swoon) // 気絶時
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss2, TRUE); // 気絶時の画像
}
else if (state == State::Angry) // 発狂モード
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss3, TRUE); // 怒ってる画像
}
else if (state == State::Dying) // 死亡中
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss2, TRUE); // 気絶時の画像
}
}
// 自機弾に当たったときの処理をoverride(上書き)する
virtual void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet) override
{
// 登場時、気絶時、死亡時は被弾しても何もしない
if (state == State::Appear || state == State::Swoon || state == State::Dying)
return;
life -= 1; // ライフを減らす
if (life <= 0)
{
// ライフが無くなったら、すぐ消滅するのではなく、死亡状態へ移行
state = State::Dying;
}
else if (state == State::Normal && life <= 50)
{
// 通常状態でライフが50以下になったら、気絶する
state = State::Swoon;
}
}
};
#endif
Game.cpp
にボスの生成処理を追加してボスが期待通りに状態遷移することを確認しましょう。
#include "Game.h"
#include "MyRandom.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"
#include "Zako0.h"
#include "Zako1.h"
#include "Zako2.h"
#include "Zako3.h"
#include "Boss.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
Image::Load(); //画像の読込み
MyRandom::Init(); // 乱数シードの初期化
(中略).................
gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
// とりあえず適当に敵を生成
gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
}
(以下略).................
さきほどのボスは登場してもじっとしたままです。
Zako0~Zako3の動きを応用して自分オリジナルのボスの行動パターンを自作してみてください。
例として下記に実装例を置いてはおきますがあくまで参考として、自分で考えて組んでみることが大事です。
ここまでの復習と集大成として、Zako0~Zako3のコードを移植しながら、プログラミングすることが肝要です。
Boss.hの行動パターンのプログラム例
(lifeとenumの状態をAngry以外にも増やせばもっと長期戦も演出できる)
#ifndef BOSS_H_
#define BOSS_H_
#include "Enemy.h"
#include "Image.h"
#include <cmath> // Sin Cosの計算に使う
#include "MyMath.h"
#include "MyRandom.h" // 乱数方向弾に使う
// ボスクラス。Enemyを継承して作る
class Boss : public Enemy
{
public:
// ボスの状態種別
enum class State
{
Appear, // 登場
Normal, // 通常時
Swoon, // 気絶時
Angry, // 発狂モード
Dying, // 死亡
};
State state = State::Appear; // 現在の状態
int swoonTime = 120; // 残り気絶時間
int dyingTime = 180; // 死亡時のアニメーション時間
int coolTime = 0; // クールタイム(冷却時間。0になるまで次の弾が撃てない)
float sinMove = 0; // Sinの動きをする際の0~360度
float sinRadius = 120; // Sinの動きをするときの半径
// コンストラクタ
Boss(Vector3 pos) : Enemy( pos )
{
this->tag = "Boss"; // オブジェクトの種類判別タグ
life = 100; // ライフ
collisionRadius = 70; // 当たり判定半径
}
// 更新処理
void Update() override
{
if (state == State::Appear) // 登場状態
{
position.x -= 1; // 左へ移動
if (position.x <= 750) // x座標が750以下になったら
{
state = State::Normal; // 通常状態へ移行
}
}
else if (state == State::Normal) // 通常状態
{
coolTime--;
if (coolTime <= 0)
{ // ボスの弾はスピード16で2倍の速度で撃ち出す
gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, 180 * MyMath::Deg2Rad, 16));
coolTime += 200; // そのかわり発射間隔は長め
}
float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
sinMove += 2.5f; // Sinの波運動の角度を進める
float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
// (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
position.y += currentSinY - prevSinY; // 上下方向のSinの波運動
}
else if (state == State::Swoon) // 気絶状態
{
swoonTime--; // タイマー減少
if (swoonTime <= 0) // タイマーが0になったら
{
coolTime = 0; // クールタイムを0にリセットしてから
state = State::Angry; // 発狂モードへ
}
}
else if (state == State::Angry) // 発狂モード
{
coolTime--;
if (coolTime <= 0)
{
gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, 180 * MyMath::Deg2Rad, 16));
coolTime += 20;
}
float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
sinMove += 10.5f; // Sinの波運動の角度を進める
float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
// (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
position.y += currentSinY - prevSinY; // 上下方向のSinの波運動
}
else if (state == State::Dying) // 死亡中
{
dyingTime--; // タイマー減少
if (dyingTime <= 0) // タイマーが0になったら
{
isDead = true; // 完全に消滅
}
}
}
// 描画処理
void Draw() override
{
if (state == State::Appear || state == State::Normal) // 登場時と通常時
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss1, TRUE); // 普通の画像
}
else if (state == State::Swoon) // 気絶時
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss2, TRUE); // 気絶時の画像
}
else if (state == State::Angry) // 発狂モード
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss3, TRUE); // 怒ってる画像
}
else if (state == State::Dying) // 死亡中
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss2, TRUE); // 気絶時の画像
}
}
// 自機弾に当たったときの処理をoverride(上書き)する
virtual void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet) override
{
// 登場時、気絶時、死亡時は被弾しても何もしない
if (state == State::Appear || state == State::Swoon || state == State::Dying)
return;
life -= 1; // ライフを減らす
if (life <= 0)
{
// ライフが無くなったら、すぐ消滅するのではなく、死亡状態へ移行
state = State::Dying;
}
else if (state == State::Normal && life <= 50)
{
// 通常状態でライフが50以下になったら、気絶する
state = State::Swoon;
}
}
};
#endif
Player.h
に現在のライフをバーで表示する処理の定義を追加します。
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"
#include "GameObject.h"
#include "GameManager.h"
class Player : public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:
(中略)...........
int lifeMax = 20; // 最大ライフ
int life = 3; // ライフ
int mutekiTimer = 0; // 残り無敵時間。0以下なら無敵じゃないってこと
(中略)...........
// 描画処理
void Draw();//【★←Player.cppを新規作成してそちらに処理を書き移す】
// ライフをバーで表示する処理
void DrawLifeBar();
// 敵にぶつかった時の処理
void OnCollisionEnemy(std::shared_ptr<GameObject> other);
// 敵弾とぶつかった時の処理
void OnCollisionEnemyBullet(std::shared_ptr<GameObject> other);
// ダメージを受ける処理
void TakeDamage();
};
#endif
Player.cpp
に現在のライフをバーで表示する処理の定義を追加します。
#include "Player.h"
(中略).........
// 更新処理
void Player::Update()
{
(中略).........
}
// 描画処理
void Player::Draw()
{
(中略).........
// ライフをバーで表示する処理
DrawLifeBar();
}
// ライフをバーで表示する処理
void Player::DrawLifeBar()
{
// ライフをバー表示する際の色設定
unsigned int frameColor = GetColor(255, 255, 255); // バーの枠の色
unsigned int backColor = GetColor(0, 0, 0); // バーの背景の色
unsigned int lineColor = GetColor(0, 0, 0); // バーの区切り目盛りの色
unsigned int red = GetColor(255, 0, 0), orange = GetColor(255, 100, 0); // ピンチ時の赤、オレンジ
unsigned int green = GetColor(0, 255, 0), blue = GetColor(0, 255, 255); // 10までは緑、11以上は水色
int barMax = (lifeMax > 10) ? 10 : lifeMax; // バーの目盛りの数
int barWidth = 40, barPart = barWidth / barMax, rectWidth = (barPart + 1) * barMax;
int barHeight = 4; // バーの表示する高さ(4+1+1) 枠は上+1, 下+1, 目盛ぶん↑+1して×10し直すとバーの四角形幅になる
Vector3 barPos{ position.x - rectWidth / 2, position.y + collisionRadius }; // バーの表示位置
// バーの背景を描画
DrawBox(barPos.x - 1, barPos.y - 1, barPos.x + 1 + rectWidth, barPos.y + 1 + barHeight, backColor, TRUE);
// バーの枠を描画
DrawBox(barPos.x - 1, barPos.y - 1, barPos.x + 1 + rectWidth, barPos.y + 1 + barHeight, frameColor, FALSE);
auto barColor = (life <= 1) ? red : (life <= 2) ? orange : green; // lifeが 残り1:赤,残り2:橙 10以下:緑
// バーの中身を10まで描画
int barNum = (life < 10) ? life : 10;
for (int i = 0; i < barNum; ++i)
DrawBox(barPos.x + i * (barPart + 1), barPos.y,
barPos.x + (i + 1) * (barPart + 1), barPos.y + barHeight, barColor, TRUE);
// バーの中身(水色)を20まで描画
int blueNum = (life > 20) ? 10 : (life > 10) ? life - 10 : 0; // lifeが残り10以上のときは水色
for (int i = 0; i < blueNum; ++i)
DrawBox(barPos.x + i * (barPart + 1), barPos.y,
barPos.x + (i + 1) * (barPart + 1), barPos.y + barHeight, blue, TRUE);
// バーの目盛りを描画
for (int i = 0; i < 9; ++i)
DrawLine(barPos.x + (i + 1) * (barPart + 1), barPos.y,
barPos.x + (i + 1) * (barPart + 1), barPos.y + barHeight, (i + 1 == life) ? barColor : lineColor, TRUE);
}
(以下略).........
ザコからプレイヤのライフを回復するアイテムをドロップさせてみましょう。
GIMPで16×16のハートのドット絵画像を自作してheart.pngという名前でpng画像としてDXプロジェクトのImageフォルダにエクスポート
しておきましょう。
Image.h
にheart.pngの読込み処理を追加します。
#ifndef IMAGE_H_
#define IMAGE_H_
#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
#include "DivImage.h" // 分割画像の読込みに使う
class Image
{
public:
Image() {}; // 初期化コンストラクタ:定義と{}空の処理
~Image() {}; // 破棄する処理デストラクタ:定義と{}空の処理
static void Load();
static int LoadDivGraph(const TCHAR* FileName, DivImage& divImage);
static int bossImage; //ボス画像のハンドラ(読込画像番号)
static int player; //プレイヤ画像のハンドラ
static int playerBullet; //プレイヤの弾画像のハンドラ
static int enemyBullet16; //敵弾 画像のハンドラ(読込画像番号)
static int zako0; //ザコ0 画像のハンドラ(読込画像番号)
static int zako1; //ザコ1 画像のハンドラ(読込画像番号)
static int zako2; //ザコ2 画像のハンドラ(読込画像番号)
static int zako3; //ザコ3 画像のハンドラ(読込画像番号)
static int boss1; //ボス通常 画像のハンドラ(読込画像番号)
static int boss2; //ボス2気絶 画像のハンドラ(読込画像番号)
static int boss3; //ボス3発狂 画像のハンドラ(読込画像番号)
static DivImage explosion; // [分割画像]爆発エフェクト
static int item0; //アイテム0 画像のハンドラ(読込画像番号)
private:
};
#endif
次に対となるcppファイルも変更します。
Image.cpp
の内容を次のようにします:
#include "Image.h"
int Image::bossImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{-1};
int Image::playerBullet{-1};
int Image::enemyBullet16{-1};
int Image::zako0{-1};
int Image::zako1{-1};
int Image::zako2{-1};
int Image::zako3{-1};
int Image::boss1{-1};
int Image::boss2{-1};
int Image::boss3{-1};
// ★分割画像は初期化のときに{X方向画像数, Y方向画像数, 画像横幅XSize,画像縦幅YSize}を指定
DivImage Image::explosion{ 8, 2, 64, 64 };
int Image::item0{-1};
void Image::Load()
{
bossImage = LoadGraph("Image/boss1.png");
assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
player= LoadGraph("Image/player.png");
assert(player!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
playerBullet = LoadGraph("Image/player_bullet.png");
assert(playerBullet!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
enemyBullet16 = LoadGraph("Image/enemy_bullet_16.png");
assert(enemyBullet16!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako0 = LoadGraph("Image/zako0.png");
assert(zako0!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako1 = LoadGraph("Image/zako1.png");
assert(zako1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako2 = LoadGraph("Image/zako2.png");
assert(zako2!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako3 = LoadGraph("Image/zako3.png");
assert(zako3!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
boss1 = LoadGraph("Image/boss1.png");
assert(boss1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
boss2 = LoadGraph("Image/boss2.png");
assert(boss2!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
boss3 = LoadGraph("Image/boss3.png");
assert(boss3!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
// div分割画像をロード
Image::LoadDivGraph("Image/explosion.png", explosion);
for (int i = 0; i < explosion.AllNum; i++)
{ // 画像読込失敗
if (explosion.HandleArray[i] == -1) assert("爆発分割画像読込失敗" == "");
}
item0 = LoadGraph("Image/heart.png");
assert(item0!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
}
// DXライブラリのLoadDivGraphを使いやすくラッピング
// ★関数に参照渡しを行う
// 関数の引数に参照渡しを行うことでdivImageの中身を書き換えることができる(値渡しでは書換不可)
// https://qiita.com/agate-pris/items/05948b7d33f3e88b8967
// https://qiita.com/RuthTaro/items/f35c3a26779c0ca1a41a
int Image::LoadDivGraph(const TCHAR* FileName, DivImage& divImage)
{
return DxLib::LoadDivGraph(FileName, divImage.XNum * divImage.YNum,
divImage.XNum, divImage.YNum, divImage.XSize, divImage.YSize, divImage.HandleArray);
};
Player.h
にライフを回復する処理を追加します。
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"
#include "GameObject.h"
#include "GameManager.h"
class Player : public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:
(中略)...........
// ダメージを受ける処理
void TakeDamage();
// ライフが回復する処理
void RecoverLife(int amount);
};
#endif
Player.cpp
にライフを回復する処理を追加します。
#include "Player.h"
(中略).........
// 更新処理
void Player::Update()
{
(中略).........
}
(中略).........
// ダメージを受ける処理
void Player::TakeDamage()
{
life -= 1; // ライフ減少
if (life <= 0)
{
// ライフが無くなったら死亡
isDead = true;
}
else
{
// 無敵時間発動
mutekiTimer = MutekiJikan;
}
}
// ライフが回復する処理
void Player::RecoverLife(int amount)
{
life += amount; // ライフ回復
}
Item.hを新規作成
します。#ifndef ITEM_H_
#define ITEM_H_
#include "GameObject.h"
#include "GameManager.h"
// アイテムのベース基底クラス
class Item : public GameObject
{
public:
//【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
float collisionRadius; // 当たり判定の半径
// コンストラクタ v:移動速度(方向) radius:当たり判定の半径
Item(Vector3 pos, Vector3 v, float radius = 8)
: collisionRadius{ radius }, GameObject( pos ) // GameObjectをベースとして初期化処理を継承する
{
this->typeTag = "Item"; // オブジェクトの種類判別タグ
this->v = v; // GameObjectの速度vを利用する
}
// 更新処理。派生クラスでオーバーライドして使う
virtual void Update() = 0;//virtual ~ = 0;で【純粋仮想関数に】これで【C#のabstract型に】
// 描画処理。派生クラスでオーバーライドして使う
virtual void Draw() = 0; //virtual ~ = 0;で【純粋仮想関数に】【関数 = 0で未定義状態を表現】
// 自機に当たったときの処理 派生クラスでオーバーライドして使う
virtual void OnCollisionPlayer(std::shared_ptr<GameObject> object)
{
// アイテムごとに継承でoverrideして個別処理を書く
}
};
#endif
Item0.hを新規作成
します。#ifndef ITEM_0_H_
#define ITEM_0_H_
#include "Item.h"
#include "Image.h"
#include "Screen.h"
// アイテム0クラス ライフを回復するアイテム
class Item0 : public Item
{
public:
int amount; // 回復量など
// コンストラクタ
Item0(Vector3 pos,
Vector3 v = Vector3{ -0.5f,0.0f }, // デフォルトは-0.5fで遅めのスクロール量
int amount = 1)
: amount{ amount }, Item(pos, v, 8)
{
this->tag = "Item0"; // オブジェクトの判別タグ
}
// 更新処理
virtual void Update() override
{
this->position += this->v; // vのx,y量ぶん移動
// スクリーンの画面外になったらisDeadにして消す
if (position.x + collisionRadius < 0 || Screen::Width < position.x - collisionRadius
|| position.y + collisionRadius < 0 || Screen::Height < position.y - collisionRadius )
{
isDead = true;
}
}
// 描画処理
virtual void Draw() override
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::item0, TRUE);
}
// 自機に当たったときの処理 上書きoverrideする
virtual void OnCollisionPlayer(std::shared_ptr<GameObject> object) override;
};
#endif
Item0.cppを新規作成
します。#include "Item0.h"
#include "Player.h" // アイテムとプレイヤは相互にインクルードしそうなのでcppで#include
// 自機に当たったときの処理 上書きoverrideする
void Item0::OnCollisionPlayer(std::shared_ptr<GameObject> object)
{
//[共有ポインタをダウンキャスト(親GameObject → 子Playerに)]
// https://cpprefjp.github.io/reference/memory/shared_ptr/dynamic_pointer_cast.html
if (std::shared_ptr<Player> pPlayer = std::dynamic_pointer_cast<Player>(object))
{
pPlayer->RecoverLife(amount); // ライフを回復する
}
isDead = true; // 取ったら消される
}
まずはベースとなるEnemy.hにアイテムをドロップするvirtual仮想関数を追加
して継承overrideできるように準備します。
#ifndef ENEMY_H_
#define ENEMY_H_
#include "GameObject.h"
#include "GameManager.h"
// 敵の基底クラス。
// 全ての敵は、このクラスを継承して作る。
// ★【勉強】abstract型はC++にない!!【純粋仮想関数】を使って作る
class Enemy : public GameObject
{
public:
//【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
float collisionRadius = 32; // 当たり判定の半径
float life = 1; // 耐久力
// コンストラクタ
Enemy(Vector3 pos) : GameObject( pos ) // GameObjectをベースとして初期化処理を継承する
{
this->typeTag = "Enemy";
}
// 更新処理。派生クラスでオーバーライドして使う
virtual void Update() = 0;//virtual ~ = 0;で【純粋仮想関数に】これで【C#のabstract型に】
// 描画処理。派生クラスでオーバーライドして使う
virtual void Draw() = 0; //virtual ~ = 0;で【純粋仮想関数に】【関数 = 0で未定義状態を表現】
// 自機弾に当たったときの処理 関数の【★引数をGameObject型↓にすることで循環インクルード抑止】
virtual void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet)
{
life -= 1; // ライフを減らす
// ライフが無くなったら、死亡
if (life <= 0)
{
isDead = true;
}
DropItem(); // アイテムを落とす処理(落とすかどうかはoverrideされた関数次第)
}
// アイテムを落とす処理 継承overrideして各ザコごとに違うドロップを実装できる
virtual void DropItem()
{
// 継承overrideして各ザコごとに違う処理を実装する
}
};
#endif
例として、Zako1.hにアイテム0をドロップさせる処理を追加
します。
#ifndef ZAKO_1_H_
#define ZAKO_1_H_
#include "Enemy.h"
#include <cmath> // Sinの計算に使う
#include "MyMath.h"
#include "Image.h"
#include "MyRandom.h" // 乱数でアイテムのドロップを抽選する
#include "Item0.h"
// ザコ1クラス
class Zako1 : public Enemy
{
public:
float sinMove = 0; // Sinの動きをする際の0~360度
float sinRadius = 70; // Sinの動きをするときの半径
// コンストラクタ
Zako1(Vector3 pos) : Enemy ( pos )
{
this->tag = "Zako1";//オブジェクトの種類判別タグ
life = 5;
}
// 更新処理
void Update() override
{
position.x -= 1; // 左へ移動
float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
sinMove += 2.5f; // Sinの波運動の角度を進める
float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
// (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
position.y += currentSinY - prevSinY; // 上下方向のSinの波運動
}
// 描画処理
void Draw() override
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako1, TRUE);
}
// アイテムを落とす処理
void DropItem() override
{
if (isDead) // 消えるまえに
{
if (MyRandom::Percent(50.0f)) // 50%の確率でアイテムをドロップ
{
gm.items.emplace_back(std::make_shared<Item0>(position));
}
}
}
};
#endif
GameManager.h
にItemを管理するリストの定義を追加します。
#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_
#include <memory>
#include <vector>
#include "Singleton.h"
class Player; // クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class PlayerBullet;
class Enemy;
class EnemyBullet;
class Explosion;
class Item;
class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可
std::shared_ptr<Player> player{ nullptr }; // 自機のポインタ
std::list<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
std::list<std::shared_ptr<EnemyBullet>> enemyBullets; // 敵弾のリスト
std::list<std::shared_ptr<Explosion>> explosions; // 爆発エフェクトのリスト
std::list<std::shared_ptr<Item>> items; // アイテムのリスト
// 複数リスト↑list ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる
(中略)..............
protected:
GameManager() {}; // 外部からのインスタンス作成は禁止
virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};
#endif
Game.cpp
にアイテムの当たり判定の処理を追加します。
#include "Game.h"
#include "MyRandom.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "EnemyBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Explosion.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako2.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako3.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako3.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Item.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
(中略)..................
};
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
(中略)..................
// 爆発エフェクトの更新処理
for (const auto& e : gm.explosions)
{
e->Update();
}
// アイテムの更新処理
for (const auto& itm : gm.items)
{
itm->Update();
}
// 自機とアイテムの衝突判定
for (const auto& itm : gm.items)
{
// 自機が死んでたらこれ以上判定しない
if (gm.player->isDead)
break;
// アイテムが死んでたらスキップ
if (itm->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
gm.player->position, gm.player->collisionRadius,
itm->position, itm->collisionRadius) )
{
itm->OnCollisionPlayer(gm.player);
}
}
//return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
// 自機弾のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.playerBullets,
[](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 敵のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.enemies,
[](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 敵弾のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.enemyBullets,
[](std::shared_ptr<EnemyBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
//爆発エフェクトのリストから死んでるものを除去する
gm.EraseRemoveIf(gm.explosions,
[](std::shared_ptr<Explosion>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
//アイテムのリストから死んでるものを除去する
gm.EraseRemoveIf(gm.items,
[](std::shared_ptr<Item>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
};
void Game::Draw()
{// 描画処理
(中略)......
// 敵弾の描画処理
for (const auto& b : gm.enemyBullets)
{
b->Draw();
}
// アイテムの描画処理
for (const auto& itm : gm.items)
{
itm->Draw();
}
}
Item0.hに画面のはしっこの反射処理とisFlashフラグで一瞬光らせる処理を追加
します。#ifndef ITEM_0_H_
#define ITEM_0_H_
#include "Item.h"
#include "Image.h"
#include "Screen.h"
// アイテム0クラス ライフを回復するアイテム
class Item0 : public Item
{
public:
int amount; // 回復量など
int bounceNum; // 跳ね返る回数
bool isFlash; // 画面が一瞬真っ白になる(跳ね返る瞬間)
// コンストラクタ
Item0(Vector3 pos,
Vector3 v = Vector3{ -0.5f,0.0f }, // デフォルトは-0.5fで遅めのスクロール量
bool isFlash = false, int bounceNum = 0,
int amount = 1)
: isFlash{ isFlash }, bounceNum{ bounceNum }, amount{ amount }, Item(pos, v, 8)
{
this->tag = "Item0"; // オブジェクトの判別タグ
}
// 更新処理
virtual void Update() override
{
this->position += this->v; // vのx,y量ぶん移動
if (bounceNum > 0) // 反射可能回数が 0 以上なら
{
bool isBounce = false; // 反射するか
if (position.x < collisionRadius)
{
v.x = -v.x; // x逆方向に反射(マイナスをつけて方向反転)
position.x = collisionRadius; // めりこんだぶんをスクリーンの境界線に沿わす
isBounce = true;
}
else if (Screen::Width - collisionRadius < position.x)
{
v.x = -v.x; // x逆方向に反射(マイナスをつけて方向反転)
position.x = Screen::Width - collisionRadius; // めりこんだぶんを境界線に沿わす
isBounce = true;
}
if (position.y < collisionRadius)
{
v.y = -v.y; // y逆方向に反射(マイナスをつけて方向反転)
position.y = collisionRadius; // めりこんだぶんをスクリーンの境界線に沿わす
isBounce = true;
}
else if (Screen::Height - collisionRadius < position.y)
{
v.y = -v.y; // y逆方向に反射(マイナスをつけて方向反転)
position.y = Screen::Height - collisionRadius; // めりこんだぶんを境界線に沿わす
isBounce = true;
}
if (isBounce) // 反射判定がtrueならば
{
--bounceNum; // 跳ね返り可能回数を -1 する
v *= 0.94f; // 反射時のスピード減衰率
isFlash = true; // 画面を一瞬フラッシュする
}
}
// スクリーンの画面外になったらisDeadにして消す
if (position.x + collisionRadius < 0 || Screen::Width < position.x - collisionRadius
|| position.y + collisionRadius < 0 || Screen::Height < position.y - collisionRadius )
{
isDead = true;
}
}
// 描画処理
virtual void Draw() override
{
if (isFlash) // 画面を一瞬真っ白にする(跳ね返る瞬間)
{
// 画面のスクリーンと同じサイズの大きな白い四角を描く
DrawBox(0, 0, Screen::Width, Screen::Height, GetColor(200, 200, 200), TRUE);
// 白画面の上に黒丸を描くとバウンド位置が目立つ(衝撃波っぽい)
DrawCircle(position.x, position.y, collisionRadius * 5, GetColor(0, 0, 0), TRUE);
}
DrawRotaGraphF(position.x, position.y, (isFlash)? 2.0f : 1, 0, Image::item0, TRUE);
isFlash = false; // すぐにfalseに戻せば一瞬だけしか光らない
}
// 自機に当たったときの処理 上書きoverrideする
void OnCollisionPlayer(std::shared_ptr<GameObject> object) override;
};
#endif
例として、Zako1.hにアイテム0の初速度と反射する回数と乱数で飛ぶ角度を決めて初期化する処理を追加
します。
#ifndef ZAKO_1_H_
#define ZAKO_1_H_
#include "Enemy.h"
#include <cmath> // Sinの計算に使う
#include "MyMath.h"
#include "Image.h"
#include "MyRandom.h" // 乱数でアイテムのドロップと飛ぶ角度を抽選する
#include "Item0.h"
// ザコ1クラス
class Zako1 : public Enemy
{
public:
float sinMove = 0; // Sinの動きをする際の0~360度
float sinRadius = 70; // Sinの動きをするときの半径
// コンストラクタ
Zako1(Vector3 pos) : Enemy ( pos )
{
this->tag = "Zako1";//オブジェクトの種類判別タグ
life = 5;
}
// 更新処理
void Update() override
{
position.x -= 1; // 左へ移動
float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
sinMove += 2.5f; // Sinの波運動の角度を進める
float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
// (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
position.y += currentSinY - prevSinY; // 上下方向のSinの波運動
}
// 描画処理
void Draw() override
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako1, TRUE);
}
// アイテムを落とす処理
void DropItem() override
{
if (isDead) // 消えるまえに
{
if (MyRandom::Percent(50.0f)) // 50%の確率でアイテムをドロップ
{
float itemSpeed = 25.0f; // アイテムの飛び出すスピード
float angle = MyRandom::Range(0.0f, 360.0f) * MyMath::Deg2Rad; // 飛び出す角度
Vector3 v_Item{ std::cos(angle) * itemSpeed, std::sin(angle) * itemSpeed }; // 飛ぶ方向ベクトル
int bounceNum = 10; // バウンド回数
gm.items.emplace_back(std::make_shared<Item0>(position, v_Item, true, bounceNum));
}
}
}
};
#endif
ボスから「プレイヤの周りをまわって、敵の弾からプレイヤを守る星バリアのアイテム」をドロップさせてみましょう。
GIMPで16×16のスターのドット絵画像を自作してstar.pngという名前でpng画像としてDXプロジェクトのImageフォルダにエクスポート
しておきましょう。
Image.h
にstar.pngの読込み処理を追加します。
#ifndef IMAGE_H_
#define IMAGE_H_
#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
#include "DivImage.h" // 分割画像の読込みに使う
class Image
{
public:
Image() {}; // 初期化コンストラクタ:定義と{}空の処理
~Image() {}; // 破棄する処理デストラクタ:定義と{}空の処理
static void Load();
static int LoadDivGraph(const TCHAR* FileName, DivImage& divImage);
static int bossImage; //ボス画像のハンドラ(読込画像番号)
static int player; //プレイヤ画像のハンドラ
static int playerBullet; //プレイヤの弾画像のハンドラ
static int enemyBullet16; //敵弾 画像のハンドラ(読込画像番号)
static int zako0; //ザコ0 画像のハンドラ(読込画像番号)
static int zako1; //ザコ1 画像のハンドラ(読込画像番号)
static int zako2; //ザコ2 画像のハンドラ(読込画像番号)
static int zako3; //ザコ3 画像のハンドラ(読込画像番号)
static int boss1; //ボス通常 画像のハンドラ(読込画像番号)
static int boss2; //ボス2気絶 画像のハンドラ(読込画像番号)
static int boss3; //ボス3発狂 画像のハンドラ(読込画像番号)
static DivImage explosion; // [分割画像]爆発エフェクト
static int item0; //アイテム0 画像のハンドラ(読込画像番号)
static int item1; //アイテム1 画像のハンドラ(読込画像番号)
private:
};
#endif
次に対となるcppファイルも変更します。
Image.cpp
の内容を次のようにします:
#include "Image.h"
int Image::bossImage{-1}; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{-1};
int Image::playerBullet{-1};
int Image::enemyBullet16{-1};
int Image::zako0{-1};
int Image::zako1{-1};
int Image::zako2{-1};
int Image::zako3{-1};
int Image::boss1{-1};
int Image::boss2{-1};
int Image::boss3{-1};
// ★分割画像は初期化のときに{X方向画像数, Y方向画像数, 画像横幅XSize,画像縦幅YSize}を指定
DivImage Image::explosion{ 8, 2, 64, 64 };
int Image::item0{-1};
int Image::item1{-1};
void Image::Load()
{
bossImage = LoadGraph("Image/boss1.png");
assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
player= LoadGraph("Image/player.png");
assert(player!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
playerBullet = LoadGraph("Image/player_bullet.png");
assert(playerBullet!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
enemyBullet16 = LoadGraph("Image/enemy_bullet_16.png");
assert(enemyBullet16!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako0 = LoadGraph("Image/zako0.png");
assert(zako0!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako1 = LoadGraph("Image/zako1.png");
assert(zako1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako2 = LoadGraph("Image/zako2.png");
assert(zako2!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako3 = LoadGraph("Image/zako3.png");
assert(zako3!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
boss1 = LoadGraph("Image/boss1.png");
assert(boss1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
boss2 = LoadGraph("Image/boss2.png");
assert(boss2!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
boss3 = LoadGraph("Image/boss3.png");
assert(boss3!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
// div分割画像をロード
Image::LoadDivGraph("Image/explosion.png", explosion);
for (int i = 0; i < explosion.AllNum; i++)
{ // 画像読込失敗
if (explosion.HandleArray[i] == -1) assert("爆発分割画像読込失敗" == "");
}
item0 = LoadGraph("Image/heart.png");
assert(item0!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
item1 = LoadGraph("Image/star.png");
assert(item1!= -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
}
// DXライブラリのLoadDivGraphを使いやすくラッピング
// ★関数に参照渡しを行う
// 関数の引数に参照渡しを行うことでdivImageの中身を書き換えることができる(値渡しでは書換不可)
// https://qiita.com/agate-pris/items/05948b7d33f3e88b8967
// https://qiita.com/RuthTaro/items/f35c3a26779c0ca1a41a
int Image::LoadDivGraph(const TCHAR* FileName, DivImage& divImage)
{
return DxLib::LoadDivGraph(FileName, divImage.XNum * divImage.YNum,
divImage.XNum, divImage.YNum, divImage.XSize, divImage.YSize, divImage.HandleArray);
};
Item.hに敵弾と衝突した際に呼び出されるベースとなるコールバック関数を定義
します。#ifndef ITEM_H_
#define ITEM_H_
#include "GameObject.h"
#include "GameManager.h"
// アイテムのベース基底クラス
class Item : public GameObject
{
public:
//【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
float collisionRadius; // 当たり判定の半径
enum class CollisionTo // 当たり判定の対象(ビット32桁に1を立てて表現)
{
None = 0,
Player = 0b0000'0000'0000'0000'0000'0000'0000'0001,
EnemyBullet = 0b0000'0000'0000'0000'0000'0000'0000'0010,
};
CollisionTo collisionTo = CollisionTo::Player; // デフォルトはプレイヤとの当たりを判定する
// コンストラクタ v:移動速度(方向) radius:当たり判定の半径
Item(Vector3 pos, Vector3 v, float radius = 8)
: collisionRadius{ radius }, GameObject( pos ) // GameObjectをベースとして初期化処理を継承する
{
this->typeTag = "Item"; // オブジェクトの種類判別タグ
this->v = v; // GameObjectの速度vを利用する
}
// 更新処理。派生クラスでオーバーライドして使う
virtual void Update() = 0;//virtual ~ = 0;で【純粋仮想関数に】これで【C#のabstract型に】
// 描画処理。派生クラスでオーバーライドして使う
virtual void Draw() = 0; //virtual ~ = 0;で【純粋仮想関数に】【関数 = 0で未定義状態を表現】
// 自機に当たったときの処理 派生クラスでオーバーライドして使う
virtual void OnCollisionPlayer(std::shared_ptr<GameObject> object)
{
// アイテムごとに継承でoverrideして個別処理を書く
}
// 敵弾とぶつかった時の処理
virtual void OnCollisionEnemyBullet(std::shared_ptr<GameObject> object)
{
// アイテムごとに継承でoverrideして個別処理を書く
}
};
#endif
Item1.hを新規作成
します。#ifndef ITEM_1_H_
#define ITEM_1_H_
#include "Item.h"
#include "Image.h"
#include "Screen.h"
#include "MyMath.h"
#include <cmath>
// アイテム1クラス プレイヤを敵弾から守り身代わりになるバリアアイテム
class Item1 : public Item
{
public:
int life; // バリアー(星)の耐える回数
float xForce = 0.1f; // 加速の力(流れ星の加速)
enum class State
{
Popping, // ポップ(飛び出した勢いのあるあいだ)
Shooting,// 流れ星(加速して流れていく)
Barrier, // バリアモードのとき
};
State state = State::Popping; // 動作状態
GameObject* pParent{ nullptr }; // 親となっているプレイヤなどへのリンク(バリアの中心点)
float barrierRadius; // バリアの円回転の半径
float barrierAngle = 0.0f; // バリアの回転角度
bool isHiting = false; // 敵弾にヒットした時に一瞬拡大して表示
// コンストラクタ
Item1(Vector3 pos,
Vector3 v = Vector3{ -0.5f,0.0f }, // デフォルトは-0.5fで遅めのスクロール量
int life = 3, float xForce = 0.8f, float barrierRadius = 40.0f)
: life{ life }, xForce{ xForce }, barrierRadius{ barrierRadius },
Item(pos, v, 8)
{
this->tag = "Item1"; // オブジェクトの判別タグ
}
// 更新処理
virtual void Update() override
{
if (state == State::Popping) // 飛び出した勢いは段々減る
{
position += v; // vのx,y量ぶん移動
v *= 0.85f; // 速度が減衰
if (v.magnitude() < 0.01f)
state = State::Shooting; // 流れ星モードに移行
}
else if (state == State::Shooting)
{
position += v; // vのx,y量ぶん移動
v.x -= xForce; // 流れ星が少しずつ加速する
}
else if (state == State::Barrier && pParent != nullptr)
{
barrierAngle += 5.5f; // 毎フレーム5.5度ずつ回転
position.x = pParent->position.x + barrierRadius * std::cos(barrierAngle * MyMath::Deg2Rad);
position.y = pParent->position.y + barrierRadius * std::sin(barrierAngle * MyMath::Deg2Rad);
if (pParent->isDead)
isDead = true; // 親のプレイヤリンク先がisDeadなら連動して消す
}
if (state != State::Barrier) // バリア状態でないときに画面外に出たら消す
{
// スクリーンの画面外になったらisDeadにして消す
if (position.x + collisionRadius < 0 || Screen::Width < position.x - collisionRadius
|| position.y + collisionRadius < 0 || Screen::Height < position.y - collisionRadius )
{
isDead = true;
}
}
}
// 描画処理
virtual void Draw() override
{
DrawRotaGraphF(position.x, position.y, (isHiting)? 2.0f : 1, 0, Image::item1, TRUE);
}
// 自機に当たったときの処理 上書きoverrideする
virtual void OnCollisionPlayer(std::shared_ptr<GameObject> object) override;
// 敵弾とぶつかった時の処理
virtual void OnCollisionEnemyBullet(std::shared_ptr<GameObject> object) override
{
if (!((int)collisionTo & (int)CollisionTo::EnemyBullet))
return; // 敵弾と当たり判定しない設定のときは何もせずreturn
if (life <= 0) // ライフ(バリアの耐久回数)が0以下になったら
{
isDead = true; // 当たった直後に消える
isHiting = true; // 一瞬拡大して表示
}
--life; // ライフ(バリアの耐久回数)を減らす
}
};
#endif
Item1.cppを新規作成
します。#include "Item1.h"
#include "Player.h" // アイテムとプレイヤは相互にインクルードしそうなのでcppで#include
// 自機に当たったときの処理 上書きoverrideする
void Item1::OnCollisionPlayer(std::shared_ptr<GameObject> object)
{
if (!((int)collisionTo & (int)CollisionTo::Player))
return; // プレイヤと当たり判定しないenumビット設定のときは何もせずreturn
//[共有ポインタをダウンキャスト(親GameObject → 子Playerに)]
// https://cpprefjp.github.io/reference/memory/shared_ptr/dynamic_pointer_cast.html
if (std::shared_ptr<Player> pPlayer = std::dynamic_pointer_cast<Player>(object))
{
pParent = pPlayer.get(); // プレイヤへのリンクを親リンクとしてリンクされた(x,y)を中心に回転
state = State::Barrier; // バリアモードへ移行
collisionTo = CollisionTo::EnemyBullet; // 敵弾との当たり判定モードに移行
}
}
Item0.cppにも当たり判定の種類の判定(PlayerかEnemyBulletかの判定)を追加
します。#include "Item0.h"
#include "Player.h" // アイテムとプレイヤは相互にインクルードしそうなのでcppで#include
// 自機に当たったときの処理 上書きoverrideする
void Item0::OnCollisionPlayer(std::shared_ptr<GameObject> object)
{
if (!((int)collisionTo & (int)CollisionTo::Player))
return; // プレイヤと当たり判定しないenumビット設定のときは何もせずreturn
//[共有ポインタをダウンキャスト(親GameObject → 子Playerに)]
// https://cpprefjp.github.io/reference/memory/shared_ptr/dynamic_pointer_cast.html
if (std::shared_ptr<Player> pPlayer = std::dynamic_pointer_cast<Player>(object))
{
pPlayer->RecoverLife(amount); // ライフを回復する
}
isDead = true; // 取ったら消される
}
例としてBoss.hで弾が当たったy位置が ±5 以内のときクリティカルと判定してバリアーの星を出す処理
を追加します。
#ifndef BOSS_H_
#define BOSS_H_
#include "Enemy.h"
#include "Image.h"
#include <cmath> // Sin Cosの計算に使う
#include "MyMath.h"
#include "MyRandom.h" // 乱数方向弾とバリアーの星の飛び出す方向の抽選に使う
#include "Item1.h"
#include "PlayerBullet.h"
// ボスクラス。Enemyを継承して作る
class Boss : public Enemy
{
public:
// ボスの状態種別
enum class State
{
Appear, // 登場
Normal, // 通常時
Swoon, // 気絶時
Angry, // 発狂モード
Dying, // 死亡
};
State state = State::Appear; // 現在の状態
int swoonTime = 120; // 残り気絶時間
int dyingTime = 180; // 死亡時のアニメーション時間
int coolTime = 0; // クールタイム(冷却時間。0になるまで次の弾が撃てない)
float sinMove = 0; // Sinの動きをする際の0~360度
float sinRadius = 120; // Sinの動きをするときの半径
bool isCritical = false; // 弾に当たった瞬間の yが ±2 以内のときクリティカルと判定
Vector3 criticalPos{0,0,0}; // クリティカルヒットした位置
// コンストラクタ
Boss(Vector3 pos) : Enemy( pos )
{
this->tag = "Boss"; // オブジェクトの種類判別タグ
life = 100; // ライフ
collisionRadius = 70; // 当たり判定半径
}
// 更新処理
void Update() override
{
if (state == State::Appear) // 登場状態
{
position.x -= 1; // 左へ移動
if (position.x <= 750) // x座標が750以下になったら
{
state = State::Normal; // 通常状態へ移行
}
}
else if (state == State::Normal) // 通常状態
{
coolTime--;
if (coolTime <= 0)
{ // ボスの弾はスピード16で2倍の速度で撃ち出す
gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, 180 * MyMath::Deg2Rad, 16));
coolTime += 200; // そのかわり発射間隔は長め
}
float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
sinMove += 2.5f; // Sinの波運動の角度を進める
float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
// (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
position.y += currentSinY - prevSinY; // 上下方向のSinの波運動
}
else if (state == State::Swoon) // 気絶状態
{
swoonTime--; // タイマー減少
if (swoonTime <= 0) // タイマーが0になったら
{
coolTime = 0; // クールタイムを0にリセットしてから
state = State::Angry; // 発狂モードへ
}
}
else if (state == State::Angry) // 発狂モード
{
coolTime--;
if (coolTime <= 0)
{
gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, 180 * MyMath::Deg2Rad, 16));
coolTime += 20;
}
float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
sinMove += 10.5f; // Sinの波運動の角度を進める
float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
// (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
position.y += currentSinY - prevSinY; // 上下方向のSinの波運動
}
else if (state == State::Dying) // 死亡中
{
dyingTime--; // タイマー減少
if (dyingTime <= 0) // タイマーが0になったら
{
isDead = true; // 完全に消滅
}
}
}
// 描画処理
void Draw() override
{
if (state == State::Appear || state == State::Normal) // 登場時と通常時
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss1, TRUE); // 普通の画像
}
else if (state == State::Swoon) // 気絶時
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss2, TRUE); // 気絶時の画像
}
else if (state == State::Angry) // 発狂モード
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss3, TRUE); // 怒ってる画像
}
else if (state == State::Dying) // 死亡中
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::boss2, TRUE); // 気絶時の画像
}
if (isCritical)
{
// 画面のスクリーンと同じサイズの大きな白い四角(フラッシュ)を描く
DrawBox(0, 0, Screen::Width, Screen::Height, GetColor(200, 200, 200), TRUE);
// 白フラッシュの上に黒丸を描くとクリティカルヒットした位置が目立つ(衝撃波っぽい)
DrawCircle(position.x, position.y, 10, GetColor(0, 0, 0), TRUE);
isCritical = false; // クリティカルフラグをfalseにすぐ戻す(フラッシュ点滅)
}
}
// 自機弾に当たったときの処理をoverride(上書き)する
void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet) override
{
// 登場時、気絶時、死亡時は被弾しても何もしない
if (state == State::Appear || state == State::Swoon || state == State::Dying)
return;
life -= 1; // ライフを減らす
float posY = position.y; // [ボス中心y]と[当たった瞬間の弾のy]が ±5 ならクリティカル
if (posY - 5 <= playerBullet->position.y && playerBullet->position.y <= posY + 5)
{
isCritical = true; // クリティカルフラグをtrueに
criticalPos = playerBullet->position; // クリティカルヒットした位置を記録する
DropItem(); // バリアの星アイテムを落とす
}
if (life <= 0)
{
// ライフが無くなったら、すぐ消滅するのではなく、死亡状態へ移行
state = State::Dying;
}
else if (state == State::Normal && life <= 50)
{
// 通常状態でライフが50以下になったら、気絶する
state = State::Swoon;
}
}
// アイテムを落とす処理
void DropItem() override
{
if (MyRandom::Percent(100.0f)) // 100%の確率でバリアになる星をドロップ
{
float itemSpeed = 25.0f; // アイテムの飛び出すスピード
float angle = MyRandom::Range(0.0f, 360.0f) * MyMath::Deg2Rad; // 飛び出す角度
Vector3 v_Item{ std::cos(angle) * itemSpeed, std::sin(angle) * itemSpeed }; // 飛ぶ方向ベクトル
int barrierLife = 3; // バリアは敵弾に3回当たるまでガードしてくれる
gm.items.emplace_back(std::make_shared<Item1>(position, v_Item, barrierLife));
}
}
};
#endif
EnemyBullet.hにアイテムと衝突したさいの関数
を追加します。
#ifndef ENEMYBULLET_H_
#define ENEMYBULLET_H_
#include <cmath> //sin,cosを使うのに必要
#include "DxLib.h"
#include "Image.h"
#include "Screen.h"
#include "Vector3.h"
#include "GameObject.h"
// 敵弾クラス
class EnemyBullet: public GameObject
{
public:
const int VisibleRadius = 8; // 見た目の半径
float angle; // 移動角度(垂直Vertical)
float angleH=0; // 移動角度(水平Horizontal)
// コンストラクタ
EnemyBullet(Vector3 pos, float angle, float speed) : GameObject( pos )
{
this->angle = angle;
// 角度からx方向の移動速度を算出
v.x = (float)std::cos(angle) * speed;
// 角度からy方向の移動速度を算出
v.y = (float)std::sin(angle) * speed;
}
// コンストラクタ
EnemyBullet(Vector3 pos, float angleV, float angleH, float speed) : GameObject( pos )
{
this->angle = angleV;
this->angleH = angleH;
// [3Dの角度は水平・垂直に分割]https://teratail.com/questions/126004
float cosAngleV = (float)std::cos(angle);
// 角度からx方向の移動速度を算出
v.x = cosAngleV * (float)std::sin(angleH) * speed;
// 角度からy方向の移動速度を算出
v.y = (float)std::sin(angle) * speed;
// 角度からz方向の移動速度を算出
v.z = cosAngleV * (float)std::cos(angleH) * speed;
}
// 更新処理
void Update()
{
// 速度の分だけ移動
position += v;
// 画面外に出たら死亡フラグを立てる
if (position.y + VisibleRadius < 0 || position.y - VisibleRadius > Screen::Height ||
position.x + VisibleRadius < 0 || position.x - VisibleRadius > Screen::Width)
{
isDead = true;
}
}
// 描画処理
void Draw()
{
DrawRotaGraphF(position.x, position.y, 1, angle, Image::enemyBullet16,TRUE);
}
// プレイヤにぶつかった時の処理
void OnCollisionPlayer(std::shared_ptr<GameObject> other)
{
isDead = true;
}
// アイテム(プレイヤの回りをまわるバリア)にぶつかった時の処理
void OnCollisionItem(std::shared_ptr<GameObject> other)
{
isDead = true;
}
};
#endif
Game.cpp
にアイテムの当たり判定の処理を追加します。
#include "Game.h"
#include "MyRandom.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "EnemyBullet.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Explosion.h" // Update(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako0.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako1.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako2.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako3.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako3.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Item.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
(中略)..................
};
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
(中略)..................
// 爆発エフェクトの更新処理
for (const auto& e : gm.explosions)
{
e->Update();
}
// アイテムの更新処理
for (const auto& itm : gm.items)
{
itm->Update();
}
// 自機とアイテム、敵弾とアイテムの衝突判定
for (const auto& itm : gm.items)
{
// 自機が死んでたらこれ以上判定しない
if (gm.player->isDead)
break;
// アイテムが死んでたらスキップ
if (itm->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
gm.player->position, gm.player->collisionRadius,
itm->position, itm->collisionRadius) )
{
itm->OnCollisionPlayer(gm.player);
}
for (const auto& enemyBullet : gm.enemyBullets)
{
// 敵弾が死んでたらスキップ
if (enemyBullet->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
itm->position, itm->collisionRadius,
enemyBullet->position, enemyBullet->collisionRadius) )
{
enemyBullet->OnCollisionItem(itm);
itm->OnCollisionEnemyBullet(enemyBullet);
}
}
}
//return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
// 自機弾のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.playerBullets,
[](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 敵のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.enemies,
[](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 敵弾のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.enemyBullets,
[](std::shared_ptr<EnemyBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
//爆発エフェクトのリストから死んでるものを除去する
gm.EraseRemoveIf(gm.explosions,
[](std::shared_ptr<Explosion>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
//アイテムのリストから死んでるものを除去する
gm.EraseRemoveIf(gm.items,
[](std::shared_ptr<Item>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
};
void Game::Draw()
{// 描画処理
(中略)......
}
[C++]シューティングゲーム 4
次のテキスト5はこちら。
タイトルやプレイ画面やゲームオーバーのシーンを分けましょう。
シーンは構造的にタイトル⇒プレイ画面⇒ゲームオーバー⇒タイトル...とループする
ので
普通に考えれば循環インクルードしやすいですね。
ということで基本方針としては
上記の構造であれば、マネージャの.hヘッダで循環は止まります、なぜか?
マネージャの.hヘッダには【画面チェンジ機能の定義だけ】があり、各シーンをインクルード必要としません。
実際の【画面チェンジ機能の処理はcppに書き】【cppで各シーンの.hヘッダをインクルードします】
さて、実際のプログラムをしてみましょう。
【シーン専門のマネージャーSceneManagerを作ることにしましょう】(GameManagerでやっても良いですが)。
管理方針は人によりますが、現状のマネージャーの役割は一度ここで整理しておいた方がよいですね
シーンの遷移こそがある意味【一番のメモリの管理の「要所」】。
なぜなら【画面チェンジは実質≒前の画面のメモリへの参照リンクお掃除】に近いですから。
気をつけてくださいね【メモリの回し読みポインタの参照リンクお掃除しないと】前の画面のメモリはフリーになりません
Scene.hを新規作成してベースとなるシーンの基底クラスを定義しましょう。
ポイント要点は【開始Initialize()】と【終了Finalize()】を区別していることです。
いままでもInit()や初期化コンストラクタはありましたが
【終了の Finalize() 】を分けることでシーンの切替えのタイミングで弾のメモリなどのリセットの処理を書けます。
【Initialize()には生成や初期化】【Finalize()にはメモリなどのリセット処理】を書き分けましょう。
つまり【シーンの移り変わりが出てきたからこそ】【終了のFinalize()の概念も必要になってきた】ということですね。
逆に今までは1つの画面しかなかったので初期化のInit()だけでよかったわけですね。
Scene.hを新規作成
してベースとなるシーンの基底クラスを定義します。
#ifndef SCENE_H_
#define SCENE_H_
#include <string> // シーンの判別文字列tagに使う
// シーンの基底クラス あいまいなabstract型タイプ
class Scene
{
public:
std::string tag = ""; // シーンの種類の判別に使う
// コンストラクタ
Scene()
{
}
//★仮想デストラクタ【忘れるとメモリがヤバいダメ絶対】
//【注意!】ベースの基底では必ず定義しないとstringなどが浪費され極悪なメモリ被害に発展
virtual ~Scene()
{
// 特に配列とかなくても★string文字列も内部では実質配列だから!5文字分のメモリとかがすり減ってく..
// 仮想デストラクタはstringなど暗黙に内部に配列を持つ要素のデストラクタを暗黙に呼んでくれている!
}
//★純粋仮想関数=0は継承したPlaySceneなどの必須機能(継承したら絶対override必須縛り)
// 必須縛りによってSceneを継承したものは【Update,Drawが実装されてるはずの確証があるので】
// for文であいまいなSceneのまま【まとめてUpdate,Drawできる】のだ。
// 初期化処理
virtual void Initialize() = 0; //純粋仮想関数に(継承したらoverride必須)
// 終了処理(大抵Initializeと同じくリセット処理を行うがInitと区別して終了時だけやりたいリセットもある)
virtual void Finalize() = 0; //純粋仮想関数に(継承したらoverride必須)
// 更新処理
virtual void Update() = 0; //純粋仮想関数に(継承したらoverride必須)
// 描画処理
virtual void Draw() = 0; //純粋仮想関数に(継承したらoverride必須)
};
#endif
SceneManager.hを新規作成
してシーンを管理するクラスを定義しましょう。
#ifndef SCNENEMANAGER_H_
#define SCNENEMANAGER_H_
#include <vector>
#include <memory>
#include <string>
#include <assert.h> // シーン読み込みの失敗表示用
#include "Singleton.h"
class Scene; //クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class SceneManager : public Singleton<SceneManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
friend class Singleton<SceneManager>; // Singleton でのインスタンス作成は許可
// マネージャを【どこからでもアクセスしやすい「変数の掲示板」として使えばシーンをまたぐ変数も定義できる】
std::string selectStage = "stage1"; // 選択中のステージ名など
int scoreMax = -1; // 【シーンをまたぐスコアなど】はマネージャに定義すれば【シーンをまたいだあとも】消えず残る
std::shared_ptr<Scene> prevScene{ nullptr }; // 一つ前のシーン
std::shared_ptr<Scene> currentScene{ nullptr }; // 現在のシーン
std::string currentSceneName = ""; // 現在のシーン名を保管しておく
std::string changingSceneName = ""; // 予約された次のシーンのクラス名(次に移動予定のシーン名を保管しておく)
// シーン名を比較、何の変化もなければfalseでシーン移動不必要の判定
bool isChanging(const std::string& nextSceneName);
// 次のシーンの予約だけしてLoadScene()を引数なしで呼び出したタイミングで遷移する(ループの途中じゃないキリのいいタイミングを狙って遷移させる)
void NextScene(const std::string& nextSceneName);
// シーンをチェンジし遷移する(前のシーンのリセット処理もする)
void LoadScene(const std::string& sceneName = "");
protected:
SceneManager() {}; // 外部からのインスタンス作成は禁止
virtual ~SceneManager() {}; //外部からのインスタンス破棄も禁止
};
#endif
TitleScene.hを新規作成
してタイトル画面のひな型を作成しましょう。
#ifndef TITLESCENE_H_
#define TITLESCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "GameManager.h"
#include "SceneManager.h"
class TitleScene : public Scene
{
public:
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
// コンストラクタ
TitleScene() : Scene()
{
this->tag = "TitleScene";
}
// 初期化処理
void Initialize() override
{
}
// 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
void Finalize() override
{
}
// 更新処理
void Update() override
{
if (Input::GetButtonDown(PAD_INPUT_1))
{
sm.LoadScene("PlayScene"); //シーン遷移
return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
}
}
// 描画処理
void Draw() override
{
DrawString(0, 0, "TitleSceneです。ボタン押下でPlaySceneへ。", GetColor(255, 255, 255));
if (sm.scoreMax >= 0)
{
DrawString(0, 200, "MAXスコア: " + sm.scoreMax, GetColor(255, 255, 255));
}
}
};
#endif
GameOverScene.hを新規作成
してゲームオーバー画面のひな型を作成しましょう。
#ifndef GAMEOVERSCENE_H_
#define GAMEOVERSCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "GameManager.h"
#include "SceneManager.h"
class GameOverScene : public Scene
{
public:
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
// コンストラクタ
GameOverScene() : Scene()
{
this->tag = "GameOverScene";
}
// 初期化処理
void Initialize() override
{
}
// 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
void Finalize() override
{
}
// 更新処理
void Update() override
{
if (Input::GetButtonDown(PAD_INPUT_1))
{
sm.LoadScene("TitleScene"); //シーン遷移
return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
}
}
// 描画処理
void Draw() override
{
DrawString(0, 0, "GameOver....ボタン押下でPlaySceneへ", GetColor(255, 255, 255));
}
};
#endif
PlayScene.hを新規作成
してプレイ画面のひな型を作成しましょう。
なお、実際のプレイに関するコードはGame.cppから頑張って移植してみましょう。
ここに全部書くと逆にシーン遷移に関するコードがわかりづらくなりますから。
個々人で作成中のGame.cppのゲームのコードをUpdate(),Draw(),Init()はInitialize()へ移植
してみましょう。
#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "GameManager.h"
#include "SceneManager.h"
#include "Player.h"
class PlayScene : public Scene
{
public:
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる
// コンストラクタ
PlayScene() : Scene()
{
this->tag = "PlayScene";
}
// 初期化処理
void Initialize() override
{
// ここでプレイヤの初期化などのプレイ開始時の処理
}
// 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
void Finalize() override
{
// ここにプレイ終了時のメモリのリセット処理(ゲームクリアやゲームオーバーなど)
// ゲーム終了時のスコアがMAXなら記録して終了
if(score > sm.scoreMax) sm.scoreMax = score;
}
// 更新処理
void Update() override
{
// ここにプレイ中の更新処理を持ってくる
// ゲームオーバー判定と画面チェンジ処理
bool isGameover = (gm.player == nullptr);
if (!isGameover && gm.player->isDead) isGameover = true;
if (isGameover)
{
sm.LoadScene("GameOverScene"); //シーン遷移
return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
}
}
// 描画処理
void Draw() override
{
// ここにプレイ画面の描画処理を持ってくる
}
};
#endif
SceneManager.cppを新規作成
してシーンを遷移する機能を作成しましょう。
#include "SceneManager.h"
#include "Scene.h"
#include "TitleScene.h"
#include "PlayScene.h"
#include "GameOverScene.h"
bool SceneManager::isChanging(const std::string& nextSceneName)
{
if (currentSceneName != nextSceneName) //シーン名不一致
return true; // シーン名が変更
return false; // シーン名に変更無し
}
void SceneManager::NextScene(const std::string& nextSceneName)
{
// 遷移予定nextシーン名と現在のシーン名を比較、何の変化もなければfalseでシーン移動不必要の判定
if (isChanging(nextSceneName) == false) return; //特に変更なしでシーン移動の必要なし
changingSceneName = nextSceneName; // 次に移動予定のシーン名を保管しておく
}
void SceneManager::LoadScene(const std::string& sceneName)
{
std::string loadSceneName = sceneName;
// sceneNameの指定が空の場合にはNextScene()での事前のシーン変更予約があるかチェック
if (sceneName == "" && changingSceneName != "")
loadSceneName = changingSceneName;
if (loadSceneName == "") return; // シーン指定がないときは終了
// 現在のシーンの終了処理
if(currentScene !=nullptr)
currentScene->Finalize(); // 終了処理を呼び出す
if (loadSceneName == "TitleScene")
{
// 次のシーンの生成
currentScene = std::make_shared<TitleScene>();
}
else if (loadSceneName == "PlayScene")
{
// 次のシーンの生成
currentScene = std::make_shared<PlayScene>();
}
else if (loadSceneName == "GameOverScene")
{
// 次のシーンの生成
currentScene = std::make_shared<GameOverScene>();
}
else
assert("指定されたシーン名の生成処理が見つからなかった→SceneManager.cppを見直しましょう" == "");
currentSceneName = loadSceneName; // 現在のシーン名の更新
changingSceneName = ""; // NextScenceのシーン予約名をリセット
// 次のシーンの初期化
currentScene->Initialize(); // 初期化を呼び出す
}
Game.cppからPlayScene.h、TitleScene.hにプレイに関するコードを移植してみます
ですが、あくまでも移植【例】です。
皆さん独自の敵などいますからあくまで移植の【例】です。
Game.cppからTitleScene.hにテスト用のボス表示のコードを移植してみます。
#ifndef TITLESCENE_H_
#define TITLESCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "GameManager.h"
#include "SceneManager.h"
class TitleScene : public Scene
{
public:
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
// テスト用のボス画像をタイトル画面で左右移動させる
int x = 0; //staticじゃないintはここで=0で初期化できる
int vx = 0;
// コンストラクタ
TitleScene() : Scene()
{
this->tag = "TitleScene";
}
// 初期化処理
void Initialize() override
{
// テスト用のボス画像をタイトル画面で左右移動させる
x = 100; // Xの初期位置
vx = 10; // ボスの初期速度
}
// 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
void Finalize() override
{
}
// 更新処理
void Update() override
{
if (Input::GetButtonDown(PAD_INPUT_1))
{
sm.LoadScene("PlayScene"); //シーン遷移
return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
}
// テスト用のボス画像をタイトル画面で左右移動させる
if (x < 0)
{
vx = 10; //速度の変更
}
else if (x > Screen::Width)
{
vx = -10; //画面端で移動方向反転
}
x += vx; // ボス画像のX位置の更新
}
// 描画処理
void Draw() override
{
// テスト用のボス画像をタイトル画面で左右移動させる
DrawRotaGraphF((float)x, 200, 0.9f, 0, Image::bossImage, TRUE);
DrawString(0, 0, "TitleSceneです。ボタン押下でPlaySceneへ。", GetColor(255, 255, 255));
if (sm.scoreMax >= 0)
{
DrawString(0, 200, "MAXスコア: " + sm.scoreMax, GetColor(255, 255, 255));
}
}
};
#endif
Game.hから移植したコードを削除します。
#ifndef GAME_H_
#define GAME_H_
#include <vector>//配列std::vectorを使う
#include <memory>//スマートなポインタを使うのに必要(shared_ptrなど)
#include "Image.h"
#include "Screen.h"
#include "Input.h"
#include "GameManager.h"
class Game
{
public:
/* ↓{ }を.hに書くことでcppに分けて書くはずの処理ぶぶんを.hに書いてもよい */
Game() { }; // 初期化コンストラクタ
~Game() { }; // 破棄処理デストラクタ
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
void Init(); // Init処理(定義だけ)
void Update(); // 更新処理(定義だけ)
void Draw();// 描画処理(定義だけ)
int x = 0; //staticじゃないintはここで=0で初期化できる
int vx = 0;
};
#endif
Game.cppから移植したコードを削除し、ImageやRandomなどの共通機能の初期化だけに絞ります。
#include "Game.h"
#include "MyRandom.h"
#include "Player.h" // ★初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
....
(中略)... その他いろいろなヘッダ............
....
#include "Zako2.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Zako3.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "Boss.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
Image::Load(); //画像の読込み
MyRandom::Init(); // 乱数シードの初期化
x = 100; // Xの初期位置
vx = 10; // ボスの初期速度
gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
// とりあえず適当に敵を生成
gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)600, (float)100)));
(中略).....
gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
}
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
if (x < 0)
{
vx = 10; //速度の変更
}
else if (x > Screen::Width)
{
vx = -10; //画面端で移動方向反転
}
x += vx; // ボス画像のX位置の更新
if (!gm.player->isDead) // 自機が死んでいなければ
gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
(中略)......................
}
void Game::Draw()
{// 描画処理
DrawRotaGraphF((float)x, 200, 0.9f, 0, Image::bossImage, TRUE);
(中略)..................
}
PlayScene.hにGame.cppからプレイ中に関するコードを移植してみます。
#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "GameManager.h"
#include "SceneManager.h"
#include "Player.h"
#include "Boss.h"
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"
#include "Item.h"
class PlayScene : public Scene
{
public:
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる
// コンストラクタ
PlayScene() : Scene()
{
this->tag = "PlayScene";
}
// 初期化処理
void Initialize() override
{
// ここでプレイヤの初期化などのプレイ開始時の処理
gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
// 敵の仮生成
gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
}
// 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
void Finalize() override
{
// ここにプレイ終了時のメモリのリセット処理(ゲームクリアやゲームオーバーなど)
gm.player = nullptr; // プレイヤのメモリからの解放
gm.enemies.clear(); // 敵のメモリからの解放
gm.playerBullets.clear(); // プレイヤの弾のメモリからの解放
gm.enemyBullets.clear(); // 敵弾のメモリからの解放
gm.explosions.clear(); // 爆発エフェクトのメモリからの解放
gm.items.clear(); // アイテムのメモリからの解放
// ゲーム終了時のスコアがMAXなら記録して終了
if(score > sm.scoreMax) sm.scoreMax = score;
}
// 更新処理
void Update() override
{
// ここにプレイ中の更新処理を持ってくる
if (!gm.player->isDead) // 自機が死んでいなければ
gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
// 自機弾の更新処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Update();
}
// 敵弾の更新処理
for (const auto& b : gm.enemyBullets)
{
b->Update();
}
// 自機と敵弾の衝突判定
for (const auto& enemyBullet : gm.enemyBullets)
{
// 自機が死んでたらこれ以上判定しない
if (gm.player->isDead)
break;
// 敵弾が死んでたらスキップ
if (enemyBullet->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
gm.player->position, gm.player->collisionRadius,
enemyBullet->position, enemyBullet->collisionRadius))
{
gm.player->OnCollisionEnemyBullet(enemyBullet);
}
}
// 敵の更新処理
for (const auto& b : gm.enemies)
{
b->Update();
}
// 自機弾と敵の衝突判定
for (const auto& playerBullet : gm.playerBullets)
{
// 自機弾が死んでたらスキップする
if (playerBullet->isDead)
continue;
for (const auto& enemy : gm.enemies)
{
// 敵が死んでたらスキップする
if (enemy->isDead)
continue;
// 自機弾と敵が重なっているか?
if (MyMath::CircleCircleIntersection(
playerBullet->position, playerBullet->collisionRadius,
enemy->position, enemy->collisionRadius))
{
// 重なっていたら、それぞれのぶつかったときの処理を呼び出す
enemy->OnCollisionPlayerBullet(playerBullet);
playerBullet->OnCollisionEnemy(enemy);
// 衝突の結果、自機弾が死んだら、この弾のループはおしまい
if (playerBullet->isDead)
break;
}
}
}
// 自機と敵の衝突判定
for (const auto& enemy : gm.enemies)
{
// 自機が死んでたらこれ以上判定しない
if (gm.player->isDead)
break;
// 敵が死んでたらスキップ
if (enemy->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
gm.player->position, gm.player->collisionRadius,
enemy->position, enemy->collisionRadius))
{
gm.player->OnCollisionEnemy(enemy);
}
}
// 爆発エフェクトの更新処理
for (const auto& e : gm.explosions)
{
e->Update();
}
// アイテムの更新処理
for (const auto& itm : gm.items)
{
itm->Update();
}
// 自機とアイテム、敵弾とアイテムの衝突判定
for (const auto& itm : gm.items)
{
// 自機が死んでたらこれ以上判定しない
if (gm.player->isDead)
break;
// アイテムが死んでたらスキップ
if (itm->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
gm.player->position, gm.player->collisionRadius,
itm->position, itm->collisionRadius) )
{
itm->OnCollisionPlayer(gm.player);
}
for (const auto& enemyBullet : gm.enemyBullets)
{
// 敵弾が死んでたらスキップ
if (enemyBullet->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
itm->position, itm->collisionRadius,
enemyBullet->position, enemyBullet->collisionRadius) )
{
enemyBullet->OnCollisionItem(itm);
itm->OnCollisionEnemyBullet(enemyBullet);
}
}
}
//return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
// 自機弾の削除処理
gm.EraseRemoveIf(gm.playerBullets,
[](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 敵のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.enemies,
[](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 敵弾のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.enemyBullets,
[](std::shared_ptr<EnemyBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
//爆発エフェクトのリストから死んでるものを除去する
gm.EraseRemoveIf(gm.explosions,
[](std::shared_ptr<Explosion>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
//アイテムのリストから死んでるものを除去する
gm.EraseRemoveIf(gm.items,
[](std::shared_ptr<Item>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// ゲームオーバー判定と画面チェンジ処理
bool isGameover = (gm.player == nullptr);
if (!isGameover && gm.player->isDead) isGameover = true;
if (isGameover)
{
sm.LoadScene("GameOverScene"); //シーン遷移
return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
}
}
// 描画処理
void Draw() override
{
// ここにプレイ画面の描画処理を持ってくる
// 敵の描画処理
for (const auto& b : gm.enemies)
{
b->Draw();
}
// 爆発エフェクトの描画処理
for (const auto& e : gm.explosions)
{
e->Draw();
}
if (!gm.player->isDead) // 自機が死んでいなければ
gm.player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
// 自機弾の描画処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Draw();
}
// 敵弾の描画処理
for (const auto& b : gm.enemyBullets)
{
b->Draw();
}
// アイテムの描画処理
for (const auto& itm : gm.items)
{
itm->Draw();
}
}
};
#endif
Game.cppから移植したコードを削除します。
#include "Game.h"
#include "MyRandom.h"
#include "Player.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"
#include "Zako0.h"
#include "Zako1.h"
#include "Zako2.h"
#include "Zako3.h"
#include "Item.h"
#include "Boss.h" // 初期化やUpdate(),Draw()など【実際の処理を行うのでインクルード必要】
void Game::Init()
{// Init処理
Image::Load(); //画像の読込み
MyRandom::Init(); // 乱数シードの初期化
gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
// とりあえず適当に敵を生成
gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)600, (float)100)));
gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3((float)1000, (float)500)));
gm.enemies.emplace_back(std::make_shared<Zako1>(Vector3((float)1000, (float)150)));
gm.enemies.emplace_back(std::make_shared<Zako2>(Vector3((float)1200, (float)250)));
gm.enemies.emplace_back(std::make_shared<Zako3>(Vector3((float)1300, (float)400)));
gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
}
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
if (!gm.player->isDead) // 自機が死んでいなければ
gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
// 自機弾の更新処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Update();
}
// 敵弾の更新処理
for (const auto& b : gm.enemyBullets)
{
b->Update();
}
(中略)................
// 自機と敵の衝突判定
for (const auto& enemy : gm.enemies)
{
// 自機が死んでたらこれ以上判定しない
if (gm.player->isDead)
break;
// 敵が死んでたらスキップ
if (enemy->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
gm.player->position, gm.player->collisionRadius,
enemy->position, enemy->collisionRadius))
{
gm.player->OnCollisionEnemy(enemy);
}
}
// 爆発エフェクトの更新処理
for (const auto& e : gm.explosions)
{
e->Update();
}
(中略)................
//return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
// 自機弾の削除処理
gm.EraseRemoveIf(gm.playerBullets,
[](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
(中略)..............
//爆発エフェクトのリストから死んでるものを除去する
gm.EraseRemoveIf(gm.explosions,
[](std::shared_ptr<Explosion>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
//アイテムのリストから死んでるものを除去する
gm.EraseRemoveIf(gm.items,
[](std::shared_ptr<Item>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
}
void Game::Draw()
{// 描画処理
// 敵の描画処理
for (const auto& b : gm.enemies)
{
b->Draw();
}
// 爆発エフェクトの描画処理
for (const auto& e : gm.explosions)
{
e->Draw();
}
if (!gm.player->isDead) // 自機が死んでいなければ
gm.player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
// 自機弾の描画処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Draw();
}
// 敵弾の描画処理
for (const auto& b : gm.enemyBullets)
{
b->Draw();
}
// アイテムの描画処理
for (const auto& itm : gm.items)
{
itm->Draw();
}
}
さて、移植しただけではゲームは動きません。
Game.hとGame.cppにシーンの初期遷移、更新、描画の処理
を追加しましょう。
Game.hにシーンのマネージャへの連絡先sm
を追加しましょう。
#ifndef GAME_H_
#define GAME_H_
#include <vector>//配列std::vectorを使う
#include <memory>//スマートなポインタを使うのに必要(shared_ptrなど)
#include "Image.h"
#include "Screen.h"
#include "Input.h"
#include "GameManager.h"
#include "SceneManager.h"
#include "Scene.h"
class Game
{
public:
/* ↓{ }を.hに書くことでcppに分けて書くはずの処理ぶぶんを.hに書いてもよい */
Game() { }; // 初期化コンストラクタ
~Game() { }; // 破棄処理デストラクタ
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
void Init(); // Init処理(定義だけ)
void Update(); // 更新処理(定義だけ)
void Draw();// 描画処理(定義だけ)
};
#endif
Game.cppにシーンの初期遷移、更新、描画の処理
を追加しましょう。
#include "Game.h"
#include "MyRandom.h"
void Game::Init()
{// Init処理
Image::Load(); //画像の読込み
MyRandom::Init(); // 乱数シードの初期化
// ゲーム開始して最初はタイトルシーンに遷移
sm.LoadScene("TitleScene");
}
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れると全シーン反応しない
// シーンの更新処理
sm.currentScene->Update();
}
void Game::Draw()
{// 描画処理
// シーンの描画処理
sm.currentScene->Draw();
}
さて、ここまでで画面遷移できるようになりました。
一度実行して画面遷移できるか確認してみてください。
さて、画面遷移は実現しましたが、現状ではメモリの節約効果は薄いです。
なぜか? 答えはImageクラスではゲーム開始時に全画像データを読み込んだままずっと持ち続けてます。
ゆえにゲームが壮大になれば【冒険で使う全画像データがメモリにのり続けてしまい】ます。
よって【画面やステージマップごと】に【使用する画像以外をメモリから解放する】処理を取り入れましょう。
【補足と考察】
(実はプレイ中の敵や弾[1個=数byte]の削除処理はメモリより【当たり判定のCPU計算を軽くする】効果の方が強かった)
(敵や弾[1個=数byte]×1000個でも1000バイト=1KBキロバイト、一方で【大きめの背景画像ファイルは1個で1~1000KBキロバイト=1MBメガバイト】)
(つまり画像1つ分で弾数万発ぶんのメモリ節約、まあでも【数万発の当たり判定したらCPU重くて画面フリーズしますが】)
Image.hにロード済み画像の辞書に関する定義
を追加しましょう(mapタイプ型 = 辞書配列型)。
#ifndef IMAGE_H_
#define IMAGE_H_
#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
#include <unordered_map> // ロード済の画像辞書に使う
#include <string> // 文字列string
#include <memory> // 辞書の指す先を回し読みポインタにする
#include <vector> // シーンのリストに使う
#include <algorithm> // シーンのリストからのfindに使う
#include "DivImage.h" // 分割画像の読込みに使う
class Image
{
public:
Image() {}; // 初期化コンストラクタの定義だけ
~Image() {}; // 破棄する処理デストラクタの定義だけ
static void Load(std::string scene=""); // シーンを指定して使う画像だけロードすればメモリを節約できる
static int LoadDivGraph(const TCHAR* FileName, DivImage& divImage, std::string scene="", std::vector<std::string> useScenes={""});
static int LoadGraph(const TCHAR* FileName, std::string scene = "", std::vector<std::string> useScenes = { "" });
static std::unordered_map<std::string, DivImage*> loadDic; // ロード済の画像辞書
static int bossImage; //ボス画像のハンドラ(読込画像番号)
static int player; //プレイヤ画像のハンドラ
static int playerBullet; //プレイヤの弾画像のハンドラ
static int enemyBullet16; //敵弾 画像のハンドラ(読込画像番号)
static int zako0; //ザコ0 画像のハンドラ(読込画像番号)
static int zako1; //ザコ1 画像のハンドラ(読込画像番号)
static int zako2; //ザコ2 画像のハンドラ(読込画像番号)
static int zako3; //ザコ3 画像のハンドラ(読込画像番号)
static int boss1; // ボス通常時
static int boss2; // ボス気絶
static int boss3; // ボス発狂
static DivImage explosion; // [分割画像]爆発エフェクト
static int item0; // アイテム0 画像のハンドラ(読込画像番号)
private:
};
#endif
Image.cppにロード済み画像を辞書に登録しシーンで使わない画像を開放する処理
を追加しましょう。
#include "Image.h"
std::unordered_map<std::string, DivImage*> Image::loadDic;
int Image::bossImage{ -1 }; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{ -1 };
int Image::playerBullet{ -1 };
int Image::enemyBullet16{ -1 };
int Image::zako0{ -1 };
int Image::zako1{ -1 };
int Image::zako2{ -1 };
int Image::zako3{ -1 };
int Image::boss1{ -1 };
int Image::boss2{ -1 };
int Image::boss3{ -1 };
// ★分割画像は初期化のときに{X方向画像数, Y方向画像数, 画像横幅XSize,画像縦幅YSize}を指定
DivImage Image::explosion{ 8, 2, 64, 64 };
int Image::item0{ -1 };
void Image::Load(std::string scene)
{
bossImage = LoadGraph("Image/boss1.png", scene, { "TitleScene" });
assert(bossImage != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
player = LoadGraph("Image/player.png", scene, { "PlayScene" });
assert(player != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
playerBullet = LoadGraph("Image/player_bullet.png", scene, { "PlayScene" });
assert(playerBullet != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
enemyBullet16 = LoadGraph("Image/enemy_bullet_16.png", scene, { "PlayScene" });
assert(enemyBullet16 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako0 = LoadGraph("Image/zako0.png", scene, { "PlayScene" });
assert(zako0 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako1 = LoadGraph("Image/zako1.png", scene, { "PlayScene" });
assert(zako1 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako2 = LoadGraph("Image/zako2.png", scene, { "PlayScene" });
assert(zako2 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
zako3 = LoadGraph("Image/zako3.png", scene, { "PlayScene" });
assert(zako3 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
boss1 = LoadGraph("Image/boss1.png", scene, { "PlayScene" });
assert(boss1 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
boss2 = LoadGraph("Image/boss2.png", scene, { "PlayScene" });
assert(boss2 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
boss3 = LoadGraph("Image/boss3.png", scene, { "PlayScene" });
assert(boss3 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
// div分割画像をロード or メモリ開放
if( Image::LoadDivGraph("Image/explosion.png", explosion, scene, { "PlayScene" }) != -2 );
for (int i = 0; i < explosion.AllNum; i++)
{ // 画像読込失敗
if (explosion.HandleArray[i] == -1)
assert("画像読込失敗(画像ファイル名やフォルダが正しい?)" == "");
}
item0 = LoadGraph("Image/heart.png", scene, { "PlayScene" });
assert(item0 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
item1 = LoadGraph("Image/star.png", scene, { "PlayScene" });
assert(item1 != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
}
// DXライブラリのLoadGraphを同じ関数名でラッピング(既存コードの書換えを最小限にする工夫)
// シーンごとにすでにメモリにロードしてある画像でシーンに使わないものはメモリから解放する
int Image::LoadGraph(const TCHAR* FileName, std::string scene, std::vector<std::string> useScenes)
{
DivImage divImage{ 1,1,1,1 }; // 1×1の1分割画像として分割画像とロードを共通化
return Image::LoadDivGraph(FileName, divImage, scene, useScenes);
}
// DXライブラリのLoadDivGraphを使いやすくラッピング
// ★関数に参照渡しを行う
// 関数の引数に参照渡しを行うことでdivImageの中身を書き換えることができる(値渡しでは書換不可)
// https://qiita.com/agate-pris/items/05948b7d33f3e88b8967
// https://qiita.com/RuthTaro/items/f35c3a26779c0ca1a41a
int Image::LoadDivGraph(const TCHAR* FileName, DivImage& divImage, std::string scene, std::vector<std::string> useScenes)
{
int result = -1;
// 次のシーンで使う画像かの判別、useScenesリストにあれば使う判定
bool use = std::find(useScenes.begin(), useScenes.end(), scene) != useScenes.end();
if (use)
{ // ロード済み辞書に指定したファイルがある(count=1)のとき
if (loadDic.count(FileName) == 1
&& loadDic[FileName] != nullptr && loadDic[FileName]->HandleArray != nullptr)
{ // 画像ハンドルをfor文で書き写す(単一:AllNum=1,分割画像:AllNum>1)
for (int i = 0; i < loadDic[FileName]->AllNum; i++)
divImage.HandleArray[i] = loadDic[FileName]->HandleArray[i];
// ロード済みなら画像ハンドルを返す(ロード時間短縮)
if (loadDic[FileName]->AllNum == 1)
return loadDic[FileName]->HandleArray[0];
else return 0; // 分割画像なら成功:0を返す(本家LoadDivGraphに合わせて)
}
// DXライブラリで画像をロードし辞書に登録
if (divImage.AllNum == 1)
{ // 単一画像のロードと画像サイズ取得
result = divImage.HandleArray[0] = DxLib::LoadGraph(FileName);
GetGraphSize(result, &(divImage.XSize), &(divImage.YSize)); // 画像サイズ取得
}
else if (divImage.AllNum > 1) // 複数分割画像の読込み
result = DxLib::LoadDivGraph(FileName, divImage.XNum * divImage.YNum,
divImage.XNum, divImage.YNum, divImage.XSize, divImage.YSize, divImage.HandleArray);
for (int i = 0; i < divImage.AllNum; i++)
{ // 画像読込失敗
if (divImage.HandleArray[i] == -1)
assert("画像読込失敗(画像ファイル名やフォルダが正しい?)" == "");
}
// 辞書用にdivImageの【★コピーを取ってポインタ】として保管
if (result != -1)
{
if (loadDic.count(FileName) == 1 && loadDic[FileName] != nullptr)
{ // 上書き登録のために既存の情報を消す
DivImage* oldHandle = loadDic[FileName];
delete oldHandle;// 辞書に保管している情報削除
loadDic.erase(FileName);// 辞書から索引も削除
}
// 新たに辞書登録
loadDic[FileName] = new DivImage(divImage);//←★コピーコンストラクタ
}
return result; // ロードした結果を返す
} // ↓ここまでの↑returnに引っかからずif抜けてきたら【シーン利用しない画像】
// 今回のシーンでは利用しない画像の時はメモリから画像を開放して節約
if (loadDic.count(FileName) == 1 &&
loadDic[FileName] != nullptr && loadDic[FileName]->HandleArray != nullptr)
{ // メモリから画像開放 https://dxlib.xsrv.jp/function/dxfunc_graph1.html#R3N15
for (int i = 0; i < loadDic[FileName]->AllNum; i++)
DxLib::DeleteGraph(loadDic[FileName]->HandleArray[i]);
DivImage* oldHandle = loadDic[FileName];
delete oldHandle;// 辞書に保管している情報削除
loadDic.erase(FileName); // 辞書からも索引削除
}
// 今回のシーンでは利用しない画像の時は-2を返す
return -2; // 画像読込み失敗の-1と区別するため-2を返します
};
DivImage.hにコピーコンストラクタをpublic定義にして
画像辞書に分割画像のコピーへのポインタを保存できるようにする。
下記スライドで変更理由に目を通しておきましょう(C++でクラス内でnewやdeleteのした際の根の深いコピー時の*ポインタとデータ分離問題)。
#ifndef DIVIMAGE_H_
#define DIVIMAGE_H_
#include "DxLib.h"
#include <assert.h> // 画像の2のべき乗チェックに使う
// 分割画像を読込むためのデータ構造(画像数や画像ハンドル配列、3Dに使う場合の2のべき乗チェック機能をもつ)
class DivImage
{ // 2Dの分割画像や3Dに分割Div画像を使うと画像全体が使われてしまうのを回避するための情報
public:
int XNum = 0;
int YNum = 0;
int XSize = 0;
int YSize = 0;
int* HandleArray = nullptr;
int AllNum = 0;
bool is3DTexture = false;
// 初期化 : X方向分割画像数 XNum, Y方向分割画像数 YNum, 画像 横 幅XSizeピクセル, 画像 縦 高さYSizeピクセル
DivImage(int XNum, int YNum, int XSize, int YSize, bool is3DTexture = false)
{ // 初期化コンストラクタ
this->XNum = XNum; // X方向分割画像数
this->YNum = YNum; // Y方向分割画像数
this->XSize = XSize; // 画像 横 幅XSize(ピクセルドット数)
this->YSize = YSize; // 画像 縦 高さYSize(ピクセルドット数)
AllNum = XNum * YNum; // トータルの分割画像数
this->is3DTexture = is3DTexture; // 3Dテクスチャ分割画像かtrueだと2のべき乗チェックが入る
// ★div分割画像読込ハンドラ保存用のint配列メモリを確保し-1で初期化
this->HandleArray = new int[AllNum]; // 配列を確保し-1で初期化
for (int i = 0; i < AllNum; i++) HandleArray[i] = -1;
//↑【演習★】初期化後の動的配列をデバッガで見てみよう[ary,256として再評価ボタン]
// http://visualstudiostudy.blog.fc2.com/blog-entry-17.html
int* ary = HandleArray;
#ifdef _DEBUG // デバッグ機能(デバッグの時だけ _DEBUG が定義)
if (is3DTexture == true) ImagePowCheck((*this)); // *(this)でthisポインタの示す変数内容を表す
#endif
};
~DivImage()
{ // デストラクタでメモリを解放
if (this->HandleArray != nullptr)
delete[] this->HandleArray;
};
// コピーコンストラクタ(動的new配列をクラス内に持つ場合にはカスタムが必要)
DivImage(const DivImage& divImage)
{
// 基本はただのコピー書き移し
XNum = divImage.XNum; YNum = divImage.YNum;
XSize = divImage.XSize; YSize = divImage.YSize;
AllNum = XNum * YNum;
is3DTexture = divImage.is3DTexture;
// ↓★この配列だけはデフォルトのコピーコンストラクタではコピーされない!
HandleArray = new int[AllNum]; //★[勉強]こちらでもnew確保
for (int i = 0; i < AllNum; i++)
HandleArray[i] = divImage.HandleArray[i];
};
//【勉強 []添え字演算子を独自に定義】https://programming.pc-note.net/cpp/operator.html
// 内部のHandleArrayに今までの様にImage::explosion[5]とかで簡単アクセスできるように【あいだに噛ます】
int const& operator [](int index) const
{
if (index < 0 || AllNum <= index) assert("画像データのアクセス番号がおかしい!" == "");
return HandleArray[index];
}
int& operator [](int index)
{
if (index < 0 || AllNum <= index) assert("画像データのアクセス番号がおかしい!" == "");
return HandleArray[index];
}
#ifdef _DEBUG // デバッグ機能(デバッグの時だけ _DEBUG が定義)
bool is_pow2(unsigned int x) // 2のべき乗か計算
{ // https://programming-place.net/ppp/contents/c/rev_res/math012.html
return (x != 0) && (x & (x - 1)) == 0;
}
void ImagePowCheck(DivImage& divImage)
{ // ★3Dに使う画像は2のべき乗でなければ受け付けないようにする
// https://yttm-work.jp/gmpg/gmpg_0031.html
// https://yappy-t.hatenadiary.org/entry/20100110/1263138881
if (divImage.XSize > 0 && divImage.YSize > 0
&& is_pow2(divImage.XSize) && is_pow2(divImage.YSize)) return;
else assert("3Dに使うなら2のべき乗の画像サイズにしなきゃ" == "");
}
#endif
private: // コピーと代入をプライベートにして禁止する
// コピーコンストラクタの禁止privateオーバーロード
DivImage(const DivImage& divImage) {};
// いままでコピー禁止してたのはnew,deleteの面倒なデータと*ポインタ分離問題を察知するため
// 代入演算子の禁止privateオーバーロード
void operator=(const DivImage& divImage) {};
};
#endif
TitleScene.hにシーンごとの画像読込みとメモリ開放する処理
を追加しましょう。
(中略)........
// 初期化処理
void Initialize() override
{
Image::Load("TitleScene"); //画像の読込みとメモリ開放
// テスト用のボス画像をタイトルでアニメで左右移動させる
x = 100; // Xの初期位置
vx = 10; // ボスの初期速度
}
(中略)........
PlayScene.hにシーンごとの画像読込みとメモリ開放する処理
を追加しましょう。
(中略)........
// 初期化処理
void Initialize() override
{
Image::Load("PlayScene"); //画像の読込みとメモリ開放
// ここでプレイヤの初期化などのプレイ開始時の処理
gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
// 敵の仮生成
gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
}
(中略)........
GameOverScene.hにシーンごとの画像読込みとメモリ開放する処理
を追加します。
(現状シーンで画像使ってないけどプレイ中画面のメモリを解放する効果が出る)。
(中略)........
// 初期化処理
void Initialize() override
{
Image::Load("GameOverScene"); //画像の読込みとメモリ開放
}
(中略)........
Game.cppでやっていたImageのLoadを削除
します。
#include "Game.h"
#include "MyRandom.h"
void Game::Init()
{// Init処理
Image::Load(); //画像の読込み
MyRandom::Init(); // 乱数シードの初期化
// ゲーム開始して最初はタイトルシーンに遷移
sm.LoadScene("TitleScene");
}
(以下略)............
これで一応画面ごとにメモリから画像データが開放される構造になりました。
でも、ほとんどメモリのグラフには変化がないかもしれません(タイトル動画など重いデータあれば威力発揮)
実は画像より音声のほうがメモリ消費が大きかったりします。
なぜか?画像はアニメしなければ1枚ですよね。
一方【音声には時間の長さがあります】再生時間が10分など長くなれば数MBになります。
特に.mp3など圧縮された音声ファイルはメモリ上では非圧縮の.wavになります!
ゲーム開始前は小さいmp3の音声ファイルがゲーム開始すると【メモリ上で数十倍に膨れ上がります】
しかも最悪なことに【圧縮をもとの音声に戻すためロードにめちゃくちゃ時間がかかります】
ゆえにロードが重くなるのを防ぐためLoadSoundMemする音声ファイルは【あらかじめ.wav変換したほうがLoadが早い】です。
以下リンクの変換ツールでmp3などを.wavの非圧縮の音声ファイルに変換して使いましょう。
https://forest.watch.impress.co.jp/library/software/flicaudiocon/
今回はまず「webでさがしたBGM素材サイト」から音声ファイルを取ってきてSoundクラスを作成してみましょう。
素材サイト例)
https://maou.audio/category/bgm/
https://maou.audio/category/se/
自分でシンセをダウンロードして効果音やBGMを自分で作っちゃうことだってできる。
(人は真にクリエイティブになればなるほど借り物に飽き足らずそれを作る道具の方に興味が行くものです)
まずは簡易版のSoundクラスを作ってみましょう。基本はImage.hと似ています。
Sound.hを新規作成
します。
#ifndef SOUND_H_
#define SOUND_H_
#include "DxLib.h"
#include <assert.h> // 音声読み込みの読込み失敗表示用
#include <string>
class Sound
{
public:
Sound() {}; // 初期化コンストラクタの定義
~Sound() {}; // 破棄処理デストラクタの定義
// メモリ再生音源管理番号
static int start; // ピロピロリン
static int bomb; // ボン!
static std::string playingMusic; // 再生中のBGM(同一BGMをPlayMusic指定しても再生し直さない)
static void Load();
// メモリから音声を再生(個別の音量設定も効かせられる)
static void Play(int handle, int PlayType = DX_PLAYTYPE_BACK);
// ディスク上の音楽を直接再生する(メモリ上に展開しない)
static void PlayMusic(std::string file, int PlayType = DX_PLAYTYPE_LOOP);
private:
};
#endif
Sound.cppを新規作成
します。
すでにプレイ中のBGMは同じBGMをPlayMusicしようとしても再生し直さない工夫をしています。
これで再生ボタンUpdate連打状態で重くなるストッパーになります。
#include "Sound.h"
// メモリ再生音源管理番号
int Sound::start; // ピロピロリン
int Sound::bomb; // ボン!
std::string Sound::playingMusic; // 再生中のBGM(同一BGMをプレイ指定しても初めから再生しない)
void Sound::Load()
{
start = LoadSoundMem("SE/start.ogg");
assert(start != -1); // 読込失敗、ファイル名かフォルダ名が間違ってる
bomb = LoadSoundMem("SE/bomb.wav");
assert(bomb != -1); // 読込失敗、ファイル名かフォルダ名が間違ってる
}
// メモリ上の音声を再生する。
void Sound::Play(int handle, int PlayType)
{
// メモリ上から音声再生
PlaySoundMem(handle, PlayType);
}
// ディスク上の音楽を直接再生する(メモリ上に展開しない)
void Sound::PlayMusic(std::string file, int PlayType)
{
// プレイ中のBGMを再指定しても再生し直さず即時return
if (file == playingMusic) return;
playingMusic = file; // プレイ中のBGM名を記録
DxLib::PlayMusic(file.c_str(), PlayType); //ディスクから直接再生(メモリにのせない)
}
Game.cppにSoundのロード処理を追加
します。
#include "Game.h"
#include "MyRandom.h"
#include "Sound.h"
void Game::Init()
{// Init処理
(中略)......
MyRandom::Init(); // 乱数シードの初期化
Sound::Load(); //音声の読込み
// ゲーム開始して最初はタイトルシーンに遷移
sm.LoadScene("TitleScene");
}
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
// シーンの更新処理
sm.currentScene->Update();
}
void Game::Draw()
{// 描画処理
// シーンの描画処理
sm.currentScene->Draw();
}
TitleScene.hにサウンドの読込みとBGM再生、ゲームスタート時の再生処理と再生終わり待ちで画面遷移する処理
を追加します。
#ifndef TITLESCENE_H_
#define TITLESCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"
#include "GameManager.h"
#include "SceneManager.h"
class TitleScene : public Scene
{
public:
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
// スタートボタンを押したら音声再生完了を待って画面移動
bool isStartPushed = false;
// テスト用のボス画像をタイトルでアニメで左右移動させる
int x = 0; //staticじゃないintはここで=0で初期化できる
int vx = 0;
// コンストラクタ
TitleScene() : Scene()
{
this->tag = "TitleScene";
}
// 初期化処理
void Initialize() override
{
(中略).....
Sound::PlayMusic("BGM/title.mp3"); //タイトルBGMを再生
isStartPushed = false; // ボタンのフラグをリセット
// テスト用のボス画像をタイトルでアニメで左右移動させる
x = 100; // Xの初期位置
vx = 10; // ボスの初期速度
}
// 終了処理(大抵Initializeと同じリセットだが終了時だけやりたいリセットの仕方もあるかも)
void Finalize() override
{
}
// ボタン入力に応じた処理
void HandleInput()
{
if (Input::GetButtonDown(PAD_INPUT_1))
{ // ボタンを押したらスタート音声を再生
Sound::Play(Sound::start);
isStartPushed = true; // 画面遷移トリガーON
}
}
// 更新処理
void Update() override
{
// スタートの効果音の再生が終わるまで画面遷移を待つ
if (isStartPushed && CheckSoundMem(Sound::start) == 0)
{ // スタート音声再生終了をトリガーとして画面遷移
sm.LoadScene("PlayScene"); //シーン遷移
}
HandleInput(); // キー入力処理
// テスト用のボス画像をタイトルでアニメで左右移動させる
if (x < 0)
{
vx = 10; //速度の変更
}
else if (x > Screen::Width)
{
vx = -10; //画面端で移動方向反転
}
x += vx; // ボス画像のX位置の更新
}
// 描画処理
void Draw() override
{
DrawString(0, 0, "TitleSceneです。ボタン押下でPlaySceneへ。", GetColor(255, 255, 255));
if (sm.scoreMax >= 0)
{
DrawString(0, 200, "MAXスコア: " + sm.scoreMax, GetColor(255, 255, 255));
}
// テスト用のボス画像をタイトルでアニメで左右移動させる
DrawRotaGraphF((float)x, 200, 1.0f, 0, Image::bossImage, TRUE);
}
};
#endif
PlayScene.hにプレイBGM再生と敵の被弾の効果音の処理
を追加します。
音声のハンドルを辞書管理してメモリから解放したり個別音量調整できるようにします。
#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"
#include "GameManager.h"
#include "SceneManager.h"
#include "Player.h"
#include "Boss.h"
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"
#include "Item.h"
class PlayScene : public Scene
{
public:
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる
// コンストラクタ
PlayScene() : Scene()
{
this->tag = "PlayScene";
}
// 初期化処理
void Initialize() override
{
(中略).....
Sound::PlayMusic("BGM/gameplay.mp3"); //プレイ画面BGMを再生
// ここでプレイヤの初期化などのプレイ開始時の処理
gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
// 敵の仮生成
gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
}
(中略)...................
// 更新処理
void Update() override
{
(中略)...................
// 自機弾と敵の衝突判定
for (const auto& playerBullet : gm.playerBullets)
{
// 自機弾が死んでたらスキップする
if (playerBullet->isDead)
continue;
for (const auto& enemy : gm.enemies)
{
// 敵が死んでたらスキップする
if (enemy->isDead)
continue;
// 自機弾と敵が重なっているか?
if (MyMath::CircleCircleIntersection(
playerBullet->position, playerBullet->collisionRadius,
enemy->position, enemy->collisionRadius))
{
// 重なっていたら、それぞれのぶつかったときの処理を呼び出す
enemy->OnCollisionPlayerBullet(playerBullet);
playerBullet->OnCollisionEnemy(enemy);
Sound::Play(Sound::bomb); // 効果音の再生
// 衝突の結果、自機弾が死んだら、この弾のループはおしまい
if (playerBullet->isDead)
break;
}
}
}
(中略)...................
// ゲームオーバー判定と画面チェンジ処理
bool isGameover = (gm.player == nullptr);
if (!isGameover && gm.player->isDead) isGameover = true;
if (isGameover)
{
Sound::PlayMusic("BGM/ending.mp3");
sm.LoadScene("GameOverScene"); //シーン遷移
return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
}
}
// 描画処理
void Draw() override
{
(中略)...................
}
};
#endif
ここまでで簡易的なサウンドの再生できるようになります。
実行してみてうまく音声が再生できるか試してみましょう。
次は高度なサウンドの機能を作成しますが、難易度が上がりますので
簡単に再生できるだけでよい人は高度なサウンド機能は一旦後回しでもよいかもしれません。
さて、一応基本的なサウンド再生機能は実現しました。
この項では少し高度なサウンドの音量管理とメモリ管理を辞書配列で実現します。
とりあえず再生できればよいや、という人はあとから取り入れる形でもよいかもしれません。
辞書配列には辞書配列mapの【高速版unordered_map】を使用しました。
普通の辞書は【あいうえお順に毎度並べ替えに時間かけてる】ですが、
高速版辞書は【いちいち、あいうえお並び替えしないから高速】と覚えておきましょう。
並べ替え順のない高速辞書を【ハッシュ】と呼びます。
ハッシュは【辞書のキー⇒順番の必要ないデータへの高速アクセス可】と暗記しておきましょう。
配列などの中では【条件付きでほぼ最速】という認識で大丈夫です。
まとめて複数の音声をフェードアウトするときはUpdateで音声辞書を秒間60回連続検索することになるので導入しました。
ちなみにどれくらい高速かは下記リンク参照(ハッシュはO(1)という最速水準の単位です)
https://qiita.com/drken/items/872ebc3a2b5caaa4a0d0
Sound.hに辞書配列や音量管理関数の定義を追加
します。
#ifndef SOUND_H_
#define SOUND_H_
#include "DxLib.h"
#include <assert.h> // 音声読み込みの読込み失敗表示用
#include <string>
#include <unordered_map> // 音声の辞書【map高速版】https://qiita.com/sileader/items/a40f9acf90fbda16af51
#include <vector> // シーンのリストに使う
#include <algorithm> // シーンのリストからのfindに使う
class Sound
{
public:
Sound() {}; // 初期化コンストラクタの定義
~Sound() {}; // 破棄処理デストラクタの定義
// メモリ再生音源管理番号
static int start; // ピロピロリン
static int bomb; // ボン!
// ディスク再生音源管理番号
static int title; // タイトル画面BGM
static int gameplay; // プレイ画面BGM
static int ending; // ゲームオーバ画面BGM
static std::unordered_map<std::string, int> loadDic; // ロード済の音声辞書
static std::unordered_map<int, std::string> SE_Dic; // SEファイル名辞書
static std::unordered_map<int, std::string> BGM_Dic; // BGMファイル名辞書
static std::unordered_map<std::string, float> volumeDic; // 【音声ごと】の[音量]辞書
static float volumeSE; // 効果音のベース音量(0~100%で指定)
static float volumeBGM; // 音楽BGMのベース音量(0~100%で指定)
static std::string playingMusic; // 再生中のBGM(同一BGMをPlayMusic指定しても再生し直さない
static int musicId; // 音楽ID -3,-4,-5..メモリ上にのせないが番号を管理し個別に音量設定できるよう
// シーンを指定してそのシーンで使うサウンドだけロードすればメモリ節約できる
static void Load(std::string scene = "");
// メモリから音声を再生(個別の音量設定も効かせられる)
static void Play(int handle, int PlayType = DX_PLAYTYPE_BACK);
// ディスク上の音楽を直接再生する(メモリ上に展開しない)
static void PlayMusic(std::string file, int PlayType = DX_PLAYTYPE_LOOP);
// IDからディスク上の音楽を直接再生する(メモリ上に展開しない)
static void PlayMusic(int musicId, int PlayType = DX_PLAYTYPE_LOOP);
// ファイルを辞書登録しmusicIDで管理する(メモリにはのせずファイル名とIDだけ登録)
static int SetMusicFile(std::string file, bool isSE = false);
// 音声ファイルごとのボリューム設定
static void SetVolume(float VolumeParcent, int SoundHandle);
// メモリ音源の音量の更新(再生中に減らせばフェードアウト)
static int UpdateVolume(int SoundHandle = -1);
// 再生中のディスク音楽の音量の更新
static int UpdateMusicVolume();
// 音声をメモリへ読込み番号GET or シーンで使わない場合はメモリから解放
static int LoadSoundMem(const TCHAR *FileName, std::string scene, std::vector<std::string> useScenes, bool isBGM=false, int BufferNum = 3);
private:
};
#endif
Sound.cppに音声辞書とメモリ開放と音量調節処理を追加
します。
音声のハンドルを辞書管理してメモリから解放したり個別音量調整できるようにします。
#include "Sound.h"
// メモリ再生音源管理番号
int Sound::start; // ピロピロリン
int Sound::bomb; // ボン!
// ディスク再生音源管理番号
int Sound::title; // タイトル画面BGM
int Sound::gameplay; // プレイ画面BGM
int Sound::ending; // ゲームオーバ画面BGM
std::unordered_map<std::string, int> Sound::loadDic; // ロード済の音声辞書
std::unordered_map<int, std::string> Sound::SE_Dic; // SEファイル名辞書
std::unordered_map<int, std::string> Sound::BGM_Dic; // BGMファイル名辞書
std::unordered_map<std::string, float> Sound::volumeDic; // 【音声ごと】の[音量]辞書
float Sound::volumeSE{ 100 }; // 効果音のベース音量(0~100%で指定)
float Sound::volumeBGM{ 100 }; // 音楽BGMのベース音量(0~100%で指定)
std::string Sound::playingMusic{ "" }; // 再生中のBGM(同一BGMをPlayMusic指定しても再生し直さない
int Sound::musicId{ -3 }; // 音楽ID -3,-4,-5..メモリ上にのせないが番号を管理し個別に音量設定できるよう
// シーンを指定してそのシーンで使うサウンドだけロードすればメモリ節約できる
void Sound::Load(std::string scene)
{
start = Sound::LoadSoundMem("SE/start.ogg", scene, { "TitleScene" });
assert(start != -1); // 読込失敗、ファイル名かフォルダ名が間違ってる
// オプションで同時[8つ]爆発音再生を可能に(デフォルトは3)falseをtrueにするとBGM扱い
bomb = Sound::LoadSoundMem("SE/bomb.wav",scene,{"TitleScene","PlayScene"},false, 8);
assert(bomb != -1); // 読込失敗、ファイル名かフォルダ名が間違ってる
title = SetMusicFile("BGM/title.mp3");
gameplay = SetMusicFile("BGM/gameplay.mp3");
ending = SetMusicFile("BGM/ending.mp3");
}
// メモリ上の音声を再生する。
void Sound::Play(int handle, int PlayType)
{
assert(handle != -1 && "プレイする音声がメモリにない?" != "");
// 音量の更新
UpdateVolume(handle);
// メモリ上から音声再生
PlaySoundMem(handle, PlayType);
}
// ディスク上の音楽を直接再生する(メモリ上に展開しない)
void Sound::PlayMusic(std::string file, int PlayType)
{
// プレイ中のBGMを再指定しても初めから再生せず即時return
if (file == playingMusic) return;
playingMusic = file; // プレイ中のBGM名を設定
DxLib::PlayMusic(file.c_str(), PlayType); //ディスクから直接再生(メモリにのせない)
Sound::UpdateMusicVolume(); // // 音量の更新(プレイ【中】の音楽だから↑PlayMusicしてから)
}
// IDからディスク上の音楽を直接再生する(メモリ上に展開しない)
void Sound::PlayMusic(int musicId, int PlayType)
{
if (musicId > -1) return;//音楽IDはマイナスのID番号を割り振ったはず
std::string file = "";
// 辞書から対応するファイル名を取得
if (BGM_Dic.count(musicId) == 1)
file = BGM_Dic[musicId];
else if (SE_Dic.count(musicId) == 1)
file = SE_Dic[musicId];
if (file == "") return;//対応する音楽ファイルが辞書に未登録
// ファイル名から音楽ファイルを再生
Sound::PlayMusic(file, PlayType);
}
// ファイルを辞書登録しmusicIDで管理する(メモリにはのせずファイル名とIDだけ登録)
int Sound::SetMusicFile(std::string file, bool isSE)
{ // 既に辞書登録済なら即時return
if (loadDic.count(file) == 1) return loadDic[file];
// 新規に辞書にファイル登録
loadDic[file] = musicId; //IDの辞書に登録
if (isSE) SE_Dic[musicId] = file; //SEの辞書に登録
else BGM_Dic[musicId] = file; //BGMの辞書に登録
return musicId--; // 後ろに--つけるとカウントダウン前をreturn
}
// 音声ファイルごとのボリューム設定
void Sound::SetVolume(float VolumeParcent, int SoundHandle)
{
if (SoundHandle == -1) return;
if (SE_Dic.count(SoundHandle) == 1)
{ // ハンドル番号がSEの辞書で見つかれば
std::string file = SE_Dic[SoundHandle];
volumeDic[file] = VolumeParcent;//個別音量を辞書登録
}
else if (BGM_Dic.count(SoundHandle) == 1)
{ // ハンドル番号がBGMの辞書で見つかれば
std::string file = BGM_Dic[SoundHandle];
volumeDic[file] = VolumeParcent;//個別音量を辞書登録
}
// セットした後の音声には音量が反映されるが
// すでに再生中の音量は変わらないので↓UpdateVolumeとセットで実行
}
// メモリ音源の音量の更新(再生中に減らせばフェードアウト)
int Sound::UpdateVolume(int SoundHandle)
{
// 特に指定がなければすべての音量を更新
if (SoundHandle == -1)
{ // 辞書のfor文はキーとバリューがペアになってる
int result = 0, resultAll = 0;
for (const auto& kv : loadDic)
{ // 辞書のキーがkv.first,バリューがkv.second
int handle = kv.second;
// 【★勉強】自分自身を呼出す奥の手
if (handle != -1) //↓【再起呼出し】
result = Sound::UpdateVolume(handle);
if (result == -1)
resultAll += -1; // 失敗した数ぶん-1がカウントされる
}
return resultAll; // 全音量更新完了【再起呼出しループで】
}
// 音源指定があるときはhandleを指定して音量を更新
int handle = SoundHandle;
int volume = 255;
float volumeParcent = 100;
std::string file = "";
if (SE_Dic.count(handle) == 1)
{ // サウンド番号がSE辞書で見つかれば
file = SE_Dic[handle];
volumeParcent = volumeSE; //音量をSEのベース音量に
}
else if (BGM_Dic.count(handle) == 1)
{ // サウンド番号がBGM辞書で見つかれば
file = BGM_Dic[handle];
volumeParcent = volumeBGM; //音量をSEのベース音量に
}
if (volumeDic.count(file) == 1)
{ // 音量辞書をファイル名で検索して
float fileParcent = volumeDic[file];
volumeParcent *= fileParcent / 100; //個別音量%割合反映
}
if (volumeParcent < 100) // 音量に%パーセントを掛ける
volume = (int)(((float)volume) * volumeParcent / 100);
// 音量反映
int result = ChangeVolumeSoundMem(volume, handle);
if (result == 0) // 音量変更成功
return volume;
else return result; // 音量変更失敗
}
// 再生中のディスク音楽の音量の更新
int Sound::UpdateMusicVolume()
{
std::string file = playingMusic;
int volume = 255;
float volumeParcent = volumeBGM;
if (volumeDic.count(file) == 1)
{ // 音量辞書をファイル名で検索して
float fileParcent = volumeDic[file];
volumeParcent *= fileParcent / 100; //個別音量%割合反映
}
if (volumeParcent < 100) // 音量に%パーセントを掛ける
volume = (int)(((float)volume) * volumeParcent / 100);
// 音量反映
int result = SetVolumeMusic(volume);
if (result == 0) // 音量変更成功
return volume;
else return result; // 音量変更失敗
}
// 音声をメモリへ読込み番号GET or シーンで使わない場合はメモリから解放
int Sound::LoadSoundMem(const TCHAR *FileName, std::string scene, std::vector<std::string> useScenes, bool isBGM, int BufferNum)
{
int result = -1;
// 次のシーンで使う音声かの判別、useScenesリストにあれば使う判定
bool use = std::find(useScenes.begin(), useScenes.end(), scene) != useScenes.end();
if (use)
{ // ロード済み辞書に指定したファイルがある(count=1)のとき
if (loadDic.count(FileName) == 1 && loadDic[FileName] != -1)
{ // ロード済みなら音声ハンドルを返す(ロード時間短縮)
return loadDic[FileName];
}
// DXライブラリで音声をロード、BufferNumは同時再生可能数
result = DxLib::LoadSoundMem(FileName, BufferNum);
if (result != -1)
{ // 辞書にサウンドハンドル番号を保管
int handle = result; //ロード結果からハンドル番号取得
loadDic[FileName] = handle;//ロード済辞書に登録
if (isBGM) BGM_Dic[handle] = FileName;//BGMの辞書に登録
else SE_Dic[handle] = FileName;//SEの辞書に登録
}
return result; // ロードした結果を返す
} // ↓ここまでの↑returnに引っかからずif抜けてきたら【シーン利用しないサウンド】
// 今回のシーンでは利用しないサウンドの時はメモリからサウンドを開放して節約
if (loadDic.count(FileName) == 1 && loadDic[FileName] != -1)
{ // メモリからサウンド開放 https://dxlib.xsrv.jp/function/dxfunc_sound.html#R8N8
int handle = loadDic[FileName];
result = DxLib::DeleteSoundMem(handle);
assert(result != -1 && "音声メモリ開放失敗" != "");
// 辞書からハンドルを削除
if (SE_Dic.count(handle) == 1)
SE_Dic.erase(handle);//SEの辞書から削除
else if (BGM_Dic.count(handle) == 1)
BGM_Dic.erase(handle);//BGMの辞書から削除
loadDic.erase(FileName); // ID辞書からも索引削除
}
// 今回のシーンでは利用しないサウンドの時は-2を返す
return -2; // サウンド読込み失敗の-1と区別するため-2を返します
}
TitleScene.hにシーンごとのサウンドのロードと音量調節機能を追加
します。
タイトル画面で左右キーを押すと音量調整できるようになり上下キーでSE⇔BGM調整モードを切り替えられます。
#ifndef TITLESCENE_H_
#define TITLESCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"
#include "GameManager.h"
#include "SceneManager.h"
class TitleScene : public Scene
{
public:
// 設定モード
enum class SettingMode
{
BGM=0, // BGM設定モード
SE, // SE設定モード
NUM // 列挙数[enumの数を得る] https://qiita.com/k-satoda/items/2985e81216b3ee500956
};
SettingMode settingMode = SettingMode::BGM; // 現在の設定モード
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
// スタートボタンを押したら音声再生完了を待って画面移動
bool isStartPushed = false;
// テスト用のボス画像をタイトルでアニメで左右移動させる
int x = 0; //staticじゃないintはここで=0で初期化できる
int vx = 0;
// コンストラクタ
TitleScene() : Scene()
{
this->tag = "TitleScene";
}
// 初期化処理
void Initialize() override
{
Image::Load("TitleScene"); //画像の読込みとメモリ開放
Sound::Load("TitleScene"); //音声の読込みとメモリ開放
Sound::PlayMusic(Sound::title); //タイトルBGMを再生
isStartPushed = false; // ボタンのフラグをリセット
// テスト用のボス画像をタイトルでアニメで左右移動させる
x = 100; // Xの初期位置
vx = 10; // ボスの初期速度
}
// 終了処理(大抵Initializeと同じリセットだが終了時だけやりたいリセットの仕方もあるかも)
void Finalize() override
{
}
// ボタン入力に応じた処理
void HandleInput()
{
if (Input::GetButtonDown(PAD_INPUT_1))
{ // ボタンを押したらスタート音声を再生
Sound::Play(Sound::start);
isStartPushed = true; // 画面遷移トリガーON
}
if (Input::GetButtonDown(PAD_INPUT_UP))
{ //上を押すと設定モードを-1する
int index = ((int)settingMode - 1);
if (index < 0) //↓番号ループ 0→最大数-1
index = ((int)SettingMode::NUM) - 1;
settingMode = (SettingMode)index;
}
else if (Input::GetButtonDown(PAD_INPUT_DOWN))
{ //下を押すと設定モードを+1する
int index = ((int)settingMode + 1);
if (index >= ((int)SettingMode::NUM))
index = 0; //番号ループ
settingMode = (SettingMode)index;
}
if (Input::GetButton(PAD_INPUT_RIGHT))
{
if (settingMode == SettingMode::BGM)
{
Sound::volumeBGM++;//音量をプラス
if (Sound::volumeBGM > 100)
Sound::volumeBGM = 100;
}
else if (settingMode == SettingMode::SE)
{
Sound::volumeSE++;//音量をプラス
if (Sound::volumeSE > 100)
Sound::volumeSE = 100;
Sound::Play(Sound::bomb); //効果音を聞きながら調節
}
Sound::UpdateMusicVolume(); //再生中の音量の更新
}
else if (Input::GetButton(PAD_INPUT_LEFT))
{
if (settingMode == SettingMode::BGM)
{
Sound::volumeBGM--;//音量をマイナス
if (Sound::volumeBGM < 0)
Sound::volumeBGM = 0;
}
else if (settingMode == SettingMode::SE)
{
Sound::volumeSE--;//音量をマイナス
if (Sound::volumeSE <0)
Sound::volumeSE = 0;
Sound::Play(Sound::bomb); //効果音を聞きながら調節
}
Sound::UpdateMusicVolume(); //再生中の音量の更新
}
}
// 更新処理
void Update() override
{
// スタートの効果音の再生中は遷移を待つ
if (isStartPushed && CheckSoundMem(Sound::start) == 0)
{ // スタート音声再生終了をトリガーとして画面遷移
sm.LoadScene("PlayScene"); //シーン遷移
}
HandleInput(); // キー入力処理
// テスト用のボス画像をタイトルでアニメで左右移動させる
if (x < 0)
{
vx = 10; //速度の変更
}
else if (x > Screen::Width)
{
vx = -10; //画面端で移動方向反転
}
x += vx; // ボス画像のX位置の更新
}
// 描画処理
void Draw() override
{
DrawString(0, 0, "TitleSceneです。ボタン押下でPlaySceneへ。", GetColor(255, 255, 255));
if (sm.scoreMax >= 0)
{
DrawString(0, 200, "MAXスコア: " + sm.scoreMax, GetColor(255, 255, 255));
}
// 音量設定目盛りの文字列の生成
std::string volumeBGM = " ";
if (settingMode == SettingMode::BGM)
volumeBGM = "⇒"; //[選択中]⇒BGM音量設定モード
volumeBGM += "BGM音量設定:" + std::to_string((int)Sound::volumeBGM);
int volBGM_NUM = (int)(Sound::volumeBGM / 10);
if (volBGM_NUM != 10) volumeBGM += " ";
for (int i = 0; i < volBGM_NUM; i++) volumeBGM += "■";//音量目盛り表示
std::string volumeSE = " ";
if (settingMode == SettingMode::SE)
volumeSE = "⇒"; //[選択中]⇒BGM音量設定モード
volumeSE += " SE音量設定:" + std::to_string((int)Sound::volumeSE);
int volSE_NUM = (int)(Sound::volumeSE / 10);
if (volSE_NUM != 10) volumeSE += " ";
for (int i = 0; i < volSE_NUM; i++) volumeSE += "■";//音量目盛り表示
// 音量設定の表示
DrawString(0, Screen::Height - 60, volumeBGM.c_str(), GetColor(255, 255, 255));
DrawString(0, Screen::Height - 30, volumeSE.c_str(), GetColor(255, 255, 255));
// テスト用のボス画像をタイトルでアニメで左右移動させる
DrawRotaGraphF((float)x, 200, 0.9f, 0, Image::bossImage, TRUE);
}
};
#endif
Game.cppからSoundのロード処理を削除
します(シーンごとのロードに変更)。
#include "Game.h"
#include "MyRandom.h"
#include "Sound.h"
void Game::Init()
{// Init処理
MyRandom::Init(); // 乱数シードの初期化
Sound::Load(); //音声の読込み
// ゲーム開始して最初はタイトルシーンに遷移
sm.LoadScene("TitleScene");
}
void Game::Update()
{// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
// シーンの更新処理
sm.currentScene->Update();
}
void Game::Draw()
{// 描画処理
// シーンの描画処理
sm.currentScene->Draw();
}
PlayScene.hに画面ごとのサウンドのロード処理を追加しBGM再生をBGM番号経由に変更
します。
#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"
#include "GameManager.h"
#include "SceneManager.h"
#include "Player.h"
#include "Boss.h"
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"
class PlayScene : public Scene
{
public:
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる
// コンストラクタ
PlayScene() : Scene()
{
this->tag = "PlayScene";
}
// 初期化処理
void Initialize() override
{
Image::Load("PlayScene"); //画像の読込みとメモリ開放
Sound::Load("PlayScene"); //音声の読込みとメモリ開放
Sound::PlayMusic(Sound::gameplay); //プレイ画面BGMを再生
// ここでプレイヤの初期化などのプレイ開始時の処理
gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
// 敵の仮生成
gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
}
(中略)...................
// 更新処理
void Update() override
{
(中略)...................
// ゲームオーバー判定と画面チェンジ処理
bool isGameover = (gm.player == nullptr);
if (!isGameover && gm.player->isDead) isGameover = true;
if (isGameover)
{
Sound::PlayMusic(Sound::ending);
sm.LoadScene("GameOverScene"); //シーン遷移
}
}
// 描画処理
void Draw() override
{
(中略)...................
}
};
#endif
いかがでしょうか?これでタイトル画面で音量調整ができ、
画面が変わるタイミングでシーンで使わない音声のメモリを解放する処理を実現できました。
一度実行して試してみましょう。
Tiledエディタで敵マップ(stage.csv)を作成したら、
MapフォルダをImageのフォルダを作ったときのように新規作成して
その中に
stage1.csvを
Map/stage1.csv
にcsv形式でエクスポートした前提条件でそれをロードするプログラムを作成していきます。
csvファイルの準備ができたらマップの読込みプログラムを作成します。
Map.hを新規作成
してマップのCSVファイルの読込みとスクロールを作成しましょう。
#ifndef MAP_H_
#define MAP_H_
#include <assert.h> // 読込み失敗表示用
#include <memory> // 回し読みポインタをザコやボスの生成に使う
#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列に必要
#include <fstream> // ファイル読み出しifstreamに必要
#include <sstream> // 文字列ストリームに必要
#include "DxLib.h"
#include "Screen.h"
#include "GameManager.h"
#include "Zako0.h"
#include "Zako1.h"
#include "Zako2.h"
#include "Boss.h"
class Map
{
public:
int CellSize = 32; // マップの1マスのピクセル数
int Width = 0; // マップデータの横のマス数
int Height = 0; // マップデータの縦のマス数
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
std::vector<std::vector<int>> enemyData;// 敵配置データ
float position = 0; // マップ読出し位置のスクロール量
// コンストラクタ
// filePath : CSVファイルのパス
// startPosition : 開始位置(デバッグやリスタート時に、面の途中から開始できるように)
Map(std::string filePath, float startPosition=0)
{
position = startPosition;
Load(filePath); // csvファイルをロード
}
//デストラクタ
~Map()
{
enemyData.clear();
}
// csvファイルの読み込み
void Load(std::string filePath)
{
// 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
std::ifstream ifs_csv_file(filePath);
std::string line; //1行単位でcsvファイルから文字列を読み込む
Width = 0; //初期化
int MaxWidth = 0; // 1行の数字の最大個数
Height = 0; //初期化
while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
{
std::vector<int> valuelist; // 1行の数字リスト
std::istringstream linestream(line); // 各行の文字列ストリーム
std::string splitted; // カンマで分割された文字列
int widthCount = 0; //この行の幅をカウント
while (std::getline(linestream, splitted, { ',' }))
{
std::istringstream ss; //文字列ストリームの初期化
ss = std::istringstream(splitted); //文字列ストリーム
int num; // 数字単体
ss >> num; // 文字列ストリーム>>で数字へ変換
valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
widthCount++; //この行のカンマで区切られた数字の数をカウントアップ
}
// 1行の数字の数が記録を更新してMAXになるかチェック
if (widthCount > MaxWidth) MaxWidth = widthCount; //暫定Max幅を更新
// 数字1行分をvectorに追加
if (valuelist.size() != 0) enemyData.emplace_back(valuelist);
Height++; //マップの高さをカウントアップ
}
Width = MaxWidth; //マップ幅は一番数字の個数の多かった行に合わせる
//↓読込んだCSVの幅と高さをチェック
assert(Width > 0 &&"マップ読込み失敗ファイル名間違いでは?"!="");
assert(Height > 0 &&"マップ読込み失敗ファイル名間違いでは?"!="");
}
// 画面スクロール(位置の更新)
void Scroll(float delta)
{
// スクロールする前の、画面右端のセルの列番号(x)
int prevRightCell = (int)(position + Screen::Width) / CellSize;
position += delta; // スクロールする
// スクロールした後の、画面右端のセルの列番号(x)
int currentRightCell = (int)(position + Screen::Width) / CellSize;
// スクロールした後の画面右端のセルがマップの範囲外に出ちゃったら何もしないで終了
if (currentRightCell >= Width)
return;
// スクロールする前と後で、画面右端のセルが同じ場合は、何もしないで終了
if (prevRightCell == currentRightCell)
return;
// 画面右端のセルの左端のx座標
float x = currentRightCell * CellSize - position;
// 上端のセルから下端のセルまで舐めて、敵データが配置されていたら、敵を生成する
for (int cellY = 0; cellY < Height; cellY++)
{
float y = cellY * CellSize;
//【★注意!】2次元vectorではxとyがC#と逆![行Y][列X]⇒行列の順と覚えよう
int id = enemyData[cellY][currentRightCell];
// 番号に応じて敵を生成する
if (id == -1) continue; // -1は空白なので、何もしない
else if (id == 0) gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3(x + 32, y + 32)));
else if (id == 1) gm.enemies.emplace_back(std::make_shared<Zako1>(Vector3(x + 32, y + 32)));
else if (id == 2) gm.enemies.emplace_back(std::make_shared<Zako2>(Vector3(x + 32, y + 32)));
else if (id == 100) gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(x + 90, y + 32))); // ボスは横幅180pxだから90px右に生成
else assert("まだ追加してない敵IDがマップCSVにありました。"=="");
}
}
};
#endif
GameManager.hにMapの回し読みポインタの参照リンクを追加
します。
#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_
#include <memory>
#include <vector>
#include "Singleton.h"
class Map; //クラス宣言だけでインクルードしないので循環しない(前方宣言)
class Player;
class PlayerBullet;
class Enemy;
class EnemyBullet;
class Explosion;
class Item;
class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可
std::shared_ptr<Map> map{ nullptr }; // マップ
std::shared_ptr<Player> player{ nullptr }; // 自機
std::list<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
std::list<std::shared_ptr<EnemyBullet>> enemyBullets; // 敵弾のリスト
std::list<std::shared_ptr<Explosion>> explosions; // 爆発エフェクトのリスト
std::list<std::shared_ptr<Item>> items; // アイテムのリスト
// 複数リスト↑list ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる
(中略)..........
protected:
GameManager() {}; // 外部からのインスタンス作成は禁止
virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};
#endif
PlayScene.hにMapの初期化とメモリ参照クリアとスクロール処理を追加
します。(シーン分けしていない人はGame.cppで)
#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"
#include "Map.h"
#include "GameManager.h"
#include "SceneManager.h"
#include "Player.h"
#include "Boss.h"
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"
#include "Item.h"
class PlayScene : public Scene
{
public:
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる
float scrollSpeed = 1.5f; // スクロール速度
// コンストラクタ
PlayScene() : Scene()
{
this->tag = "PlayScene";
}
// 初期化処理
void Initialize() override
{
(中略)......
Sound::PlayMusic(Sound::gameplay); //プレイ画面BGMを再生
// マップの初期化
gm.map = std::make_shared<Map>("Map/stage1.csv");
// ここでプレイヤの初期化などのプレイ開始時の処理
gm.player = std::make_shared<Player>(Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
// 敵の仮生成
//gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
}
// 終了処理(大抵Initializeと同じリセットだが終了時だけやりたいリセットの仕方もあるかも)
void Finalize() override
{
// ここにプレイ終了時のメモリのリセット処理(ゲームクリアやゲームオーバー時など)
gm.map = nullptr; // マップのメモリからの解放
gm.player = nullptr; // プレイヤのメモリからの解放
gm.enemies.clear(); // 敵のメモリからの解放
gm.playerBullets.clear(); // プレイヤの弾のメモリからの解放
gm.enemyBullets.clear(); // 敵弾のメモリからの解放
gm.explosions.clear(); // 爆発エフェクトのメモリからの解放
gm.items.clear(); // アイテムのメモリからの解放
// ゲーム終了時のスコアがMAXなら記録して終了
if(score > sm.scoreMax) sm.scoreMax = score;
}
// 更新処理
void Update() override
{
// ここにプレイ中の更新処理を持ってくる
// マップデータの読出し位置のスクロール
gm.map->Scroll(scrollSpeed);
if (!gm.player->isDead) // 自機が死んでいなければ
gm.player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
(中略)....................
}
// 描画処理
void Draw() override
{
(中略)....................
}
};
#endif
さて、マップ読込みうまくいきましたか?
現状のコードは敵のみ読込みですが【地形やその他アイテムなどのデータが増えると面倒です】
いちいちMap.hにint stageWidth, int terrainWidth, int itemWidth...などと定義を増やしますか?
いちいちMap.hにLoadTerrain(),LoadItem()など長いロード処理を個別に書きますか?
そんなの僕は嫌だ!
という人はDataCsv.hを取り入れましょう。
データはデータ自身がLoad処理を持ち、幅Width高さHeightを持つ構造になればよいのです。
コードでは【[]演算子を独自定義】しています。
これはぜひ勉強してほしいテクニックです。
DataCsv型のデータは[]演算子を使わないとenemyData.Data[y][x]といったアクセスの仕方になってしまいます。
すると、DataCsvを導入するとすでに書いたコードすべてに.Data[y][x]の書き足しが必要になっちゃいますよね(めんどくさい)。
[]演算子を間に噛ますだけで【既存のコードの書き直しがゼロ】になるのです!
どうでしょう?[]演算子の威力を実感できそうですか?
同じ目的で【関数名かぶらせテクニック】も使っています。
clear()の定義も同様です。enemyData.Data.clear()と書き直さずenemyData.clear()のままで済むのです。
size()の定義も同様です。enemyData.Data.size()と書き直さずenemyData.size()のままで済むのです。
どうでしょう?【関数名かぶらせテクニック】を実感できそうですか?(C#でも使えるテクニック)
DataCsv.hを新規作成
します。
#ifndef DATACSV_H_
#define DATACSV_H_
#include <assert.h> // 読込み失敗表示用
#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列に必要
#include <fstream> // ファイル読み出しifstreamに必要
#include <sstream> // 文字列ストリームに必要
// CSVファイルを読込み幅や高さとデータ本体を保持するデータ型
struct DataCsv // ←structはC++ではほぼclassと同じ【違いはデフォルトがpublic】
{ // 読込んだデータファイルの情報
int Width = 0; // csvファイルの表の幅
int Height = 0;// csvファイルの表の高さ
bool isInitialized = false; //[2重ロード対策]1度ロードしたらtrueにしてclear()されるまでロード抑止
std::string FilePath { "" };
std::vector<std::vector<int>> Data;// csvデータ
// 初期化コンストラクタでファイル名を指定して初期化と同時にファイル読込
DataCsv(std::string filePath = "") :FilePath{ filePath }
{// csvファイルの読込み★【初期化と同時なのでファイルとデータ型が一心同体で使いやすい】
if (FilePath != "") Load(FilePath); // ファイル読込み
};
virtual ~DataCsv()
{// 仮想デストラクタ
Data.clear();// 2次元配列データのお掃除
};
// ★スムーズに[][]でアクセスできるように[]演算子を独自定義する
std::vector<int>& operator[](std::size_t index) { // ★ &参照にしないといちいちデータのコピーを返すので遅くなるよ
return Data[index]; // 書き込み
}
std::vector<int> operator[](std::size_t index) const { // ★constは添え字[]読み取りの処理を定義
return Data[index]; // 読み取り
}
std::size_t size()
{ // size()関数の名前をvectorと被らせることで使う側はvectorインvectorのままのコードで使える
return Data.size();
}
// データをクリアしてメモリを解放する
virtual void clear()
{ // データをクリアしてメモリを解放する
Data.clear();// 2次元配列データのお掃除
isInitialized = false; //ロード済みフラグをOFF
}
// csvファイルの読み込み
virtual void Load(std::string filePath)
{
if (filePath == "" || isInitialized) return; //ファイル名がないもしくはロード済
this->FilePath = filePath; // ファイル名を保管
Data.clear(); //データを一旦クリア
// 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
std::ifstream ifs_csv_file(filePath);
std::string line; //1行単位でcsvファイルから文字列を読み込む
int readWidth = 0; //読込みデータの幅
int maxWidth = 0; // 1行の数字の最大個数
int readHeight = 0; //初期化
//↓2重while文でCSVファイルを読み取る
while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
{
std::vector<int> valuelist; // 1行の数字リスト
std::istringstream linestream(line); // 各行の文字列ストリーム
std::string splitted; // カンマで分割された文字列
int widthCount = 0; //この行の幅をカウント
while (std::getline(linestream, splitted, { ',' }))
{
std::istringstream ss; //文字列ストリームの初期化
ss = std::istringstream(splitted); //文字列ストリーム
int num; // 数字単体
ss >> num; // 文字列ストリーム>>で数字へ変換
valuelist.emplace_back(num); // 数字を数字のリスト(valuelist)に追加
++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
}
// 1行の幅の数が記録を更新してMAXになるかチェック
if (widthCount > maxWidth) maxWidth = widthCount; //暫定Max幅を更新
// 1行分をvectorに追加
if (valuelist.size() != 0) Data.emplace_back(valuelist);
++readHeight; //読み込んだ行(縦)の数カウントアップ
}
readWidth = maxWidth; //読み込んだ列(横)の数は一番数字の個数の多かった行に合わせる
//↓読込んだCSVの幅と高さをチェック
assert(readWidth > 0 && "CSV読込み失敗ファイル名間違いでは?" != "");
assert(readHeight > 0 && "CSV読込み失敗ファイル名間違いでは?" != "");
this->Width = readWidth; // 読込み成功したデータの幅を記録
this->Height = readHeight; // 読込み成功したデータの高さを記録
// コンストラクタで初期化でロードの場合とLoad関数経由で読む経路があるから2重ロード対策
isInitialized = true; // 読込み初期化済みフラグをON
return;
}
};
#endif
Map.hで敵データを2次元vectorからDataCsvの使用に変更
しましょう。
#ifndef MAP_H_
#define MAP_H_
#include <assert.h> // 失敗表示用
#include <memory> // 回し読みポインタをザコやボスの生成に使う
#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列に必要
#include <fstream> // ファイル読み出しifstreamに必要
#include <sstream> // 文字列ストリームに必要
#include "DxLib.h"
#include "Screen.h"
#include "GameManager.h"
#include "Zako0.h"
#include "Zako1.h"
#include "Zako2.h"
#include "Boss.h"
#include "DataCsv.h"
class Map
{
public:
int CellSize = 32; // マップの1マスのピクセル数
int Width = 0; // マップデータの横のマス数
int Height = 0; // マップデータの縦のマス数
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
DataCsv enemyData;// 敵配置データ
float position; // 現在の位置(画面左端の座標)
// コンストラクタ
// filePath : CSVファイルのパス
// startPosition : 開始位置(デバッグやリスタート時に、面の途中から開始できるように)
Map(std::string filePath, float startPosition=0)
{
position = startPosition;
enemyData.Load(filePath); // csvファイルをロード
}
//デストラクタ
~Map()
{
enemyData.clear();
}
// csvファイルの読み込み
void Load(std::string filePath)
{
// 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
std::ifstream ifs_csv_file(filePath);
std::string line; //1行単位でcsvファイルから文字列を読み込む
Width = 0; //初期化
int MaxWidth = 0; // 1行の数字の最大個数
Height = 0; //初期化
while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
{
std::vector<int> valuelist; // 1行の数字リスト
std::istringstream linestream(line); // 各行の文字列ストリーム
std::string splitted; // カンマで分割された文字列
int widthCount = 0; //この行の幅をカウント
while (std::getline(linestream, splitted, { ',' }))
{
std::istringstream ss; //文字列ストリームの初期化
ss = std::istringstream(splitted); //文字列ストリーム
int num; // 数字単体
ss >> num; // 文字列ストリーム>>で数字へ変換
valuelist.emplace_back(num); // 数字をこの行のリスト(valuelist)に追加
widthCount++; //この行のカンマで区切られた数字の数をカウントアップ
}
// 1行の数字の数が記録を更新してMAXになるかチェック
if (widthCount > MaxWidth) MaxWidth = widthCount; //暫定Max幅を更新
// 数字1行分をvectorに追加
if (valuelist.size() != 0) enemyData.emplace_back(valuelist);
Height++; //マップの高さをカウントアップ
}
Width = MaxWidth; //マップ幅は一番数字の個数の多かった行に合わせる
//↓読込んだマップの幅と高さをチェック
assert(Width > 0 &&"マップ読込み失敗ファイル名間違いでは?"!="");
assert(Height > 0 &&"マップ読込み失敗ファイル名間違いでは?"!="");
}
// 画面スクロール(位置の更新)
void Scroll(float delta)
{
// スクロールする前の、画面右端のセルの列番号(x)
int prevRightCell = (int)(position + Screen::Width) / CellSize;
position += delta; // スクロールする
// スクロールした後の、画面右端のセルの列番号(x)
int currentRightCell = (int)(position + Screen::Width) / CellSize;
// スクロールした後の画面右端のセルがマップの範囲外に出ちゃったら何もしないで終了
if (currentRightCell >= enemyData.Width)
return;
// スクロールする前と後で、画面右端のセルが同じ場合は、何もしないで終了
if (prevRightCell == currentRightCell)
return;
// 画面右端のセルの左端のx座標
float x = currentRightCell * CellSize - position;
// 上端のセルから下端のセルまで舐めて、敵データが配置されていたら、敵を生成する
for (int cellY = 0; cellY < enemyData.Height; cellY++)
{
float y = cellY * CellSize;
//【★注意!】2次元vectorではxとyがC#と逆![行Y][列X]⇒行列の順と覚えよう
int id = enemyData[cellY][currentRightCell];
// 番号に応じて敵を生成する
if (id == -1) continue; // -1は空白なので、何もしない
else if (id == 0) gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3(x + 32, y + 32)));
else if (id == 1) gm.enemies.emplace_back(std::make_shared<Zako1>(Vector3(x + 32, y + 32)));
else if (id == 2) gm.enemies.emplace_back(std::make_shared<Zako2>(Vector3(x + 32, y + 32)));
else if (id == 100) gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(x + 90, y + 32))); // ボスは横幅180pxだから90px右に生成
else assert("まだ追加してない敵IDがマップCSVにありました。"=="");
}
}
};
#endif
さて現状のコードでは縦スクロールの一方向スクロール限定です。
全方向進めるようなRPGや3Dゲームなどには今のままでは使いづらいです。
シンプルな解決法としては開始時に全敵キャラを読み込むのも一つの手です。
ある程度敵の数が限られるゲームであれば有効な策です。
しかし広大なマップや大量の敵の出るゲームでやると
【大量の画面にいない敵も一気に生成してしまうので、大量の当たり判定計算などにより計算が追いつかなくなります】
ゆえにある程度の敵の規模のあるゲームのためには、キャラの移動にあわせて順次【敵マップの全方向読み出し】が必要です。
敵の読出しのアルゴリズムについては以下スライドを基本的な考え方としています。
CellXY.hを新規作成して
マス目の型を定義して、std::unordered_map辞書配列のキーにできるように独自operatorを定義しましょう。
#ifndef CELLXY_H_
#define CELLXY_H_
#include <unordered_map> // ハッシュ値計算 std::hashに必要
#include <cmath>
#include <algorithm> // std::maxに必要
// マス目のXとYを保持するデータ型(std::mapやstd::unordered_mapのキーにするためのoperator定義を持つ)
struct CellXY
{
int X = 0, Y = 0, dist = 0; bool distModeOn = false;
CellXY() = default;
~CellXY() {};
CellXY(int X, int Y, bool distModeOn = false) : X{ X }, Y{ Y }, distModeOn{ distModeOn }
{
if (distModeOn == true) dist = X * X + Y * Y; // 原点(0,0)からの距離比較機能オンの時
};
// ★連想配列mapのキーで使うため < 比較演算子(連想配列並べ替えのため)をカスタム定義 http://programming-tips.info/use_struct_as_key_of_map/cpp/index.html
virtual bool operator < (const CellXY& other) const {
//★原点からの距離dist順になるようにするとstd::mapにいれれば自動で原点からの距離順にならぶ(敵の近い順に使える)
if (this->distModeOn && other.distModeOn) return this->dist < other.dist;
if (this->Y < other.Y) return true; // Yを比較CSVのように左上から列→行の順にならぶ
else if (this->Y > other.Y) return false;
if (this->X < other.X) return true; // Yが同じ時はXを比較
else if (this->X > other.X) return false;
return false; // X と Yがすべて等しい場合
}
//★std::unordered_map のキーで使うため == 比較演算子カスタム定義 https://sygh.hatenadiary.jp/entry/2014/02/13/222356
virtual inline bool operator==(const CellXY& other) const {
const CellXY& self = *this;
return self.X == other.X && self.Y == other.Y;
}
virtual inline bool operator!=(const CellXY& other) const {
return !(this->operator==(other));
}
virtual size_t GetHashCode() const //X,Yの値からハッシュ値を計算 https://sygh.hatenadiary.jp/entry/2014/02/13/222356
{
return
std::hash<decltype(X)>()(this->X) ^
std::hash<decltype(Y)>()(this->Y);
}
struct Hash // ハッシュ値取得用の関数オブジェクト(std::unordered_map のキーで使うため) https://sygh.hatenadiary.jp/entry/2014/02/13/222356
{
size_t operator ()(const CellXY& cellXY) const
{
return cellXY.GetHashCode();
}
};
};
#endif
Map.hで敵データを全方位読み出しできるように変更
しましょう。
#ifndef MAP_H_
#define MAP_H_
#include <assert.h> // 読込み失敗表示用
#include <memory> // 回し読みポインタをザコやボスの生成に使う
#include <string> // 文字列に必要
#include <cmath> // 敵出現楕円射程のsin cos計算に
#include "DxLib.h"
#include "Screen.h"
#include "GameManager.h"
#include "Zako0.h"
#include "Zako1.h"
#include "Zako2.h"
#include "Boss.h"
#include "DataCsv.h"
#include "CellXY.h"
// 空間をグリッドでマス目として分割する(世界位置worldXをマス目位置番号cellXに変換したりできる)
class MapGridXY
{
public:
int cellWidth = 32; // マス目セルのサイズ
int cellHeight = 32; // マス目セルのサイズ
// CellXYZをキーにそのマス目にあるboolを得る辞書配列
std::unordered_map<CellXY, bool, CellXY::Hash> cellXY; // 読み込み済マス目の辞書
std::unordered_map<CellXY, bool, CellXY::Hash> rangeXY; // 視界レンジは読み込み済cellXYと別に保管
virtual int cellX(float worldX)
{ // マイナス方向にも対応
return (worldX >= 0) ? (int)worldX / cellWidth : (int)worldX / cellWidth - 1;
}
virtual int cellY(float worldY)
{ // マイナス方向にも対応
return (worldY >= 0) ? (int)worldY / cellHeight : (int)worldY / cellHeight - 1;
}
bool& operator[](CellXY searchXYZ) { // マス目に入っている先頭の参照を得る
return cellXY[searchXYZ];
}
virtual void clear()
{ // 空のテンポラリオブジェクトでリセット https://stackoverflow.com/questions/42114044/how-to-release-unordered-map-memory/42115076
std::unordered_map<CellXY, bool, CellXY::Hash>().swap(cellXY);
};
MapGridXY(int cellWidth = 32, int cellHeight = 32)
: cellWidth{ cellWidth }, cellHeight{ cellHeight }
{
clear();
}
virtual ~MapGridXY()
{
clear();
}
};
class Map
{
public:
int CellSize = 32; // マップの1マスのピクセル数
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
int spawnRangeX = 0; // 敵出現射程スポーンレンジ【注意!Xの単位はマス目】
int spawnRangeY = 0; // 敵出現射程スポーンレンジ【注意!Yの単位はマス目】
MapGridXY spawnGrid{ CellSize, CellSize }; // 敵データの読み込み済のマス目を管理するグリッド
DataCsv enemyData;// 敵配置データ
float positionX = 0; // X位置(マップ読出し位置のXスクロール量)
float positionY = 0; // Y位置(マップ読出し位置のYスクロール量)
// コンストラクタ
// filePath : CSVファイルのパス
// startPosition : 開始位置(デバッグやリスタート時に、面の途中から開始できるように)
Map(std::string filePath, float startPositionX = 0, float startPositionY = 0)
{
positionX = startPositionX;
positionX = startPositionY;
enemyData.Load(filePath); // csvファイルをロード
spawnRangeX = Screen::Width / CellSize; // 敵出現射程スポーンレンジ【注意!Xの単位はマス目】
spawnRangeY = Screen::Height / CellSize; // 敵出現射程スポーンレンジ【注意!Yの単位はマス目】
//【注意!】辞書の初期化をわすれると敵が出てこなくなる
InitSpawnGrid(spawnRangeX, spawnRangeY);
}
//デストラクタ
~Map()
{
enemyData.clear();
}
// 敵出現射程の辞書初期化【先に計算して辞書化】すると計算が必要なくなり【高速化する】
//【注意!XとYの単位はマス目】
void InitSpawnGrid(int rangeCellX, int rangeCellY)
{
spawnGrid.clear();//一旦辞書をクリアするのでゲーム中の再設定も可(だが処理時間はかかる)
// 敵出現射程の辞書初期化
// ★ X = A cosθ Y = B sinθ(←楕円の方程式)
// ★ 楕円の半径 r = √(A×A×cosθ×cosθ + B×B×sinθ×sinθ)
// ★ xの2乗 + yの2乗 < rならば楕円の内側
float A2 = (float)(rangeCellX * rangeCellX); // 2乗
float B2 = (float)(rangeCellY * rangeCellY); // 2乗
for (int x = -rangeCellX; x <= rangeCellX; x++)
{
for (int y = -rangeCellY; y <= rangeCellY; y++)
{ //★[逆三角関数] https://cpprefjp.github.io/reference/cmath/atan2.html
//float theta = (float)std::atan2(y, x); // ★[逆三角関数]
//float cos_t = std::cos(theta);
//float sin_t = std::sin(theta);
//float r2 = A2 * cos_t * cos_t + B2 * sin_t * sin_t;
//if (x * x + y * y <= r2) // ★ xの2乗 + yの2乗 < rならば楕円の内側
//{ //楕円の内側なら辞書spawnGrid.rangeXY[(x,y)] = true;として登録
// //【★楕円にしたいときはこちら】spawnGrid.rangeXY[CellXY(x,y)] = true; //【例】spawnGrid.rangeXY[(3,2)] = true;
//}
//【★四角形にしたいときはこちら】
spawnGrid.rangeXY[CellXY(x, y)] = true;
}
}
}
// 画面スクロール(位置の更新)
void Scroll(float deltaX, float deltaY=0)
{
int diffX = spawnGrid.cellX(positionX + deltaX) - spawnGrid.cellX(positionX);
int diffY = spawnGrid.cellY(positionY + deltaY) - spawnGrid.cellY(positionY);
float prevPosX = positionX; // デルタの変化を足す前の位置を保管する
float prevPosY = positionY;
positionX += deltaX; // 現在の位置を更新
positionY += deltaY; // 現在の位置を更新
if (diffX == 0 && diffY == 0)
return; // 同じマス目のままなら何も変化なし
// 読み込むマス目の絞り込み判定 (参考:説明スライド)
std::vector<CellXY> targetCells;
for (auto iter = spawnGrid.rangeXY.begin(); iter != spawnGrid.rangeXY.end(); ++iter)
{ // ★連想辞書配列のfor文 https://qiita.com/_EnumHack/items/f462042ec99a31881a81
CellXY cell = iter->first;
CellXY target = CellXY(cell.X + diffX, cell.Y + diffY);
if (spawnGrid.rangeXY.count(target) == 0)
targetCells.emplace_back(target); // 移動後にレンジの外側にはみ出たマス目を収集
}
// 敵新規出現マスを探す
for (auto& target : targetCells)
{
CellXY cell = CellXY(spawnGrid.cellX(prevPosX) + target.X, spawnGrid.cellY(prevPosY) + target.Y);
// [判定] count(cell) == 0なら未ロードマス、count(cell) == 1 && cellXY[cell] == falseならロード後falseに戻されたマス
if (spawnGrid.cellXY.count(cell) == 0 || spawnGrid.cellXY.count(cell) == 1 && spawnGrid.cellXY[cell] == false)
{
int objectID = -1;//出現させる敵ID
if (0 <= cell.Y && 0 <= cell.X && cell.Y < enemyData.size() && cell.X < enemyData[cell.Y].size())
{ //データがアクセスできればIDを取得
objectID = enemyData[cell.Y][cell.X];
// ★一度出現させたマスはIDを上書きして二重に敵が出現しないように
enemyData[cell.Y][cell.X] = -1;
}
// ★(cell.X,cell.Y)の位置のマップデータを読み出し!!
SpawnObject(cell.X, cell.Y, objectID);
}
}
}
// オブジェクトを生成・配置する
void SpawnObject(int mapX, int mapY, int id)
{
// 生成位置
float x = (float)(mapX * CellSize) - positionX;
float y = (float)(mapY * CellSize) - positionY;
//[コメント外すとテスト表示]if (objectID != -1)printfDx("newID %d (%d,%d,%f,%f)\n", id, mapX, mapY, x, y);
// 番号に応じて敵を生成する
if (id == -1) return; // -1は空白なので、何もしない
else if (id == 0) gm.enemies.emplace_back(std::make_shared<Zako0>(Vector3(x + 32, y + 32)));
else if (id == 1) gm.enemies.emplace_back(std::make_shared<Zako1>(Vector3(x + 32, y + 32)));
else if (id == 2) gm.enemies.emplace_back(std::make_shared<Zako2>(Vector3(x + 32, y + 32)));
else if (id == 100) gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(x + 90, y + 32))); // ボスは横幅180pxだから90px右に生成
else assert("まだ追加してない敵IDがマップCSVにありました。"=="");
}
};
#endif
うまくいけば、これで全方向のスクロールが可能です。
Game.cppやPlayScene.hでmap->Scroll(ScrollSpeedX, ScrollSpeedY);でY方向のスクロールもできるようになりました。
今回はシューティングなのでY方向はデフォルトの0のままで大丈夫です。
MyMath.hに逆三角関数でプレイヤへの角度を求める関数を追加
しましょう。
#ifndef _MYMATH_H
#define _MYMATH_H
#include <cmath>
#include "Vector3.h"
// 数学関連クラス
class MyMath
{
public:
// C++標準では、静的static定数は【整数型】または【列挙型】のみを.hのクラス内で初期化できます。
// これが、intやenumの初期化はエラーにならないのに他のstatic floatなどがエラーになる理由です。
// float小数を定義するためだけにcppファイルを作成しないと
//static const float Sqrt2 =【このイコールでエラー】 1.41421356237f;
// ルート2(変数名の定義だけ)
static const float Sqrt2;// = 1.41421356237f;
static const float PI; // 円周率 3.14159265359f;
static const float Deg2Rad; // 度からラジアンに変換する定数 PI / 180f;
/// <summary>
/// 円と円が重なっているかを調べる
/// </summary>
/// <param name="pos1">円1の中心</param>
/// <param name="radius1">円1の半径</param>
/// <param name="pos2">円2の中心</param>
/// <param name="radius2">円2の半径</param>
/// <returns>重なっていればtrue、重なっていなければfalseを返却する</returns>
static bool CircleCircleIntersection(
Vector3 pos1, float radius1,
Vector3 pos2, float radius2)
{
return ((pos1 - pos2).sqrMagnitude()
< (radius1 + radius2) * (radius1 + radius2));
}
/// <summary>
/// 点から点への角度(ラジアン)を求める。
/// </summary>
/// <param name="fromXY">始点</param>
/// <param name="toXY">終点</param>
/// <returns></returns>
static float PointToPointAngle(Vector3 fromXY, Vector3 toXY)
{
return (float)std::atan2(toXY.y - fromXY.y, toXY.x - fromXY.x);
}
}; // 【注意】セミコロン抜けで【宣言が必要ですエラー】
#endif // 【宣言が必要ですエラーは上のセミコロン抜け】
Zako0
を改造して自機狙い弾を撃たせてみます(別に他の敵クラスでも構いません)。
ついでにZako0の画面外に出たら消す処理も入れましょう【敵が消えないとCPUが当たり判定で激重くなりますよ】
これを機にZako1,Zako2,Zako3の画面外で消す判定も追加しておいた方がいいですよ。
敵がステージに100体出て(撃ち漏らしたら)プレイヤが10発弾を撃つとすると【100×10=1000回の当たり判定】になりますから。
#ifndef ZAKO_0_H_
#define ZAKO_0_H_
#include "Enemy.h"
#include "Image.h"
#include "MyMath.h"
#include "EnemyBullet.h" // make_sharedなど【実際の弾を撃ち生成処理を行うのでインクルード必要】
#include "Player.h" // Player.hでもZako0.hをインクルードすると循環するので注意
// ザコ0クラス
class Zako0 : public Enemy
{
public:
int coolTime = 0; // クールタイム(冷却時間。0になるまで次の弾が撃てない)
// コンストラクタ
Zako0(Vector3 pos) : Enemy( pos )
{
this->tag = "Zako0";
}
// 更新処理
void Update() override
{
position.x -= 1; // とりあえず左へ移動
coolTime--;
if (coolTime <= 0)
{
// 自機狙い弾
float angle = MyMath::PointToPointAngle(position, gm.player->position);
gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, angle, 8));
coolTime += 10;
}
// 【重要】画面外の削除処理【CPU処理重くなる原因】
if (position.x < 0)
{ // 左端の画面外に出たらisDeadしないと
// 画面外の敵の当たり判定で【だんだん処理が重くなるZako1,Zako2..も】
isDead = true;
}
}
// 描画処理
void Draw() override
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako0, TRUE);
}
};
#endif
Zako2
を改造してばらまき自機狙い弾を撃たせてみます(別に他の敵クラスでも構いません)。
#ifndef ZAKO_2_H_
#define ZAKO_2_H_
#include "Enemy.h"
#include <cmath> // Sin Cosの計算に使う
#include "Image.h"
#include "Screen.h"
#include "MyMath.h"
#include "MyRandom.h"
#include "EnemyBullet.h" // make_sharedなど【実際の弾を撃ち生成処理を行うのでインクルード必要】
#include "Player.h" // Player.hでもZako2.hをインクルードすると循環するので注意
// ザコ2クラス
class Zako2 : public Enemy
{
public:
int coolTime = 0; // クールタイム(冷却時間。0になるまで次の弾が撃てない)
// コンストラクタ
Zako2(Vector3 pos) : Enemy( pos )
{
this->tag = "Zako2";
}
// 更新処理
void Update() override
{
if (position.x > Screen::Width - 200)
{ // スクリーンの端から200の位置で止まる
position.x -= 1; // とりあえず左へ移動
}
coolTime--;
if (coolTime <= 0)
{
// 自機狙いばらまき弾
float angle = MyMath::PointToPointAngle(position, gm.player->position);
float randomAngle = MyRandom::Range(-15.0f, 15.0f); // ±15度の乱数の角度を取得
gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, angle + randomAngle * MyMath::Deg2Rad, 8));
coolTime += 10;
}
// 200の位置で止まるので左端にこないから削除処理はなくて済む
}
// 描画処理
void Draw() override
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako2, TRUE);
}
};
#endif
さて、ザコ2の場合は画面内の200で止まるので削除処理を入れませんでした。
そう、削除処理も敵により臨機応変にやるのですね。例えば下から出て上に向かう敵はyが0以下で削除です。
つまり、削除処理も敵の個性の一つの扱いというわけです。
自機に向かってくる敵も基本は追尾弾と同じですね。
Zako1.hにプレイヤへ向かってくる処理を追加
しましょう。
#ifndef ZAKO_1_H_
#define ZAKO_1_H_
#include "Enemy.h"
#include <cmath> // Sinの計算に使う
#include "MyMath.h"
#include "Image.h"
#include "MyRandom.h" // 乱数でアイテムのドロップを抽選する
#include "Item0.h"
#include "Player.h" // Player.hでもZako1.hをインクルードすると循環するので注意
// ザコ1クラス
class Zako1 : public Enemy
{
public:
// コンストラクタ
Zako1(Vector3 pos) : Enemy( pos )
{
this->tag = "Zako1";//オブジェクトの種類判別タグ
life = 5;
}
// 更新処理
void Update() override
{
// 自機をおいかけてくる処理
float angleToPlayer = MyMath::PointToPointAngle(position, gm.player->position);
float speed = 3;
position += Vector3((float)std::cos(angleToPlayer) * speed, (float)std::sin(angleToPlayer) * speed);
}
// 描画処理
void Draw() override
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako1, TRUE);
}
// アイテムを落とす処理
void DropItem() override
{
(中略).......
}
};
#endif
ゆらゆらしながら【自機に】向かってくる敵にしてみましょうか。
Zako1.hにSin波動でゆらゆら【自機に向かってくる】処理を追加
しましょう。
#ifndef ZAKO_1_H_
#define ZAKO_1_H_
#include "Enemy.h"
#include <cmath> // Sinの計算に使う
#include "MyMath.h"
#include "Image.h"
#include "MyRandom.h" // 乱数でアイテムのドロップを抽選する
#include "Item0.h"
#include "Player.h" // Player.hでもZako1.hをインクルードすると循環するので注意
// ザコ1クラス
class Zako1 : public Enemy
{
public:
float sinMove = 0; // Sinの動きをする際の0~360度
float sinRadius = 70; // Sinの動きをするときの半径
// コンストラクタ
Zako1(Vector3 pos) : Enemy( pos )
{
this->tag = "Zako1";//オブジェクトの種類判別タグ
life = 5;
}
// 更新処理
void Update() override
{
// 自機をおいかけてくる処理
float angleToPlayer = MyMath::PointToPointAngle(position, gm.player->position);
float speed = 3;
position += Vector3((float)std::cos(angleToPlayer) * speed, (float)std::sin(angleToPlayer) * speed);
//ゆらゆらSin波動の動きをy上下方向にプラス
float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
sinMove += 2.5f; // Sinの波運動の角度を進める
float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
// (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
position.y += currentSinY - prevSinY; // 上下方向のSinの波運動
}
// 描画処理
void Draw() override
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako1, TRUE);
}
// アイテムを落とす処理
void DropItem() override
{
(中略).......
}
};
#endif
ゆらゆら向かってくるほうが理論上は避けやすいはずなのに、見た目上は逆に怖く見えませんか?
あれ?何か2回、2種類の y += 〜;やってるけどアリなの?と思った人。
今から数字のマジックを示すので実感してください。
毎回 y += 〜;されるということは、つまり
y = (今の角度のY)-(前の角度のY) + (前の角度のY)-(前の前の角度のY) + (前の前の角度のY)-(前の前の前の角度のY)...
となりますよね。そしてここで数字のマジックを見てみましょう
y = (今の角度のY) -(前の角度のY) + (前の角度のY) -(前の前の角度のY) + (前の前の角度のY) - (前の前の前の角度のY)........
y = (今の角度のY) - (一番最初の角度のY=0)←sin 0°=0だから0
おお!見事に打ち消しあって(今の角度のY)だけ残りましたね(一番最初の角度0°のYは0になるので)
じゃあ、最初からy += せずにy = (今の角度のY);にすればいいんじゃ?って思う人もいると思います。
そうするとどうなるか?2つの違う動きの合成が難しくなります。
y += 1; // 【メイン】の動き(少しづつ下に行く)
y = (今の角度のY); // あ、上書き固定値で y += 1;の効果消えちゃう..
なので【動きを += ~;で表現しつつも毎回打ち消しあう】必要が出てくるのです。
y += 1; // 【メイン】の動き(少しづつ下に行く)
y += (今の角度のY) - (前の角度のY); // これなら y += 1;の効果が消えない!
これなら色々な動きを【合成した動きなのに】【お互いの動きを消す心配】がなくなります。
これが±プラマイ0で合成テクニック⇒微分•積分的テクニックです。
え、微分積分て難しい式なんじゃなかったっけ?って思うでしょうが
これもすこしトリッキーですが積分的な式です。(離散数学的に n と n-1 の漸化式を使っている感じ?)
積分は【ちりが積もれば山となる式】です。
で、この場合【「ちり」が微分です】(1フレームごとの値、それを0~60フレーム1秒間ぶん足し合わせるとの移動量が積分されて求まる)
今回の例では【yが山です】
そして【「ちり」が(今の角度のY) - (前の角度のY)です】
これを【(今の角度のsin) - (前の角度のsin)の積分がsinθになった】
逆に言うと【sinθを微分すると(今の角度のsin) - (前の角度のsin)になった】とも言えます。
式で書くとy += 半径×sinθ1(←今の角度のY) - 半径×sinθ2(←前の角度のY)となってます。
どうでしょう意外と微分積分を身近に感じられませんか?
数学の難しい式、実際使う場合は意外とシンプルなやり方になったりします。
この【今と前の差】を+= するテクニックはSin以外でも使えるので心に留めておきましょう。
たとえばxも同じようにすれば、円を描きながら追ってくる敵も作れます。
x += 半径×cosθ1(←今の角度のX) - 半径×cosθ2(←前の角度のX)
気になった人は試してみてくださいね。
class Zako1 : public Enemy
{
public:
float sinMove = 0; // Sinの動きをする際の0~360度
float sinRadius = 70; // Sinの動きをするときの半径
float cosMove = 0; // cosの動きをする際の0~360度
float cosRadius = 70; // cosの動きをするときの半径
//ゆらゆらCos波動の動きをx左右方向にプラス
float prevCosX = std::cos(cosMove * MyMath::Deg2Rad) * cosRadius;
cosMove += 2.5f; // Cosの波運動の角度を進める
float currentCosX = std::cos(cosMove * MyMath::Deg2Rad) * cosRadius;
// (前の角度でのX) - (今の角度でのX)の差を足すことで【ベースのxの位置 ± cosの動き】になる
position.x += currentCosX - prevCosX; // 上下方向のSinの波運動
[C++]シューティングゲーム 5【応用1】
複数コントローラーでプレイするためにはまずはInputクラスの改造が必要です。
Input.hのボタン状態を配列
にしてゆきましょう。
#ifndef INPUT_H_
#define INPUT_H_
#include "DxLib.h"
// コントローラのパッド番号の列挙型enum
enum class Pad
{
None=-1, // パッド割当て無(やられたり待機中のプレイヤに)
Key=0, // キーボード
One, // コントローラ1
Two, // コントローラ2
Three, // コントローラ3
Four, // コントローラ4
NUM, // コントローラの数NUM=5(ちょうど配列の数にもなる)
};
// 入力クラス
class Input
{
public:
static int prevStates[(int)Pad::NUM]; // 1フレーム前の状態
static int currentStates[(int)Pad::NUM]; // 現在の状態
// 初期化。タイトルなどキー入力状態リセット時に
static void Init()
{
// キー状態をゼロリセット
prevStates[(int)Pad::Key] = currentStates[(int)Pad::Key] = 0;
prevStates[(int)Pad::One] = currentStates[(int)Pad::One] = 0;
prevStates[(int)Pad::Two] = currentStates[(int)Pad::Two] = 0;
prevStates[(int)Pad::Three] = currentStates[(int)Pad::Three] = 0;
prevStates[(int)Pad::Four] = currentStates[(int)Pad::Four] = 0;
}
// 最新の入力状況に更新する処理。
// 毎フレームの最初に(ゲームの処理より先に)呼んでください。
static void Update()
{
prevStates[(int)Pad::Key] = currentStates[(int)Pad::Key];
currentStates[(int)Pad::Key] = GetJoypadInputState(DX_INPUT_KEY);
prevStates[(int)Pad::One] = currentStates[(int)Pad::One];
currentStates[(int)Pad::One] = GetJoypadInputState(DX_INPUT_PAD1);
prevStates[(int)Pad::Two] = currentStates[(int)Pad::Key];
currentStates[(int)Pad::Two] = GetJoypadInputState(DX_INPUT_PAD2);
prevStates[(int)Pad::Three] = currentStates[(int)Pad::Key];
currentStates[(int)Pad::Three] = GetJoypadInputState(DX_INPUT_PAD3);
prevStates[(int)Pad::Four] = currentStates[(int)Pad::Key];
currentStates[(int)Pad::Four] = GetJoypadInputState(DX_INPUT_PAD4);
// ↑ちょ!...ちょい待てーい!!待たれ給えよ!!
// 【こんなのいちいち書いてられるかー!!!!!】
}
(以下略)..................
はい、ダメでござる。こんなのいちいち拙者書きたくないでござるよ!!
こういう時こそ、for文や辞書配列の出番でござるよ。絶対そのほうが良いでござるよ。
Input.hの複数プレイのボタン状態を辞書配列とfor文を駆使して書くでござるよ。
#ifndef INPUT_H_
#define INPUT_H_
#include <unordered_map> // 辞書配列【map高速版】https://qiita.com/sileader/items/a40f9acf90fbda16af51
#include "DxLib.h"
// コントローラのパッド番号の列挙型enum
enum class Pad
{
None=-1, // パッド割当て無(やられたり待機中のプレイヤに)
Key=0, // キーボード
One, // コントローラ1
Two, // コントローラ2
Three, // コントローラ3
Four, // コントローラ4
NUM, // コントローラの数NUM=5(ちょうど配列の数にもなる)
};
// 入力クラス
class Input
{
public:
static int prevStates[(int)Pad::NUM]; // 1フレーム前の状態
static int currentStates[(int)Pad::NUM]; // 現在の状態
static std::unordered_map<int, int> padDic; // パッド番号からDXの定義への変換辞書
// 辞書配列の初期化
static void InitPadDictionary()
{
// 辞書配列で対応関係の辞書を作成しておく
padDic[(int)Pad::Key] = DX_INPUT_KEY;
padDic[(int)Pad::One] = DX_INPUT_PAD1;
padDic[(int)Pad::Two] = DX_INPUT_PAD2;
padDic[(int)Pad::Three] = DX_INPUT_PAD3;
padDic[(int)Pad::Four] = DX_INPUT_PAD4;
}
// 初期化。タイトルなどキー入力状態リセット時に
static void Init()
{
InitPadDictionary(); // パッドの辞書配列の初期化
// キー状態をゼロリセット
for (int i = 0; i < (int)Pad::NUM; i++)
prevStates[i] = currentStates[i] = 0;
}
// 最新の入力状況に更新する処理。
// 毎フレームの最初に(ゲームの処理より先に)呼んでください。
static void Update()
{
for (int i = 0; i < (int)Pad::NUM; i++)
{ // 現在の状態を一つ前の状態として保存してGetJoypad..
prevStates[i] = currentStates[i];
currentStates[i] = GetJoypadInputState(padDic[i]);
}
}
// ボタンが押されているか?
static bool GetButton(Pad pad, int buttonId)
{
if(pad == Pad::None) return false; // Noneなら判別不要
// 今ボタンが押されているかどうかを返却
return (currentStates[(int)pad] & buttonId) != 0;
}
// ボタンが押された瞬間か?
static bool GetButtonDown(Pad pad, int buttonId)
{
if(pad == Pad::None) return false; // Noneなら判別不要
// 今は押されていて、かつ1フレーム前は押されていない場合はtrueを返却
return ((currentStates[(int)pad] & buttonId) & ~(prevStates[(int)pad] & buttonId)) != 0;
}
// ボタンが離された瞬間か?
static bool GetButtonUp(Pad pad, int buttonId)
{
if(pad == Pad::None) return false; // Noneなら判別不要
// 1フレーム前は押されていて、かつ今は押されている場合はtrueを返却
return ((prevStates[(int)pad] & buttonId) & ~(currentStates[(int)pad] & buttonId)) != 0;
}
}; //←【注意】クラス定義の終わりにはコロンが必要だよ!
#endif //ここでエラー出た人は↑【注意】の;コロン忘れ
Input.cppの変数定義を変更します。
#include "Input.h"
int Input::prevStates[]; // 1フレーム前の状態
int Input::currentStates[]; // 現在の状態
std::unordered_map<int, int> Input::padDic; // パッド番号からDXの定義への辞書
これでボタン判定にはPad番号の指定が必須になりました。
したがって既存のボタン入力コードにはエラーが出るはずです。
むしろ出たほうが良いと思い GetButtonDown(int buttonId, Pad pad = Pad::key)みたいな
デフォルト = Pad::key設定をしませんでした。(デフォルト=があれば省略できたが)
なぜなら明確にエラーが出ないと複数プレイ対応直し忘れがでちゃいますからね。
Player.hにそのプレイヤを操るコントローラpadの定義
を追加しましょう。
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "MyMath.h"
#include "GameObject.h"
#include "GameManager.h"
class Player : public GameObject
{ // 継承は↑: public ~にする。publicをつけ忘れるとGameObjectの変数がすべてprivateとして継承されちゃう
public:
Pad pad = Pad::None; // デフォルトは未割当て
const float MoveSpeed = 6; // 移動速度
int MutekiJikan = 120; // 無敵時間
float collisionRadius = 32; // 当たり判定半径
int lifeMax = 20; // 最大ライフ
int life = 3; // ライフ
int mutekiTimer = 0; // 残り無敵時間。0以下なら無敵じゃないってこと
//【★注意!】ここに【x,yの定義が残ってるとGameObjectと被る】ので
// 名前の同じ変数が2つ【パラレルワールドに存在してしまう!】
// Playerからx=13,y=5に変えたつもりでも【ベースのGameObjectはx=0,y=0のままの不思議バグで混乱!】
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
// コンストラクタ
// pad : 対応するコントローラ
// pos : 初期位置
Player(Pad pad, Vector3 pos) : GameObject(pos) //【忘れると】GameObjectの初期化処理が飛ばされて大混乱!
{
this->pad = pad;
};
// 更新処理
void Update();
// 描画処理
void Draw();
(中略)....
// 敵にぶつかった時の処理
void OnCollisionEnemy(std::shared_ptr<GameObject> other);
// 敵弾とぶつかった時の処理
void OnCollisionEnemyBullet(std::shared_ptr<GameObject> other);
// ダメージを受ける処理
void TakeDamage();
// ライフが回復する処理
void RecoverLife(int amount);
// ライフをバーで表示する処理
void DrawLifeBar();
};
#endif
Player.cppのInput判定にコントローラpadの指定
を追加しましょう。
なおデフォルトのPad::Noneの状態のままの時は反応しなくなります!
#include "Player.h"
#include "MyMath.h"
#include "PlayerBullet.h" // ★弾を生成して【使う処理があるのでインクルード必要】
#include "Explosion.h"
// 更新処理
void Player::Update()
{
v = Vector3(0, 0, 0); // xyz方向移動速度
if (Input::GetButton(pad, PAD_INPUT_LEFT))
{
v.x = -MoveSpeed; // 左
}
else if (Input::GetButton(pad, PAD_INPUT_RIGHT))
{
v.x = MoveSpeed; // 右
}
if (Input::GetButton(pad, PAD_INPUT_UP))
{
v.y = -MoveSpeed; // 上
}
else if (Input::GetButton(pad, PAD_INPUT_DOWN))
{
v.y = MoveSpeed; // 下
}
// 斜め移動も同じ速度になるように調整
if (v.magnitude() > 0)
{
v /= MyMath::Sqrt2;
}
// 実際に位置を動かす
position += v;
// ボタン押下で自機弾を発射
if (Input::GetButtonDown(pad, PAD_INPUT_1))
{
// C++ではAddがemplace_backに↓ newがmake_shared↓に
gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, 0));//右
gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, -15 * MyMath::Deg2Rad));//右上
gm.playerBullets.emplace_back(std::make_shared<PlayerBullet>(position, +15 * MyMath::Deg2Rad));//右下
}
mutekiTimer--;// 無敵タイマーは無条件で毎フレームカウントダウン
}
(以下略)................
TitleScene.hのInput判定にコントローラpadの指定
を書換えましょう。
#ifndef TITLESCENE_H_
#define TITLESCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"
#include "GameManager.h"
#include "SceneManager.h"
class TitleScene : public Scene
{
public:
(中略).....................
// ボタン入力に応じた処理
void HandleInput()
{
// 全部のパッドぶんボタンが押されてるか検査
for (int i = 0; i < (int)Pad::NUM; i++)
{
if (Input::GetButtonDown((Pad)i, PAD_INPUT_1))
{ // ボタンを押したらスタート音声を再生
Sound::Play(Sound::start);
isStartPushed = true; // 画面遷移トリガーON
}
}
// いや..ちょ、ちょい待たれよー!いちいちfor文書くの??
(以下略)................
はい、来ました!使い勝手の悪さを発見しましたね!
いちいち全部のコントローラ確かめるときはfor文で囲うのがめんどくさいですよね。
よいアイデアがあります。Inputの方を改造しましょう。
Input.hのコントローラpadの種類にAll(すべての内どれか押されたら)を指定できるように
改造しましょう。
#ifndef INPUT_H_
#define INPUT_H_
#include <unordered_map> // 辞書配列【map高速版】https://qiita.com/sileader/items/a40f9acf90fbda16af51
#include "DxLib.h"
// コントローラのパッド番号定義
enum class Pad
{
All = -2, // すべてのうちどれか1つでも押されたとき
None = -1, // パッド割当て無(やられたり待機中のプレイヤに)
Key=0, // キーボード
One, // コントローラ1
Two, // コントローラ2
Three, // コントローラ3
Four, // コントローラ4
NUM, // コントローラの数=5
};
// 入力クラス
class Input
{
public:
static int prevStates[(int)Pad::NUM]; // 1フレーム前の状態
static int currentStates[(int)Pad::NUM]; // 現在の状態
static std::unordered_map<int, int> padDic; // パッド番号からDXの定義への辞書
// 辞書配列の初期化
static void InitPadDictionary()
{
// 辞書配列で対応関係を結び付けておく
padDic[(int)Pad::Key] = DX_INPUT_KEY;
padDic[(int)Pad::One] = DX_INPUT_PAD1;
padDic[(int)Pad::Two] = DX_INPUT_PAD2;
padDic[(int)Pad::Three] = DX_INPUT_PAD3;
padDic[(int)Pad::Four] = DX_INPUT_PAD4;
}
// 初期化。タイトルなどキー入力状態リセット時に
static void Init()
{
InitPadDictionary(); // パッドの辞書配列の初期化
// キー状態をゼロリセット
for (int i = 0; i < (int)Pad::NUM; i++)
prevStates[i] = currentStates[i] = 0
}
// 最新の入力状況に更新する処理。
// 毎フレームの最初に(ゲームの処理より先に)呼んでください。
static void Update()
{
for (int i = 0; i < (int)Pad::NUM; i++)
{ // 現在の状態を一つ前の状態として保存してGetJoypad..
prevStates[i] = currentStates[i];
currentStates[i] = GetJoypadInputState(padDic[i]);
}
}
// ボタンが押されているか?
static bool GetButton(Pad pad, int buttonId)
{
if (pad == Pad::None) return false; // Noneなら判別不要
else if (pad == Pad::All) // All指定の時は全てのパッド
{ // GetButtonの中でGetButtonを呼ぶ【再起呼出し】テクニック
for (int i = 0; i < (int)Pad::NUM; i++)
if (GetButton((Pad)i, buttonId))
return true; //押されているPadを発見!
return false; // 一つも押されていなかった
}
// 今ボタンが押されているかどうかを返却
return (currentStates[(int)pad] & buttonId) != 0;
}
// ボタンが押された瞬間か?
static bool GetButtonDown(Pad pad, int buttonId)
{
if (pad == Pad::None) return false; // Noneなら判別不要
else if (pad == Pad::All) // All指定の時は全てのパッド
{ // 再起呼出しテクニック
for (int i = 0; i < (int)Pad::NUM; i++)
if (GetButtonDown((Pad)i, buttonId))
return true; //押されているPadを発見!
return false; // 一つも押されていなかった
}
// 今は押されていて、かつ1フレーム前は押されていない場合はtrueを返却
return ((currentStates[(int)pad] & buttonId) & ~(prevStates[(int)pad] & buttonId)) != 0;
}
// ボタンが離された瞬間か?
static bool GetButtonUp(Pad pad, int buttonId)
{
if (pad == Pad::None) return false; // Noneなら判別不要
else if (pad == Pad::All) // All指定の時は全てのパッド
{ // 再起呼出しテクニック
for (int i = 0; i < (int)Pad::NUM; i++)
if (GetButtonUp((Pad)i, buttonId))
return true; //押されているPadを発見!
return false; // 一つも押されていなかった
}
// 1フレーム前は押されていて、かつ今は押されている場合はtrueを返却
return ((prevStates[(int)pad] & buttonId) & ~(currentStates[(int)pad] & buttonId)) != 0;
}
}; //←【注意】クラス定義の終わりにはコロンが必要だよ!
#endif //ここでエラー出た人は↑【注意】の;コロン忘れ
極意【再起呼出し】(自分自身を呼び出すこと)が炸裂しましたね!(再起はきれいにハマると気持ちいいです)
自分自身を呼び出すなんてそんなのアリ!?ですよね。
でもこのテク便利です。【if条件の絞りミスると呼出し無限ループしますがね】
あと、もう一つのテク【enumの種類をAll=-2でマイナス方向に拡張してます】
生真面目な人は0から始めたらプラス方向しかできないと思いがちですよね。
頭が固いです。柔軟にマイナスのenum番号も有効活用すればいいんです。
0から上はコントローラ番号に割り当てることでfor(int i=0;i< (int)Pad::NUM; i++)で全チェックのiに使えます。
0から上の番号にALLとか例外の番号が割り込んでくるとforで回すのに邪魔ですからね。
だからマイナスのenum番号は意外に便利なんです。普通はfor文でマイナスのiを使わないからこそ【マイナスの配列番号は絶対空いてるんです】
さあ、これで使う側はPad::Allを指定するだけでコントローラのどれか1つでもボタンが押されたら検知できます。
TitleScene.hのInput判定にコントローラpadの指定
を書換えましょう。
#ifndef TITLESCENE_H_
#define TITLESCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"
#include "GameManager.h"
#include "SceneManager.h"
class TitleScene : public Scene
{
public:
(中略)....................
// ボタン入力に応じた処理
void HandleInput()
{
if (Input::GetButtonDown(Pad::All, PAD_INPUT_1))
{ // ボタンを押したらスタート音声を再生
Sound::Play(Sound::start);
isStartPushed = true; // 画面遷移トリガーON
}
if (Input::GetButtonDown(Pad::All, PAD_INPUT_UP))
{ //上を押すと設定モードを-1する
int index = ((int)settingMode - 1);
if (index < 0) //↓番号ループ 0→最大数-1
index = ((int)SettingMode::NUM) - 1;
settingMode = (SettingMode)index;
}
else if (Input::GetButtonDown(Pad::All, PAD_INPUT_DOWN))
{ //下を押すと設定モードを+1する
int index = ((int)settingMode + 1);
if (index >= ((int)SettingMode::NUM))
index = 0; //番号ループ
settingMode = (SettingMode)index;
}
if (Input::GetButton(Pad::All, PAD_INPUT_RIGHT))
{
if (settingMode == SettingMode::BGM)
{
Sound::volumeBGM++;//音量をプラス
if (Sound::volumeBGM > 100)
Sound::volumeBGM = 100;
}
else if (settingMode == SettingMode::SE)
{
Sound::volumeSE++;//音量をプラス
if (Sound::volumeSE > 100)
Sound::volumeSE = 100;
Sound::Play(Sound::bomb); //効果音を聞きながら調節
}
Sound::UpdateMusicVolume(); //再生中の音量の更新
}
else if (Input::GetButton(Pad::All, PAD_INPUT_LEFT))
{
if (settingMode == SettingMode::BGM)
{
Sound::volumeBGM--;//音量をマイナス
if (Sound::volumeBGM < 0)
Sound::volumeBGM = 0;
}
else if (settingMode == SettingMode::SE)
{
Sound::volumeSE--;//音量をマイナス
if (Sound::volumeSE <0)
Sound::volumeSE = 0;
Sound::Play(Sound::bomb); //効果音を聞きながら調節
}
Sound::UpdateMusicVolume(); //再生中の音量の更新
}
}
(以下略).....................
PlayScene.hのプレイヤの生成にパッド番号指定
を書き加えましょう。
#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_
(中略)...........
class PlayScene : public Scene
{
public:
(中略)............
// 初期化処理
void Initialize() override
{
(中略)....................
// ここでプレイヤの初期化などのプレイ開始時の処理
gm.player = std::make_shared<Player>(Pad::Key, Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
// 敵の仮生成
//gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
}
(以下略).....................
GameOverScene.hのInputボタン判定にPad::Allの指定
を書き加えましょう。
#ifndef GAMEOVERSCENE_H_
#define GAMEOVERSCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "GameManager.h"
#include "SceneManager.h"
class GameOverScene : public Scene
{
public:
(中略).....................
// 更新処理
void Update() override
{
if (Input::GetButtonDown(Pad::All, PAD_INPUT_1))
{
sm.LoadScene("TitleScene"); //シーン遷移
}
}
// 描画処理
void Draw() override
{
DrawString(0, 0, "GameOver....ボタン押下でPlaySceneへ", GetColor(255, 255, 255));
}
};
#endif
さて、これで大丈夫か!...と思いきや反応しなくなりました。
原因はInput::Init(); // 入力コントローラの初期化 忘れでした!
逆にいままでなぜ大丈夫だったかというとint型や配列currentStatesの初期値は0だったからです(たまたま実質0初期化になってた)
Game.cppに忘れていたInputの初期化
を書き加えましょう。
#include "Game.h"
#include "MyRandom.h"
#include "Input.h"
void Game::Init()
{// Init処理
Input::Init(); // コントローラ入力の初期化
(中略)............
}
(以下略).....................
これでコントローラをつなげばどのコントローラからでもタイトル画面を操作できます。
なぜ各シーンではなくGame.cppでInput入力とMyRandom乱数はInit初期化するのか?
それはコントローラ入力と乱数はシーンによらない共通機能だからです。
さて、次はプレイヤの参加処理を書いてゆきましょう。
プレイヤの参加は色んな画面で使うのでInputクラスで共通処理とした方が良さそうです。
すでにInputを使い慣れた人も辞書配列の使い道に注目してください。
辞書配列は【対応関係を結ぶのにめちゃ便利】です。
辞書配列に慣れると【vector配列が辞書配列の劣化版のようにすら感じるほど】です。
ですがvector配列はデータがぎっしりとかっつめられているというメモリ節約の利点があります、機動力とメモリ節約の両観点から選択しましょう。
Input.hに参加中パッドの辞書と参加チェック処理
を書き加えましょう。
#ifndef INPUT_H_
#define INPUT_H_
#include <unordered_map> // 辞書配列【map高速版】https://qiita.com/sileader/items/a40f9acf90fbda16af51
#include "DxLib.h"
// コントローラのパッド番号定義
enum class Pad
{
All = -2, // すべてのうちどれか1つでも押されたとき
None = -1, // パッド割当て無(やられたり待機中のプレイヤに)
Key=0, // キーボード
One, // コントローラ1
Two, // コントローラ2
Three, // コントローラ3
Four, // コントローラ4
NUM, // コントローラの数=5
};
// 入力クラス
class Input
{
public:
static int prevStates[(int)Pad::NUM]; // 1フレーム前の状態
static int currentStates[(int)Pad::NUM]; // 現在の状態
static std::unordered_map<Pad, bool> isJoin; // 参加中のパッド番号辞書
// 参加中のパッドかどうか?
static bool IsJoin(Pad pad) { return isJoin.count(pad) == 1 && Input::isJoin[pad]; };
static std::unordered_map<int, int> padDic; // パッド番号からDXの定義への辞書
// 辞書配列の初期化
static void InitPadDictionary()
{
// 辞書配列で対応関係を結び付けておく
padDic[(int)Pad::Key] = DX_INPUT_KEY;
padDic[(int)Pad::One] = DX_INPUT_PAD1;
padDic[(int)Pad::Two] = DX_INPUT_PAD2;
padDic[(int)Pad::Three] = DX_INPUT_PAD3;
padDic[(int)Pad::Four] = DX_INPUT_PAD4;
}
// 初期化。タイトルなどキー入力状態リセット時に
static void Init()
{
InitPadDictionary(); // 辞書配列の初期化
// キー状態をゼロリセット
for (int i = 0; i < (int)Pad::NUM; i++)
prevStates[i] = currentStates[i] = 0;
}
// 最新の入力状況に更新する処理。
// 毎フレームの最初に(ゲームの処理より先に)呼んでください。
static void Update()
{
for (int i = 0; i < (int)Pad::NUM; i++)
{ // 現在の状態を一つ前の状態として保存してGetJoypad..
prevStates[i] = currentStates[i];
currentStates[i] = GetJoypadInputState(padDic[i]);
}
}
// プレイヤの参加処理(参加辞書登録して参加中の数も返す)
static int JoinCheck()
{
int joinCount = 0;
for (int i = 0; i < (int)Pad::NUM; i++)
{ // 参加ボタンが押されたら
if (Input::GetButtonDown((Pad)i, PAD_INPUT_1))
isJoin[(Pad)i] = true; // 参加パッド辞書に登録
if (isJoin.count((Pad)i) == 1 && isJoin[(Pad)i] == true)
joinCount++; // 参加中の人数をカウント
}
return joinCount; // 参加中のコントローラ数を返す
}
(以下略)...............
Input.cppに変数定義を追加します。
#include "Input.h"
int Input::prevStates[]; // 1フレーム前の状態
int Input::currentStates[]; // 現在の状態
std::unordered_map<Pad, bool> Input::isJoin; // 参加中のパッド番号辞書
std::unordered_map<int, int> Input::padDic; // パッド番号からDXの定義への辞書
辞書配列、使う際にはvector配列のようにisJoin[(Pad)i] = true;と書けば使えるんですね。
if文の時はあえてcountを間にはさんでます。
直接isJoin[(Pad)i] == trueの判別するとisJoin[(Pad)i]の登録がなかった時に辞書にfalseで登録が増えちゃうんです。
辞書に未登録のものの判定を行うと気を利かせてデフォルトのfalseとか0を勝手に登録してくれちゃうのです。
勝手に登録してくれてありがとうとはなりません。辞書の数が増えると検索数も増えるので無駄なんです。
.count(~)==1のときだけ判定を行えば辞書に無駄なデフォルト登録が発生せずに済みます。
【辞書配列の判定は.count(~)==1の時だけ行わないと勝手に無駄な項目が増えバグ原因】と覚えておきましょう。
次に現状では、PAD_INPUT_1(Xボタン)にしか反応しないので他のボタンでも参加できるようにします。
Input.hのボタン判定処理を増やしましょう。
(中略)..............
// プレイヤの参加処理(参加辞書登録して参加中の数も返す)
static int JoinCheck()
{
int joinCount = 0;
for (int i = 0; i < (int)Pad::NUM; i++)
{ // 参加ボタンが押されたら
if (Input::GetButtonDown((Pad)i, PAD_INPUT_1))
isJoin[(Pad)i] = true; // 参加パッド辞書に登録
// 参加ボタンが押されたら
if (Input::GetButtonDown((Pad)i, PAD_INPUT_2))
isJoin[(Pad)i] = true; // 参加パッド辞書に登録
// 参加ボタンが押されたら
if (Input::GetButtonDown((Pad)i, PAD_INPUT_3))
isJoin[(Pad)i] = true; // 参加パッド辞書に登録
// 参加ボタンが押されたら
if (Input::GetButtonDown((Pad)i, PAD_INPUT_4))
isJoin[(Pad)i] = true; // 参加パッド辞書に登録
// ちょ待ち!、あ、これボタンの数だけめっちゃif文増えるやつだ..やだな
if (isJoin.count((Pad)i) == 1 && isJoin[(Pad)i] == true)
joinCount++; // 参加中の人数をカウント
}
return joinCount; // 参加中のコントローラ数を返す
}
(以下略)...............
あ、このままだと【if else文がボタンの数だけめっちゃ増えちゃいます】ね。
もっと良い書きかたが実はあります。
こんな時のために
PAD_INPUT_1,PAD_INPUT_2,...の定義は2進数に直すと以下のようになってます。
パッドボタン 2進数(32ビット32桁) ← 16進数
PAD_INPUT_1 00000000000000000000000000010000 ← 0x00000010
PAD_INPUT_2 00000000000000000000000000100000 ← 0x00000020
PAD_INPUT_3 00000000000000000000000001000000 ← 0x00000040
PAD_INPUT_4 00000000000000000000000010000000 ← 0x00000080
PAD_INPUT_5 00000000000000000000000100000000 ← 0x00000100
PAD_INPUT_6 00000000000000000000001000000000 ← 0x00000200
PAD_INPUT_7 00000000000000000000010000000000 ← 0x00000400
PAD_INPUT_8 00000000000000000000100000000000 ← 0x00000800
PAD_INPUT_9 00000000000000000001000000000000 ← 0x00001000
PAD_INPUT_10 00000000000000000010000000000000 ← 0x00002000
............................................................
PAD_INPUT_28 10000000000000000000000000000000 ← 0x80000000
おお!見事に1が一つずつ左にずれていってますね。
偶然ではありません。人為的にこういう構造になっているのです。
この左ビットシフト構造を【うまくビット論理和で合体させてみましょう】
【ビット論理和「 | 」(または)演算子を使います】(ただの棒 | ではありませんよ!)
ビット論理和「 | 」は「または」の意味になりA | Bで AまたはB どちらかが1の場合に1となるを表します。
ボタン1とボタン4をビット論理和「 | 」でかけ合わせると以下のようになります!
パッドボタン 2進数(32ビット32桁)
PAD_INPUT_1 00000000000000000000000000010000
PAD_INPUT_4 00000000000000000000000010000000
............................................................
PAD_INPUT_1 | PAD_INPUT_4 00000000000000000000000010010000
はい、うまいことビットが合成できて0..010010000で1が2か所にできました。
この合成したビットの並びをButton::Joinと名付けましょう。
パッドボタン 2進数(32ビット32桁)
PAD_INPUT_1 00000000000000000000000000010000
PAD_INPUT_4 00000000000000000000000010000000
.................................................
Button::Join 00000000000000000000000010010000
そしてこの合成したビットを判定に使うときは【ビット論理積「 & 」(かつ)演算子を使います】
ビット論理積「 & 」は「かつ」の意味になりA & Bで AかつB 両方が1の場合に1を表します。
合成したビットPad::Join と PAD_INPUT_1 の【ビット論理積&】をかけ合わせてみましょう。
パッドボタン 2進数(32ビット32桁)
Button::Join 00000000000000000000000100010000
PAD_INPUT_1 00000000000000000000000000010000
...........................................................
Button::Join & PAD_INPUT_1 00000000000000000000000000010000 = true
(1つでも1がある)
この【ビット論理積「 & 」(かつ)演算子】どこかで見たと思いませんか?
そうInput.hのGetButtonの中ボタン押し判定のreturnには【ビット論理積「 & 」演算子】を使っていたのです。
Input.hのGetButtonを見てみてください。
// ボタンが押されているか?
static bool GetButton(Pad pad, int buttonId)
{
if (pad == Pad::None) return false; // Noneなら判別不要
else if (pad == Pad::All) // 全てAllの指定だった時は
{ // GetButtonの中でGetButtonを呼ぶ再起呼出しテクニック
for (int i = 0; i < (int)Pad::NUM; i++)
if (GetButton((Pad)i, buttonId))
return true; //押されているPadを発見!
return false; // 一つも押されていなかった
}
// 今ボタンが押されているかどうかを返却
return (currentStates[(int)pad] & buttonId) != 0;
}
ただ注意してください。【「 & 」マークは色んな異なる意味がありすぎなので】
ややこしいです【「&&」や「&メモリ参照」など文脈によってビット論理積と勘違いしないように(文脈で判断)しましょう】
まあ普通はあんまりビット演算ばかりやるコードはないので気づけると思います。
では【ビット合成】でボタンの種類を合成したButton::Joinを使って
Input.hのボタン判定処理をすっきり書きましょう。
#ifndef INPUT_H_
#define INPUT_H_
#include <unordered_map> // 辞書配列【map高速版】https://qiita.com/sileader/items/a40f9acf90fbda16af51
#include "DxLib.h"
// コントローラのパッド番号定義
enum class Pad
{
All = -2, // すべてのうちどれか1つでも押されたとき
None = -1, // パッド割当て無(やられたり待機中のプレイヤに)
Key=0, // キーボード
One, // コントローラ1
Two, // コントローラ2
Three, // コントローラ3
Four, // コントローラ4
NUM, // コントローラの数=5
};
// ある動作として反応するボタン一覧(ビット論理和「|または」)
enum class Button
{ //[反応ボタン一覧定義] https://dixq.net/g/04_05.html
// 参加ボタン 1=Z 2=X 3=C 4=A 5=S 6=D 7=Q 8=W 9=ESC 10=SPACE
Join = PAD_INPUT_1 | PAD_INPUT_2 | PAD_INPUT_3 | PAD_INPUT_4
| PAD_INPUT_5 | PAD_INPUT_6 | PAD_INPUT_7 | PAD_INPUT_8
| PAD_INPUT_9 | PAD_INPUT_10, //1Z~10SPACEキー全て反応
};
// 入力クラス
class Input
{
public:
(中略)..............
// プレイヤの参加処理(参加辞書登録して参加中の数も返す)
static int JoinCheck()
{
int joinCount = 0;
for (int i = 0; i < (int)Pad::NUM; i++)
{ // 参加ボタンが押されたら
if (Input::GetButtonDown((Pad)i, (int)Button::Join))
isJoin[(Pad)i] = true; // 参加パッド辞書に登録
// 参加ボタンが押されたら
if (Input::GetButtonDown((Pad)i, PAD_INPUT_2))
isJoin[(Pad)i] = true; // 参加パッド辞書に登録
// 参加ボタンが押されたら
if (Input::GetButtonDown((Pad)i, PAD_INPUT_3))
isJoin[(Pad)i] = true; // 参加パッド辞書に登録
// 参加ボタンが押されたら
if (Input::GetButtonDown((Pad)i, PAD_INPUT_4))
isJoin[(Pad)i] = true; // 参加パッド辞書に登録
// ちょ待ち!、あ、これボタンの数だけめっちゃif文増えるやつだ..やだな
if (isJoin.count((Pad)i) == 1 && isJoin[(Pad)i] == true)
joinCount++; // 参加中の人数をカウント
}
return joinCount; // 参加中のコントローラ数を返す
}
(以下略)...............
最終的に赤部分を消すと以下のようにすっきりします。
一目瞭然、見事にすっきりしますね【複数ボタンに反応させるときはビット合成テクニックを使いましょう】
// プレイヤの参加処理(参加辞書登録して参加中の数も返す)
static int JoinCheck()
{
int joinCount = 0;
for (int i = 0; i < (int)Pad::NUM; i++)
{ // 参加ボタンが押されたら
if (Input::GetButtonDown((Pad)i, (int)Button::Join))
isJoin[(Pad)i] = true; // 参加パッド辞書に登録
if (isJoin.count((Pad)i) == 1 && isJoin[(Pad)i] == true)
joinCount++; // 参加中の人数をカウント
}
return joinCount; // 参加中のコントローラ数を返す
}
今度はプレイヤの数を増やすためにプレイヤをリスト化しましょう。
今までの敵リストのようにvector配列のリストでもいいですが【使い勝手を考えて辞書配列に挑戦】しましょう。
GameManager.hのプレイヤを複数にできるよう辞書配列に変更
します。
#ifndef GAMEMANAGER_H_
#define GAMEMANAGER_H_
#include <memory>
#include <vector>
#include <list>
#include <unordered_map> // 辞書配列【map高速版】
#include "Singleton.h"
class Map;
class Player; //クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class PlayerBullet;
class Enemy;
class EnemyBullet;
class Explosion;
class Item;
class GameManager : public Singleton<GameManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
friend class Singleton<GameManager>; // Singleton でのインスタンス作成は許可
std::shared_ptr<Map> map{ nullptr }; // マップ
std::shared_ptr<Player> player{ nullptr }; // 自機の初期化
std::unordered_map<int, std::shared_ptr<Player>> players; // 自機の辞書配列(複数プレイ対応)
std::list<std::shared_ptr<PlayerBullet>> playerBullets; // 自機弾のリスト
std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
std::list<std::shared_ptr<EnemyBullet>> enemyBullets; // 敵弾のリスト
std::list<std::shared_ptr<Explosion>> explosions; // 爆発エフェクトのリスト
std::list<std::shared_ptr<Item>> items; // アイテムのリスト
// 複数リスト↑list ↑【shared_ptr】回し読みポインタ:メモリにデータを回し読み状態として確保できる
(中略)....................
};
protected:
GameManager() {}; // 外部からのインスタンス作成は禁止
virtual ~GameManager() {}; //外部からのインスタンス破棄も禁止
};
#endif
playerをplayersに変えたので【つづりの変わった部分は全部エラーになるはず】です。
逆にいうと【今から変えなきゃいけないとこがエラーになってくれた】のです。
エラーが出ないと逆に【忘れて謎のバグの原因になりますからね】
変更の必要なところの目印になってくれて逆に方針が立てやすいのです。
今度はプレイヤの数を増やすためにプレイヤをリスト化しましょう。
今までの敵リストのようにvector配列のリストでもいいですが【使い勝手を考えて辞書配列に挑戦】しましょう。
まずはプレイヤをたくさん使ってるだけあってエラーもたくさん出ている
PlayScene.hのプレイヤを単数から複数に変更
するとこから修正を始めましょう。
#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Map.h"
#include "GameManager.h"
#include "SceneManager.h"
#include "Player.h"
#include "Boss.h"
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"
#include "Item.h"
class PlayScene : public Scene
{
public:
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる
float scrollSpeed = 1.5f; // スクロール速度
// コンストラクタ
PlayScene() : Scene()
{
this->tag = "PlayScene";
}
// 初期化処理
void Initialize() override
{
(中略).........
// マップの初期化
gm.map = std::make_shared<Map>("Map/stage1.csv");
// ここでプレイヤの初期化などのプレイ開始時の処理
int joinCount = Input::JoinCheck(); // プレイヤの参加検出処理
if(joinCount > 0)
MakePlayer(); // プレイヤの生成処理
//gm.player = std::make_shared<Player>(Pad::Key, Vector3((float)100, (float)(Screen::Height / 2))); // 自機の初期化
// 敵の仮生成
//gm.enemies.emplace_back(std::make_shared<Boss>(Vector3(Screen::Width + 90, Screen::Height / 2)));
}
// 終了処理(大抵Initializeと同じリセットだが終了時だけやりたいリセットの仕方もあるかも)
void Finalize() override
{
// ここにプレイ終了時のメモリのリセット処理(ゲームクリアやゲームオーバー時など)
gm.map = nullptr; // マップのメモリからの解放
gm.players.clear(); // プレイヤのメモリからの解放
gm.enemies.clear(); // 敵のメモリからの解放
gm.playerBullets.clear(); // プレイヤの弾のメモリからの解放
gm.enemyBullets.clear(); // 敵弾のメモリからの解放
gm.explosions.clear(); // 爆発エフェクトのメモリからの解放
gm.items.clear(); // アイテムのメモリからの解放
// ゲーム終了時のスコアがMAXなら記録して終了
if(score > sm.scoreMax) sm.scoreMax = score;
}
// プレイヤの生成処理
void MakePlayer()
{
for (int i = 0; i < (int)Pad::NUM; i++)
{ // 参加辞書登録されたプレイヤの自機の生成
if (Input::IsJoin((Pad)i) && gm.players.count(i) == 0)
gm.players[i] = std::make_shared<Player>((Pad)i, Vector3((float)100, (float)(Screen::Height / 2)));
}
}
// 更新処理
void Update() override
{
// ここにプレイ中の更新処理を持ってくる
// プレイヤの参加検出処理
int joinCount = Input::JoinCheck();
if (joinCount > 0)
MakePlayer(); // プレイヤの生成処理
// マップデータの読出し位置のスクロール
if(gm.players.size() > 0) //プレイヤ配列0の内はスクロールしない
gm.map->Scroll(scrollSpeed);
// プレイヤの更新処理
for (const auto& pair : gm.players)
{
auto player = pair.second;
if (!player->isDead) // 自機が死んでいなければ
player->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
}
// 自機弾の更新処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Update();
}
// 敵弾の更新処理
for (const auto& b : gm.enemyBullets)
{
b->Update();
}
// 自機と敵弾の衝突判定
for (const auto& enemyBullet : gm.enemyBullets)
{
// 敵弾が死んでたらスキップ
if (enemyBullet->isDead)
continue;
for (const auto& pair : gm.players)
{
auto player = pair.second;
// 自機が死んでたらこれ以上判定しない
if (player->isDead)
break;
// 敵弾が死んでたらスキップ
if (enemyBullet->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
player->position, player->collisionRadius,
enemyBullet->position, enemyBullet->collisionRadius))
{
player->OnCollisionEnemyBullet(enemyBullet);
}
}
}
// 敵の更新処理
for (const auto& b : gm.enemies)
{
b->Update();
}
// 自機弾と敵の衝突判定
for (const auto& playerBullet : gm.playerBullets)
{
// 自機弾が死んでたらスキップする
if (playerBullet->isDead)
continue;
for (const auto& enemy : gm.enemies)
{
// 敵が死んでたらスキップする
if (enemy->isDead)
continue;
// 自機弾と敵が重なっているか?
if (MyMath::CircleCircleIntersection(
playerBullet->position, playerBullet->collisionRadius,
enemy->position, enemy->collisionRadius))
{
// 重なっていたら、それぞれのぶつかったときの処理を呼び出す
enemy->OnCollisionPlayerBullet(playerBullet);
playerBullet->OnCollisionEnemy(enemy);
Sound::Play(Sound::bomb); // 効果音の再生
// 衝突の結果、自機弾が死んだら、この弾のループはおしまい
if (playerBullet->isDead)
break;
}
}
}
// 自機と敵の衝突判定
for (const auto& enemy : gm.enemies)
{
// 敵が死んでたらスキップ
if (enemy->isDead)
continue;
for (const auto& pair : gm.players)
{
auto player = pair.second;
// 自機が死んでたらこれ以上判定しない
if (player->isDead)
break;
// 敵が死んでたらスキップ
if (enemy->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
player->position, player->collisionRadius,
enemy->position, enemy->collisionRadius))
{
player->OnCollisionEnemy(enemy);
}
}
}
// 爆発エフェクトの更新処理
for (const auto& e : gm.explosions)
{
e->Update();
}
// アイテムの更新処理
for (const auto& itm : gm.items)
{
itm->Update();
}
// 自機とアイテムの衝突判定
for (const auto& itm : gm.items)
{
// アイテムが死んでたらスキップ
if (itm->isDead)
continue;
for (const auto& pair : gm.players)
{
auto player = pair.second;
// 自機が死んでたらこれ以上判定しない
if (player->isDead)
break;
// アイテムが死んでたらスキップ
if (itm->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
player->position, player->collisionRadius,
itm->position, itm->collisionRadius) )
{
itm->OnCollisionPlayer(player);
}
}
for (const auto& enemyBullet : gm.enemyBullets)
{
// 敵弾が死んでたらスキップ
if (enemyBullet->isDead)
continue;
// 円同士の衝突判定で調べる
if (MyMath::CircleCircleIntersection(
itm->position, itm->collisionRadius,
enemyBullet->position, enemyBullet->collisionRadius) )
{
enemyBullet->OnCollisionItem(itm);
itm->OnCollisionEnemyBullet(enemyBullet);
}
}
}
//return; //【★実験】ここでreturnで処理を終わると弾のメモリを解放しない!下のerase前でそこまで処理が行かないからメモリ解放せずに終わる実験
// 自機弾の削除処理
gm.EraseRemoveIf(gm.playerBullets,
[](std::shared_ptr<PlayerBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 敵のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.enemies,
[](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 敵弾のリストから死んでるものを除去する
gm.EraseRemoveIf(gm.enemyBullets,
[](std::shared_ptr<EnemyBullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
//爆発エフェクトのリストから死んでるものを除去する
gm.EraseRemoveIf(gm.explosions,
[](std::shared_ptr<Explosion>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
//アイテムのリストから死んでるものを除去する
gm.EraseRemoveIf(gm.items,
[](std::shared_ptr<Item>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
int aliveCount = 0; // 生きているプレイヤ数のカウント
for (const auto& pair : gm.players)
{
auto player = pair.second;
if (player->isDead != true)
aliveCount++; // 生存プレイヤ数をカウント
}
// ゲームオーバー判定と画面チェンジ処理
// プレイヤの数が0じゃないのに生存数が0ならゲームオーバー
bool isGameover = (gm.players.size() != 0 && aliveCount == 0);
if (!isGameover && gm.player->isDead) isGameover = true;
if (isGameover)
{
Sound::PlayMusic(Sound::ending); //BGMを変更する
sm.LoadScene("GameOverScene"); //シーン遷移
return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
}
}
// 描画処理
void Draw() override
{
// ここにプレイ画面の描画処理を持ってくる
// 敵の描画処理
for (const auto& b : gm.enemies)
{
b->Draw();
}
// 爆発エフェクトの描画処理
for (const auto& e : gm.explosions)
{
e->Draw();
}
// プレイヤの描画処理
for (const auto& pair : gm.players)
{
auto player = pair.second;
if (!player->isDead) // 自機が死んでいなければ
player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
}
// 自機弾の描画処理
// for文で全自機弾をループで回してUpdateで更新する
for (const auto& b : gm.playerBullets)
{ // autoキーワードはビルドのコンパイル機械語翻訳の時に型を自動推論してくれる
b->Draw();
}
// 敵弾の描画処理
for (const auto& b : gm.enemyBullets)
{
b->Draw();
}
// アイテムの描画処理
for (const auto& itm : gm.items)
{
itm->Draw();
}
}
};
#endif
さてここで辞書配列のfor文に慣れてしまいましょう。
vector配列との違いは【辞書のキーとバリューを各ループで得られること】です。
あいうえおの辞書を例にすると【「あ」がキー、「アイス」がバリューですね】
あ、すみません違います!【「アイス」がキー、「アイスとは冷たくておいしいもの(説明)」がバリューです】
ただし一つのキーに複数のバリューのあるタイプの辞書もあり
その場合は【「あ」がキー、「アイス」「愛」..などがバリューですね】
プログラム上では【キーがfirst、バリューがsecondでアクセスできます】
// プレイヤの描画処理
for (const auto& pair : gm.players)
{
auto player = pair.second;
if (!player->isDead) // 自機が死んでいなければ
player->Draw(); // プレイヤの更新【忘れるとプレイヤ表示されない】
}
もう一つ辞書配列に登録する際の形も見ておきましょう。
players[i] = ~;のような配列に代入するような形で登録可能です。
すでに登録済か未登録かは.count(キー)で判別します。
このcount()でキーがあるか探せるのがvector配列より手軽なところ
5文字ぐらいでさらっと登録済かわかるので。
// プレイヤの生成処理
void MakePlayer()
{
for (int i = 0; i < (int)Pad::NUM; i++)
{ // 参加辞書登録されたプレイヤの自機の生成
if (Input::IsJoin((Pad)i) && gm.players.count(i) == 0)
gm.players[i] = std::make_shared<Player>((Pad)i, Vector3((float)100, (float)(Screen::Height / 2)));
}
}
さてタイトル画面などにもプレイヤ参加の検出処理を加えておきましょう。
プレイに移る前に参加辞書Input::isJoinに1人は登録がないとプレイヤのいない空のプレイ画面から始まるので。
TitleScene.hにプレイヤの参加検出処理を追加
します。
#ifndef TITLESCENE_H_
#define TITLESCENE_H_
(中略)..................
class TitleScene : public Scene
{
public:
(中略)..................
// ボタン入力に応じた処理
void HandleInput()
{
Input::JoinCheck(); // プレイヤの参加検出処理
if (Input::GetButtonDown(Pad::All, PAD_INPUT_1))
{ // ボタンを押したらスタート音声を再生
Sound::Play(Sound::start);
isStartPushed = true; // 画面遷移トリガーON
}
(中略)..................
}
(以下略)..................
他にもザコにプレイヤの追撃弾を撃たせている人は単体のプレイヤを複数対応しなければなりません。
プレイヤを追撃するにも複数いるとどのプレイヤを追撃するかあいまいになるので。
ここからのザコの処理の修正は人それぞれです。プレイヤを追っかけない独自のザコのままがいい!という人は不要かもしれません。
Zako0.hに複数のプレイヤから一番近いプレイヤを追撃する処理を追加
します。
#ifndef ZAKO_0_H_
#define ZAKO_0_H_
#include <memory>
#include "Enemy.h"
#include "Image.h"
#include "MyMath.h"
#include "Player.h" // Player.hでもZako1.hをインクルードすると循環するので注意
// ザコ0クラス
class Zako0 : public Enemy
{
public:
int coolTime = 0; // クールタイム(冷却時間。0になるまで次の弾が撃てない)
// コンストラクタ
Zako0(Vector3 pos) : Enemy( pos )
{
this->tag = "Zako0";
}
// 更新処理
void Update() override
{
position.x -= 1; // とりあえず左へ移動
coolTime--;
if (coolTime <= 0)
{
float minDistance = 1000000; // 最小の距離
std::shared_ptr<Player> minPlayer = nullptr; // 一番近いプレイヤを探す
for (const auto& pair : gm.players)
{
auto p = pair.second;
float dist = (position - p->position).magnitude();
if (dist < minDistance)
{ // より小さい距離のプレイヤがいれば記録更新
minDistance = dist;
minPlayer = p;
}
}
if (minPlayer != nullptr)
{
// 自機狙い弾
float angle = MyMath::PointToPointAngle(position, minPlayer->position);
gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, angle, 8));
}
coolTime += 10;
}
// 【重要】画面外の削除処理【CPU処理重くなる原因】
if (x < 0)
{ // 左端の画面外に出たらisDeadしないと
// 画面外の敵の当たり判定で【だんだん処理が重くなるZako1,Zako2..も】
isDead = true;
}
}
// 描画処理
void Draw() override
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako0, TRUE);
}
};
#endif
Zako1.hに複数のプレイヤから一番近いプレイヤを追撃する処理を追加
します。
#ifndef ZAKO_1_H_
#define ZAKO_1_H_
#include "Enemy.h"
#include <memory>
#include <cmath> // Sinの計算に使う
#include "MyMath.h"
#include "Image.h"
#include "Player.h" // Player.hでもZako1.hをインクルードすると循環するので注意
// ザコ1クラス
class Zako1 : public Enemy
{
public:
float sinMove = 0; // Sinの動きをする際の0~360度
float sinRadius = 70; // Sinの動きをするときの半径
// コンストラクタ
Zako1(Vector3 pos) : Enemy( pos )
{
this->tag = "Zako1";//オブジェクトの種類判別タグ
life = 5;
}
// 更新処理
void Update() override
{
float minDistance = 1000000; // 最小の距離
std::shared_ptr<Player> minPlayer = nullptr; // 一番近いプレイヤを探す
for (const auto& pair : gm.players)
{
auto p = pair.second;
float dist = (position - p->position).magnitude();
if (dist < minDistance)
{ // より小さい距離のプレイヤがいれば記録更新
minDistance = dist;
minPlayer = p;
}
}
if (minPlayer != nullptr)
{
// 自機をおいかけてくる処理
float angleToPlayer = MyMath::PointToPointAngle(position, minPlayer->position);
float speed = 3;
position += Vector3((float)std::cos(angleToPlayer) * speed, (float)std::sin(angleToPlayer) * speed);
//ゆらゆらSin波動の動きをy上下方向にプラス
float prevSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
sinMove += 2.5f; // Sinの波運動の角度を進める
float currentSinY = std::sin(sinMove * MyMath::Deg2Rad) * sinRadius;
// (前の角度でのY) - (今の角度でのY)の差を足すことで【ベースのyの位置 ± sinの動き】になる
position.y += currentSinY - prevSinY; // 上下方向のSinの波運動
}
}
// 描画処理
void Draw() override
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako1, TRUE);
}
};
#endif
Zako2.hに複数のプレイヤから一番近いプレイヤを追撃する処理を追加
します。
#ifndef ZAKO_2_H_
#define ZAKO_2_H_
#include "Enemy.h"
#include <memory>
#include <cmath> // Sin Cosの計算に使う
#include "Image.h"
#include "Screen.h"
#include "MyMath.h"
#include "MyRandom.h"
#include "Player.h" // Player.hでもZako2.hをインクルードすると循環するので注意
// ザコ2クラス
class Zako2 : public Enemy
{
public:
int coolTime = 0; // クールタイム(冷却時間。0になるまで次の弾が撃てない)
// コンストラクタ
Zako2(Vector3 pos) : Enemy( pos )
{
this->tag = "Zako2";
}
// 更新処理
void Update() override
{
if (position.x > Screen::Width - 200)
{ // スクリーンの端から200の位置で止まる
position.x -= 1; // とりあえず左へ移動
}
coolTime--;
if (coolTime <= 0)
{
float minDistance = 1000000; // 最小の距離
std::shared_ptr<Player> minPlayer = nullptr; // 一番近いプレイヤを探す
for (const auto& pair : gm.players)
{ // より小さい距離のプレイヤがいれば記録更新
auto p = pair.second;
float dist = (position - p->position).magnitude();
if (dist < minDistance)
{
minDistance = dist;
minPlayer = p;
}
}
if (minPlayer != nullptr)
{
// 自機狙いばらまき弾
float angle = MyMath::PointToPointAngle(position, minPlayer->position);
float randomAngle = MyRandom::Range(-15.0f, 15.0f); // ±15度の乱数の角度を取得
gm.enemyBullets.emplace_back(std::make_shared<EnemyBullet>(position, angle + randomAngle * MyMath::Deg2Rad, 8));
coolTime += 10;
}
}
// 200の位置で止まるので左端にこないから削除処理はなくて済む
}
// 描画処理
void Draw() override
{
DrawRotaGraphF(position.x, position.y, 1, 0, Image::zako2, TRUE);
}
};
#endif
さて、これで独自のザコや弾など単数プレイヤを使用しているコードがなくなれば
複数プレイヤでプレイできるようになるはずです。
一度プレイして複数プレイヤでプレイできるか確かめてみましょう。
Input.hでコントローラの初回ボタンプッシュを管理して、2つのコントローラの初回プッシュが被ったら同一コントローラ扱いとしてスルーする機能
を追加しましょう。
#ifndef INPUT_H_
#define INPUT_H_
#include <climits> //int型の最大値2147483647につかう
#include <unordered_map> // 辞書配列【map高速版】https://qiita.com/sileader/items/a40f9acf90fbda16af51
#include "DxLib.h"
// コントローラのパッド番号定義
enum class Pad
{
All = -2, // すべてのうちどれか1つでも押されたとき
None = -1, // パッド割当て無(やられたり待機中のプレイヤに)
Key=0, // キーボード
One, // コントローラ1
Two, // コントローラ2
Three, // コントローラ3
Four, // コントローラ4
NUM, // コントローラの数=5
};
// 入力クラス
class Input
{
public:
const int MaxPadNum = (int)Pad::Four + 1; // 最大パッド数(Keyぶん+1)
static int prevStates[(int)Pad::NUM]; // 1フレーム前の状態
static int currentStates[(int)Pad::NUM]; // 現在の状態
static std::unordered_map<Pad, bool> isJoin; // 参加中のパッド番号辞書
// 参加中のパッドかどうか?
static bool IsJoin(Pad pad) { return isJoin.count(pad) == 1 && Input::isJoin[pad]; };
static std::unordered_map<int, int> padDic; // パッド番号からDXの定義への辞書
//★【DXコントローラバグ検知】初回プッシュが被ればDXバグで同一コントローラ
static int firstPushTiming[(int)Pad::Four]; // Pad::Fourが最後だから8とかに増やすなら要書き換え
static int timing;// = 0;
static bool isBugCheckMode;// = false; //バグチェックを開始するか
// 辞書配列の初期化
static void InitPadDictionary()
{
// 辞書配列で対応関係を結び付けておく
padDic[(int)Pad::Key] = DX_INPUT_KEY;
padDic[(int)Pad::One] = DX_INPUT_PAD1;
padDic[(int)Pad::Two] = DX_INPUT_PAD2;
padDic[(int)Pad::Three] = DX_INPUT_PAD3;
padDic[(int)Pad::Four] = DX_INPUT_PAD4;
}
// 初期化。タイトルなどキー入力状態リセット時に
static void Init()
{
InitPadDictionary(); // パッドの辞書配列の初期化
// キー状態をゼロリセット
for (int i = 0; i < (int)Pad::NUM; i++)
prevStates[i] = currentStates[i] = 0;
timing = 0, isBugCheckMode = false; //バグチェック
}
// プレイヤの参加処理(参加辞書登録して参加中の数も返す)
static int JoinCheck()
{
int joinCount = 0;
for (int i = 0; i < (int)Pad::NUM; i++)
{ // 参加ボタンが押されたら
if (Input::GetButtonDown((Pad)i, PAD_INPUT_1))
isJoin[(Pad)i] = true; // 参加パッド辞書に登録
if (isJoin.count((Pad)i) == 1 && isJoin[(Pad)i] == true)
joinCount++; // 参加中の人数をカウント
}
return joinCount; // 参加中のコントローラ数を返す
}
// 最新の入力状況に更新する処理。
// 毎フレームの最初に(ゲームの処理より先に)呼んでください。
static void Update()
{
for (int i = 0; i < (int)Pad::NUM; i++)
{ // 現在の状態を一つ前の状態として保存してGetJoypad..
prevStates[i] = currentStates[i];
currentStates[i] = GetJoypadInputState(padDic[i]);
}
isBugCheckMode = true;
++timing; //タイミングのカウントを+1
if (timing == INT_MAX) timing = 0; // int型最大値になったら0にループ
}
// ボタンが押されているか?
static bool GetButton(Pad pad, int buttonId)
{ //コントローラバグチェック
if (ControllerBugCheck(pad, buttonId)) return false;
if (pad == Pad::None) return false; // Noneなら判別不要
else if (pad == Pad::All) // All指定の時は全てのパッド
{ // GetButtonの中でGetButtonを呼ぶ【再起呼出し】テクニック
for (int i = 0; i < (int)Pad::NUM; i++)
if (GetButton((Pad)i, buttonId))
return true; //押されているPadを発見!
return false; // 一つも押されていなかった
}
// 今ボタンが押されているかどうかを返却
return (currentStates[(int)pad] & buttonId) != 0;
}
// ボタンが押された瞬間か?
static bool GetButtonDown(Pad pad, int buttonId)
{ //コントローラバグチェック
if (ControllerBugCheck(pad, buttonId)) return false;
if (pad == Pad::None) return false; // Noneなら判別不要
else if (pad == Pad::All) // All指定の時は全てのパッド
{ // 再起呼出しテクニック
for (int i = 0; i < (int)Pad::NUM; i++)
if (GetButtonDown((Pad)i, buttonId))
return true; //押されているPadを発見!
return false; // 一つも押されていなかった
}
// 今は押されていて、かつ1フレーム前は押されていない場合はtrueを返却
return ((currentStates[(int)pad] & buttonId) & ~(prevStates[(int)pad] & buttonId)) != 0;
}
// ボタンが離された瞬間か?
static bool GetButtonUp(Pad pad, int buttonId)
{ //コントローラバグチェック
if (ControllerBugCheck(pad, buttonId)) return false;
if (pad == Pad::None) return false; // Noneなら判別不要
else if (pad == Pad::All) // All指定の時は全てのパッド
{ // 再起呼出しテクニック
for (int i = 0; i < (int)Pad::NUM; i++)
if (GetButtonUp((Pad)i, buttonId))
return true; //押されているPadを発見!
return false; // 一つも押されていなかった
}
// 1フレーム前は押されていて、かつ今は押されている場合はtrueを返却
return ((prevStates[(int)pad] & buttonId) & ~(currentStates[(int)pad] & buttonId)) != 0;
}
//コントローラのボタンの初回プッシュを検知し
//他コントローラと初回タイミングが完全被りかチェックして重複コントローラの判定処理をスルーする対策
//★コントローラ3を押すと7も反応するなどコントローラ4以降の動作が怪しいのでその対策(DXのバグ?仕様?)
static bool ControllerBugCheck(Pad pad, int buttonId)
{
// 今は押されていて、かつ1フレーム前は押されていない場合
bool buttonDown = ((currentStates[(int)pad] & buttonId) & ~(prevStates[(int)pad] & buttonId)) != 0;
//if (isBugCheckMode == false) return false;
//コントローラの初回ボタンプッシュを検知
if (buttonDown && firstPushTiming[(int)pad] == 0)
{
for (int i = 0; i < (int)Pad::NUM - 1; i++)
{ //全コントローラ初回プッシュ被りがないかチェック完全同時は怪しいので-1
bool isDown = ((currentStates[i] & buttonId) & ~(prevStates[i] & buttonId)) != 0;
if ((Pad)i != pad && isDown ////初回プッシュ被り検出
&& (timing == firstPushTiming[i] || firstPushTiming[i] == 0))
{ //初回プッシュのタイミング被りは-1フラグを記録
firstPushTiming[i] = -1;
}
}
if (firstPushTiming[(int)pad] != -1)
firstPushTiming[(int)pad] = timing;//初回プッシュを記録
}
if (firstPushTiming[(int)pad] == -1) return true; //DXのコントローラ被りバグ!
else return false; //コントローラ被りOK!
}
}; //←【注意】クラス定義の終わりにはコロンが必要だよ!
#endif //ここでエラー出た人は↑【注意】の;コロン忘れ
Input.cppに変数定義を追加します。
#include "Input.h"
int Input::prevStates[]; // 1フレーム前の状態
int Input::currentStates[]; // 現在の状態
std::unordered_map<Pad, bool> Input::isJoin; // 参加中のパッド番号辞書
std::unordered_map<int, int> Input::padDic; // パッド番号からDXの定義への辞書
//★【DXコントローラバグ検知】初回プッシュが被ればDXバグで同一コントローラ
int Input::firstPushTiming[(int)Pad::Four]; // Pad::Fourが最後だから8とかに増やすなら要書き換え
int Input::timing{ 0 };
bool Input::isBugCheckMode = false; //バグチェックを開始するか
Image.hに背景画像の読み込み定義
を追加しましょう。
#ifndef IMAGE_H_
#define IMAGE_H_
#include "DxLib.h"
#include <assert.h> // 画像読み込みの読込み失敗表示用
(中略)..........
class Image
{
public:
Image() {}; // 初期化コンストラクタの定義だけ
~Image() {}; // 破棄する処理デストラクタの定義だけ
(中略)..........
static int background; //背景画像のハンドラ(読込画像番号)
static int bossImage; //ボス画像のハンドラ(読込画像番号)
static int player; //プレイヤ画像のハンドラ
(中略)..........
};
#endif
Image.cppにも背景画像の読み込みの処理
を追加しましょう。
#include "Image.h"
(中略)..........
int Image::background{ -1 };
int Image::bossImage{ -1 }; // Load終わっても-1(初期値)のままだと画像ロードが失敗してますね
int Image::player{ -1 };
(中略)..........
void Image::Load()
{
(中略)..........
background = LoadGraph("Image/background.jpg"); //[メモリ節約機能付の場合追記→] scene, { "TitleScene" });
assert(background != -1); // 画像読込失敗、ファイル名かフォルダ名が間違ってる
(中略)..........
}
PlayScene.hに背景をえがく処理を追加
します。
#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"
#include "GameManager.h"
#include "SceneManager.h"
#include "Player.h"
#include "Boss.h"
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"
class PlayScene : public Scene
{
(中略)..........
// 描画処理
void Draw() override
{
// 背景の描画(一番後ろに表示させるものから最初に描く、描く順が早い方が後ろのレイヤー)
DrawGraphF(0, 0, Image::background, TRUE); // DrawGraphF関数はDrawRotaGraphFと違い(0,0)を指定すると左上が(0,0)になるように描く。
(中略)...................
}
};
#endif
PlayScene.hに背景をスクロールさせる処理を追加
します。ポイントは背景を2枚、画像の幅だけずらして描いていることです。
#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
#include "Sound.h"
#include "GameManager.h"
#include "SceneManager.h"
#include "Player.h"
#include "Boss.h"
#include "PlayerBullet.h"
#include "EnemyBullet.h"
#include "Explosion.h"
class PlayScene : public Scene
{
public:
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる
float scrollSpeed = 1.5f; // スクロール速度
float bgX = 0.0f; // 背景のスクロール位置
float bgSpeed = 1.5f; // 背景のスクロールスピード
// コンストラクタ
PlayScene() : Scene()
{
this->tag = "PlayScene";
}
(中略)..........
// 描画処理
void Draw() override
{
float bgWidth, bgHeight; // 画像の幅と高さ
// &で&bgWidth と &bgHeightを参照渡し↓することでGetGraphSizeF内部で書き換えてもらう(&をつけない渡し方は変数のコピーを渡しだから関数が終わっても書き変わらない)
GetGraphSizeF(Image::background, &bgWidth, &bgHeight); // 画像の縦横のピクセルサイズを取得する
bgX -= bgSpeed; // マイナスで左方向にスクロール
if (bgX < -bgWidth) bgX = 0; // 画像サイズの幅ぶんスクロールしたら0に戻してループ
// 背景の描画(一番後ろに表示させるものから最初に描く、描く順が早い方が後ろのレイヤー)
DrawGraphF(bgX, 0, Image::background, TRUE); // DrawGraphF関数はDrawRotaGraphFと違い(0,0)を指定すると左上が(0,0)になるように描く。
// 2枚背景を画像の幅のぶんだけ右端にずらして描くことで無限にループしてるようにみせる
DrawGraphF(bgX + bgWidth, 0, Image::background, TRUE);
(中略)...................
}
};
#endif
TitleScene.hには実はすでにスコアを表示するコードがありますが実は文字列の扱い方が間違っていて
表示されていません。
#ifndef TITLESCENE_H_
#define TITLESCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
(中略)...........
class TitleScene : public Scene
{
public:
(中略)...........
// 描画処理
void Draw() override
{
DrawString(0, 0, "TitleSceneです。ボタン押下でPlaySceneへ。", GetColor(255, 255, 255));
if (sm.scoreMax >= 0)
{
DrawString(0, 200, "MAXスコア: " + sm.scoreMax, GetColor(255, 255, 255));
}
(中略)...........
}
};
#endif
// 文字列描画関数
extern int DrawString( int x, int y, const TCHAR *String, unsigned int Color, unsigned int EdgeColor DEFAULTPARAM( = 0 ) ) ;
typedef char TCHAR, *PTCHAR;
typedef char ~,~...; つまりはchar型です(C言語の1byteの1文字のアスキーコード型もしくは-127 〜 128の1byteの整数型)std::string("MAXスコア: ") + std::to_string(sm.scoreMax)
として足し合わせて、( std::string("MAXスコア: ") + std::to_string(sm.scoreMax) ).c_str()
TitleScene.hのスコアを表示するコードを修正します。
#ifndef TITLESCENE_H_
#define TITLESCENE_H_
#include "Scene.h"
#include <string>
#include "DxLib.h"
#include "Screen.h"
#include "Input.h"
#include "Image.h"
(中略)...........
class TitleScene : public Scene
{
public:
(中略)...........
// 描画処理
void Draw() override
{
DrawString(0, 0, "TitleSceneです。ボタン押下でPlaySceneへ。", GetColor(255, 255, 255));
if (sm.scoreMax >= 0)
{
DrawString(0, 200, (std::string("MAXスコア: ") + std::to_string(sm.scoreMax)).c_str(), GetColor(255, 255, 255));
}
(中略)...........
}
};
#endif
#ifndef TITLESCENE_H_
#define TITLESCENE_H_
#include "Scene.h"
#include <string>
#include "DxLib.h"
(中略)...........
class TitleScene : public Scene
{
public:
(中略)...........
// 描画処理
void Draw() override
{
SetFontLineSpace(40); // 1行ごとのスペース
int prevFontSize = GetFontSize(); // フォントのサイズを一旦保管
SetFontSize(40); // フォントのサイズを変える
DrawString(0, 0, "TitleSceneです。\nボタン押下でPlaySceneへ。", GetColor(0, 255, 0));
SetFontSize(prevFontSize); // フォントのサイズを元に戻す(戻さないと以降のすべての文字が40のサイズになる)
if (sm.scoreMax >= 0)
{
DrawString(0, 200, (std::string("MAXスコア: ") + std::to_string(sm.scoreMax)).c_str(), GetColor(255, 255, 255));
}
(中略)...........
}
};
#endif
#ifndef TITLESCENE_H_
#define TITLESCENE_H_
#include "Scene.h"
#include <string>
#include "DxLib.h"
(中略)...........
class TitleScene : public Scene
{
public:
(中略)...........
int font1 = -1; //フォントのハンドルをCreateFontToHandle関数で得る
// コンストラクタ
TitleScene() : Scene()
{
this->tag = "TitleScene";
}
// 初期化処理
void Initialize() override
{
// "MS 明朝"のフォントで、サイズ40、太さ7
// エッジサイズ3、DX_FONTTYPE_ANTIALIASING_EDGE_4X4 : アンチエイリアス&エッジ付きフォント( 4x4サンプリング )
// イタリック=TRUEのフォントを作成し
// 作成したデータの識別番号を変数 font1 に保存する
font1 = DxLib::CreateFontToHandle("MS 明朝", 40, 7, DX_FONTTYPE_ANTIALIASING_EDGE_4X4, -1, 3, TRUE);
(中略)...........
}
(中略)...........
// 描画処理
void Draw() override
{
SetFontLineSpaceToHandle(40, font1); // 1行ごとのスペース
int prevFontSize = GetFontSize(); // フォントのサイズを一旦保管
SetFontSize(40); // フォントのサイズを変える
DrawStringToHandle(0, 0, "TitleSceneです。\nボタン押下でPlaySceneへ。", GetColor(0, 255, 0), font1, GetColor(0, 0, 255));
SetFontSize(prevFontSize); // フォントのサイズを元に戻す(戻さないと以降のすべての文字が40のサイズになる)
if (sm.scoreMax >= 0)
{
DrawString(0, 200, (std::string("MAXスコア: ") + std::to_string(sm.scoreMax)).c_str(), GetColor(255, 255, 255));
}
(中略)...........
}
};
#endif
Font.hを新規作成
してフォントデータを生成して辞書配列std::unordered_mapで管理するクラスを定義しましょう。
#ifndef FONT_H_
#define FONT_H_
#include "DxLib.h"
#include <assert.h> // 読込み失敗表示用
#include <string>
#include <unordered_map>
// フォント設定の管理や読み込みを行うクラス
class Font
{
public:
// フォントの設定の構造体
struct Setting
{
int size = -1; // フォントのサイズ( おおよそのドット数 -1にするとデフォルトのサイズ )
int thick = -1; // フォントの太さ( 0~9 -1にするとデフォルトの太さ )
int fontType = -1; // フォントのタイプ [参考] https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10
int charSet = -1;
int edgeSize = -1; // フォントの縁のエッジのサイズ
int italic = FALSE; // イタリック体で表示するか
int handle = -1; // CreateFontToHandle関数で帰ってくるハンドル https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10
const std::string tag = ""; // 設定にタグをつけて管理する
std::string fontName; // 作成するフォント名(NULL にするとデフォルトのフォント)
// std::unordered_mapの値に使うにはデフォルトコンストラクタが必要
Setting() = default;//←これが無いと「クラス、構造体、..に規定のコンストラクタがありません」エラー
Setting(const std::string tag, const TCHAR* FontName, int Size, int Thick,
int FontType = -1, int CharSet = -1, int EdgeSize = -1, int Italic = FALSE)
: size{ Size }, thick{ Thick }, fontType{ FontType }, charSet{ CharSet }, edgeSize{ EdgeSize }, italic{ Italic },
tag{ tag }, fontName{ (FontName == NULL) ? "" : std::string(FontName) }
{
}
virtual ~Setting() {}
};
// 自分で設定したフォント設定の辞書
static std::unordered_map<std::string, Setting> fonts;
Font() {}; // 初期化コンストラクタ
virtual ~Font() {}; // 破棄するデストラクタ
static void Init()
{
// ゲーム起動時から終了までずっと使うフォントはまとめてここで読込む形もアリ
//Setting font1{ "設定1", "MS 明朝", 40, 3 };
//Load(&font1); // 設定に沿ってフォントをデータ化しておく
}
// Settingに沿ってCreateFontToHandle関数でフォントデータを作成して辞書に登録しておく https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10
static int Load(Setting* setting = nullptr);
// 作成しておいたフォントデータを削除する(ひとつあたり1.2MB程のメモリを使う)
static int DeleteFontToHandle(Setting* setting = nullptr);
private:
};
#endif
Font.cppを新規作成
してフォントデータを生成したり、メモリ上から削除して節約したりする処理をつくりましょう。
#include "Font.h"
std::unordered_map<std::string, Font::Setting> Font::fonts; // フォントの辞書
int Font::Load(Setting* setting)
{
// 設定がnullptrだったらデフォルトのフォントを返す
if (setting == nullptr)
return GetDefaultFontHandle();
// すでに辞書に登録があったら作成したフォントデータを削除しておく
if (fonts.count(setting->tag) != 0)
DxLib::DeleteFontToHandle(fonts[setting->tag].handle);
// https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10
// フォントを作成し作成したデータの識別番号をsetting->handle に保存する
setting->handle = CreateFontToHandle((setting->fontName == "") ? NULL : setting->fontName.c_str(), setting->size, setting->thick,
setting->fontType, setting->charSet, setting->edgeSize, setting->italic, setting->handle);
assert(setting->handle != -1 && "Font名が間違っているかFontフォルダ指定をミスっていませんか?"); // フォント作成失敗
// 読み込んだハンドルを辞書に登録しておく
fonts.emplace(setting->tag, *setting);
return setting->handle;
}
// フォントがメモリを圧迫してるときに使う(フォントデータはひとつ1.2MB程のメモリを使う)
int Font::DeleteFontToHandle(Setting* setting)
{
// 設定がnullptrだったら何もせずreturn
if (setting == nullptr)
return 0;
// すでに辞書に登録があったら
if (fonts.count(setting->tag) != 0)
{ // 作成したフォントデータを削除
int result = DxLib::DeleteFontToHandle(fonts[setting->tag].handle);
if(result == 0)
fonts.erase(setting->tag); // 辞書からも削除
}
setting->handle = -1; // -1に戻して未ロード状態がわかるようにする
return setting->handle;
}
TitleScene.hを修正して
Fontクラスから生成したフォントデータからスコアを表示してみましょう。
#ifndef TITLESCENE_H_
#define TITLESCENE_H_
#include "Scene.h"
#include <string>
#include "DxLib.h"
(中略)...........
#include "Font.h"
(中略)...........
class TitleScene : public Scene
{
public:
(中略)...........
// ★dp4FontViewerを使ってInfoからFont名を得て↓コピぺすればつづりを間違えない https://us.fontviewer.de/Download/
Font::Setting font = Font::Setting("設定1", "HG丸ゴシックM-PRO", 40, 7, DX_FONTTYPE_ANTIALIASING_EDGE_4X4, -1, 3, FALSE);
int font1 = -1; //フォントのハンドルをCreateFontToHandle関数で得る
// コンストラクタ
TitleScene() : Scene()
{
this->tag = "TitleScene";
}
// 初期化処理
void Initialize() override
{
Font::Load(&font); // 設定に沿ってフォントをデータ化しておく
// "MS 明朝"のフォントで、サイズ40、太さ7
// エッジサイズ3、DX_FONTTYPE_ANTIALIASING_EDGE_4X4 : アンチエイリアス&エッジ付きフォント( 4x4サンプリング )
// イタリック=TRUEのフォントを作成し
// 作成したデータの識別番号を変数 font1 に保存する
font1 = DxLib::CreateFontToHandle("MS 明朝", 40, 7, DX_FONTTYPE_ANTIALIASING_EDGE_4X4, -1, 3, TRUE);
(中略)...........
}
(中略)...........
// 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
void Finalize() override
{
Font::DeleteFontToHandle(&font); // フォントデータを削除してメモリ節約(1フォントあたり1.2MB節約できる)
(中略)...........
}
(中略)...........
// 描画処理
void Draw() override
{
SetFontLineSpaceToHandle(40, font1); // 1行ごとのスペース
DrawStringToHandle(0, 0, "TitleSceneです。\nボタン押下でPlaySceneへ。", GetColor(0, 255, 0), font1, GetColor(0, 0, 255));
//[テストしやすくするため一旦コメントアウト] if (sm.scoreMax >= 0)
{
DrawStringToHandle(0, 200, (std::string("MAXスコア: ") + std::to_string(sm.scoreMax)).c_str(), GetColor(255, 255, 255), Font::fonts["設定1"].handle, GetColor(255, 0, 0));
}
(中略)...........
}
};
#endif
Imageフォルダと同じフォルダにFontフォルダを作成して、生成したMyFont1.dftファイルをコピーしておきましょう
Font.hを修正してDFT形式のファイルからフォントデータを生成
して表示する処理を定義しましょう。
#ifndef FONT_H_
#define FONT_H_
#include "DxLib.h"
#include <assert.h> // 読込み失敗表示用
#include <string>
#include <unordered_map>
// フォント設定の管理や読み込みを行うクラス
class Font
{
public:
// フォントの設定の構造体
struct Setting
{
int size = -1; // フォントのサイズ( おおよそのドット数 -1にするとデフォルトのサイズ )
int thick = -1; // フォントの太さ( 0~9 -1にするとデフォルトの太さ )
int fontType = -1; // フォントのタイプ [参考] https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10
int charSet = -1;
int edgeSize = -1; // フォントの縁のエッジのサイズ
int italic = FALSE; // イタリック体で表示するか
int handle = -1; // CreateFontToHandle関数で帰ってくるハンドル https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10
bool isDFTFont = false; // DXライブラリ付属ツール C:\DxLib_VC\Tool\CreateDXFontData で書き出したDFT形式を使用するかどうか
const std::string tag = ""; // 設定にタグをつけて管理する
std::string fontName; // 作成するフォント名(NULL にするとデフォルトのフォント)
// std::unordered_mapの値に使うにはデフォルトコンストラクタが必要
Setting() = default;//←これが無いと「クラス、構造体、..に規定のコンストラクタがありません」エラー
Setting(const std::string tag, const TCHAR* FontName, int Size, int Thick,
int FontType = -1, int CharSet = -1, int EdgeSize = -1, int Italic = FALSE)
: size{ Size }, thick{ Thick }, fontType{ FontType }, charSet{ CharSet }, edgeSize{ EdgeSize }, italic{ Italic },
tag{ tag }, fontName{ (FontName == NULL) ? "" : std::string(FontName) }
{
this->isDFTFont = false; // 引数を4つ以上指定する初期化ならCreateFontToHandleを使用する
}
Setting(const std::string tag, const TCHAR* DFTFontPath, int EdgeSize = -1)
: tag{ tag }, fontName{ (DFTFontPath == NULL) ? "" : std::string(DFTFontPath) }, edgeSize{ EdgeSize }
{
this->isDFTFont = true; // 引数を3つ以下指定する初期化ならDFT形式のフォントをロードする
}
virtual ~Setting() {}
};
// 自分で設定したフォント設定の辞書
static std::unordered_map<std::string, Setting> fonts;
Font() {}; // 初期化コンストラクタ
virtual ~Font() {}; // 破棄するデストラクタ
static void Init()
{
// ゲーム起動時から終了までずっと使うフォントはまとめてここで読込む形もアリ
//Setting font1{ "設定1", "MS 明朝", 40, 3 };
//Load(&font1); // 設定に沿ってフォントをデータ化しておく
}
// Settingに沿ってCreateFontToHandle関数でフォントデータを作成して辞書に登録しておく https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10
static int Load(Setting* setting = nullptr);
// 作成しておいたフォントデータを削除する(ひとつあたり1.2MB程のメモリを使う)
static int DeleteFontToHandle(Setting* setting = nullptr);
private:
};
#endif
Font.cppを修正してLoadFontDataToHandle関数でDFT形式のフォントファイルを読み込む
処理をつくりましょう。
#include "Font.h"
std::unordered_map<std::string, Font::Setting> Font::fonts; // フォントの辞書
int Font::Load(Setting* setting)
{
// 設定がnullptrだったらデフォルトのフォントを返す
if (setting == nullptr)
return GetDefaultFontHandle();
// すでに辞書に登録があったら作成したフォントデータを削除しておく
if (fonts.count(setting->tag) != 0)
DxLib::DeleteFontToHandle(fonts[setting->tag].handle);
// https://dxlib.xsrv.jp/function/dxfunc_graph2.html#R17N10
// フォントを作成し作成したデータの識別番号をsetting->handle に保存する
if (setting->isDFTFont == false || setting->fontName == "")
setting->handle = CreateFontToHandle((setting->fontName == "") ? NULL : setting->fontName.c_str(), setting->size, setting->thick,
setting->fontType, setting->charSet, setting->edgeSize, setting->italic, setting->handle);
else // DXフォントデータファイルを読込み、フォントハンドルを保存する
setting->handle = LoadFontDataToHandle(setting->fontName.c_str(), setting->edgeSize);
assert(setting->handle != -1 && "Font名が間違っているかFontフォルダ指定をミスっていませんか?"); // フォント作成失敗
// 読み込んだハンドルを辞書に登録しておく
fonts.emplace(setting->tag, *setting);
return setting->handle;
}
// フォントがメモリを圧迫してるときに使う(フォントデータはひとつ1.2MB程のメモリを使う)
int Font::DeleteFontToHandle(Setting* setting)
{
// 設定がnullptrだったら何もせずreturn
if (setting == nullptr)
return 0;
// すでに辞書に登録があったら
if (fonts.count(setting->tag) != 0)
{ // 作成したフォントデータを削除
int result = DxLib::DeleteFontToHandle(fonts[setting->tag].handle);
if(result == 0)
fonts.erase(setting->tag); // 辞書からも削除
}
setting->handle = -1; // -1に戻して未ロード状態がわかるようにする
return setting->handle;
}
TitleScene.hを修正して
Fontクラスから生成したフォントデータからスコアを表示してみましょう。
#ifndef TITLESCENE_H_
#define TITLESCENE_H_
#include "Scene.h"
#include <string>
#include "DxLib.h"
(中略)...........
#include "Font.h"
(中略)...........
class TitleScene : public Scene
{
public:
(中略)...........
// ★dp4FontViewerを使ってInfoからFont名を得て↓コピぺすればつづりを間違えない https://us.fontviewer.de/Download/
//Font::Setting font = Font::Setting("設定1", "HG丸ゴシックM-PRO", 40, 7, DX_FONTTYPE_ANTIALIASING_EDGE_4X4, -1, 3, FALSE);
Font::Setting font = Font::Setting("設定1", "Font/MyFont1.dft", 2);
int font1 = -1; //フォントのハンドルをCreateFontToHandle関数で得る
// 初期化処理
void Initialize() override
{
Font::Load(&font); // 設定に沿ってフォントをデータ化しておく
// "MS 明朝"のフォントで、サイズ40、太さ7
// エッジサイズ3、DX_FONTTYPE_ANTIALIASING_EDGE_4X4 : アンチエイリアス&エッジ付きフォント( 4x4サンプリング )
// イタリック=TRUEのフォントを作成し
// 作成したデータの識別番号を変数 font1 に保存する
font1 = DxLib::CreateFontToHandle("MS 明朝", 40, 7, DX_FONTTYPE_ANTIALIASING_EDGE_4X4, -1, 3, TRUE);
(中略)...........
}
(中略)...........
// 描画処理
void Draw() override
{
SetFontLineSpaceToHandle(40, font1); // 1行ごとのスペース
DrawStringToHandle(0, 0, "TitleSceneです。\nボタン押下でPlaySceneへ。", GetColor(0, 255, 0), font1, GetColor(0, 0, 255));
//[テストしやすくするため一旦コメントアウト] if (sm.scoreMax >= 0)
{
DrawStringToHandle(0, 200, (std::string("MAXスコア: ") + std::to_string(sm.scoreMax)).c_str(), GetColor(255, 255, 255), Font::fonts["設定1"].handle, GetColor(0, 255, 255));
}
(中略)...........
}
};
#endif
PlayScene.hのプレイヤ弾と敵の衝突判定にスコアをふやす処理
を追加してみましょう。
#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_
#include "Scene.h"
(中略)................
class PlayScene : public Scene
{
public:
(中略)................
int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる
(中略)................
// 更新処理
void Update() override
{
// ここにプレイ中の更新処理を持ってくる
(中略)................
// 自機弾と敵の衝突判定
for (const auto& playerBullet : gm.playerBullets)
{
// 自機弾が死んでたらスキップする
if (playerBullet->isDead)
continue;
for (const auto& enemy : gm.enemies)
{
// 敵が死んでたらスキップする
if (enemy->isDead)
continue;
// 自機弾と敵が重なっているか?
if (MyMath::CircleCircleIntersection(
playerBullet->position, playerBullet->collisionRadius,
enemy->position, enemy->collisionRadius))
{
// 重なっていたら、それぞれのぶつかったときの処理を呼び出す
enemy->OnCollisionPlayerBullet(playerBullet);
playerBullet->OnCollisionEnemy(enemy);
Sound::Play(Sound::bomb); // 効果音の再生
++score; // 敵を倒すとスコアが+1される
bgSpeed += 0.05f; // 敵を倒せば倒すほど背景を加速させる演出
// 衝突の結果、自機弾が死んだら、この弾のループはおしまい
if (playerBullet->isDead)
break;
}
}
}
(中略)................
}
(中略)................
};
#endif
SceneManager.hにスコアのランクとランキング配列を定義
してランキングを管理できるようにしましょう。
#ifndef SCNENEMANAGER_H_
#define SCNENEMANAGER_H_
(中略)..........
#include "Singleton.h"
class Scene; //クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class SceneManager : public Singleton<SceneManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
friend class Singleton<SceneManager>; // Singleton でのインスタンス作成は許可
// マネージャを【どこからでもアクセスしやすい「変数の掲示板」として使えばシーンをまたぐ変数も定義できる】
std::string selectStage = "stage1"; // 選択中のステージ名など
int scoreMax = -1; // 【シーンをまたぐスコアなど】はマネージャに定義すれば【シーンをまたいだあとも】消えず残る
int ranking[5] = {-1,-1,-1,-1,-1}; // スコアトップ5を記録しておく配列
int rank = 0; // 直前のゲーム終了したときの順位
(中略)..........
};
#endif
PlayScene.hのシーンの終了時(GameOver時でもよいが..どこでやるかは迷いどころ)にスコアをランキングに順序をソートしながら登録する
修正をくわえましょう。
#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_
#include "Scene.h"
#include "DxLib.h"
(中略).....................
class PlayScene : public Scene
{
public:
(中略).....................
// 終了処理(大抵Initializeと同じリセットだが終了時だけやりたいリセットの仕方もあるかも)
void Finalize() override
{
(中略).....................
// ゲーム終了時のスコアがMAXなら記録して終了
if(score > sm.scoreMax) sm.scoreMax = score;
// ゲーム終了時のスコアをranking配列に挿入ソートして記録
int j, iSize = sizeof(sm.ranking) / sizeof(int);
if (score < sm.ranking[iSize - 1])
sm.rank = -1; // ランキング外
else
{ // ranking配列の一番後ろは一番小さいのでまずはそれと入れ替え
sm.ranking[iSize - 1] = score;
// 挿入ソートでランキングを降順に並べ替え
// https://www.momoyama-usagi.com/entry/info-algo-sort-basic#i-3
// https://ufcpp.net/study/algorithm/sort_insert.html
for (int i = 1; i < iSize; ++i)
{
j = i; // 交換要素のためのインデックス
// while(j > 0 && sm.ranking[j-1] > sm.ranking[j]) に変えると昇順ソートになる
while ((j > 0) && (sm.ranking[j - 1] < sm.ranking[j]))
{
// 整列されていない隣り合う要素を交換する
int tmp = sm.ranking[j - 1];
sm.ranking[j - 1] = sm.ranking[j];
sm.ranking[j] = tmp;
j--; // 1つ左のデータへ
}
}
// 何位かを判定
for (int i = 0; i < iSize; ++i)
if (sm.ranking[i] == score)
{
sm.rank = i; // ランキング順位 0位~4位
break; // スコアが=イコールになるものを見つけたらすぐbreakでfor文を抜ける
}
}
}
(中略).....................
};
#endif
TitleScene.hを修正してプレイしたスコアのランキングを
Fontで文字列を改行しながら表示してみましょう。
#ifndef TITLESCENE_H_
#define TITLESCENE_H_
#include "Scene.h"
#include <string>
#include "DxLib.h"
(中略)...........
#include "Font.h"
(中略)...........
class TitleScene : public Scene
{
public:
(中略)...........
// 描画処理
void Draw() override
{
int lineSize = 40; // 1行の縦方向の改行サイズ
SetFontLineSpaceToHandle(lineSize, font1); // 1行ごとのスペース
DrawStringToHandle(0, 0, "TitleSceneです。\nボタン押下でPlaySceneへ。", GetColor(0, 255, 0), font1, GetColor(0, 0, 255));
if (sm.scoreMax >= 0)
{
DrawStringToHandle(0, 200, (std::string("MAXスコア: ") + std::to_string(sm.scoreMax)).c_str(), GetColor(255, 255, 255), Font::fonts["設定1"].handle, GetColor(0, 255, 255));
// 順位 0位~4位を1位から5位に直すため +1する→ sm.rank + 1
std::string myRankText = (sm.rank < 0) ? std::string("ランク イン シッパイ") : std::string("アナタ ハ イマ No.") + std::to_string(sm.rank + 1) + std::string(" ") + std::to_string(sm.ranking[sm.rank]);
DrawStringToHandle(0, 200 + lineSize, myRankText.c_str(), GetColor(255, 255, 255), Font::fonts["設定1"].handle, GetColor(255, 0, 255));
std::string rankText; // ランキングを改行しながら複数行のテキストに
for (int i=0, iSize = sizeof(sm.ranking) / sizeof(int); i < iSize; ++i)
{
if(sm.ranking[i] >= 0) // 配列の初期値は-1は表示せずとばす
rankText += std::string("No.") + std::to_string(i + 1) + std::string(" ") + std::to_string(sm.ranking[i]) + std::string("\n");
}
DrawStringToHandle(0, 200 + lineSize*2, rankText.c_str(), GetColor(255, 255, 255), Font::fonts["設定1"].handle);
}
(中略)...........
}
};
#endif
DataCsv.hにCSVのセル(int,float,文字列の3種類)を読み取れるCsvValueクラスを追加して、さらにSave機能を追加
します。
#ifndef DATACSV_H_
#define DATACSV_H_
#include <assert.h> // 読込み失敗表示用
#include <vector> // csvを2次元vector配列に格納
#include <string> // 文字列に必要
#include <fstream> // ファイル読み出しifstreamに必要
#include <sstream> // 文字列ストリームに必要
//★CsvValue型をint型や文字列string型や小数float型として【カメレオン】のように使える。
// 【エクセルはマス目がカメレオン】整数も小数も文字列もありうる。
// それを解消して統一して扱える便利な★CsvValue型。
class CsvValue
{
public:
enum class Type
{
Int,
Float,
String,
NUM
};
Type type = Type::String;
protected: // 内部データを書き換え禁止:書き換えられるとintと文字列とのずれが起きうる
// エクセルのCsvデータはマス目がint,float,stringの3種類ありうる
union { // ★共用体unionテクニック https://inemaru.hatenablog.com/entry/2016/03/02/005408
int intData = 0; // 整数int型データを格納
float floatData; // 小数float型データを格納
};
std::string stringData{""}; // 文字列string型データを格納
public:
CsvValue() = default;
// 3種類のCSVセル(int,float,文字列std::stringとconst char*:計4種)のコンストラクタ
CsvValue(int intData) : type{ Type::Int }
{
this->intData = intData;
this->stringData = std::to_string(intData);
}
CsvValue(float floatData) : type{ Type::Float }
{
this->floatData = floatData;
this->stringData = std::to_string(floatData);
}
CsvValue(std::string& stringData) : type{ Type::String }
{
this->stringData = stringData;
}
CsvValue(const char* stringData) : type{ Type::String }
{
this->stringData = stringData;
}
virtual ~CsvValue() {}; // 仮想デストラクタ
// 変換演算子によりCSVのセルをintとしてもstringとしても扱えるように
// https://programming-place.net/ppp/contents/cpp/language/019.html#conversion_op
// 3種類の読み取りオペレータ
inline operator int()
{
if (type == Type::Int) return intData;
else if (type == Type::Float) return (int)floatData;
else if (type == Type::String && intData == 0)
intData = intFromString(); // デフォルトで文字列として読込んだデータをintに変換
return intData;
}
inline operator float()
{
if (type == Type::Float) return floatData;
else if (type == Type::Int) return (float)intData;
else if (type == Type::String && intData == 0)
floatData = floatFromString(); // デフォルトで文字列として読込んだデータをfloatに変換
return floatData;
}
inline operator std::string()&
{
return stringData;
}
int intFromString() // 文字列データからint型への変換する関数
{
std::istringstream ss(stringData); //文字列ストリームの初期化
int num; // 数字単体
ss >> num; // 文字列ストリームから数字への変換
return num;
}
float floatFromString() // 文字列データからfloat型への変換する関数
{
std::istringstream ss(stringData); //文字列ストリームの初期化
float num; // 数字単体
ss >> num; // 文字列ストリームから数字への変換
return num;
}
private:
void clearString() // 文字列をcapacityごとクリアしてメモリ節約
{ // https://stackoverflow.com/questions/740030/how-to-release-the-unused-capacity-of-a-string
std::string("").swap(stringData); // [capacityをリセットしてメモリ節約]
}
public:
CsvValue& operator = (const int other) { // 代入=演算子
type = Type::Int; // タイプを更新
if (type == Type::Float) floatData = (float)other;
else intData = other;
if (type == Type::String) std::to_string(other).swap(stringData); // [swapテクでcapacity節約]
else clearString();
this->stringData = std::to_string(intData);
return *this;
}
CsvValue& operator = (const float other) { // 代入=演算子
type = Type::Float; // タイプを更新
if (type == Type::Int) intData = (int)other;
else floatData = other;
if (type == Type::String) std::to_string(other).swap(stringData); // [swapテクでcapacity節約]
else clearString();
this->stringData = std::to_string(floatData);
return *this;
}
CsvValue& operator = (const std::string& other) { // 代入=演算子
type = Type::String; // タイプを更新
intData = 0; // 初期値 0 としてリセット
stringData = other;
return *this;
}
CsvValue& operator = (const char* other) { // 代入=演算子
type = Type::String; // タイプを更新
intData = 0; // 初期値 0 としてリセット
stringData = std::string(other);
return *this;
}
};
namespace { // 無名名前空間で囲んで、.hヘッダにグローバル関数定義をする
// ファイル出力ストリーム演算子<<を定義する
std::ostream& operator << (std::ostream& stream, CsvValue& csvValue)
{
if (csvValue.type == CsvValue::Type::Int) stream << (int)csvValue;
else if (csvValue.type == CsvValue::Type::Float) stream << (float)csvValue;
else if (csvValue.type == CsvValue::Type::String) stream << (std::string)csvValue;
return stream;
}
}
// CSVファイルを読込み幅や高さとデータ本体を保持するデータ型
struct DataCsv // ←structはC++ではほぼclassと同じ【違いはデフォルトがpublic】
{ // 読込んだデータファイルの情報
enum class CsvType
{
IntMap,
CsvValue
};
int Width = 0; // csvファイルの表の幅
int Height = 0;// csvファイルの表の高さ
bool isInitialized = false; //[2重ロード対策]1度ロードしたらtrueにしてclear()されるまでロード抑止
bool isForceCreate = false; // ファイルが無い、もしくは空のときにアラートを出さず無理やり(force)新しく作るか(create)
// isForceCreateをセット[*thisをreturnで関数連鎖テク] https://flat-leon.hatenablog.com/entry/cpp_named_parameter
DataCsv& IsForceCreate(bool b) { isForceCreate = b; return *this; }
std::string FilePath { "" };
std::vector<std::vector<CsvValue>> Data;// csvデータ
// 初期化コンストラクタでファイル名を指定して初期化と同時にファイル読込
DataCsv(std::string filePath = "", CsvType csvType = CsvType::CsvValue) :FilePath{ filePath }
{// csvファイルの読込み★【初期化と同時なのでファイルとデータ型が一心同体で使いやすい】
if (FilePath != "") Load(FilePath, csvType); // ファイル読込み
};
virtual ~DataCsv()
{// 仮想デストラクタ
Data.clear();// 2次元配列データのお掃除
};
// ★スムーズに[][]でアクセスできるように[]演算子を独自定義する
std::vector<CsvValue>& operator[](std::size_t index) { // ★ &参照にしないといちいちデータのコピーを返すので遅くなるよ
return Data[index]; // 書き込み
}
std::vector<CsvValue> operator[](std::size_t index) const { // ★constは添え字[]読み取りの処理を定義
return Data[index]; // 読み取り
}
std::size_t size()
{ // size()関数の名前をvectorと被らせることで使う側はvectorインvectorのままのコードで使える
return Data.size();
}
// データをクリアしてメモリを解放する
virtual void clear()
{ // [確実にメモリを空にするには] http://vivi.dyndns.org/tech/cpp/vector.html#shrink_to_fit
std::vector<std::vector<CsvValue>>().swap(Data); // 空のテンポラリオブジェクトでリセット
isInitialized = false; //ロード済みフラグをOFF
}
// csvファイルの読み込み
virtual void Load(std::string filePath, CsvType csvType = CsvType::CsvValue)
{
if (filePath == "" || isInitialized) return; //ファイル名がないもしくはロード済
this->FilePath = filePath; // ファイル名を保管
Data.clear(); //データを一旦クリア
// 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
std::ifstream ifs_csv_file(filePath);
// [.good()でファイル存在確認]https://stackoverflow.com/questions/12774207/fastest-way-to-check-if-a-file-exists-using-standard-c-c11-14-17-c
if (isForceCreate && !ifs_csv_file.good())
{
std::ofstream ofs_csv_file(filePath);
ofs_csv_file << ""; // 空文字のファイルを生成
}
std::string line; //1行単位でcsvファイルから文字列を読み込む
int readWidth = 0; //読込みデータの幅
int maxWidth = 0; // 1行の数字の最大個数
int readHeight = 0; //初期化
//↓2重while文でCSVファイルを読み取る
while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
{
std::vector<CsvValue> valuelist; // 1行の数字リスト
std::istringstream linestream(line); // 各行の文字列ストリーム
std::string splitted; // カンマで分割された文字列
int widthCount = 0; //この行の幅をカウント
if (csvType == CsvType::IntMap)
{
while (std::getline(linestream, splitted, { ',' }))
{
std::istringstream ss; //文字列ストリームの初期化
ss = std::istringstream(splitted); //文字列ストリーム
int num; // 数字単体
ss >> num; // 文字列ストリーム>>で数字へ変換
valuelist.emplace_back(num); // 数字を数字のリスト(valuelist)に追加
++widthCount; //この行のカンマで区切られた数字の数をカウントアップ
}
}
else if (csvType == CsvType::CsvValue)
{
while (std::getline(linestream, splitted, { ',' }))
{
valuelist.emplace_back(splitted); // 文字列としてリスト(valuelist)に追加
++widthCount; //この行のカンマで区切られた数をカウントアップ
}
}
// 1行の幅の数が記録を更新してMAXになるかチェック
if (widthCount > maxWidth) maxWidth = widthCount; //暫定Max幅を更新
// 1行分をvectorに追加
if (valuelist.size() != 0) Data.emplace_back(valuelist);
++readHeight; //読み込んだ行(縦)をカウントアップ
}
readWidth = maxWidth; //読み込んだ列(横)の数は一番数字の個数の多かった行に合わせる
if (!isForceCreate) // ファイルがないとき強制的に空ファイルを生成した場合は幅と高さのチェックをスキップする
{
//↓読込んだCSVの幅と高さをチェック
assert(readWidth > 0 && "CSV読込み失敗ファイル名間違いでは?" != "");
assert(readHeight > 0 && "CSV読込み失敗ファイル名間違いでは?" != "");
}
this->Width = readWidth; // 読込み成功したデータの幅を記録
this->Height = readHeight; // 読込み成功したデータの高さを記録
// コンストラクタで初期化でロードの場合とLoad関数経由で読む経路があるから2重ロード対策
isInitialized = true; // 読込み初期化済みフラグをON
return;
}
// csvファイルの保存
void Save(std::string filePath = "")
{
std::string outputPath = filePath;
if (filePath == "") // ファイル名がないときは読込時のパス
outputPath = FilePath;
if (outputPath == "") return; // ファイル名がないとき
FilePath = filePath; // ファイル名を保管
// 読み込むcsvファイルを開く(std::ofstreamのコンストラクタで開く)
std::ofstream ofs_csv_file(filePath);
std::string line; //1行単位でcsvファイルから文字列を読み込む
//↓2重for文でCSVファイルを列i,行jごとに書き出していく
for (auto j = 0; j < Data.size(); ++j)
{
for (auto i = 0; i < Data[j].size(); ++i)
{
if (i > 0) ofs_csv_file << ","; // 区切りのカンマを書き出す
ofs_csv_file << Data[j][i]; // Data配列を出力
if (Data[j][i].type == CsvValue::Type::String)
ofs_csv_file << "\"" << Data[j][i] << "\""; // "を両端に付け足してData配列を文字列として出力
else
ofs_csv_file << Data[j][i]; // Data配列を数値として出力
}
ofs_csv_file << std::endl; // 1行の終わりに改行を出力
}
return;
 }
};
#endif
〇〇 & 関数名()
{
........
return *this; // 最終的に*thisで自分のクラス自身の参照を返す
}
bool isForceCreate = false; // ファイルが無い、もしくは空のときにアラートを出さず無理やり(force)新しく作るか(create)
// isForceCreateをセット[*thisをreturnで関数連鎖テク] https://flat-leon.hatenablog.com/entry/cpp_named_parameter
DataCsv& IsForceCreate(bool b) { isForceCreate = b; return *this; }
DataCsv data;
data.isForceCreate = true;
data.Load(filePath, DataCsv::CsvType::CsvValue);
DataCsv data;
data.IsForceCreate(true).Load(filePath, DataCsv::CsvType::CsvValue);
DataCsv data;
data.SetA(true).SetB(-1).SetC("テスト").SetD(3.145f).Load(filePath, DataCsv::CsvType::CsvValue);
void Load(std::string filePath, CsvType csvType = CsvType::CsvValue, bool isForceCreate=false);
........
data.Load(filePath, DataCsv::CsvType::CsvValue, true); // うーん、あいだに挟まったDataCsv::CsvType::CsvValueをいちいち書かないとtrue渡せないな..省略しにくい
void Load(std::string filePath, CsvType csvType = CsvType::CsvValue);
........
data.IsForceCreate(true).Load(filePath, DataCsv::CsvType::CsvValue, true); // 省略!!
↓
data.IsForceCreate(true).Load(filePath); // 省略しても、csvTypeはデフォルトのDataCsv::CsvType::CsvValueになる
SaveData.hを新規作成
します。
#ifndef SAVEDATA_H_
#define SAVEDATA_H_
#include <vector> // セーブデータのファイルパスを複数vector配列で関数に渡すために使用
#include <string> // 文字列に必要
struct SaveData // データをSaveするタイプ あいまいなabstract型
{
std::vector<std::string> filePaths{ {""} }; // 複数のファイルを同時に呼び出したい場合のため配列に
SaveData(std::vector<std::string> filePaths = { "" } ) : filePaths { filePaths }
{
}
virtual ~SaveData() {}; // 破棄する処理デストラクタ
// セーブデータのファイルパスを複数vector配列で関数に渡す
virtual void Load(std::vector<std::string> filePaths = { "" }) {};
virtual void Save(std::vector<std::string> filePaths = { "" }) {};
private:
};
#endif
RankingData.hを新規作成してSaveDataを継承してCSVのロードとセーブ機能を実装
します。
#ifndef RANKING_H_
#define RANKING_H_
#include "SaveData.h"
#include "DataCsv.h"
struct RankingData : public SaveData
{
public:
const size_t listMAX = 100; // 過去のランキングリストを100位までcsvに保管
DataCsv list;
std::vector<CsvValue>& operator[](std::size_t index) {
return list[index]; // 読み書き
}
// コンストラクタ
RankingData(std::vector<std::string> filePaths = { "" }) : SaveData(filePaths)
{
Load(filePaths);
}
virtual void Load(std::vector<std::string> filepaths = { "" }) override
{
if (filepaths.size() > 0 && filepaths[0] != "")
filePaths = filepaths;
for (std::size_t i = 0, iSize = filePaths.size(); i < iSize; ++i)
{
if (i == 0)
{ // csvファイルの読み込み
list.IsForceCreate(true).Load(filePaths[0]);
if (list.size() == 0)
{ // 何もデータがないときは初期生成
std::vector<CsvValue> firstScore = { -1 }; // 0行目: スコア(最後にプレイしたとき)
list.Data.emplace_back(firstScore);
std::vector<CsvValue> firstRank = { 0 }; // 1行目: ランク順位(最後にプレイしたとき)
list.Data.emplace_back(firstRank);
std::vector<CsvValue> firstList = { -1 }; // 2行目: 過去スコアのリスト
list.Data.emplace_back(firstList);
}
}
//else if (i == 1)
//{
// // 複数まとめて同時にファイルを読み込みたければi=1,2..も定義
//}
}
return;
}
virtual void Save(std::vector<std::string> filepaths = { "" }) override
{
if (filepaths.size() > 0 && filepaths[0] != "")
filePaths = filepaths;
for (std::size_t i = 0, iSize = filePaths.size(); i < iSize; ++i)
{
if (i == 0)
{ // csvファイルの保存
list.Save(filePaths[0]);
}
//else if (i == 1)
//{
// // 複数まとめて同時にファイルを保存したければi=1,2..も定義
//}
}
return;
}
};
#endif
SaveDataという名前でフォルダを新規作成
しておかないと保存もロードも失敗しますので注意してください。SceneManager.hのスコアのランクとランキング配列をRankingDataクラスからロードする形に変えて
してランキングを管理できるようにしましょう。
#ifndef SCNENEMANAGER_H_
#define SCNENEMANAGER_H_
(中略)..........
#include "DataCsv.h"
#include "RankingData.h"
#include "Singleton.h"
class Scene; //クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class SceneManager : public Singleton<SceneManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
friend class Singleton<SceneManager>; // Singleton でのインスタンス作成は許可
// マネージャを【どこからでもアクセスしやすい「変数の掲示板」として使えばシーンをまたぐ変数も定義できる】
std::string selectStage = "stage1"; // 選択中のステージ名など
RankingData ranking{ {"SaveData/ranking.csv"} }; // スコアのランキング記録するCSV
CsvValue& scoreMax = ranking[0][0]; // 今までのMAXスコア &参照でセーブするデータの住所を参照して直接書き換えられるように
// int ranking[5] = {-1,-1,-1,-1,-1}; // スコアトップ5を記録しておく配列
CsvValue& rank = ranking[1][0]; // 最後にプレイしたセーブデータのランク順位への&参照
(中略)..........
};
#endif
CsvValue& scoreMax = ranking[0][0]; // 今までのMAXスコア &参照でセーブするデータの住所を参照して直接書き換えられるように
CsvValue& rank = ranking[1][0]; // 最後にプレイしたセーブデータのランク順位への&参照
TitleScene.hのスコアへアクセスする際に、何型かあいまいなCsvValue型を(int)でキャストして型を確定する変更と、CSVのセーブデータ配列の[2]行目形にアクセスする形に修正
をくわえましょう。
#ifndef TITLESCENE_H_
#define TITLESCENE_H_
#include "Scene.h"
#include <string>
#include "DxLib.h"
(中略)...........
class TitleScene : public Scene
{
public:
(中略)...........
// 描画処理
void Draw() override
{
(中略)...........
if ((int)sm.scoreMax >= 0)
{
DrawStringToHandle(0, 200, (std::string("MAXスコア: ") + std::to_string((int)sm.scoreMax)).c_str(), GetColor(255, 255, 255), Font::fonts["設定1"].handle, GetColor(0, 255, 255));
// 順位 0位~4位を1位から5位に直すため +1する→ sm.rank + 1
std::string myRankText = ((int)sm.rank < 0) ? std::string("ランク イン シッパイ") : std::string("アナタ ハ イマ No.") + std::to_string((int)sm.rank + 1) + std::string(" ") + (std::string)(sm.ranking[2][(int)sm.rank]);
DrawStringToHandle(0, 200 + lineSize, myRankText.c_str(), GetColor(255, 255, 255), Font::fonts["設定1"].handle, GetColor(255, 0, 255));
std::string rankText; // ランキングを改行しながら複数行のテキストに
for (int i=0, iSize = sm.ranking[2].size(); i < iSize; ++i)
{
if((int)sm.ranking[2][i] >= 0) // 配列の初期値は-1は表示せずとばす
rankText += std::string("No.") + std::to_string(i + 1) + std::string(" ") + std::to_string((int)sm.ranking[2][i]) + std::string("\n");
}
DrawStringToHandle(0, 200 + lineSize*2, rankText.c_str(), GetColor(255, 255, 255), Font::fonts["設定1"].handle);
}
(中略)...........
}
};
#endif
PlayScene.hのスコアへアクセスする部分をあいまいなCsvValue型にアクセスする部分は(int)に変換し、CSVのセーブデータ配列の[2]行目形にアクセスする形に修正
をくわえましょう。
#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_
#include "Scene.h"
#include "DxLib.h"
(中略).....................
class PlayScene : public Scene
{
public:
(中略).....................
// 終了処理(大抵Initializeと同じリセットだが終了時だけやりたいリセットの仕方もあるかも)
void Finalize() override
{
(中略).....................
// ゲーム終了時のスコアがMAXなら記録
if(score > (int)sm.scoreMax) sm.scoreMax = score;
if (sm.ranking[2].size() < sm.ranking.listMAX)
sm.ranking[2].emplace_back( -1 ); // sizeがlistMAX以下のときは後ろに-1を追加
// ゲーム終了時のスコアをranking配列に挿入ソートして記録
int j, iSize = sm.ranking[2].size();
if (score < (int)sm.ranking[2][iSize - 1])
sm.rank = -1; // ランキング外
else
{ // ranking配列の一番後ろは一番小さいのでまずはそれと入れ替え
sm.ranking[2][iSize - 1] = score;
// 挿入ソートでランキングを降順に並べ替え
// https://www.momoyama-usagi.com/entry/info-algo-sort-basic#i-3
// https://ufcpp.net/study/algorithm/sort_insert.html
for (int i = 1; i < iSize; ++i)
{
j = i; // 交換要素のためのインデックス
// while(j > 0 && (int)sm.ranking[2][j-1] > (int)sm.ranking[2][j]) に変えると昇順ソートになる
while ((j > 0) && ((int)sm.ranking[2][j - 1] < (int)sm.ranking[2][j]))
{
// 整列されていない隣り合う要素を交換する
int tmp = sm.ranking[2][j - 1];
sm.ranking[2][j - 1] = sm.ranking[2][j];
sm.ranking[2][j] = tmp;
j--; // 1つ左のデータへ
}
}
// 何位かを判定
for (int i = 0; i < iSize; ++i)
if ((int)sm.ranking[2][i] == score)
{
sm.rank = i; // ランキング順位 0位~4位
break; // スコアが=イコールになるものを見つけたらすぐbreakでfor文を抜ける
}
}
sm.ranking.Save(); // 並べ替えたあとにランキングをCSVファイルにセーブする
}
(中略).....................
};
#endif
Input.hにキーボードとマウス入力を検知できる機能
を追加しましょう。
#ifndef INPUT_H_
#define INPUT_H_
#include <climits> //int型の最大値2147483647につかう
#include <unordered_map> // 辞書配列【map高速版】https://qiita.com/sileader/items/a40f9acf90fbda16af51
#include <string> // キーボード押下の文字列出力につかう
#include "DxLib.h"
// コントローラのパッド番号定義
enum class Pad
{
Keyboard = -3, // キーボード入力を得る
All = -2, // すべてのうちどれか1つでも押されたとき
None = -1, // パッド割当て無(やられたり待機中のプレイヤに)
Key=0, // キーボード
One, // コントローラ1
Two, // コントローラ2
Three, // コントローラ3
Four, // コントローラ4
Mouse, // マウスのボタン判定に
NUM, // コントローラの数=6(マウス込み)
};
enum class Mouse
{
All = -2, // すべてのうちどれか1つでも押されたとき
None = -1,
DownL, //[左クリック押した瞬間=1]
DragL, //[左ドラッグ中=1]
UpL, //[左クリック離した瞬間=1]
DownR, //[右クリック押した瞬間=1]
DragR, //[右ドラッグ中=1]
UpR, //[右クリック離した瞬間=1]
X, // 現在のマウス位置
ReleaseX, // 離した状態のマウス位置(ドラッグ中は開始前位置)
DownLX, //[左] 押した位置
DragLX, //[左] ドラッグ中の位置
UpLX, //[左] 離した位置
DownRX,//[右] 押した位置
DragRX, //[右] ドラッグ中の位置
UpRX, //[右] 離した位置
Y, // 現在のマウス位置
ReleaseY, // 離した状態のマウス位置(ドラッグ中は開始前位置)
DownLY, //[左] 押した位置
DragLY, //[左] ドラッグ中の位置
UpLY, //[左] 離した位置
DownRY,//[右] 押した位置
DragRY, //[右] ドラッグ中の位置
UpRY, //[右] 離した位置
NUM
};
// 入力クラス
class Input
{
public:
const int MaxPadNum = (int)Pad::Four + 1; // 最大パッド数(Keyぶん+1)
static int prevStates[(int)Pad::NUM]; // 1フレーム前の状態
static int currentStates[(int)Pad::NUM]; // 現在の状態
static std::unordered_map<Pad, bool> isJoin; // 参加中のパッド番号辞書
// 参加中のパッドかどうか?
static bool IsJoin(Pad pad) { return isJoin.count(pad) == 1 && Input::isJoin[pad]; };
static std::unordered_map<int, int> padDic; // パッド番号からDXの定義への辞書
static int prevMouse; // 1フレーム前のマウス状態
static int currentMouse; // 現在のマウス状態
static int MouseX, MouseY; // 現在のマウス位置
static int ClickX, ClickY, MButton, LogType;
static float MouseWheel; // マウスのホイールの回転
static std::unordered_map<Mouse, int> Click;//<Mouse⇒int> マウスのクリックXY位置辞書
//[キーボード入力]https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N28
static char prevKey[256]; // 1フレーム前のキーボード状態
static char currentKey[256]; // 現在のキーボード状態
static std::string KeyString[256];//<キーコード⇒キー文字列>の辞書
static std::string CapsString[256];//<Caps状態キーコード⇒キー文字列>の辞書
static bool isCapsLocked;// CapsLock状態か?
static bool isShifted;// Shiftが押された状態か?
//★【DXコントローラバグ検知】初回プッシュが被ればDXバグで同一コントローラ
static int firstPushTiming[(int)Pad::Four]; // Pad::Fourが最後だから8とかに増やすなら要書き換え
static int timing;// = 0;
static bool isBugCheckMode;// = false; //バグチェックを開始するか
// 辞書配列の初期化
static void InitPadDictionary()
{
// 辞書配列で対応関係を結び付けておく
padDic[(int)Pad::Key] = DX_INPUT_KEY;
padDic[(int)Pad::One] = DX_INPUT_PAD1;
padDic[(int)Pad::Two] = DX_INPUT_PAD2;
padDic[(int)Pad::Three] = DX_INPUT_PAD3;
padDic[(int)Pad::Four] = DX_INPUT_PAD4;
}
// マウスの状態初期化
static void InitMouse()
{
MouseX = -1, MouseY = -1; // 現在のマウス位置を-1で初期状態では画面外として初期化
ClickX = -1, ClickY = -1, MButton = 0, LogType = 0;
Click[Mouse::ReleaseX] = -1; Click[Mouse::ReleaseY] = -1;
Click[Mouse::UpLX] = -1; Click[Mouse::UpLY] = -1;
Click[Mouse::UpRX] = -1; Click[Mouse::UpRY] = -1;
Click[Mouse::DownLX] = -1; Click[Mouse::DownLY] = -1;
Click[Mouse::DownRX] = -1; Click[Mouse::DownRY] = -1;
}
// キーボードの状態初期化
static void InitKeyboard()
{
// キー状態をゼロリセット
for (int i = 0; i < 256; i++)
prevKey[i] = currentKey[i] = 0;
}
// 初期化。タイトルなどキー入力状態リセット時に
static void Init()
{
InitPadDictionary(); // パッドの辞書配列の初期化
InitKeyString(); // キーボード辞書配列の初期化
// キー状態をゼロリセット
for (int i = 0; i < (int)Pad::NUM; i++)
prevStates[i] = currentStates[i] = 0;
InitMouse(); // マウスの状態初期化
InitKeyboard(); // キーボードの状態初期化
timing = 0, isBugCheckMode = false; //バグチェック
}
// プレイヤの参加処理(参加辞書登録して参加中の数も返す)
static int JoinCheck()
{
int joinCount = 0;
for (int i = 0; i < (int)Pad::NUM; i++)
{ // 参加ボタンが押されたら
if (Input::GetButtonDown((Pad)i, PAD_INPUT_1))
isJoin[(Pad)i] = true; // 参加パッド辞書に登録
if (isJoin.count((Pad)i) == 1 && isJoin[(Pad)i] == true)
joinCount++; // 参加中の人数をカウント
}
return joinCount; // 参加中のコントローラ数を返す
}
// 最新の入力状況に更新する処理。
// 毎フレームの最初に(ゲームの処理より先に)呼んでください。
static void Update()
{
for (int i = 0; i < (int)Pad::NUM; i++)
{ // 現在の状態を一つ前の状態として保存してGetJoypad..
prevStates[i] = currentStates[i];
currentStates[i] = GetJoypadInputState(padDic[i]);
}
UpdateMouse(); // マウス状態を更新
UpdateKeyboard(); // キーボード状態を更新
isBugCheckMode = true;
++timing; //タイミングのカウントを+1
if (timing == INT_MAX) timing = 0; // int型最大値になったら0にループ
}
// キーボードの状態を更新する
static void UpdateKeyboard()
{
memcpy(prevKey, currentKey, sizeof(prevKey));
// キーボードの状態配列を更新する
GetHitKeyStateAll(currentKey); // https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N28
if (GetButtonUp(Pad::Keyboard, KEY_INPUT_CAPSLOCK))
isCapsLocked = !isCapsLocked; // CapsLock状態を反転
// シフトが押された状態かを更新
isShifted = (GetButton(Pad::Keyboard, KEY_INPUT_LSHIFT) || GetButton(Pad::Keyboard, KEY_INPUT_RSHIFT));
}
// マウスの状態を更新する
static void UpdateMouse()
{
Click[Mouse::DownL] = 0; Click[Mouse::DownR] = 0; //Click[Mouse.UpL] = 0; Click[Mouse.UpR] = 0;
Click[Mouse::All] = 0; // 状態辞書の0リセット
prevMouse = currentMouse; // 前フレームの最新のマウス情報を1つ前の情報とする
prevStates[(int)Pad::Mouse] = currentMouse;
MouseWheel = GetMouseWheelRotVolF(); // マウスのホイール回転量を更新
// マウスの位置を取得
GetMousePoint(&MouseX, &MouseY);
Click[Mouse::X] = MouseX; Click[Mouse::Y] = MouseY;
currentMouse = GetMouseInput(); // マウス状態を得る
currentStates[(int)Pad::Mouse] = currentMouse; // マウス状態をパッド状態にも併合
// 押してない離した状態のマウス位置を更新
if (currentMouse == 0)
{
Click[Mouse::ReleaseX] = MouseX; Click[Mouse::ReleaseY] = MouseY;
}
else Click[Mouse::All] = 1; // マウスのいずれかのボタンが押されている
// 左マウスドラッグ中のXY位置を更新
if (GetButton(Pad::Mouse, MOUSE_INPUT_LEFT) && !GetButtonDown(Pad::Mouse, MOUSE_INPUT_LEFT))
{
Click[Mouse::DragLX] = MouseX; Click[Mouse::DragLY] = MouseY; Click[Mouse::DragL] = 1;
}
else
Click[Mouse::DragL] = 0;
// 右マウスドラッグ中のXY位置を更新
if (GetButton(Pad::Mouse, MOUSE_INPUT_RIGHT) && !GetButtonDown(Pad::Mouse, MOUSE_INPUT_RIGHT))
{
Click[Mouse::DragRX] = MouseX; Click[Mouse::DragRY] = MouseY; Click[Mouse::DragR] = 1;
}
else
Click[Mouse::DragR] = 0;
//[マウスクリック検知] https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N40
// マウスのボタンが押されたり離されたりしたかどうかの情報を取得する
if (GetMouseInputLog2(&MButton, &ClickX, &ClickY, &LogType, TRUE) == 0)
{ // [左ボタン]が押されたり離されたりしていた
if ((MButton & MOUSE_INPUT_LEFT) != 0)
{ // クリックされた瞬間と離された瞬間のマウス位置を記録
if (LogType == MOUSE_INPUT_LOG_DOWN)
{
Click[Mouse::DownLX] = ClickX; Click[Mouse::DownLY] = ClickY; Click[Mouse::DownL] = 1;
}
else if (LogType == MOUSE_INPUT_LOG_UP)
{
Click[Mouse::UpLX] = ClickX; Click[Mouse::UpLY] = ClickY; Click[Mouse::UpL] = 1;
}
}// [右ボタン]が押されたり離されたりしていた
else if ((MButton & MOUSE_INPUT_RIGHT) != 0)
{ // クリックされた瞬間と離された瞬間のマウス位置を記録
if (LogType == MOUSE_INPUT_LOG_DOWN)
{
Click[Mouse::DownRX] = ClickX; Click[Mouse::DownRY] = ClickY; Click[Mouse::DownR] = 1;
}
else if (LogType == MOUSE_INPUT_LOG_UP)
{
Click[Mouse::UpRX] = ClickX; Click[Mouse::UpRY] = ClickY; Click[Mouse::UpR] = 1;
}
}
}
}
// ボタンが押されているか?
static bool GetButton(Pad pad, int buttonId)
{ //コントローラバグチェック
if (ControllerBugCheck(pad, buttonId)) return false;
if (pad == Pad::None) return false; // Noneなら判別不要
else if (pad == Pad::All) // All指定の時は全てのパッド
{ // GetButtonの中でGetButtonを呼ぶ【再起呼出し】テクニック
for (int i = 0; i < (int)Pad::NUM; i++)
if (GetButton((Pad)i, buttonId))
return true; //押されているPadを発見!
return false; // 一つも押されていなかった
}
if(pad == Pad::Keyboard) // 今ボタンが押されているかどうかを返却
return (currentKey[buttonId] ==1); //https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N28
// 今ボタンが押されているかどうかを返却
return (currentStates[(int)pad] & buttonId) != 0;
}
// ボタンが押された瞬間か?
static bool GetButtonDown(Pad pad, int buttonId)
{ //コントローラバグチェック
if (ControllerBugCheck(pad, buttonId)) return false;
if (pad == Pad::None) return false; // Noneなら判別不要
else if (pad == Pad::All) // All指定の時は全てのパッド
{ // 再起呼出しテクニック
for (int i = 0; i < (int)Pad::NUM; i++)
if (GetButtonDown((Pad)i, buttonId))
return true; //押されているPadを発見!
return false; // 一つも押されていなかった
}
if (pad == Pad::Keyboard) //https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N28
return (currentKey[buttonId] == 1) && (prevKey[buttonId] != 1);
// 今は押されていて、かつ1フレーム前は押されていない場合はtrueを返却
return ((currentStates[(int)pad] & buttonId) & ~(prevStates[(int)pad] & buttonId)) != 0;
}
// ボタンが離された瞬間か?
static bool GetButtonUp(Pad pad, int buttonId)
{ //コントローラバグチェック
if (ControllerBugCheck(pad, buttonId)) return false;
if (pad == Pad::None) return false; // Noneなら判別不要
else if (pad == Pad::All) // All指定の時は全てのパッド
{ // 再起呼出しテクニック
for (int i = 0; i < (int)Pad::NUM; i++)
if (GetButtonUp((Pad)i, buttonId))
return true; //押されているPadを発見!
return false; // 一つも押されていなかった
}
if (pad == Pad::Keyboard) //https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N28
return (prevKey[buttonId] == 1) && (currentKey[buttonId] != 1);
// 1フレーム前は押されていて、かつ今は押されている場合はtrueを返却
return ((prevStates[(int)pad] & buttonId) & ~(currentStates[(int)pad] & buttonId)) != 0;
}
//コントローラのボタンの初回プッシュを検知し
//他コントローラと初回タイミングが完全被りかチェックして重複コントローラの判定処理をスルーする対策
//★コントローラ3を押すと7も反応するなどコントローラ4以降の動作が怪しいのでその対策(DXのバグ?仕様?)
static bool ControllerBugCheck(Pad pad, int buttonId)
{
// 今は押されていて、かつ1フレーム前は押されていない場合
bool buttonDown = ((currentStates[(int)pad] & buttonId) & ~(prevStates[(int)pad] & buttonId)) != 0;
//if (isBugCheckMode == false) return false;
//コントローラの初回ボタンプッシュを検知
if (buttonDown && firstPushTiming[(int)pad] == 0)
{
for (int i = 0; i < (int)Pad::NUM - 1; i++)
{ //全コントローラ初回プッシュ被りがないかチェック完全同時は怪しいので-1
bool isDown = ((currentStates[i] & buttonId) & ~(prevStates[i] & buttonId)) != 0;
if ((Pad)i != pad && isDown ////初回プッシュ被り検出
&& (timing == firstPushTiming[i] || firstPushTiming[i] == 0))
{ //初回プッシュのタイミング被りは-1フラグを記録
firstPushTiming[i] = -1;
}
}
if (firstPushTiming[(int)pad] != -1)
firstPushTiming[(int)pad] = timing;//初回プッシュを記録
}
if (firstPushTiming[(int)pad] == -1) return true; //DXのコントローラ被りバグ!
else return false; //コントローラ被りOK!
}
// キーボードのキーと文字列の対応関係配列の初期化 https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N2
static void InitKeyString()
{
// 辞書配列で対応関係を結び付けておく
KeyString[KEY_INPUT_BACK] = "BackSpace";//"\b";
KeyString[KEY_INPUT_TAB] = "\t";
KeyString[KEY_INPUT_RETURN] = "\n";
KeyString[KEY_INPUT_LSHIFT] = "ShiftL";
KeyString[KEY_INPUT_RSHIFT] = "ShiftR";
KeyString[KEY_INPUT_LCONTROL] = "CtrlL";
KeyString[KEY_INPUT_RCONTROL] = "CtrlR";
KeyString[KEY_INPUT_SPACE] = " ";
KeyString[KEY_INPUT_PGUP] = "PgUp";
KeyString[KEY_INPUT_PGDN] = "PgDn";
KeyString[KEY_INPUT_END] = "End";
KeyString[KEY_INPUT_HOME] = "Home";
KeyString[KEY_INPUT_LEFT] = "Left";
KeyString[KEY_INPUT_UP] = "Up";
KeyString[KEY_INPUT_RIGHT] = "Right";
KeyString[KEY_INPUT_DOWN] = "Down";
KeyString[KEY_INPUT_INSERT] = "Insert";
KeyString[KEY_INPUT_DELETE] = "Delete";
KeyString[KEY_INPUT_MINUS] = "-"; CapsString[KEY_INPUT_MINUS] = "=";
KeyString[KEY_INPUT_YEN] = "\\"; CapsString[KEY_INPUT_YEN] = "|";
KeyString[KEY_INPUT_PREVTRACK] = "^"; CapsString[KEY_INPUT_PREVTRACK] = "~";
KeyString[KEY_INPUT_PERIOD] = "."; CapsString[KEY_INPUT_PERIOD] = ">";
KeyString[KEY_INPUT_SLASH] = "/"; CapsString[KEY_INPUT_SLASH] = "?";
KeyString[KEY_INPUT_LALT] = "AltL";
KeyString[KEY_INPUT_RALT] = "AltR";
KeyString[KEY_INPUT_SCROLL] = "Scroll";
KeyString[KEY_INPUT_SEMICOLON] = ";"; CapsString[KEY_INPUT_SEMICOLON] = "+";
KeyString[KEY_INPUT_COLON] = ":"; CapsString[KEY_INPUT_COLON] = "*";
KeyString[KEY_INPUT_LBRACKET] = "["; CapsString[KEY_INPUT_LBRACKET] = "{";
KeyString[KEY_INPUT_RBRACKET] = "]"; CapsString[KEY_INPUT_RBRACKET] = "}";
KeyString[KEY_INPUT_AT] = "@"; CapsString[KEY_INPUT_AT] = "`";
KeyString[KEY_INPUT_BACKSLASH] = "\\"; CapsString[KEY_INPUT_BACKSLASH] = "_";
KeyString[KEY_INPUT_COMMA] = ","; CapsString[KEY_INPUT_COMMA] = "<";
KeyString[KEY_INPUT_CAPSLOCK] = "CapsLock";
KeyString[KEY_INPUT_PAUSE] = "Pause";
KeyString[KEY_INPUT_NUMPAD0] = "0";
KeyString[KEY_INPUT_NUMPAD1] = "1";
KeyString[KEY_INPUT_NUMPAD2] = "2";
KeyString[KEY_INPUT_NUMPAD3] = "3";
KeyString[KEY_INPUT_NUMPAD4] = "4";
KeyString[KEY_INPUT_NUMPAD5] = "5";
KeyString[KEY_INPUT_NUMPAD6] = "6";
KeyString[KEY_INPUT_NUMPAD7] = "7";
KeyString[KEY_INPUT_NUMPAD8] = "8";
KeyString[KEY_INPUT_NUMPAD9] = "9";
KeyString[KEY_INPUT_MULTIPLY] = "*";
KeyString[KEY_INPUT_ADD] = "+";
KeyString[KEY_INPUT_SUBTRACT] = "-";
KeyString[KEY_INPUT_DECIMAL] = ".";
KeyString[KEY_INPUT_DIVIDE] = "/";
KeyString[KEY_INPUT_NUMPADENTER] = "\n";
KeyString[KEY_INPUT_F1] = "F1";
KeyString[KEY_INPUT_F2] = "F2";
KeyString[KEY_INPUT_F3] = "F3";
KeyString[KEY_INPUT_F4] = "F4";
KeyString[KEY_INPUT_F5] = "F5";
KeyString[KEY_INPUT_F6] = "F6";
KeyString[KEY_INPUT_F7] = "F7";
KeyString[KEY_INPUT_F8] = "F8";
KeyString[KEY_INPUT_F9] = "F9";
KeyString[KEY_INPUT_F10] = "F10";
KeyString[KEY_INPUT_F11] = "F11";
KeyString[KEY_INPUT_F12] = "F12";
KeyString[KEY_INPUT_A] = "a"; CapsString[KEY_INPUT_A] = "A";
KeyString[KEY_INPUT_B] = "b"; CapsString[KEY_INPUT_B] = "B";
KeyString[KEY_INPUT_C] = "c"; CapsString[KEY_INPUT_C] = "C";
KeyString[KEY_INPUT_D] = "d"; CapsString[KEY_INPUT_D] = "D";
KeyString[KEY_INPUT_E] = "e"; CapsString[KEY_INPUT_E] = "E";
KeyString[KEY_INPUT_F] = "f"; CapsString[KEY_INPUT_F] = "F";
KeyString[KEY_INPUT_G] = "g"; CapsString[KEY_INPUT_G] = "G";
KeyString[KEY_INPUT_H] = "h"; CapsString[KEY_INPUT_H] = "H";
KeyString[KEY_INPUT_I] = "i"; CapsString[KEY_INPUT_I] = "I";
KeyString[KEY_INPUT_J] = "j"; CapsString[KEY_INPUT_J] = "J";
KeyString[KEY_INPUT_K] = "k"; CapsString[KEY_INPUT_K] = "K";
KeyString[KEY_INPUT_L] = "l"; CapsString[KEY_INPUT_L] = "L";
KeyString[KEY_INPUT_M] = "m"; CapsString[KEY_INPUT_M] = "M";
KeyString[KEY_INPUT_N] = "n"; CapsString[KEY_INPUT_N] = "N";
KeyString[KEY_INPUT_O] = "o"; CapsString[KEY_INPUT_O] = "O";
KeyString[KEY_INPUT_P] = "p"; CapsString[KEY_INPUT_P] = "P";
KeyString[KEY_INPUT_Q] = "q"; CapsString[KEY_INPUT_Q] = "Q";
KeyString[KEY_INPUT_R] = "r"; CapsString[KEY_INPUT_R] = "R";
KeyString[KEY_INPUT_S] = "s"; CapsString[KEY_INPUT_S] = "S";
KeyString[KEY_INPUT_T] = "t"; CapsString[KEY_INPUT_T] = "T";
KeyString[KEY_INPUT_U] = "u"; CapsString[KEY_INPUT_U] = "U";
KeyString[KEY_INPUT_V] = "v"; CapsString[KEY_INPUT_V] = "V";
KeyString[KEY_INPUT_W] = "w"; CapsString[KEY_INPUT_W] = "W";
KeyString[KEY_INPUT_X] = "x"; CapsString[KEY_INPUT_X] = "X";
KeyString[KEY_INPUT_Y] = "y"; CapsString[KEY_INPUT_Y] = "Y";
KeyString[KEY_INPUT_Z] = "z"; CapsString[KEY_INPUT_Z] = "Z";
KeyString[KEY_INPUT_0] = "0";
KeyString[KEY_INPUT_1] = "1"; CapsString[KEY_INPUT_1] = "!";
KeyString[KEY_INPUT_2] = "2"; CapsString[KEY_INPUT_2] = "\"";
KeyString[KEY_INPUT_3] = "3"; CapsString[KEY_INPUT_3] = "#";
KeyString[KEY_INPUT_4] = "4"; CapsString[KEY_INPUT_4] = "$";
KeyString[KEY_INPUT_5] = "5"; CapsString[KEY_INPUT_5] = "%";
KeyString[KEY_INPUT_6] = "6"; CapsString[KEY_INPUT_6] = "&";
KeyString[KEY_INPUT_7] = "7"; CapsString[KEY_INPUT_7] = "'";
KeyString[KEY_INPUT_8] = "8"; CapsString[KEY_INPUT_8] = "(";
KeyString[KEY_INPUT_9] = "9"; CapsString[KEY_INPUT_9] = ")";
}
}; //←【注意】クラス定義の終わりにはコロンが必要だよ!
#endif //ここでエラー出た人は↑【注意】の;コロン忘れ
Input.cppに変数定義を追加します。
#include "Input.h"
int Input::prevStates[]; // 1フレーム前の状態
int Input::currentStates[]; // 現在の状態
std::unordered_map<Pad, bool> Input::isJoin; // 参加中のパッド番号辞書
std::unordered_map<int, int> Input::padDic; // パッド番号からDXの定義への辞書
int Input::prevMouse; // 1フレーム前のマウス状態
int Input::currentMouse; // 現在のマウス状態
int Input::MouseX{ -1 }, Input::MouseY{ -1 }; // 現在のマウス位置
float Input::MouseWheel{ 0 };
int Input::ClickX{ -1 }, Input::ClickY{ -1 }, Input::MButton{ 0 }, Input::LogType{ 0 };
std::unordered_map<Mouse, int> Input::Click;//<Mouse⇒int> マウスのクリックXY位置辞書
//[キーボード入力]https://dxlib.xsrv.jp/function/dxfunc_input.html#R5N28
char Input::prevKey[256]; // 1フレーム前のキーボード状態
char Input::currentKey[256]; // 現在のキーボード状態
std::string Input::KeyString[256];//<キーコード⇒キー文字列>の辞書
std::string Input::CapsString[256];//<Caps状態キーコード⇒キー文字列>の辞書
bool Input::isCapsLocked{ false };// CapsLock状態か?
bool Input::isShifted{ false };// Shiftが押された状態か?
//★【DXコントローラバグ検知】初回プッシュが被ればDXバグで同一コントローラ
int Input::firstPushTiming[(int)Pad::Four]; // Pad::Fourが最後だから8とかに増やすなら要書き換え
int Input::timing{ 0 };
bool Input::isBugCheckMode = false; //バグチェックを開始するか
TitleScene.hにマウスとキーボード入力を表示するデモのコード
をくわえましょう。#ifndef TITLESCENE_H_
#define TITLESCENE_H_
#include "Scene.h"
#include <string>
#include "DxLib.h"
(中略)...........
class TitleScene : public Scene
{
public:
(中略)...........
std::string keyboardText{""}; // キーボード入力テキスト
int keyDownCount[256]; // 各キーを押している時間をカウントして長押し動作
int longPushNum = 30; // 30フレーム0.5秒長押しで文字がだーっと出る
// コンストラクタ
TitleScene() : Scene()
{
this->tag = "TitleScene";
}
(中略)...........
// ボタン入力に応じた処理
void HandleInput()
{
(中略)...........
// キーボードでテキスト入力
bool isEntered = false; // テンキーのEnterと普通のEnterダブり問題対策
for (int i = 0; i < 256; ++i)
{
if (i == KEY_INPUT_RETURN || i == KEY_INPUT_NUMPADENTER)
if (!isEntered) isEntered = true;
else continue;
if (Input::GetButtonDown(Pad::Keyboard, i))
keyDownCount[i] = 0; // キーが押された瞬間からカウントアップ開始
else if(Input::GetButton(Pad::Keyboard, i))
++keyDownCount[i]; // キーが押されてる時間をカウントアップ
if (0 < keyDownCount[i] && keyDownCount[i] < longPushNum)
continue; // Downの瞬間=0かlongPush以上時間経過してない場合は文字を増やさない
if (Input::GetButton(Pad::Keyboard, i))
{
if (i == KEY_INPUT_BACK || i == KEY_INPUT_DELETE)
{
if (keyboardText.size() > 0)
keyboardText.pop_back(); // 末尾の文字を1つ削除する
}
else if (i == KEY_INPUT_LSHIFT || i == KEY_INPUT_RSHIFT)
{
// シフト文字は出力しない SHIFT + a ⇒ A 大文字表示に
}
else if (Input::isCapsLocked || Input::isShifted) //CapsLockかShift押下中は大文字
keyboardText += (Input::CapsString[i] != "") ? Input::CapsString[i] : Input::KeyString[i];
else
keyboardText += Input::KeyString[i]; // 押された文字をテキスト列に追加
}
}
}
(中略)...........
// 描画処理
void Draw() override
{
(中略)...........
// キーボードのデモ
DrawString(0, 100, keyboardText.c_str(),GetColor(255,255,255));
// マウスのデモ
if (Input::Click[Mouse::All] == 1)
{ // ドラッグしたら
if (Input::Click[Mouse::DragL] == 1)
{ // ドラッグしたら開始XYと四角 □ を表示
std::string drawString = "Drag開始(X,Y)=(" + std::to_string(Input::Click[Mouse::DownLX]) + "," + std::to_string(Input::Click[Mouse::DownLY]) + ")";
DrawString(Input::Click[Mouse::DownLX], Input::Click[Mouse::DownLY], drawString.c_str(), GetColor(255, 255, 255));
// 選択範囲の四角を表示
DrawLineBox(Input::Click[Mouse::DownLX], Input::Click[Mouse::DownLY], Input::Click[Mouse::DragLX], Input::Click[Mouse::DragLY], GetColor(255, 255, 255));
}
// [1] if(Input::GetButton(Pad::Mouse, MOUSE_INPUT_RIGHT) と
// [2] if(Input::Click[Mouse::DragR] == 1) は 同じ意味になる([2]のほうが短く書ける)
if (Input::Click[Mouse::DragR] == 1 && Input::MouseX >= 0 && Input::MouseY >= 0)
{
std::string drawString = "マウス右(X,Y)=(" + std::to_string(Input::MouseX) + "," + std::to_string(Input::MouseY) + ")";
DrawString(Input::MouseX, Input::MouseY, drawString.c_str(), GetColor(255, 255, 255));
}
}
else if (Input::MouseX >= 0 && Input::MouseY >= 0)
{
std::string drawString = "(X,Y)=(" + std::to_string(Input::MouseX) + "," + std::to_string(Input::MouseY) + ")";
DrawString(Input::MouseX, Input::MouseY, drawString.c_str(), GetColor(255, 255, 255));
}
// クリックを離した位置は記録が残る
if (Input::Click[Mouse::UpLX] != -1 && Input::Click[Mouse::UpLY] != -1)
{
std::string drawString = "離した(X,Y)=(" + std::to_string(Input::Click[Mouse::UpLX]) + "," + std::to_string(Input::Click[Mouse::UpLY]) + ")";
DrawString(Input::Click[Mouse::UpLX], Input::Click[Mouse::UpLY], drawString.c_str(), GetColor(255, 255, 255));
}
// ドラッグ開始XYも残るので範囲エリアの四角内のキャラ選択などにも使える
if (Input::Click[Mouse::DownLX] != -1 && Input::Click[Mouse::DownLY] != -1)
{
std::string drawString = "Drag開始(X,Y)=(" + std::to_string(Input::Click[Mouse::DownLX]) + "," + std::to_string(Input::Click[Mouse::DownLY]) + ")";
DrawString(Input::Click[Mouse::DownLX], Input::Click[Mouse::DownLY], drawString.c_str(), GetColor(255, 255, 255));
}
(中略)...........
}
};
#endif
GameClearScene.hを新規作成
して、ベースのScene.hを継承してクリア画面を作成しましょう。
#ifndef GAMECLEARSCENE_H_
#define GAMECLEARSCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Input.h"
#include "Image.h"
#include "GameManager.h"
#include "SceneManager.h"
class GameClearScene : public Scene
{
public:
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
GameManager& gm = GameManager::GetInstance(); //唯一のゲームマネージャへの参照(&)を得る
// コンストラクタ
GameClearScene() : Scene()
{
this->tag = "GameClearScene";
}
// 初期化処理
void Initialize() override
{
}
// 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
void Finalize() override
{
}
// 更新処理
void Update() override
{
if (Input::GetButtonDown(Pad::All, PAD_INPUT_1))
{
sm.LoadScene("TitleScene"); //シーン遷移
}
}
// 描画処理
void Draw() override
{
DrawString(0, 0, "おめでとう!!!!GameClear....ボタン押下でPlaySceneへ", GetColor(255, 255, 255));
}
};
#endif
SceneManager.cppにGameClearシーンの生成処理を追加
してクリアシーンへ遷移する機能を作成しましょう。
#include "SceneManager.h"
#include "Scene.h"
#include "TitleScene.h"
#include "PlayScene.h"
#include "GameOverScene.h"
#include "GameClearScene.h"
bool SceneManager::isChanging(const std::string& nextSceneName)
{
if (currentSceneName != nextSceneName) //シーン名不一致
return true; // シーン名が変更
return false; // シーン名に変更無し
}
void SceneManager::NextScene(const std::string& nextSceneName)
{
// 遷移予定nextシーン名と現在のシーン名を比較、何の変化もなければfalseでシーン移動不必要の判定
if (isChanging(nextSceneName) == false) return; //特に変更なしでシーン移動の必要なし
changingSceneName = nextSceneName; // 次に移動予定のシーン名を保管しておく
}
void SceneManager::LoadScene(const std::string& sceneName)
{
std::string loadSceneName = sceneName;
// sceneNameの指定が空の場合にはNextScene()での事前のシーン変更予約があるかチェック
if (sceneName == "" && changingSceneName != "")
loadSceneName = changingSceneName;
if (loadSceneName == "") return; // シーン指定がないときは終了
// 現在のシーンの終了処理
if(currentScene !=nullptr)
currentScene->Finalize(); // 終了処理を呼び出す
if (loadSceneName == "TitleScene")
{
// 次のシーンの生成
currentScene = std::make_shared<TitleScene>();
}
else if (loadSceneName == "PlayScene")
{
// 次のシーンの生成
currentScene = std::make_shared<PlayScene>();
}
else if (loadSceneName == "GameOverScene")
{
// 次のシーンの生成
currentScene = std::make_shared<GameOverScene>();
}
else if (loadSceneName == "GameClearScene")
{
// 次のシーンの生成
currentScene = std::make_shared<GameClearScene>();
}
else
assert("指定されたシーン名の生成処理が見つからなかった→SceneManager.cppを見直しましょう" == "");
currentSceneName = loadSceneName; // 現在のシーン名の更新
changingSceneName = ""; // NextScene関数のシーン予約名をリセット(リセットしないと次回LoadSceneを引数なしで呼ぶと残存したchangingSceneNameへ飛んじゃう)
// 次のシーンの初期化
currentScene->Initialize(); // 初期化を呼び出す
}
PlayScene.hにゲームクリア画面への遷移予約がある場合に遷移させるため、引数無しのLoadScene();関数呼び出し
を追加しましょう。
#ifndef PLAYSCENE_H_
#define PLAYSCENE_H_
#include "Scene.h"
(中略).........
class PlayScene : public Scene
{
public:
(中略).........
// 更新処理
void Update() override
{
(中略).........
(中略).........
if (isGameover)
{
Sound::PlayMusic(Sound::ending); //BGMを変更する
sm.LoadScene("GameOverScene"); //シーン遷移
return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
}
sm.LoadScene(); // シーン遷移予約がNextScneneで事前予約されていたら遷移
}
(中略).........
};
#endif
Boss.hがisDead=true;になったタイミングで連動してGameClearSceneに遷移するように
NextScene関数の呼び出しを追加します(ついでにボスの爆発も乱数でハデにします)
#ifndef BOSS_H_
#define BOSS_H_
#include "Enemy.h"
(中略).........
#include "SceneManager.h"
// ボスクラス。Enemyを継承して作る
class Boss : public Enemy
{
public:
(中略).........
SceneManager& sm = SceneManager::GetInstance(); //唯一のシーンマネージャへの参照(&)を得る
// コンストラクタ
Boss(Vector3 pos) : Enemy( pos )
{
this->tag = "Boss"; // オブジェクトの種類判別タグ
life = 100; // ライフ
collisionRadius = 70; // 当たり判定半径
}
// 更新処理
void Update() override
{
if (state == State::Appear) // 登場状態
{
(中略).........
}
else if (state == State::Normal) // 通常状態
{
(中略).........
}
else if (state == State::Swoon) // 気絶状態
{
(中略).........
}
else if (state == State::Angry) // 発狂モード
{
(中略).........
}
else if (state == State::Dying) // 死亡中
{
dyingTime--; // タイマー減少
if (dyingTime % 5 == 0) // カウント 5 おきに
{
// やられたら ±(当たり判定の半径 + 40.0の乱数) の位置に爆発を出す
float rangeX = MyRandom::Range(-40.0f - collisionRadius, 40.0f + collisionRadius);
float rangeY = MyRandom::Range(-40.0f - collisionRadius, 40.0f + collisionRadius);
// 爆発を生成
gm.explosions.emplace_back(std::make_shared<Explosion>(Vector3(position.x + rangeX, position.y + rangeY)));
}
if (dyingTime <= 0) // タイマーが0になったら
{
isDead = true; // 完全に消滅
sm.NextScene("GameClearScene"); // ゲームクリア画面への遷移を予約
}
}
}
// 描画処理
void Draw() override
{
(中略).........
}
// 自機弾に当たったときの処理をoverride(上書き)する
void OnCollisionPlayerBullet(std::shared_ptr<GameObject> playerBullet) override
{
(中略).........
}
(中略).........
};
#endif