[C++]3Dタイル描画エンジン1
Resource.h と Resource.cppは次の記事を参考に作っておきます→
画像や音声や3Dのファイルを読み込んで辞書で管理するクラス
Mapフォルダ
を作成して、地形画像ファイルmapchip.png
は2Dアクションゲームの別記事を参考にMap/mapchip.pngで保管されている前提で進めます)
main.cppを新規作成
して、分割テクスチャ画像でタイルを正しく表示できるかテストします。
#include "DxLib.h"
#include "Resource.h"
// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// 画面モードの設定
DxLib::SetGraphMode(960, 540, 32); // 画面サイズ960×540のカラービット数32ビットで起動
DxLib::SetWindowSize(960, 540);// ウィンドウサイズ960×540(こことSetGraphModeのサイズが異なると画像がゆがむ)
DxLib::ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
DxLib::SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
DxLib::SetMainWindowText("3Dタイルゲーム");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも
//↑ここまでの設定は↓下のDXライブラリ初期化より先にやらないとDxLib_Init()中は画面がフルスクリーンになって終わってからウィンドウサイズが変更になり見苦しい
// DXライブラリの初期化
if (DxLib_Init() < 0)
{
// DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
return -1;
}
DxLib::ScreenFlip();
// タイルマップを読み込む 縦8マス、横8マス、縦32ピクセル、横32ピクセル
std::string mapChipPath{ "Map/mapchip.png" };
Resource::MakeShared<Texture>(mapChipPath,8,8,32,32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
// アニメーション(パラパラ漫画)するにはWhile文
while (DxLib::ProcessMessage() == 0)
{// ProcessMessage() == 0になるのは×ボタン押したときなど
DxLib::ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
// 2Dタイル描画処理(位置x, y, 拡大率, 回転, 画像ID, TRUEなら透過有効)
DxLib::DrawRotaGraphF(100, 200, 1.0f, 0, Resource::files[mapChipPath]->m_handles[2], TRUE);
DxLib::ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
}
// キー入力待ちをする
DxLib::WaitKey();
// DXライブラリの後始末
DxLib::DxLib_End();
// ソフトの終了
return 0;
}
MyDraw.hを新規作成
して、3Dで分割テクスチャ画像を描画する関数を準備します。
#ifndef MYDRAW_H_
#define MYDRAW_H_
#include <assert.h> // 画像読込み失敗表示用
#include <cmath> // std::absを使う
#include <vector>
#include "DxLib.h"
#include "Resource.h"
// 3Dのタイルを描くための自作描画系処理のクラス
class MyDraw
{
public:
enum class Plane
{ // XYZ平面の指定
X = 1,
Y,
Z
};
// ★デフォルトではDXライブラリ2D座標系に合わせて右手系 & Yの矢印を画面下方向に設定
//static bool isLeftHanded;
static VECTOR XYZ_ARROW;
static bool IsReverseX_Texture;
static bool IsReverseY_Texture;
static bool IsReverseZ_Texture;
// ★【Y矢印の方向と右手・左手系】の設定に合わせたVECTORを取得
// このVGetを通せば座標系を統一できる
// 入力される座標x,y,zは左上を(x,y,z) = (0,0,0)としてZの奥方向をプラス
// Yをマイナス下方向としたDXライブラリの画面座標系を想定
static VECTOR VGet(float x, float y, float z)
{ // DXライブラリの3D描画座標系に変換
if (XYZ_ARROW.x == 1 && XYZ_ARROW.y == 1 && XYZ_ARROW.z == 1)
{ // DX デフォルト2D描画座標系
return DxLib::VGet(x, y, z);
}
else if (XYZ_ARROW.x == 1 && XYZ_ARROW.y == -1 && XYZ_ARROW.z == 1)
{ // DX 3D描画座標系
return DxLib::VGet(x, -y, z);
}
VECTOR Result;
if (XYZ_ARROW.x == 1) Result.x = x;
else Result.x = -x;
if (XYZ_ARROW.y == 1) Result.y = y;
else Result.y = -y;
if (XYZ_ARROW.z == 1) Result.z = z;
else Result.z = -z;
return Result;
}
inline static VECTOR_D VecAngle(Plane plane, double Angle)
{
VECTOR_D AngleVec = DxLib::VGetD(0.0F, 0.0F, 0.0F);
// 平面に合わせて回転軸を選択
if (plane == Plane::X) AngleVec.x = Angle;
else if (plane == Plane::Y) AngleVec.y = Angle;
else if (plane == Plane::Z) AngleVec.z = Angle;
return AngleVec;
}
// 回転行列からXYZ軸周りの角度(単位:ラジアン)に戻す
inline static VECTOR_D GetAngle(MATRIX_D rotMat)
{
double threshold = 0.001;
VECTOR_D radAngle; // XYZ軸周りの角度(単位:ラジアン)
//[参考:opencvは左手系なので行列位置は斜め鏡映し逆] https://qiita.com/q_tarou/items/46e5045068742dfb2fa6
if (std::abs(rotMat.m[1][2] - 1.0) < threshold) { // rotMat[1][2] = sin(x) = 1の時
radAngle.x = DX_PI / 2;
radAngle.y = 0;
radAngle.z = std::atan2(rotMat.m[0][1], rotMat.m[0][0]);
}
else if (std::abs(rotMat.m[1][2] + 1.0) < threshold) { // rotMat[1][2] = sin(x) = -1の時
radAngle.x = -DX_PI / 2;
radAngle.y = 0;
radAngle.z = std::atan2(rotMat.m[0][1], rotMat.m[0][0]);
}
else {
radAngle.x = std::asin(rotMat.m[1][2]);
radAngle.y = std::atan2(-rotMat.m[0][2], rotMat.m[2][2]);
radAngle.z = std::atan2(-rotMat.m[1][0], rotMat.m[1][1]);
}
return radAngle;
}
// UnityにならってfarClipPlane(z方向のどこまで遠いものを描くか)
// https://qiita.com/shoridevel/items/5c4a249ff09244645c93
//static int farClipPlane;// = 1000;
// カメラの視点、注視点、アップベクトルを設定する( アップベクトルはY軸方向から導き出す )
static int SetCameraPositionAndTarget_UpVecY(VECTOR Position, VECTOR Target)
{
// 右手・左手系とY軸矢印の方向に従い座標を変換
VECTOR Position_COVERT_XYZ = VGet(Position.x, Position.y, Position.z);
VECTOR Target_COVERT_XYZ = VGet(Target.x, Target.y, Target.z);
return DxLib::SetCameraPositionAndTarget_UpVecY(Position_COVERT_XYZ, Target_COVERT_XYZ);
}
// 画像のビルボード描画(常に面がカメラ向き)( 座標指定が float 版 )
static int DrawDivBillboardGraphF3D(Plane plane, float xf, float yf, float zf, double ExRate, VECTOR_D Angle, Texture& divTexture, int id, int TransFlag = TRUE, int ReverseXFlag = FALSE, int ReverseYFlag = FALSE, int ReverseZFlag = FALSE)
{
return MyDraw::DrawDivRotaGraphF3D(plane, xf, yf, zf, ExRate, Angle, divTexture, id, TransFlag, ReverseXFlag, ReverseYFlag, ReverseZFlag, true); // ビルボード描画フラグをtrueに
}
// 画像の回転描画( 座標指定が float 版 )
static int DrawDivRotaGraphF(float xf, float yf, double ExRate, double Angle, Texture& divTexture, int id, int TransFlag, int ReverseXFlag = FALSE, int ReverseYFlag = FALSE, int ReverseZFlag = FALSE)
{ // Zの値を0.0Fとして
return MyDraw::DrawDivRotaGraphF3D(Plane::Z, xf, yf, 0.0F, ExRate, Angle, divTexture, id, TransFlag, ReverseXFlag, ReverseYFlag, ReverseZFlag);
}
// 画像の回転描画( 座標指定が float 版 )【注意!】現状Angle回転機能は未対応
static int DrawDivRotaGraphF3D(Plane plane, float xf, float yf, float zf, double ExRate, double Angle, Texture& divTexture, int id, int TransFlag = TRUE, int ReverseXFlag = FALSE, int ReverseYFlag = FALSE, int ReverseZFlag = FALSE)
{
VECTOR_D AngleVec = VecAngle(plane, Angle);
// 角度だけ↓VECTOR化
return MyDraw::DrawDivRotaGraphF3D(plane, xf, yf, zf, ExRate, AngleVec, divTexture, id, TransFlag, ReverseXFlag, ReverseYFlag, ReverseZFlag);
} // ↓すぐ下のAngleベクタ版関数に引き継ぐ
// 画像の回転描画( 座標指定が float 版 )【注意!】現状Angle回転機能は未対応
static int DrawDivRotaGraphF3D(Plane plane, float xf, float yf, float zf, double ExRate, VECTOR_D Angle, Texture& divTexture, int id, int TransFlag = TRUE, int ReverseXFlag = FALSE, int ReverseYFlag = FALSE, int ReverseZFlag = FALSE, bool isBillboard = false)
{
// 遠すぎるものは描かないで即return
//if (zf > farClipPlane) return -1;
// 右手・左手系とY軸矢印の方向に従い座標を変換
VECTOR COVERT_XYZ = MyDraw::VGet(xf, yf, zf);
float Xf = COVERT_XYZ.x, Yf = COVERT_XYZ.y, Zf = COVERT_XYZ.z;
if (id < 0 && divTexture.m_handles.size() <= id) { assert("div画像id指定範囲外" == ""); return -1; }
int imageWidth = divTexture.m_XSize;
int imageHeight = divTexture.m_YSize;
float u_X0 = 0.0F, v_Y0 = 0.0F; //点Vertex[0]のuとvの値
float u_X3 = 1.0F, v_Y3 = 1.0F; //点Vertex[3]のuとvの値
if (divTexture.m_XNum > 1 || divTexture.m_YNum > 1)
{ // div分割画像の分割位置確定のため横uと縦vの値を計算
int divX = id % divTexture.m_XNum;//分割X列番号
int divY = id / divTexture.m_XNum;//分割Y行番号
u_X0 = (float)divX / (float)(divTexture.m_XNum);
v_Y0 = (float)divY / (float)(divTexture.m_YNum);
u_X3 = (float)(divX + 1) / (float)(divTexture.m_XNum);
v_Y3 = (float)(divY + 1) / (float)(divTexture.m_YNum);
}
// 【拡大縮小率ExRate】をかけて画像サイズを再計算
imageWidth = (int)(imageWidth * ExRate); // 拡大縮小した画像の幅
imageHeight = (int)(imageHeight * ExRate);// 拡大縮小した画像の高さ
float halfWidth = (float)(imageWidth / 2);//画像幅の1/2(先に計算し使いまわして処理削減)
float halfHeight = (float)(imageHeight / 2);//画像高さの1/2(先に計算し使いまわして処理削減)
//【テクスチャ反転処理】幅halfWidthと高さhalfHeightをマイナスにすることで反転
// ★ReverseXFlagとIsReverseX_Texture両方反転は【反転の反転で=反転しない!】
// 【反転の反転で=反転しない!】は★XOR【排他的論理和 ^ 演算子】
// [排他的論理和 ^ 演算子]http://www7b.biglobe.ne.jp/~robe/cpphtml/html01/cpp01047.html
if ((IsReverseX_Texture ^ (ReverseXFlag == TRUE))
&& (plane == Plane::Y || plane == Plane::Z))
{ // マイナス幅でテクスチャ反転
halfWidth = -halfWidth;
}
if ((IsReverseY_Texture ^ (ReverseYFlag == TRUE))
&& (plane == Plane::X || plane == Plane::Z))
{ // マイナス幅でテクスチャ反転
halfHeight = -halfHeight;
}
if (IsReverseZ_Texture ^ (ReverseZFlag == TRUE))
{ // マイナス幅でテクスチャ反転
if (plane == Plane::X) halfWidth = -halfWidth;
if (plane == Plane::Y) halfHeight = -halfHeight;
}
// [頂点VERTEXは四角形は6個必要] https://dixq.net/rp/57.html
// ★CGの【最小単位は三角形】2Dはドットの集合で描くが【CGは三角形の集合で描く】
VERTEX3D Vertex[6]; // ポリゴン2枚なので、頂点は6個(三角3点×2)
// VERTEX3DSHADER Vertex[6]; // シェーダの頂点を使う場合
// ★頂点情報のセット
{ // Plane=X なら X平面に(Y,Z) = (yf,zf)中心に描画
// Plane=Y なら Y平面に(X,Z) = (xf,zf)中心に描画
// Plane=Z なら Z平面に(X,Y) = (xf,yf)中心に描画
// 回転は以下リンク参照
// https://dxlib.xsrv.jp/cgi/patiobbs/patio.cgi?mode=past&no=2749
MATRIX TransformMatrix;
// 回転( x, y, z軸回転の順に回転した後、座標移動行列により平行移動)
// ★★MMultは行列の乗算【乗算した順にx軸回転→y軸回転→z軸回転→平行移動】
//★【複数軸回転は頭が混乱するのでクォータニオンをつかうやり方もある】
if (isBillboard)
{
VECTOR_D billboardAngle = (isBillboard) ? GetAngle(DxLib::GetCameraBillboardMatrixD()) : DxLib::VGetD(0, 0, 0);
Angle.x += billboardAngle.x; Angle.y += billboardAngle.y; Angle.z += billboardAngle.z;
}
TransformMatrix = MGetRotX((float)Angle.x);
TransformMatrix = MMult(TransformMatrix, MGetRotY((float)Angle.y));
TransformMatrix = MMult(TransformMatrix, MGetRotZ((float)Angle.z));
//★【座標移動行列により移動】
TransformMatrix = MMult(TransformMatrix, MGetTranslate(VGet(Xf, Yf, Zf)));
if (plane == Plane::X)
{ //座標固定平面→→→→→→→→→↓X平面指定
Vertex[0].pos = VTransform(VGet(0.0F, +halfHeight, -halfWidth), TransformMatrix);
Vertex[1].pos = VTransform(VGet(0.0F, +halfHeight, +halfWidth), TransformMatrix);
Vertex[2].pos = VTransform(VGet(0.0F, -halfHeight, -halfWidth), TransformMatrix);
Vertex[3].pos = VTransform(VGet(0.0F, -halfHeight, +halfWidth), TransformMatrix);
}
else if (plane == Plane::Y)
{ //座標固定平面→→→→→→→→→→→→→→→→↓Y平面指定
Vertex[0].pos = VTransform(VGet(-halfWidth, 0.0F, +halfHeight), TransformMatrix);
Vertex[1].pos = VTransform(VGet(+halfWidth, 0.0F, +halfHeight), TransformMatrix);
Vertex[2].pos = VTransform(VGet(-halfWidth, 0.0F, -halfHeight), TransformMatrix);
Vertex[3].pos = VTransform(VGet(+halfWidth, 0.0F, -halfHeight), TransformMatrix);
}
else // if (plane == Plane::Z)
{ //座標固定平面→→→→→→→→→→→→→→→→→→→→→→↓Z平面指定
Vertex[0].pos = VTransform(VGet(-halfWidth, +halfHeight, 0.0F), TransformMatrix);
Vertex[1].pos = VTransform(VGet(+halfWidth, +halfHeight, 0.0F), TransformMatrix);
Vertex[2].pos = VTransform(VGet(-halfWidth, -halfHeight, 0.0F), TransformMatrix);
Vertex[3].pos = VTransform(VGet(+halfWidth, -halfHeight, 0.0F), TransformMatrix);
}
Vertex[4].pos = Vertex[2].pos; // 点2と点4は同じ位置
Vertex[5].pos = Vertex[1].pos; // 点1と点5は同じ位置
//↑点0 点1&5 点4&2 点3 の4点の四角形を描く
// ★div分割画像を考慮に入れて【UV展開】切り出し位置を0.0~1.0で指定
Vertex[0].u = u_X0; Vertex[0].v = v_Y0; Vertex[0].su = u_X0; Vertex[0].sv = v_Y0;
Vertex[1].u = u_X3; Vertex[1].v = v_Y0; Vertex[1].su = u_X3; Vertex[1].sv = v_Y0;
Vertex[2].u = u_X0; Vertex[2].v = v_Y3; Vertex[2].su = u_X0; Vertex[2].sv = v_Y3;
Vertex[3].u = u_X3; Vertex[3].v = v_Y3; Vertex[3].su = u_X3; Vertex[3].sv = v_Y3;
Vertex[4].u = u_X0; Vertex[4].v = v_Y3; Vertex[4].su = u_X0; Vertex[4].sv = v_Y3;
Vertex[5].u = u_X3; Vertex[5].v = v_Y0; Vertex[5].su = u_X3; Vertex[5].sv = v_Y0;
// 輝度(拡散光:Diffuseの明るさ)は全地点100% a(拡散光:Diffuseのアルファ不透明度) も最大値(255=不透明 0=透明)
Vertex[0].dif = GetColorU8(255, 255, 255, 255);
Vertex[1].dif = GetColorU8(255, 255, 255, 255);
Vertex[2].dif = GetColorU8(255, 255, 255, 255);
Vertex[3].dif = GetColorU8(255, 255, 255, 255);
Vertex[4].dif = GetColorU8(255, 255, 255, 255);
Vertex[5].dif = GetColorU8(255, 255, 255, 255);
// 鏡面反射光(スペキュラ)は全地点30
Vertex[0].spc = GetColorU8(0, 0, 0, 255);
Vertex[1].spc = GetColorU8(0, 0, 0, 255);
Vertex[2].spc = GetColorU8(0, 0, 0, 255);
Vertex[3].spc = GetColorU8(0, 0, 0, 255);
Vertex[4].spc = GetColorU8(0, 0, 0, 255);
Vertex[5].spc = GetColorU8(0, 0, 0, 255);
// 法線を設定
Vertex[0].norm = VGet(0.0f, 0.0f, -1.0f); // Vertex[0].binorm = // シェーダーを利用するとき
Vertex[1].norm = VGet(0.0f, 0.0f, -1.0f); // Vertex[1].binorm =
Vertex[2].norm = VGet(0.0f, 0.0f, -1.0f); // Vertex[2].binorm =
Vertex[3].norm = VGet(0.0f, 0.0f, -1.0f); // Vertex[3].binorm =
Vertex[4].norm = VGet(0.0f, 0.0f, -1.0f); // Vertex[4].binorm =
Vertex[5].norm = VGet(0.0f, 0.0f, -1.0f); // Vertex[5].binorm =
// 0 1 2
// 5 4 3
//↑上の順の点で点0 点1&5 点4&2 点3 の4点の四角形を描く
}
//DxLib::DrawSphere3D(VGet(Xf, Yf, Zf),1, 4, GetColor(255, 255, 255),GetColor(255, 255, 255),FALSE); // テストとして四角形の中心に目印の球体を描く
// ポリゴンを2枚描画
DxLib::DrawPolygon3D(Vertex, 2, divTexture[id], TransFlag);
//if (divTexture[id] != -1) //[シェーダーを利用する場合] https://dxlib.xsrv.jp/cgi/patiobbs/patio.cgi?mode=view&no=4204
// DxLib::SetUseTextureToShader(0, divTexture[id]); // 使用するテクスチャを0番にセット
// シェーダーを利用して描く場合はこちら https://dxlib.xsrv.jp/cgi/patiobbs/patio.cgi?mode=view&no=4204
//DxLib::DrawPolygon3DToShader(Vertex, 2); // シェーダーにも対応できるようにToShader系関数を通して描く
}
// 画像ボックスの回転描画( 座標指定が float 版 )
static void DrawDivBoxF3D(float xf, float yf, float zf, double ExRate, Texture& divTexture, int id, int TransFlag = TRUE, int ReverseXFlag = FALSE, int ReverseYFlag = FALSE, int ReverseZFlag = FALSE)
{
int imageWidth = (int)((double)divTexture.m_XSize * ExRate);
int imageHeight = (int)((double)divTexture.m_YSize * ExRate);
VECTOR_D Angle = VGetD(0,0,0);
//サイコロの6面を描く
MyDraw::DrawDivRotaGraphF3D(Plane::Y, xf, yf - imageHeight / 2, zf, ExRate, Angle, divTexture, id, TransFlag, ReverseXFlag, ReverseYFlag, ReverseZFlag);
MyDraw::DrawDivRotaGraphF3D(Plane::Y, xf, yf + imageHeight / 2, zf, ExRate, Angle, divTexture, id, TransFlag, ReverseXFlag, ReverseYFlag, ReverseZFlag);
// X方向の裏側は左右反転
MyDraw::DrawDivRotaGraphF3D(Plane::X, xf - imageWidth / 2, yf, zf, ExRate, Angle, divTexture, id, TransFlag, ReverseXFlag, ReverseYFlag, !ReverseZFlag);
MyDraw::DrawDivRotaGraphF3D(Plane::X, xf + imageWidth / 2, yf, zf, ExRate, Angle, divTexture, id, TransFlag, ReverseXFlag, ReverseYFlag, ReverseZFlag);
MyDraw::DrawDivRotaGraphF3D(Plane::Z, xf, yf, zf - imageHeight / 2, ExRate, Angle, divTexture, id, TransFlag, ReverseXFlag, ReverseYFlag, ReverseZFlag);
// Z方向の裏側は左右反転
MyDraw::DrawDivRotaGraphF3D(Plane::Z, xf, yf, zf + imageHeight / 2, ExRate, Angle, divTexture, id, TransFlag, !ReverseXFlag, ReverseYFlag, ReverseZFlag);
return;
}
// 床の回転描画( 座標指定が float 版 )
static int DrawDivRotaFloorF3D(Plane plane, float xf, float yf, float zf, double ExRate, double Angle, Texture& divTexture, int id, int TransFlag = TRUE, int ReverseXFlag = FALSE, int ReverseYFlag = FALSE, int ReverseZFlag = FALSE)
{
VECTOR_D AngleVec = VecAngle(plane, Angle);
int imageHeight = (int)((double)divTexture.m_YSize * ExRate);
// 角度だけ↓VECTOR化
return MyDraw::DrawDivRotaGraphF3D(plane, xf, yf - imageHeight / 2, zf, ExRate, AngleVec, divTexture, id, TransFlag, ReverseXFlag, ReverseYFlag, ReverseZFlag);
}
// 四角形(枠線のみ)を描画する
static int DrawLineBox(VECTOR startPos, VECTOR endPos, unsigned int color)
{
return DxLib::DrawCube3D(startPos, endPos, color, color, FALSE);
}
};
#endif
MyDraw.cppを新規作成
して、staticに関する定義をします。
#include "MyDraw.h"
//int MyDraw::farClipPlane = 1000; // 仮にZ=1000までの遠さのものを描く設定で
// ★デフォルトではDXライブラリ2D座標系に合わせて右手系&Yの矢印を画面下方向に設定
// http://www.f.waseda.jp/moriya/PUBLIC_HTML/education/classes/infomath6/applet/fractal/coord/
//bool MyDraw::isLeftHanded = false;
VECTOR MyDraw::XYZ_ARROW = DxLib::VGet(1, -1, 1);
bool MyDraw::IsReverseX_Texture = false;
bool MyDraw::IsReverseY_Texture = true;
bool MyDraw::IsReverseZ_Texture = false;
main.cpp
にプログラムを追加し、MyDrawクラスを通して、ポリゴンでサイコロ状のボックスを描いてみましょう。
#include "DxLib.h"
#include "Resource.h"
#include "MyDraw.h"
// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// 画面モードの設定
DxLib::SetGraphMode(960, 540, 32); // 画面サイズ960×540のカラービット数32ビットで起動
DxLib::SetWindowSize(960, 540);// ウィンドウサイズ960×540(こことSetGraphModeのサイズが異なると画像がゆがむ)
DxLib::ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
DxLib::SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
DxLib::SetMainWindowText("3Dタイルゲーム");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも
//↑ここまでの設定は↓下のDXライブラリ初期化より先にやらないとDxLib_Init()中は画面がフルスクリーンになって終わってからウィンドウサイズが変更になり見苦しい
// DXライブラリの初期化
if (DxLib_Init() < 0)
{
// DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
return -1;
}
DxLib::ScreenFlip();
// タイルマップを読み込む 縦8マス、横8マス、縦32ピクセル、横32ピクセル
std::string mapChipPath{ "Map/mapchip.png" };
Resource::MakeShared<Texture>(mapChipPath,8,8,32,32)->Load(); // メソッドチェーン -> でMakeShared読込語にLoadもする
// アニメーション(パラパラ漫画)するにはWhile文
while (DxLib::ProcessMessage() == 0)
{// ProcessMessage() == 0になるのは×ボタン押したときなど
DxLib::ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
// 光の明暗計算を無効に
DxLib::SetUseLighting(FALSE);
// 2Dタイル描画処理(位置x, y, 拡大率, 回転, 画像ID, TRUEなら透過有効)
DxLib::DrawRotaGraphF(100, 200, 1.0f, 0, Resource::files[mapChipPath]->m_handles[2], TRUE);
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(40, 40, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 2);
DxLib::ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
}
// キー入力待ちをする
DxLib::WaitKey();
// DXライブラリの後始末
DxLib::DxLib_End();
// ソフトの終了
return 0;
}
main.cpp
のプログラムを変更し、Zバッファを有効にして描いてみましょう。
#include "DxLib.h"
#include "Resource.h"
#include "MyDraw.h"
// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// 画面モードの設定
DxLib::SetGraphMode(960, 540, 32); // 画面サイズ960×540のカラービット数32ビットで起動
DxLib::SetWindowSize(960, 540);// ウィンドウサイズ960×540(こことSetGraphModeのサイズが異なると画像がゆがむ)
DxLib::ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
DxLib::SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
DxLib::SetMainWindowText("3Dタイルゲーム");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも
//↑ここまでの設定は↓下のDXライブラリ初期化より先にやらないとDxLib_Init()中は画面がフルスクリーンになって終わってからウィンドウサイズが変更になり見苦しい
// DXライブラリの初期化
if (DxLib_Init() < 0)
{
// DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
return -1;
}
DxLib::ScreenFlip();
// タイルマップを読み込む 縦8マス、横8マス、縦32ピクセル、横32ピクセル
std::string mapChipPath{ "Map/mapchip.png" };
Resource::MakeShared<Texture>(mapChipPath,8,8,32,32)->Load(); // メソッドチェーン -> でMakeShared読込語にLoadもする
// アニメーション(パラパラ漫画)するにはWhile文
while (DxLib::ProcessMessage() == 0)
{// ProcessMessage() == 0になるのは×ボタン押したときなど
DxLib::ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
// 光の明暗計算を無効に
DxLib::SetUseLighting(FALSE);
// Zバッファを有効にする [Zの深度]
//[参考] https://dxlib.xsrv.jp/function/dxfunc_3d_draw.html#R14N12
DxLib::SetUseZBuffer3D(TRUE);
// Zバッファへの書き込みを有効にする
DxLib::SetWriteZBuffer3D(TRUE);
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(40, 40, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 2);
DxLib::ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
}
// キー入力待ちをする
DxLib::WaitKey();
// DXライブラリの後始末
DxLib::DxLib_End();
// ソフトの終了
return 0;
}
Input.hとInput.cppを別記事から作成しておきます→
Inputクラスを拡張してマウス、キーボード対応させる
Vector3.hとVector3.cppを別記事から作成しておきます→
Vector3に色々な関数を用意して3D空間の当たり判定をする
Screen.hを新規作成
して、デフォルトの画面全体の縦横のサイズと違うサイズの別スクリーンがStar()~End()関数で指定されたときにも、
視野(FOV=Field Of View:フィールドのビュー)すなわちスクリーンに描かれる3Dの範囲を StartFov()~EndFov()関数でDraw系関数を囲うことで、
一時的にカメラの射影変換行列(プロジェクション行列)の幅w 高さhの行列のパラメータを変更した状態で、GPUに描かせることができるようにしましょう。
#ifndef _SCREEN_H
#define _SCREEN_H
#include "DxLib.h"
#include "Input.h"
#include <assert.h> // アサート警告表示用
// 画面解像度
class Screen
{
public: //publicはC#と違いpublic:以下にまとめて書かれるスタイル
static constexpr int Width = 1280; // 幅(Nintendoスイッチの縦横サイズ[いわゆるHD画質16:9]を採用)
static constexpr int Height = 720; // 高さ
// X,Yがウィンドウの中にあるか
static bool IsInsideWindow(int screenX = Input::MouseX, int screenY = Input::MouseY) { return (0 <= screenX && screenX < Width) && (0 <= screenY && screenY < Height); }
static int SubWidth, tmpSubWidth; // 現在指定中のサブのスクリーンの幅:SubWidth と 指定前のスクリーンの幅:tmpSubWidth
static int SubHeight, tmpSubHeight; // 現在指定中のサブのスクリーンの高さ:SubHeight と 指定前のスクリーンの高さ:tmpSubHeight
static int DrawHandle; // Start()~End()で指定中のハンドル番号
static int tmpDrawHandle; // Endまでのあいだ一旦変更前のhandleを退避
static int tmpResetSetting; // SetDrawScreen切り替え前に 設定を一旦tmpに退避
static MATRIX_D tmpViewportMatrix; // StartFov前に もとのビューポート行列を一旦退避
static MATRIX_D tmpProjectionMatrix; // StartFov前に もとの射影変換行列を一旦退避
// 射影変換行列とビューポート行列をスクリーンの大きさに合わせて再設定
static void StartFov(int handle, double fovAngle = 0)
{ // [描画の縦横領域が変わっても画角維持] https://dxlib.xsrv.jp/cgi/patiobbs/patio.cgi?mode=view&no=4639
tmpViewportMatrix = DxLib::GetCameraViewportMatrixD(); // もとのビューポート行列を一旦退避
tmpProjectionMatrix = DxLib::GetCameraProjectionMatrixD(); // もとの射影変換行列を一旦退避
int handleWidth, handleHeight; // 描画するスクリーンのサイズを得る
DxLib::GetGraphSize(handle, &handleWidth, &handleHeight);
// 射影行列を再設定
MATRIX_D ProjectionMatrix;
double fovRad = (fovAngle == 0) ? DxLib::GetCameraFovD() : DX_PI * fovAngle / 180.0; // スクリーンに描くカメラの画角の設定 0のときは既存のFOVの設定を適用
auto dotAspect = DxLib::GetCameraDotAspectD(); // スクリーンに描くカメラのドットアスペクト比率
DxLib::CreatePerspectiveFovMatrixD(&ProjectionMatrix, fovRad, DxLib::GetCameraNear(), DxLib::GetCameraFar(), (double)handleHeight / handleWidth * dotAspect);
DxLib::SetTransformToProjectionD(&ProjectionMatrix); // 射影変換行列をスクリーン幅や高さやFOVの設定に合わせて一時的に再設定
#if 0 // [参考]デフォルトの射影変換行列のDXライブラリの内部計算
#define FOV (60.0 * DX_PI / 180.0)
#define NEARZ (0.0)
#define FARZ (1000.0)
#define TAN_FOV_HALF (0.52359877559829887307710723054658) // /*(0.57735026918962573)*/ tan( FOV * 0.5 )
// (略)...
w = GSYS.DrawSetting.DrawSizeX;
h = GSYS.DrawSetting.DrawSizeY;
D = (double)((h / 2) / TAN_FOV_HALF);
// (略)...
CreatePerspectiveFovMatrixD(&mat, FOV, D * 0.1, D + FARZ, -1.0f); // デフォルトの射影変換行列 呼び出し
// (略)...
// ↓ デフォルトの射影変換行列
MATRIX_D mat; // ProjectionMatrix 射影行列
double Sin = std::sin(fov / 2), Cos = std::cos(fov / 2); // tan(θ / 2) = Sin / Cos θ = fovのとき
if (-0.0001 < Sin && Sin < 0.0001) return -1;
aspect = (double)MathScreenSizeY / (double)MathScreenSizeX;
double w = aspect * (Cos / Sin); // = 高/幅 / tan( θ / 2 )
double h = 1.0 * (Cos / Sin); // = 1 / tan( θ / 2 )
mat->m[0][0] = w; mat->m[0][1] = 0; mat->m[0][2] = 0; mat->m[0][3] = 0;
mat->m[1][0] = 0; mat->m[1][1] = h; mat->m[1][2] = 0; mat->m[1][3] = 0;
mat->m[2][0] = 0; mat->m[2][1] = 0; mat->m[2][2] = zFar / (zFar - zNear); mat->m[2][3] = 1.0;
mat->m[3][0] = 0; mat->m[3][1] = 0; mat->m[3][2] = -zNear * zFar / (zFar - zNear); mat->m[3][3] = 0.0;
#endif
// ビューポート行列の再設定
//[スクリーン上の3Dの消失点をスクリーンのサイズの1/2の真ん中の位置に設定] https://dxlib.xsrv.jp/function/dxfunc_3d_camera.html#R12N12
DxLib::SetCameraScreenCenter((float)handleWidth / 2.0f, (float)handleHeight / 2.0f);
}
// 射影変換行列とビューポート行列をStartFovする前に戻す
static void EndFov()
{
DxLib::SetTransformToProjectionD(&tmpViewportMatrix);
DxLib::SetTransformToViewportD(&tmpProjectionMatrix);
}
// ★このStartとEndのあいだの描画処理は別スクリーンに描かれるようになる is3Dに
static int Start(int handle = DX_SCREEN_BACK, bool is3D = false, double fovAngle = 0)
{ // スクリーンのハンドルが 0以下 なら普通のスクリーンに描画する
if (handle < 0) handle = DX_SCREEN_BACK;
Screen::tmpResetSetting = DxLib::GetUseSetDrawScreenSettingReset(); // 設定を一旦tmpに退避
DxLib::SetUseSetDrawScreenSettingReset(FALSE); // SetDrawScreen切り替え時に設定をリセットしない(FALSE)(これをやらないとカメラ視点などがリセットされるから)
Screen::tmpDrawHandle = DxLib::GetDrawScreen(); // Endまでのあいだ一旦変更前のhandleを退避
DxLib::GetGraphSize(tmpDrawHandle, &tmpSubWidth, &tmpSubHeight); // 現在のスクリーンのサイズを一旦退避
// 描く先を別途用意した別スクリーン(指定されたhandle)に設定
if (DxLib::SetDrawScreen(handle) == -1) DxLib::SetDrawScreen(handle = DX_SCREEN_BACK); // -1 失敗したら普通のDX_SCREEN_BACKのほうに描く
Screen::DrawHandle = handle; // 現在選択中のスクリーンのハンドル番号を handleに 変更
// 描画するスクリーンのサイズを得る
if (DxLib::GetGraphSize(handle, &Screen::SubWidth, &Screen::SubHeight) == -1) return handle; // -1 失敗したらreturn
if (is3D) // 3Dのときは
Screen::StartFov(handle, fovAngle); // 射影変換行列とビューポート行列をスクリーンの大きさに合わせて再設定
return handle;
}
// ★このStartとEndのあいだの描画処理は別スクリーンに描かれるようになる
static void End(bool is3D = false)
{ // ゲームを描く先をもとのデフォルトに戻しておく
DxLib::SetDrawScreen(tmpDrawHandle = (tmpDrawHandle < 0) ? DX_SCREEN_BACK : tmpDrawHandle);
Screen::DrawHandle = tmpDrawHandle; // 現在選択中のスクリーンのハンドル番号を handleに 変更
int beforeSubWidth = Screen::SubWidth, beforeSubHeight = Screen::SubHeight; // Start()で変更した幅と高さを 一時退避
if (DxLib::GetGraphSize(tmpDrawHandle, &Screen::SubWidth, &Screen::SubHeight) == -1) return; // -1 失敗したらreturn
if (is3D) // 3Dのときは
Screen::EndFov(); // 射影変換行列とビューポート行列をStartFovする前に戻す
DxLib::SetUseSetDrawScreenSettingReset((tmpResetSetting == -1) ? TRUE : tmpResetSetting); // 設定をStart()前の元に戻す
}
// 指定した screenHandle のキャンバスをきれいにまっさらにクリアする
static void ClearDrawScreen(int screenHandle)
{
Screen::Start(screenHandle);
{
DxLib::ClearDrawScreen(); // screenHandle のキャンバスをきれいにまっさらに
}
Screen::End();
}
};// C#と違ってクラスの定義も;セミコロンで終わる
#endif
Screen.cppを新規作成
して、Start()~End()やStartFov()~EndFov()の間にはさんでDrawコールするあいだ、
Start()で一時的にパラメータを変更する前のカメラの行列のパラメータを一旦保管するtmp~系変数をstatic定義します。
#include "Screen.h"
int Screen::SubWidth{ 0 }, Screen::tmpSubWidth{ 0 }; // 現在指定中のサブのスクリーンの幅:SubWidth と 指定前のスクリーンの幅:tmpSubWidth
int Screen::SubHeight{ 0 }, Screen::tmpSubHeight{ 0 }; // 現在指定中のサブのスクリーンの高さ:SubHeight と 指定前のスクリーンの高さ:tmpSubHeight
int Screen::DrawHandle{ DxLib::GetDrawScreen() }; // Start()~End()で指定中のハンドル番号
int Screen::tmpDrawHandle{ -1 }; // Endまでのあいだ一旦変更前のhandleを退避
int Screen::tmpResetSetting{ -1 }; // SetDrawScreen切り替え前に 設定を一旦tmpに退避
MATRIX_D Screen::tmpViewportMatrix; // StartFov前に もとのビューポート行列を一旦退避
MATRIX_D Screen::tmpProjectionMatrix; // StartFov前に もとの射影変換行列を一旦退避
Camera.hを新規作成
して、GetCameraSetting関数などで現在のFov視野角などの情報を得たり、個別にカメラをコンストラクタで初期化できるようにしましょう。
#ifndef CAMERA_H_
#define CAMERA_H_
#include <functional> // カメラのDraw関数に「引数として関数=ラムダ式」を渡してカメラのDraw関数の外側の呼び出し元から描画したい処理を渡すことができるようにする
#include "DxLib.h"
#include "Screen.h"
#include "MyDraw.h"
#include "Vector3.h"
class Camera
{
public:
// カメラの位置(ワールド座標)
union {
Vector3 pos; // カメラの位置(ワールド座標)
struct { float x, y, z; }; // unionしてx,y,zでもアクセスできるように
};
struct Rotate { double V, H, T; }; // カメラの回転角度定義 DXライブラリの内部行列ではfloatじゃなくてdouble型で保持されているので倍精度のdouble型にした
Rotate rot{ 0.0, 0.0, 0.0 }; // カメラの3軸の回転角度
double fovAngle = 0; // カメラをスクリーンに描くときの視野角(0~179度) 0のときはDxLib::GetCameraFovD()で得られる値が適用される
float cameraLookAtHeight{ 0.0f }; // LookAtする対象を見下ろす高さ
int screenHandle = DX_SCREEN_BACK; //カメラを描くスクリーンのハンドル番号
// コンストラクタ
Camera(Vector3 worldPos) : pos{ worldPos } {}
// デストラクタ
~Camera() {}
// カメラを通して描くスクリーンのハンドル番号を設定する
void SetScreenHandle(int handle) { screenHandle = handle; }
// カメラの位置をセットしなおす
void SetPosition(Vector3 worldPos)
{ // ワールド座標系として保存
this->pos = worldPos;
}
// 指定されたワールド座標が画面の中心に来るように、カメラの位置を変更する
void LookAt(Vector3 targetXYZ)
{
// カメラの位置と向きを設定
//MyDraw::SetCameraPositionAndTarget_UpVecY(pos, targetXYZ + Vector3(0, cameraLookAtHeight,0));
// 注視点はターゲットの座標から cameraLookAtHeight 分だけ高い位置
DxLib::SetCameraPositionAndTarget_UpVecY(this->pos, targetXYZ + Vector3(0, this->cameraLookAtHeight, 0));
this->rot.H = DxLib::GetCameraAngleHRotateD(); // カメラの水平方向の向きを取得
this->rot.V = DxLib::GetCameraAngleVRotateD();
this->rot.T = DxLib::GetCameraAngleTRotateD();
}
// 現在のDX内部のカメラのビュー行列などに入っているカメラの設定パラメータを得る
static void GetCameraSettings(VECTOR_D* pPosition, double* pRotV = nullptr, double* pRotH = nullptr, double* pRotT = nullptr,
double* pFov = nullptr, double* pNear = nullptr, double* pFar = nullptr)
{
if (pPosition != nullptr) *pPosition = DxLib::GetCameraPositionD(); // カメラ位置
if (pRotV != nullptr) *pRotV = DxLib::GetCameraAngleVRotateD(); // カメラの角度V
if (pRotH != nullptr) *pRotH = DxLib::GetCameraAngleHRotateD(); // カメラの角度H
if (pRotT != nullptr) *pRotT = DxLib::GetCameraAngleTRotateD(); // カメラの角度T
if (pFov != nullptr) *pFov = DxLib::GetCameraFovD(); // カメラの視野角
if (pNear != nullptr) *pNear = DxLib::GetCameraNearD(); // カメラ一番近くの描き始めるレンジ
if (pFar != nullptr) *pFar = DxLib::GetCameraFarD(); // カメラ一番遠くの描くレンジ
}
// カメラの視野角に変更があったら変えて射影変換行列を再計算する (現在と同じfovの数値の場合は再計算をしないで負荷を回避)
static inline void SetupCamera_PerspectiveD(double fov) { if (DxLib::GetCameraFovD() != fov) DxLib::SetupCamera_PerspectiveD(fov); }
// カメラの視野角に変更があったら変えて射影変換行列を再計算する (現在と同じfovの数値の場合は再計算をしないで負荷を回避)
static inline void SetCameraNearFarD(double Near, double Far) { if (DxLib::GetCameraNearD() != Near && DxLib::GetCameraFarD() != Far) DxLib::SetCameraNearFarD(Near, Far); }
// カメラの設定を変える
static void SetCameraSettings(VECTOR_D* pPosition, double* pRotV, double* pRotH, double* pRotT,
double* pFov = nullptr, double* pNear = nullptr, double* pFar = nullptr)
{
if (pPosition != nullptr && pRotV != nullptr && pRotH != nullptr && pRotT != nullptr)
DxLib::SetCameraPositionAndAngleD(*pPosition, *pRotV, *pRotH, *pRotT);
if (pFov != nullptr) Camera::SetupCamera_PerspectiveD(*pFov); // カメラの視野角に変化があったら射影変換行列を更新
if (pNear != nullptr && pFar != nullptr) Camera::SetCameraNearFarD(*pNear, *pFar); // カメラ一番近くから遠くまで描くレンジ
}
// このワールドに存在するカメラすべてに対して funcで描きたい処理を渡して 全カメラのスクリーンに対して描画処理を走らせる
inline void Draw(std::function<void()> drawFunc) noexcept
{
VECTOR_D beforeCamPos; // 設定変更前のカメラのパラメータを一旦、保管
double beforeVRot, beforeHRot, beforeTRot, beforeNear, beforeFar, beforeFov;
Camera::GetCameraSettings(&beforeCamPos, &beforeVRot, &beforeHRot, &beforeTRot, &beforeFov, &beforeNear, &beforeFar);
DxLib::SetCameraPositionAndAngle(this->pos, this->rot.V, this->rot.H, this->rot.T); // カメラの位置、回転を設定
Screen::Start(this->screenHandle, true, this->fovAngle); // カメラに関連付けられたscreenHandleのスクリーンを描画先にする
{
drawFunc(); // 渡された描画処理を実行
}
Screen::End(true); // 描画先を元に戻す
// カメラの設定をもとに戻しておく
Camera::SetCameraSettings(&beforeCamPos, &beforeVRot, &beforeHRot, &beforeTRot, &beforeFov, &beforeNear, &beforeFar);
}
};
#endif
main.cppを変更
して、複数カメラ視点、複数カメラで3Dを描いてみましょう。
#include "DxLib.h"
#include "Screen.h"
#include "Camera.h"
#include "Resource.h"
#include "MyDraw.h"
// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// 画面モードの設定
DxLib::SetGraphMode(Screen::Width, Screen::Height, 32); // 画面サイズ Width×Heightのカラービット数32ビットで起動
DxLib::SetWindowSize(Screen::Width, Screen::Height);// ウィンドウサイズ Width×Height(こことSetGraphModeのサイズが異なると画像がゆがむ)
DxLib::ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
DxLib::SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
DxLib::SetMainWindowText("3Dタイルゲーム");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも
//↑ここまでの設定は↓下のDXライブラリ初期化より先にやらないとDxLib_Init()中は画面がフルスクリーンになって終わってからウィンドウサイズが変更になり見苦しい
// DXライブラリの初期化
if (DxLib_Init() < 0)
{
// DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
return -1;
}
DxLib::ScreenFlip();
// タイルマップを読み込む 縦8マス、横8マス、縦32ピクセル、横32ピクセル
std::string mapChipPath{ "Map/mapchip.png" };
Resource::MakeShared<Texture>(mapChipPath,8,8,32,32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
// SetDrawScreen に引数として渡せる( 描画対象として使用できる )グラフィックハンドルを作成するかどうかを設定する
// ( TRUE:描画可能グラフィックハンドルを作成する FLASE:通常のグラフィックハンドルを作成する( デフォルト ) )
DxLib::SetDrawValidGraphCreateFlag(TRUE);
// サブのスクリーンを2つ生成 画面の全体の縦方向サイズの Height / 2 にして、上下分割で配置できるようにする
int screenHandle0 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
int screenHandle1 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
// スクリーンを作成したらデフォルト設定に戻しておかないと
DxLib::SetDrawValidGraphCreateFlag(FALSE);
// カメラを2つ生成して、2つのカメラ視点を2つのスクリーンにそれぞれ関連付ける
std::vector<std::shared_ptr<Camera>> cameras; // 複数のカメラへのポインタを格納する配列
cameras.emplace_back(std::make_shared<Camera>(Vector3{ 20,40,-50 })); // カメラ0
cameras.emplace_back(std::make_shared<Camera>(Vector3{ 80,30,-50 })); // カメラ1
cameras[0]->screenHandle = screenHandle0; // カメラ0 と スクリーン0 を関連付ける
cameras[1]->screenHandle = screenHandle1; // カメラ1 と スクリーン1 を関連付ける
// アニメーション(パラパラ漫画)するにはWhile文
while (DxLib::ProcessMessage() == 0)
{// ProcessMessage() == 0になるのは×ボタン押したときなど
DxLib::ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
Screen::ClearDrawScreen(screenHandle0); // 一旦スクリーン0のキャンバスをきれいにまっさらに
Screen::ClearDrawScreen(screenHandle1); // 一旦スクリーン1のキャンバスをきれいにまっさらに
// 光の明暗計算を無効に
DxLib::SetUseLighting(FALSE);
// Zバッファを有効にする [Zの深度]
//[参考] https://dxlib.xsrv.jp/function/dxfunc_3d_draw.html#R14N12
DxLib::SetUseZBuffer3D(TRUE);
// Zバッファへの書き込みを有効にする
DxLib::SetWriteZBuffer3D(TRUE);
// 複数のカメラを 範囲 for文 で回す
for (auto&& camera : cameras)
{ // ラムダ式[&](){ ~ }で{}の外側の変数すべてを & キャプチャで&参照として「Draw関数の内側へ引き連れて」処理できる
camera->Draw([&]()
{
// Draw関数() の 外側にある変数 mapChipPath も[&]効果で { } の内側で問題なくアクセスできている↓
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(40, 40, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 2);
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(0, 40, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 0);
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(0, 70, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 4);
});
}
// スクリーン0の内容を画面に描く
DxLib::DrawGraph(0, 0, screenHandle0, FALSE);
DxLib::DrawLineBox(0, 0, Screen::Width, Screen::Height / 2, GetColor(255, 0, 0));
// スクリーン1の内容を画面に描く
DxLib::DrawGraph(0, Screen::Height / 2, screenHandle1, FALSE);
DxLib::DrawLineBox(0, Screen::Height / 2, Screen::Width, Screen::Height, GetColor(0, 255, 0));
DxLib::ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
}
// キー入力待ちをする
DxLib::WaitKey();
// DXライブラリの後始末
DxLib::DxLib_End();
// ソフトの終了
return 0;
}
void Draw関数 ( drawfunc )
{
Start() ←カメラやスクリーンの設定を変える(もとの設定を一旦保管しておく)
drawfunc() ←描きたい処理をはさみこんだ真ん中で実行する
End() ←カメラやスクリーンの設定を元に戻す
}
MyHash.hを新規作成
して、文字列をハッシュのルールに基づいて32桁の0と1に変換してhash32型のタグどうしで一致判定できるようにしましょう。
#ifndef MYHASH_H_
#define MYHASH_H_
#include <string>
#include <stdint.h> // uint32_t 型 32ビットの unsined 数値型に変換する
#define CRC32_TABLE_SIZE (256) // CRC32テーブルの要素数
// 言語 C++ C# など や ビルドするコンパイラ VisualStudioやgccなどに関係なく同じハッシュを計算値できる
// [参考] https://norizn.hatenablog.com/entry/2020/10/18/145628
// 生成済みのCRC32テーブル このテーブルをベースとして使用してハッシュ値を計算する
static constexpr uint32_t s_crc32_table[CRC32_TABLE_SIZE] =
{
0x00000000, 0x04C11DB7, 0x09823B6E, 0x0D4326D9,
0x130476DC, 0x17C56B6B, 0x1A864DB2, 0x1E475005,
0x2608EDB8, 0x22C9F00F, 0x2F8AD6D6, 0x2B4BCB61,
0x350C9B64, 0x31CD86D3, 0x3C8EA00A, 0x384FBDBD,
0x4C11DB70, 0x48D0C6C7, 0x4593E01E, 0x4152FDA9,
0x5F15ADAC, 0x5BD4B01B, 0x569796C2, 0x52568B75,
0x6A1936C8, 0x6ED82B7F, 0x639B0DA6, 0x675A1011,
0x791D4014, 0x7DDC5DA3, 0x709F7B7A, 0x745E66CD,
0x9823B6E0, 0x9CE2AB57, 0x91A18D8E, 0x95609039,
0x8B27C03C, 0x8FE6DD8B, 0x82A5FB52, 0x8664E6E5,
0xBE2B5B58, 0xBAEA46EF, 0xB7A96036, 0xB3687D81,
0xAD2F2D84, 0xA9EE3033, 0xA4AD16EA, 0xA06C0B5D,
0xD4326D90, 0xD0F37027, 0xDDB056FE, 0xD9714B49,
0xC7361B4C, 0xC3F706FB, 0xCEB42022, 0xCA753D95,
0xF23A8028, 0xF6FB9D9F, 0xFBB8BB46, 0xFF79A6F1,
0xE13EF6F4, 0xE5FFEB43, 0xE8BCCD9A, 0xEC7DD02D,
0x34867077, 0x30476DC0, 0x3D044B19, 0x39C556AE,
0x278206AB, 0x23431B1C, 0x2E003DC5, 0x2AC12072,
0x128E9DCF, 0x164F8078, 0x1B0CA6A1, 0x1FCDBB16,
0x018AEB13, 0x054BF6A4, 0x0808D07D, 0x0CC9CDCA,
0x7897AB07, 0x7C56B6B0, 0x71159069, 0x75D48DDE,
0x6B93DDDB, 0x6F52C06C, 0x6211E6B5, 0x66D0FB02,
0x5E9F46BF, 0x5A5E5B08, 0x571D7DD1, 0x53DC6066,
0x4D9B3063, 0x495A2DD4, 0x44190B0D, 0x40D816BA,
0xACA5C697, 0xA864DB20, 0xA527FDF9, 0xA1E6E04E,
0xBFA1B04B, 0xBB60ADFC, 0xB6238B25, 0xB2E29692,
0x8AAD2B2F, 0x8E6C3698, 0x832F1041, 0x87EE0DF6,
0x99A95DF3, 0x9D684044, 0x902B669D, 0x94EA7B2A,
0xE0B41DE7, 0xE4750050, 0xE9362689, 0xEDF73B3E,
0xF3B06B3B, 0xF771768C, 0xFA325055, 0xFEF34DE2,
0xC6BCF05F, 0xC27DEDE8, 0xCF3ECB31, 0xCBFFD686,
0xD5B88683, 0xD1799B34, 0xDC3ABDED, 0xD8FBA05A,
0x690CE0EE, 0x6DCDFD59, 0x608EDB80, 0x644FC637,
0x7A089632, 0x7EC98B85, 0x738AAD5C, 0x774BB0EB,
0x4F040D56, 0x4BC510E1, 0x46863638, 0x42472B8F,
0x5C007B8A, 0x58C1663D, 0x558240E4, 0x51435D53,
0x251D3B9E, 0x21DC2629, 0x2C9F00F0, 0x285E1D47,
0x36194D42, 0x32D850F5, 0x3F9B762C, 0x3B5A6B9B,
0x0315D626, 0x07D4CB91, 0x0A97ED48, 0x0E56F0FF,
0x1011A0FA, 0x14D0BD4D, 0x19939B94, 0x1D528623,
0xF12F560E, 0xF5EE4BB9, 0xF8AD6D60, 0xFC6C70D7,
0xE22B20D2, 0xE6EA3D65, 0xEBA91BBC, 0xEF68060B,
0xD727BBB6, 0xD3E6A601, 0xDEA580D8, 0xDA649D6F,
0xC423CD6A, 0xC0E2D0DD, 0xCDA1F604, 0xC960EBB3,
0xBD3E8D7E, 0xB9FF90C9, 0xB4BCB610, 0xB07DABA7,
0xAE3AFBA2, 0xAAFBE615, 0xA7B8C0CC, 0xA379DD7B,
0x9B3660C6, 0x9FF77D71, 0x92B45BA8, 0x9675461F,
0x8832161A, 0x8CF30BAD, 0x81B02D74, 0x857130C3,
0x5D8A9099, 0x594B8D2E, 0x5408ABF7, 0x50C9B640,
0x4E8EE645, 0x4A4FFBF2, 0x470CDD2B, 0x43CDC09C,
0x7B827D21, 0x7F436096, 0x7200464F, 0x76C15BF8,
0x68860BFD, 0x6C47164A, 0x61043093, 0x65C52D24,
0x119B4BE9, 0x155A565E, 0x18197087, 0x1CD86D30,
0x029F3D35, 0x065E2082, 0x0B1D065B, 0x0FDC1BEC,
0x3793A651, 0x3352BBE6, 0x3E119D3F, 0x3AD08088,
0x2497D08D, 0x2056CD3A, 0x2D15EBE3, 0x29D4F654,
0xC5A92679, 0xC1683BCE, 0xCC2B1D17, 0xC8EA00A0,
0xD6AD50A5, 0xD26C4D12, 0xDF2F6BCB, 0xDBEE767C,
0xE3A1CBC1, 0xE760D676, 0xEA23F0AF, 0xEEE2ED18,
0xF0A5BD1D, 0xF464A0AA, 0xF9278673, 0xFDE69BC4,
0x89B8FD09, 0x8D79E0BE, 0x803AC667, 0x84FBDBD0,
0x9ABC8BD5, 0x9E7D9662, 0x933EB0BB, 0x97FFAD0C,
0xAFB010B1, 0xAB710D06, 0xA6322BDF, 0xA2F33668,
0xBCB4666D, 0xB8757BDA, 0xB5365D03, 0xB1F740B4,
};
// 言語 C++ C# など や ビルドするコンパイラ VisualStudioやgccなどに関係なく変換後が同じになるハッシュ値を計算する
class hash32
#ifdef _DEBUG // [デバッグビルド時だけ _DEBUG が定義] デバッグのときだけ std::string を継承して文字列と同じ扱いにできる(メモリ量は無駄なのでReleaseビルドのときは継承させない)
: protected std::string // [std::stringはデストラクタがvirtualではないのでprotected:継承] https://mahou-ptr.hatenablog.com/entry/2017/12/05/163120
#endif // [std::stringを継承するときの注意点] https://qiita.com/samakusa/items/d6f0bf48a3f1e72edae1
{
public:
#ifndef _DEBUG // Releaseビルドのときは コンストラクタに constexpr をつけて、ビルド中のうちに文字列からハッシュ値へ事前計算を走らせる
constexpr
#endif
inline hash32(const char* str) : m_hash{ hashcode(str, std::string(str).length()) }
#ifdef _DEBUG // デバッグ機能(デバッグの時だけ _DEBUG が定義)
,std::string(str) // Releaseビルドのときはstd::stringは継承されないのでメモリ量は32ビットになり、コンパクトになる
#endif
{
// ちょっと読みにくいけど ここがコンストラクタになってる
}
#ifndef _DEBUG // Releaseビルドのときは コンストラクタに constexpr をつけて、ビルド中のうちに文字列からハッシュ値へ事前計算を走らせる
constexpr
#endif
inline hash32(const std::string & str) : m_hash{ hashcode(str.c_str(), str.length()) }
#ifdef _DEBUG // デバッグ機能(デバッグの時だけ _DEBUG が定義)
,std::string(str) // Releaseビルドのときはstd::stringは継承されないのでメモリ量は32ビットになり、コンパクトになる
#endif
{
// ちょっと読みにくいけど ここがコンストラクタになってる
}
#ifdef _DEBUG // デバッグ機能(デバッグの時だけ _DEBUG が定義)
~hash32() { std::string().swap(*this); } // デバッグのときだけ念のため継承しているstd::stringを空のstd::string()とswapしておく(stringにはvirtualデストラクタがない)
#endif
// 文字列からハッシュ値を計算して返す
static constexpr uint32_t hashcode(const char* str, const size_t length)
{ // CRC32 Hashの特徴: 計算が簡単で速い 広く利用されている ハッシュの衝突が発生する確率が低い(1/43億) ハードウェアでの実装が容易
uint32_t hash = 0xffffffff; //[CRC32] https://norizn.hatenablog.com/entry/2020/10/18/145628
for (size_t i = 0; i < length; ++i) //[CRC32] https://qiita.com/He3-toolbox/items/3c40ca4b744bad2c0e04
hash = (hash << 8) ^ s_crc32_table[((hash >> 24) ^ str[i]) & 0xff];
return hash;
}
// 一致演算子 hash32型 どうしを比較する std::unordered_mapなどのキーにするときにも比較は必須
constexpr inline bool operator == (const hash32 & other) const { return this->m_hash == other.m_hash; }
// 不一致演算子 hash32型 どうしを比較する std::unordered_mapなどのキーにするときにも比較は必須
constexpr inline bool operator != (const hash32 & other) const { return this->m_hash != other.m_hash; }
// 一致演算子 m_hashの値が一致するか判定
constexpr inline bool operator == (const char* str) const { return m_hash == hashcode(str, std::string(str).length()); }
// 不一致演算子
constexpr inline bool operator != (const char* str) const { return m_hash != hashcode(str, std::string(str).length()); }
// 一致演算子 m_hashの値が一致するか判定
constexpr inline bool operator == (const std::string & str) const { return m_hash == hashcode(str.c_str(), str.length()); }
// 一致演算子 m_hashの値が一致するか判定
constexpr inline bool operator != (const std::string & str) const { return m_hash != hashcode(str.c_str(), str.length()); }
// ハッシュ値を取得
constexpr inline operator uint32_t() const { return m_hash; }
private:
// 文字列から変換した ハッシュ値
uint32_t m_hash{ 0 }; // 同じ文字列からは必ず同じハッシュ値に計算変換されるので、文字列を比較せずにすむ(文字列の比較は計算が重たいので回避できる)
};
// std::unordered_map で 自作クラスをキーにできるようにするには operator() と == 一致演算子が必須 https://qiita.com/izmktr/items/8e0fd1b6e37de59a9bd0
namespace std {
template<>
class hash<hash32> {
public: //[std::unordered_mapのキー が uint32_t型のときの 速度やメモリ使用量比較] https://tech.preferred.jp/ja/blog/sparse-vector/
size_t operator () (const hash32& hash) const { return std::hash<uint32_t>()(hash); }
};
}
// "Zako0"_hash32 などのように 文字列 "~" の後ろに_hash32 をつけるとhash32型 -> uint32_tに変換される オペレータ""_hash32 を定義
constexpr uint32_t operator""_hash32(const char* str, std::size_t length) // [参考] https://cpprefjp.github.io/lang/cpp11/user_defined_literals.html
{
return hash32::hashcode(str, length); // hash32型のハッシュルールで32ビットのuint型に変換して返す
}
#endif
main.cppを変更
して、hash32でカメラをstd::vectorではなくstd::unordered_mapの辞書にhash32タグを振って管理してみましょう。
#include "DxLib.h"
#include "Screen.h"
#include "Camera.h"
#include "MyHash.h"
#include "Resource.h"
#include "MyDraw.h"
// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// 画面モードの設定
DxLib::SetGraphMode(Screen::Width, Screen::Height, 32); // 画面サイズ Width×Heightのカラービット数32ビットで起動
DxLib::SetWindowSize(Screen::Width, Screen::Height);// ウィンドウサイズ Width×Height(こことSetGraphModeのサイズが異なると画像がゆがむ)
DxLib::ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
DxLib::SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
DxLib::SetMainWindowText("3Dタイルゲーム");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも
//↑ここまでの設定は↓下のDXライブラリ初期化より先にやらないとDxLib_Init()中は画面がフルスクリーンになって終わってからウィンドウサイズが変更になり見苦しい
// DXライブラリの初期化
if (DxLib_Init() < 0)
{
// DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
return -1;
}
DxLib::ScreenFlip();
// タイルマップを読み込む 縦8マス、横8マス、縦32ピクセル、横32ピクセル
std::string mapChipPath{ "Map/mapchip.png" };
Resource::MakeShared<Texture>(mapChipPath,8,8,32,32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
// SetDrawScreen に引数として渡せる( 描画対象として使用できる )グラフィックハンドルを作成するかどうかを設定する
// ( TRUE:描画可能グラフィックハンドルを作成する FLASE:通常のグラフィックハンドルを作成する( デフォルト ) )
DxLib::SetDrawValidGraphCreateFlag(TRUE);
// サブのスクリーンを2つ生成 画面の全体の縦方向サイズの Height / 2 にして、上下分割で配置できるようにする
int screenHandle0 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
int screenHandle1 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
// スクリーンを作成したらデフォルト設定に戻しておかないと
DxLib::SetDrawValidGraphCreateFlag(FALSE);
// カメラを2つ生成して、2つのカメラ視点を2つのスクリーンにそれぞれ関連付ける
std::unordered_map<hash32, std::shared_ptr<Camera>> cameras; // 複数のカメラへのポインタを格納するタグを振った辞書配列
cameras.emplace("カメラ0", std::make_shared<Camera>(Vector3{ 20,40,-50 })); // カメラ0
cameras.emplace("カメラ1", std::make_shared<Camera>(Vector3{ 80,30,-50 })); // カメラ1
cameras["カメラ0"]->screenHandle = screenHandle0; // カメラ0 と スクリーン0 を関連付ける
cameras["カメラ1"]->screenHandle = screenHandle1; // カメラ1 と スクリーン1 を関連付ける
// アニメーション(パラパラ漫画)するにはWhile文
while (DxLib::ProcessMessage() == 0)
{// ProcessMessage() == 0になるのは×ボタン押したときなど
DxLib::ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
Screen::ClearDrawScreen(screenHandle0); // 一旦キャンバスをきれいにまっさらに
Screen::ClearDrawScreen(screenHandle1); // 一旦キャンバスをきれいにまっさらに
// 光の明暗計算を無効に
DxLib::SetUseLighting(FALSE);
// Zバッファを有効にする [Zの深度]
//[参考] https://dxlib.xsrv.jp/function/dxfunc_3d_draw.html#R14N12
DxLib::SetUseZBuffer3D(TRUE);
// Zバッファへの書き込みを有効にする
DxLib::SetWriteZBuffer3D(TRUE);
// 複数のカメラを 範囲 for文 で回す
for (auto&& camera : cameras)
{ // ラムダ式[&](){ ~ }で{}の外側の変数すべてを & キャプチャで&参照として「Draw関数の内側へ引き連れて」処理できる
camera.second->Draw([&]()
{
// Draw関数() の 外側にある変数 mapChipPath も[&]効果で { } の内側で問題なくアクセスできている↓
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(40, 40, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 2);
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(0, 40, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 0);
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(0, 70, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 4);
if (camera.first == "カメラ0") //←"カメラ0"はビルド時に事前にハッシュ値が計算され、文字列ではない数値とおきかわっている
DxLib::DrawString(0, 0, "こちらがカメラ0", GetColor(255, 0, 0));
else if (camera.first == "カメラ1")
DxLib::DrawString(0, 0, "こちらはカメラ1", GetColor(0, 255, 0));
});
}
// スクリーン0の内容を画面に描く
DxLib::DrawGraph(0, 0, screenHandle0, FALSE);
DxLib::DrawLineBox(0, 0, Screen::Width, Screen::Height / 2, GetColor(255, 0, 0));
// スクリーン1の内容を画面に描く
DxLib::DrawGraph(0, Screen::Height / 2, screenHandle1, FALSE);
DxLib::DrawLineBox(0, Screen::Height / 2, Screen::Width, Screen::Height, GetColor(0, 255, 0));
DxLib::ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
}
// キー入力待ちをする
DxLib::WaitKey();
// DXライブラリの後始末
DxLib::DxLib_End();
// ソフトの終了
return 0;
}
別記事でImageフォルダにImage/pikkoron.pngと新たに別に自作したImage/satan.pngを保存しておいてください
→CharaMELで自作のキャラのタイル分割画像を作る
class Camera; // 前方宣言だけして#include はしなければポインタ型 Camera* の定義は許される
class Player;
class World
{
Camera* camera; // 前方宣言をすればポインタは定義が許されるようになる
Player* player;
};
#include <memory> // 共有ポインタを使う
class Camera; // 前方宣言だけして#include はしなければポインタ型 Camera* の定義は許される
class Player;
class World
{
std::shared_ptr<Camera> camera; // 前方宣言をすればポインタは定義が許されるようになる
std::shared_ptr<Player> player;
};
world2->player = world1->player; // ワールド2でプレイヤへのリンクが張られたのでplayerポインタの共有カウンタが+1になる
world1->player = nullptr; // world1のplayerポインタがnullになるのでplayerの共有カウンタが-1される
// 結果、playerはワールド2のぶんは最低限の共有カウンタ1は確保されているので消えない(ワールド移動完了!!!!)
World.hの例
#include <memory> // 共有ポインタを使う
class Camera; // 前方宣言だけして#include はしなければポインタ型 Camera* の定義は許される
class Player;
class World
{
std::shared_ptr<Camera> camera; // 前方宣言をすればポインタは定義が許されるようになる
std::shared_ptr<Player> player;
void MakePlayer(); // .hでは初期化を直で呼ばずに、間接的に初期化するための関数だけを定義
};
World.cppの例
#include "World.h";
#include "Player.h"; // .cppではstd::make_sharedで初期化コンストラクタを呼ぶためにインクルードは必要
#include "Camera.h";
void World::MakePlayer()
{
player = std::make_shared<Player>(); // 初期化コンストラクタを呼ぶ
player->hp = 10; // インクルードしてればメンバ変数にもアクセスできる
};
World.hを新規作成
して、世界で保持する(道ずれにする)要素を共有ポインタで定義しましょう。
#ifndef WORLD_H_
#define WORLD_H_
#include <list>
#include <memory>
#include <unordered_map>
#include <functional> // std::functionのラムダ式 で 関数を Draw関数の引数にして、カメラすべてに対して描画処理を発動させる
#include "MyHash.h" // ワールドのタグをhash32型で管理する
// [前方宣言] 宣言だけして、#includeはしてないので、ポインタだけはこのファイル内でつかえる
// #includeはしてないので、hpなどの変数には#include "~.h"しないとアクセスできないので循環インクルード防止のため.cppでインクルードしてください
class GameScene;
class Player;
class Bullet;
class Enemy;
class Camera;
// プレイヤや敵や弾などの共有ポインタを管理し、EraseRemoveIfで消して参照カウンタが0になったらそれらを消滅させるクラス(C#のガベージコレクタの代わり)
class World
{
public:
inline World(GameScene* pScene, const std::string& worldTag = "") : m_pScene{ pScene }, tag{ worldTag } {}
inline ~World() {}
protected: // public:にすると他で外部で後からタグやシーンを書き換えられると辞書での管理などの前提がおかしくなるのでprotected:する
hash32 tag{ "" }; // ワールド名のタグ
GameScene* m_pScene{ nullptr }; // ワールドを所有するシーン
public:
hash32 Tag() const { return tag; }
GameScene* scene() { return m_pScene; }
std::unordered_map<hash32, std::shared_ptr<Player>> players; // プレイヤのタグ辞書
std::list<std::shared_ptr<Bullet>> bullets; // 弾のリスト
std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
// 削除処理を共通テンプレート関数にする
// [共通テンプレート関数]https://programming-place.net/ppp/contents/cpp/language/009.html#function_template
template <typename TypeT, class T_if>
void EraseRemoveIf(std::list<TypeT>& 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()の位置
};
std::unordered_map<hash32, std::shared_ptr<Camera>> cameras; // このワールドに存在するカメラの辞書<カメラにつけたタグ, カメラの共有ポインタ>
// 指定されたタグのカメラへの共有ポインタを得る カメラ辞書に存在しないタグでアクセスしたときにunordered_mapの特性で勝手に意図しないカメラができるのを防ぐ関数
std::shared_ptr<Camera> camera(const std::string& cameraTag) { auto itr = cameras.find(cameraTag); return (itr != end(cameras)) ? itr->second : nullptr; }
// このワールドに存在するカメラすべてに対して drawFuncで描きたい処理を渡して 全カメラのスクリーンに対して描画処理を走らせる
void Draw(std::function<void()> drawFunc) noexcept;
// カメラにタグをつけてワールドに追加 カメラにもワールドへのポインタリンクを張る
inline bool AddCamera(const std::string& cameraTag, std::shared_ptr<Camera> pCamera);
// 指定したタグのカメラをこのワールドから削除する カメラ側の worldへのリンクのポインタも nullptr にする
inline bool EraseCamera(const std::string& cameraTag);
// カメラを nowWorld から nextWorld に移動させる
static bool MoveCamera(const std::string& cameraTag, World* nowWorld, World* nextWorld);
};
#endif
World.cppを新規作成
して、ポインタ以外の実体のインクルードが必要な処理を記述しましょう。
#include "World.h"
#include "Player.h"
#include "Camera.h"
#include "Screen.h"
// カメラにタグをつけてワールドに追加 カメラにもワールドへの生ポインタリンクを渡す
bool World::AddCamera(const std::string& cameraTag, std::shared_ptr<Camera> pCamera)
{
if (pCamera == nullptr) return false;
if (cameras.count(cameraTag) > 0) return false; // すでに同じタグのカメラがワールドにあったら false
if (pCamera->world() != nullptr) pCamera->world()->EraseCamera(cameraTag); // すでにカメラが別ワールドに配置されていたら別ワールドから削除
cameras.emplace(cameraTag, pCamera); // ワールドにカメラを追加する
pCamera->world(this); // カメラにもワールドへのポインタリンクを張る(カメラ側に渡すのは共有ポインタではない生ポインタだから共有デッドロックにはならない)
return true;
}
// 指定したタグのカメラをこのワールドから削除する カメラ側の worldへのリンクのポインタも nullptr にする
bool World::EraseCamera(const std::string& cameraTag)
{
auto itr = cameras.find(cameraTag); // 指定したタグのカメラを探す
if (itr == end(cameras)) return false; // 指定したタグのカメラがワールドにない
auto pCamera = itr->second;
pCamera->world(nullptr); // カメラ側の worldへのリンクのポインタも nullptrに
cameras.erase(cameraTag); // カメラを辞書から削除
return true;
}
// カメラを nowWorld から nextWorld に移動させる
bool World::MoveCamera(const std::string& cameraTag, World* nowWorld, World* nextWorld)
{
if (nowWorld == nullptr) return false;
auto itr = nowWorld->cameras.find(cameraTag); // タグでカメラを探す
if (itr == end(nowWorld->cameras)) return false; // タグに関連づいたカメラがなかった
auto targetCamera = itr->second; // カメラの共有ポインタをキープ(次の行でeraseしてもカメラの共有カウンタは0にはならずこのリンクのおかげでカメラはメモリに残存)
targetCamera->world(nullptr); // カメラ側の worldへのリンクのポインタを一旦 nullptrに
nowWorld->cameras.erase(cameraTag); // nowWorld からは カメラを消す
if (nextWorld == nullptr) return false;
return nextWorld->AddCamera(cameraTag, targetCamera); // 次のワールドにカメラを追加する
}
// このワールドに存在するカメラすべてに対して funcで描きたい処理を渡して 全カメラのスクリーンに対して描画処理を走らせる
void World::Draw(std::function<void()> drawFunc) noexcept
{
for (auto&& keyValue : cameras) // このワールドにあるすべてのカメラぶんループ
{
VECTOR_D beforeCamPos; // 設定変更前のカメラのパラメータを一旦、保管
double beforeVRot, beforeHRot, beforeTRot, beforeNear, beforeFar, beforeFov;
Camera::GetCameraSettings(&beforeCamPos, &beforeVRot, &beforeHRot, &beforeTRot, &beforeFov, &beforeNear, &beforeFar);
auto& camera = keyValue.second;
//camera->screenID
DxLib::SetCameraPositionAndAngle(camera->pos, camera->rot.V, camera->rot.H, camera->rot.T); // 各カメラの位置、回転を設定
Screen::Start(camera->screenHandle, true, camera->fovAngle); // カメラに関連付けられたスクリーンIDを描画先にする
{
drawFunc(); // 渡された描画処理を実行
}
Screen::End(true); // 描画先を元に戻す
// カメラの設定をもとに戻しておく
Camera::SetCameraSettings(&beforeCamPos, &beforeVRot, &beforeHRot, &beforeTRot, &beforeFov, &beforeNear, &beforeFar);
}
}
Camera.hを変更
して、ワールドに所有される立場のカメラ側からも自分の今いるワールドの情報にアクセスできるために
World* 型の生ポインタを初期化と同時に渡しておけるようにしましょう。
#ifndef CAMERA_H_
#define CAMERA_H_
#include <functional> // カメラのDraw関数に「引数として関数=ラムダ式」を渡してカメラのDraw関数の外側の呼び出し元から描画したい処理を渡すことができるようにする
#include "DxLib.h"
#include "Screen.h"
#include "MyDraw.h"
#include "Vector3.h"
#include "World.h"
class Camera
{
public:
// カメラの位置(ワールド座標)
union {
Vector3 pos; // カメラの位置(ワールド座標)
struct { float x, y, z; }; // unionしてx,y,zでもアクセスできるように
};
struct Rotate { double V, H, T; }; // カメラの回転角度定義 DXライブラリの内部行列ではfloatじゃなくてdouble型で保持されているので倍精度のdouble型にした
Rotate rot{ 0.0, 0.0, 0.0 }; // カメラの3軸の回転角度
double fovAngle = 0; // カメラをスクリーンに描くときの視野角(0~179度) 0のときはDxLib::GetCameraFovD()で得られる値が適用される
float cameraLookAtHeight{ 0.0f }; // LookAtする対象を見下ろす高さ
int screenHandle = DX_SCREEN_BACK; //カメラを描くスクリーンのハンドル番号
protected:
World* m_world; // カメラが存在するワールドへのポインタ
public:
World* world() { return m_world; } //ワールドへのポインタを得る
Camera& world(World* pWorld) { m_world = pWorld; return *this; } // ワールドへのポインタを再セットする
// コンストラクタ
Camera(World* pWorld, Vector3 worldPos) : m_world{ pWorld }, pos { worldPos } {}
// デストラクタ
~Camera() {}
// カメラを通して描くスクリーンのハンドル番号を設定する
void SetScreenHandle(int handle) { screenHandle = handle; }
// カメラの位置をセットしなおす
void SetPosition(Vector3 worldPos)
{ // ワールド座標系として保存
this->pos = worldPos;
}
// 指定されたワールド座標が画面の中心に来るように、カメラの位置を変更する
void LookAt(Vector3 targetXYZ)
{
// カメラの位置と向きを設定
//MyDraw::SetCameraPositionAndTarget_UpVecY(pos, targetXYZ + Vector3(0, cameraLookAtHeight,0));
// 注視点はターゲットの座標から cameraLookAtHeight 分だけ高い位置
DxLib::SetCameraPositionAndTarget_UpVecY(this->pos, targetXYZ + Vector3(0, this->cameraLookAtHeight, 0));
// カメラ回転後の角度を保存
this->rot.H = DxLib::GetCameraAngleHRotateD(); // カメラの水平方向の向きを取得
this->rot.V = DxLib::GetCameraAngleVRotateD();
this->rot.T = DxLib::GetCameraAngleTRotateD();
}
// 現在のDX内部のカメラのビュー行列などに入っているカメラの設定パラメータを得る
static void GetCameraSettings(VECTOR_D* pPosition, double* pRotV = nullptr, double* pRotH = nullptr, double* pRotT = nullptr,
double* pFov = nullptr, double* pNear = nullptr, double* pFar = nullptr)
{
if (pPosition != nullptr) *pPosition = DxLib::GetCameraPositionD(); // カメラ位置
if (pRotV != nullptr) *pRotV = DxLib::GetCameraAngleVRotateD(); // カメラの角度V
if (pRotH != nullptr) *pRotH = DxLib::GetCameraAngleHRotateD(); // カメラの角度H
if (pRotT != nullptr) *pRotT = DxLib::GetCameraAngleTRotateD(); // カメラの角度T
if (pFov != nullptr) *pFov = DxLib::GetCameraFovD(); // カメラの視野角
if (pNear != nullptr) *pNear = DxLib::GetCameraNearD(); // カメラ一番近くの描き始めるレンジ
if (pFar != nullptr) *pFar = DxLib::GetCameraFarD(); // カメラ一番遠くの描くレンジ
}
// カメラの視野角に変更があったら変えて射影変換行列を再計算する (現在と同じfovの数値の場合は再計算をしないで負荷を回避)
static inline void SetupCamera_PerspectiveD(double fov) { if (DxLib::GetCameraFovD() != fov) DxLib::SetupCamera_PerspectiveD(fov); }
// カメラの視野角に変更があったら変えて射影変換行列を再計算する (現在と同じfovの数値の場合は再計算をしないで負荷を回避)
static inline void SetCameraNearFarD(double Near, double Far) { if (DxLib::GetCameraNearD() != Near && DxLib::GetCameraFarD() != Far) DxLib::SetCameraNearFarD(Near, Far); }
// カメラの設定を変える
static void SetCameraSettings(VECTOR_D* pPosition, double* pRotV, double* pRotH, double* pRotT,
double* pFov = nullptr, double* pNear = nullptr, double* pFar = nullptr)
{
if (pPosition != nullptr && pRotV != nullptr && pRotH != nullptr && pRotT != nullptr)
DxLib::SetCameraPositionAndAngleD(*pPosition, *pRotV, *pRotH, *pRotT);
if (pFov != nullptr) Camera::SetupCamera_PerspectiveD(*pFov); // カメラの視野角に変化があったら射影変換行列を更新
if (pNear != nullptr && pFar != nullptr) Camera::SetCameraNearFarD(*pNear, *pFar); // カメラ一番近くから遠くまで描くレンジ
}
// このワールドに存在するカメラすべてに対して funcで描きたい処理を渡して 全カメラのスクリーンに対して描画処理を走らせる
inline void Draw(std::function<void()> drawFunc) noexcept
{
VECTOR_D beforeCamPos; // 設定変更前のカメラのパラメータを一旦、保管
double beforeVRot, beforeHRot, beforeTRot, beforeNear, beforeFar, beforeFov;
Camera::GetCameraSettings(&beforeCamPos, &beforeVRot, &beforeHRot, &beforeTRot, &beforeFov, &beforeNear, &beforeFar);
DxLib::SetCameraPositionAndAngle(this->pos, this->rot.V, this->rot.H, this->rot.T); // カメラの位置、回転を設定
Screen::Start(this->screenHandle, true, this->fovAngle); // カメラに関連付けられたscreenHandleのスクリーンを描画先にする
{
drawFunc(); // 渡された描画処理を実行
}
Screen::End(true); // 描画先を元に戻す
// カメラの設定をもとに戻しておく
Camera::SetCameraSettings(&beforeCamPos, &beforeVRot, &beforeHRot, &beforeTRot, &beforeFov, &beforeNear, &beforeFar);
}
};
#endif
MyMath.hを新規作成
して、プレイヤが歩き回るさいの計算に必要な円周率などの定数を定義しましょう。
#ifndef MYMATH_H_
#define MYMATH_H_
// 数学関連クラス
class MyMath
{
public:
// [static constexprを使うことでstaticな定数を.hヘッダで初期化] https://onihusube.hatenablog.com/entry/2019/06/30/230431
static constexpr float Sqrt2 = 1.41421356237f; // ルート2
static constexpr float PI = 3.14159265359f; // 円周率
static constexpr float Deg2Rad = PI / 180.0f; // 度からラジアンに変換する定数
};
#endif
Player.hを新規作成
して、ワールドを歩きまわるプレイヤをタイル分割画像でパラパラアニメ描画できるようにしましょう。
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Resource.h"
#include "MyHash.h" // hash32型でプレイヤが現在いるワールドのタグを管理する
#include "Vector3.h"
class World; // 前方宣言
class Camera; // 前方宣言
class Player
{
public:
// コンストラクタ
Player(World* world, Pad pad, Vector3 position)
: pad{ pad }, m_world{ world }, position{ position }, velocity{ 0,0,0 }, force{0,0,0}
{
this->tag = "Player";
}
// 仮想デストラクタ
virtual ~Player() {}
union { // ★共用体unionテクニック https://inemaru.hatenablog.com/entry/2016/03/02/005408
//★下記3つはメモリ上で共用状態になる(position、x,y,z、xyzどの名前から数値を変えたり読出してもメモリ上は同じ)
// 同じデータに3種類の名前を付けたイメージ、しかもVector3の機能や配列としてのアクセスの仕方もできて便利
struct { float x, y, z; }; // XYZ座標 [匿名共用体とは] https://code.i-harness.com/ja-jp/q/4d437c
Vector3 position; // XYZ座標
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で回せる
union {
struct { float vx, vy, vz; }; // XYZ方向の速度
Vector3 velocity; // XYZ方向の速度
std::array<float, 3> vxyz;
};
union {
struct { float vxForce, vyForce, vzForce; }; // XYZ方向にかかる力(Unityでいうと AddForce関数 )
Vector3 force; // XYZ方向の力(物理的には加速度:1フレームごとにvelocityの増える=加速する量)
std::array<float, 3> vxyzForce;
};
hash32 tag{ "" }; // プレイヤにつけるタグ
protected:
World* m_world; // 配置されたワールドへのリンク
public:
// 現在いるワールドへのポインタを得る
virtual World* world() { return m_world; }
// 現在いるワールドのポインタを変更する
virtual void world(World* changeWorld) { m_world = changeWorld; }
Pad pad; // 操作するコントローラー番号
hash32 worldTag{ "" }; // 現在いるワールドのタグ
float MoveSpeedMax = 6; // 移動速度Max値
float MoveSpeed = 0;//移動速度
float moveAngle = 0; // X軸→方向から何度か
float deltaAngle = 0; // プレイヤの動く角度の変化率、ハンドルを切ったらだんだんもとに戻る
int animCount = 0;
std::shared_ptr<Texture> image; // プレイヤの板ポリゴンで描くタイル分割画像への共有リンク
Camera* camera{ nullptr }; // プレイヤを追随するカメラ
void SetCamera(Camera* pCamera) { camera = pCamera; }
// 入力を受けての処理
virtual void HandleInput();
// 更新処理
virtual void Update();
// 描画処理
virtual void Draw();
};
#endif
Player.cppを新規作成
して、ワールドを歩きまわる処理を記述にしましょう。
#include "Player.h"
#include <cmath> // 浮動小数点の余り%演算子がC++にないのでfmodを使うため
#include "MyMath.h"
#include "MyDraw.h"
#include "Camera.h"
#include "World.h"
#include <vector>
void Player::HandleInput()
{
if (Input::GetButton(pad, PAD_INPUT_LEFT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_A)) )
{
//vx = - MoveSpeed; // 左
deltaAngle += 1; //ハンドルを左に回す
}
if (Input::GetButton(pad, PAD_INPUT_RIGHT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_D)))
{
//vx = MoveSpeed; // 右
deltaAngle -= 1; //ハンドルを右に回す
}
if (Input::GetButton(pad, PAD_INPUT_UP)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_W)))
{
//vy = - MoveSpeed; // 上
MoveSpeed += 0.6f; //アクセルを踏む
if (MoveSpeed > MoveSpeedMax)
MoveSpeed = MoveSpeedMax;//Maxスピードで止める
}
else if (Input::GetButton(pad, PAD_INPUT_DOWN)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_S)))
{
//vy = MoveSpeed; // 下
MoveSpeed -= 0.6f; //ブレーキを踏む
if (MoveSpeed < -0.9f)
MoveSpeed = -0.9f;//ちょっとだけバックできるように
}
if (pad == Pad::Key || pad == Pad::One) // コントローラ1またはキーボード操作のプレイヤのときだけ
{
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGUP)) // キーボードのPageUp ボタンを押したときは
{
vy = 2.0f; // テスト用に空中の上方向に移動
}
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGDN)) // キーボードのPageDown ボタンを押したときは
{
vy = -2.0f; // テスト用に空中の下方向に移動
}
}
}
// 更新処理
void Player::Update()
{
deltaAngle *= 0.5F; // 車のハンドルの方向をだんだんニュートラルに戻す
MoveSpeed *= 0.9F; // 移動速度も減速する
// 入力を受けての処理
HandleInput();
moveAngle += deltaAngle; // ハンドルを回したぶんプレイヤの進行方向を変える
if (moveAngle < -180) moveAngle = 360 + moveAngle; // -180以下の角度にならないようにマイナスになったら+180度にループさせる
else if (moveAngle > +180) moveAngle = -360 + moveAngle; // +180以下の角度にならないようにマイナスになったら-180度にループさせる
// ↓進行方向角度をX方向とZ方向の速度に変える
vx = (float)std::cos((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
vz = (float)std::sin((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
// 実際に位置を動かす
// まず横に移動する
x += vx;
// 次に縦に動かす
y += vy;
// 次にZ奥行き方向に動かす
z += vz;
vy += vyForce; // 勢い(力・フォースを加算
// カメラのプレイヤ追尾
float camDistance = 150; // プレイヤからカメラまでのY平面上の距離
float camHeight = 100; // プレイヤからカメラまでのY方向上の高さ
float camAngleX = (float)std::cos((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
float camAngleZ = (float)std::sin((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
if (camera != nullptr)
{ // カメラの位置をプレイヤの進行方向と真逆に設定
camera->SetPosition(position + Vector3(camAngleX, camHeight, camAngleZ));
camera->LookAt(position); //カメラはプレイヤの方を見る
}
}
// 描画処理
void Player::Draw()
{
int animIndex = animCount / 20; // キャラのパラパラアニメの画像番号(最大3)
float abs_vx = (vx > 0) ? vx : -vx; // x方向絶対値
float abs_vz = (vz > 0) ? vz : -vz; // y方向絶対値
if (abs_vx > 1.00f || abs_vz > 1.00f)
animCount += 6; // キャラが地面のXとZ方向に少しでもスピードがあったらアニメを走らせる
if (animCount >= 60) animCount = 0; // アニメ時間を0へループ
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
for (auto&& keyValue : world()->cameras) // プレイヤのいるワールドにあるすべてのカメラぶんループ
{
auto& camera = keyValue.second;
camera->Draw([&]()
{
// 常にカメラ方向を向くビルボード回転の角度を得る
VECTOR_D angle = MyDraw::GetAngle(DxLib::GetCameraBillboardMatrixD());
angle.x = 0; angle.z = 0; // Y軸周りのビルボード回転だけ有効にする X軸,Z軸まわりの回転は 0 にする
int imgIndex = 9; // キャラチップ画像の起点番号
// ビルボードの角度 と キャラの向いている向き の 差 によって、別のプレイヤのカメラから見たキャラチップの向きの画像を切り替える
// -180 ~ +180 : angle.y moveAngle : -180 ~ +180 ↓
float difAngleY = angle.y * 180.0f / DX_PI_F + moveAngle;
imgIndex = (difAngleY < -135 || 135 < difAngleY) ? 0 // カメラ向きの画像
: (difAngleY < -45) ? 6 // 右 向きの画像
: (difAngleY > 45) ? 3 // 左 向きの画像
: 9; // 奥向きの画像
MyDraw::DrawDivRotaGraphF3D(MyDraw::Plane::Z, x, y, z, 1.0f, angle, *image, imgIndex + animIndex, TRUE);
});
}
}
main.cppを変更
して、Worldクラスに移したカメラとプレイヤをworld1経由で描画してみましょう。
#include "DxLib.h"
#include "Screen.h"
#include "Camera.h"
#include "MyHash.h"
#include "Resource.h"
#include "MyDraw.h"
#include "Player.h"
#include "World.h"
// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// 画面モードの設定
DxLib::SetGraphMode(Screen::Width, Screen::Height, 32); // 画面サイズ Width×Heightのカラービット数32ビットで起動
DxLib::SetWindowSize(Screen::Width, Screen::Height);// ウィンドウサイズ Width×Height(こことSetGraphModeのサイズが異なると画像がゆがむ)
DxLib::ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
DxLib::SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
DxLib::SetMainWindowText("3Dタイルゲーム");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも
//↑ここまでの設定は↓下のDXライブラリ初期化より先にやらないとDxLib_Init()中は画面がフルスクリーンになって終わってからウィンドウサイズが変更になり見苦しい
// DXライブラリの初期化
if (DxLib_Init() < 0)
{
// DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
return -1;
}
DxLib::ScreenFlip();
// タイルマップを読み込む 縦8マス、横8マス、縦32ピクセル、横32ピクセル
std::string mapChipPath{ "Map/mapchip.png" };
Resource::MakeShared<Texture>(mapChipPath,8,8,32,32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
std::string playerImgPath1{ "Image/pikkoron.png" };
Resource::MakeShared<Texture>(playerImgPath1, 3, 4, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
std::string playerImgPath2{ "Image/satan.png" };
Resource::MakeShared<Texture>(playerImgPath2, 3, 4, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
// SetDrawScreen に引数として渡せる( 描画対象として使用できる )グラフィックハンドルを作成するかどうかを設定する
// ( TRUE:描画可能グラフィックハンドルを作成する FLASE:通常のグラフィックハンドルを作成する( デフォルト ) )
DxLib::SetDrawValidGraphCreateFlag(TRUE);
// サブのスクリーンを2つ生成 画面の全体の縦方向サイズの Height / 2 にして、上下分割で配置できるようにする
int screenHandle0 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
int screenHandle1 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
// スクリーンを作成したらデフォルト設定に戻しておかないと
DxLib::SetDrawValidGraphCreateFlag(FALSE);
// ワールドを生成
std::shared_ptr<World> world1 = std::make_shared<World>(nullptr, "ワールド1");
// カメラを2つ生成して、2つのカメラ視点を2つのスクリーンにそれぞれ関連付ける
std::unordered_map<hash32, std::shared_ptr<Camera>> cameras; // 複数のカメラへのポインタを格納するタグを振った辞書配列
world1->cameras.emplace("カメラ0", std::make_shared<Camera>(world1.get(), Vector3{ 20,40,-50 })); // カメラ0
world1->cameras.emplace("カメラ1", std::make_shared<Camera>(world1.get(), Vector3{ 80,30,-50 })); // カメラ1
world1->cameras["カメラ0"]->screenHandle = screenHandle0; // カメラ0 と スクリーン0 を関連付ける
world1->cameras["カメラ1"]->screenHandle = screenHandle1; // カメラ1 と スクリーン1 を関連付ける
world1->players.emplace("プレイヤ0", std::make_shared<Player>(world1.get(), Pad::Key, Vector3{ 20,30,-50 }));
world1->players.emplace("プレイヤ1", std::make_shared<Player>(world1.get(), Pad::Two, Vector3{ 80,30,0 }));
world1->players["プレイヤ0"]->SetCamera(world1->cameras["カメラ0"].get()); // .get()で共有ポインタstd::shared_ptrから生の*ポインタへ変換
world1->players["プレイヤ1"]->SetCamera(world1->cameras["カメラ1"].get());
world1->players["プレイヤ0"]->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath1]);
world1->players["プレイヤ1"]->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath2]);
Input::Init(); // 入力状態を初期化
// アニメーション(パラパラ漫画)するにはWhile文
while (DxLib::ProcessMessage() == 0)
{// ProcessMessage() == 0になるのは×ボタン押したときなど
DxLib::ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
Screen::ClearDrawScreen(screenHandle0); // 一旦キャンバスをきれいにまっさらに
Screen::ClearDrawScreen(screenHandle1); // 一旦キャンバスをきれいにまっさらに
Input::Update(); // 入力状態を更新
// 光の明暗計算を無効に
DxLib::SetUseLighting(FALSE);
// Zバッファを有効にする [Zの深度]
//[参考] https://dxlib.xsrv.jp/function/dxfunc_3d_draw.html#R14N12
DxLib::SetUseZBuffer3D(TRUE);
// Zバッファへの書き込みを有効にする
DxLib::SetWriteZBuffer3D(TRUE);
// world1のプレイヤを更新する
for (auto& pair : world1->players)
{
auto pPlayer = pair.second;
pPlayer->Update();
}
// world1のプレイヤを描画する
for (auto& pair : world1->players)
{
auto pPlayer = pair.second;
pPlayer->Draw();
}
// 複数のカメラを 範囲 for文 で回す
for (auto&& camera : world1->cameras)
{ // ラムダ式[&](){ ~ }で{}の外側の変数すべてを & キャプチャで&参照として「Draw関数の内側へ引き連れて」処理できる
camera.second->Draw([&]()
{
// Draw関数() の 外側にある変数 mapChipPath も[&]効果で { } の内側で問題なくアクセスできている↓
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(40, 40, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 2);
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(0, 40, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 0);
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(0, 70, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 4);
if (camera.first == "カメラ0") //←"カメラ0"はビルド時に事前にハッシュ値が計算され、文字列ではない数値とおきかわっている
DxLib::DrawString(0, 0, "こちらがカメラ0", GetColor(255, 0, 0));
else if (camera.first == "カメラ1")
DxLib::DrawString(0, 0, "こちらはカメラ1", GetColor(0, 255, 0));
});
}
// スクリーン0の内容を画面に描く
DxLib::DrawGraph(0, 0, screenHandle0, FALSE);
DxLib::DrawLineBox(0, 0, Screen::Width, Screen::Height / 2, GetColor(255, 0, 0));
// スクリーン1の内容を画面に描く
DxLib::DrawGraph(0, Screen::Height / 2, screenHandle1, FALSE);
DxLib::DrawLineBox(0, Screen::Height / 2, Screen::Width, Screen::Height, GetColor(0, 255, 0));
DxLib::ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
}
// キー入力待ちをする
DxLib::WaitKey();
// DXライブラリの後始末
DxLib::DxLib_End();
// ソフトの終了
return 0;
}
前提として乱数クラスMyRandomを別記事から作成しておきます
←乱数で散乱弾を作成する
Scene.hを新規作成
して、様々なシーンの基底ベースとなるクラスを定義します(UpdateやDrawなどを純粋仮想関数としてoverride必須にしておく)。
#ifndef SCENE_H_
#define SCENE_H_
#include <string> // シーンの判別文字列tagに使う
#include "MyHash.h" // hash32型をタグ判別に使う
// シーンの基底クラス あいまいなabstract型タイプ(virtual ~() = 0;の純粋仮想関数を含む)
class Scene
{
public:
std::string tag{ "" }; // シーンでロードするファイル名などに使う
hash32 typeTag{ "" }; // シーンのタイプの判別に使う
// コンストラクタ
Scene(const std::string& sceneTag = "") : tag{ sceneTag }
{
}
//仮想デストラクタ【忘れるとメモリがヤバいダメ絶対】
virtual ~Scene()
{
// 特に配列とかなくても★string文字列も内部では実質配列だから!5文字分のメモリとかがすり減ってく..
// 仮想デストラクタはstringなど暗黙に内部に配列を持つ要素のデストラクタを暗黙に呼んでくれている!
}
//★純粋仮想関数=0は継承したGameSceneなどの必須機能(継承したら絶対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
Singleton.hを新規作成
して、シングルトンのテンプレートクラスを定義します。
#ifndef SINGLETON_H_
#define SINGLETON_H_
// テンプレートT型シングルトンSingleton<~> ~になんでも指定して唯一物にできる
// ★Singleton<TypeT>型は【必ず唯一で複製不可】(使い方:どこからでもアクセスし変数を共有するクラス向き)
// ★ゲーム内の【どこからアクセスしても統一性が保たれる】(唯一だから)「クラス名と実体が一致」
template<class TypeT>
class Singleton
{
public:
// Instance()を通して【しか】T型クラスをつくれないうえに、
// ★作ったものは【かならず唯一で複製不可】
// それが【シングルトン型】!
// ゲーム中で唯一で複製不可にしたい【ゲームを一律管理するクラス】などに最適!
// https://teratail.com/questions/17416
// http://rudora7.blog81.fc2.com/blog-entry-393.html
// ②↓同じ関数の定義はプログラム全体で1つだけしか許されません。(static関数名は被らない)
static inline TypeT& Instance()
{ //①↓関数の中で定義されたstatic変数は、関数の定義毎に1つ領域が確保されます。
static TypeT instance; //メモリ上にstatic変数確保(静かに常に待機するinstance)
return instance; //①=(常駐)②=(唯一) ①と②両方満たすinstanceは【メモリ上に唯一】
}
protected:
Singleton() {} // 外部でのインスタンス作成は禁止(protected:内部公開、外部禁止)
virtual ~Singleton() {} // 仮想デストラクタ(シングルトンを【継承するためには忘れちゃダメ】)
private:
void operator=(const Singleton& obj) {} // 代入演算子禁止
Singleton(const Singleton& obj) {} // コピーコンストラクタ禁止(コピーできない【唯一】)
static TypeT* instance; // private:だから外部アクセスできない【GetInstance()経由しか無理】
};
// いつもcppで書いたstatic変数初期化も★【Tテンプレートクラスだからすべて.hヘッダにまとめて書いた】
template< class TypeT >
TypeT* Singleton< TypeT >::instance = 0; //cppでの定義を.h内に書いた(static変数はclass内で初期化できないから)
#endif
SceneManager.hを新規作成
して、シングルトンを継承してどこからでもアクセスできるマネージャを定義します。
#ifndef SCNENEMANAGER_H_
#define SCNENEMANAGER_H_
#include <vector>
#include <memory>
#include <string>
#include <assert.h> // シーン読み込みの失敗表示用
#include "MyHash.h" // hash32型でタグを管理する
#include "Singleton.h"
class Scene; //クラス宣言だけで★インクルードしないのでこのマネージャファイルで循環は止まる
class SceneManager : public Singleton<SceneManager>//←<~>として継承すると唯一のシングルトン型タイプとなる
{
public:
friend class Singleton<SceneManager>; // Singleton でのインスタンス作成は許可
std::shared_ptr<Scene> prevScene{ nullptr }; // 一つ前のシーン
std::shared_ptr<Scene> currentScene{ nullptr }; // 現在のシーン
hash32 currentSceneType{ "" }; // 現在のシーン名を保管しておく
std::string currentSceneTag{ "" }; // 現在のシーンのタグを保管しておく
hash32 changingSceneType{ "" }; // 予約された次のシーンのクラス名(次に移動予定のシーン名を保管しておく)
std::string changingSceneTag{ "" }; // 予約された次のシーンのタグ(次に移動予定のシーンのタグを保管しておく)
// シーン名を比較、何の変化もなければfalseでシーン移動不必要の判定
bool isChanging(const std::string& nextSceneType, const std::string& nextSceneTag = "");
// 次のシーンの予約だけしてLoadScene()を引数なしで呼び出したタイミングで遷移する(ループの途中じゃないキリのいいタイミングを狙って遷移させる)
void NextScene(const std::string& nextSceneName, const std::string& nextSceneTag = "");
// シーンをチェンジし遷移する(前のシーンのリセット処理もする)
void LoadScene(const std::string& sceneName = "", const std::string& nextSceneTag = "");
protected:
SceneManager() {}; // 外部からのインスタンス作成は禁止
virtual ~SceneManager() {}; //外部からのインスタンス破棄も禁止
};
#endif
SceneManager.cppを新規作成
して、先行してGameSceneなどをロード・切り替えする処理を先に書いておきます。
#include "SceneManager.h"
#include "Scene.h"
#include "TitleScene.h"
#include "GameScene.h"
#include "GameOverScene.h"
bool SceneManager::isChanging(const std::string& nextSceneType, const std::string& nextSceneTag)
{
if (currentSceneType != nextSceneType) //シーンのタイプ不一致
return true; // シーンのタイプが変更
if (currentSceneTag != nextSceneTag) //シーンのタグ不一致
return true; // シーンのタグが変更
return false; // シーンに変更無し
}
void SceneManager::NextScene(const std::string& nextSceneType, const std::string& nextSceneTag)
{
// 遷移予定nextシーン名と現在のシーン名を比較、何の変化もなければfalseでシーン移動不必要の判定
if (isChanging(nextSceneType) == false) return; //特に変更なしでシーン移動の必要なし
changingSceneType = nextSceneType; // 次に移動予定のシーン名を保管しておく
}
void SceneManager::LoadScene(const std::string& nextSceneType, const std::string& nextSceneTag)
{
hash32 loadSceneType = nextSceneType;
std::string loadSceneTag = nextSceneTag;
// nextSceneTypeの指定が空の場合にはNextScene()での事前のシーン変更予約があるかチェック
if (nextSceneType == "" && changingSceneType != "")
{
loadSceneType = changingSceneType;
loadSceneTag = changingSceneTag;
}
if (loadSceneType == "") return; // シーン指定がないときは終了
// 現在のシーンの終了処理
if (currentScene != nullptr)
currentScene->Finalize(); // 終了処理を呼び出す
if (loadSceneType == "TitleScene")
{
// 次のシーンの生成
currentScene = std::make_shared<TitleScene>(loadSceneTag);
}
else if (loadSceneType == "GameScene")
{
// 次のシーンの生成
currentScene = std::make_shared<GameScene>(loadSceneTag);
}
else if (loadSceneType == "GameOverScene")
{
// 次のシーンの生成
currentScene = std::make_shared<GameOverScene>(loadSceneTag);
}
else
assert("指定されたシーンの生成処理が見つからなかった→SceneManager.cppを見直しましょう" == "");
currentSceneType = loadSceneType; // 現在のシーン名の更新
currentSceneTag = loadSceneTag; // 現在のシーンのタグの更新
changingSceneType = ""; // NextScenceのシーン予約名をリセット
changingSceneTag = ""; // NextScenceのシーン予約タグをリセット
// 次のシーンの初期化
currentScene->Initialize(); // 初期化を呼び出す
}
TitleScene.hを新規作成
して、特に今回は処理を書きませんが実際に制作するときにはタイトル画面を実装するためのひな形だけ作っておきます。
#ifndef TITLESCENE_H_
#define TITLESCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Input.h"
#include "SceneManager.h"
class TitleScene : public Scene
{
public:
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::Instance(); //唯一のシーンマネージャへの参照(&)を得る
// コンストラクタ
TitleScene(const std::string& sceneTag = "") : Scene(sceneTag)
{
this->typeTag = "TitleScene";
}
// 初期化処理
void Initialize() override
{
}
// 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
void Finalize() override
{
}
// 更新処理
void Update() override
{
if (Input::GetButtonDown(Pad::All, PAD_INPUT_1))
{
sm.LoadScene("GameScene"); //シーン遷移
return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
}
}
// 描画処理
void Draw() override
{
DxLib::DrawString(0, 0, "TitleSceneです。ボタン押下でGameSceneへ。", GetColor(255, 255, 255));
}
};
#endif
GameOverScene.hを新規作成
して、特に今回は処理を書きませんが実際に制作するときにはゲームオーバー画面を実装するためのひな形だけ作っておきます。
#ifndef GAMEOVERSCENE_H_
#define GAMEOVERSCENE_H_
#include "Scene.h"
#include "DxLib.h"
#include "Input.h"
#include "SceneManager.h"
class GameOverScene : public Scene
{
public:
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::Instance(); //唯一のシーンマネージャへの参照(&)を得る
// コンストラクタ
GameOverScene(const std::string& sceneTag = "") : Scene(sceneTag)
{
this->typeTag = "GameOverScene";
}
// 初期化処理
void Initialize() override
{
}
// 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
void Finalize() override
{
}
// 更新処理
void Update() override
{
if (Input::GetButtonDown(Pad::All, PAD_INPUT_1))
{
sm.LoadScene("TitleScene"); //シーン遷移
return; // シーンをロードしたらUpdateを即終了しないとUpdateの他の処理が走っちゃう
}
}
// 描画処理
void Draw() override
{
DxLib::DrawString(0, 0, "GameOver....ボタン押下でPlaySceneへ", GetColor(255, 255, 255));
}
};
#endif
DataCsv.hを新規作成
して、CSV形式のファイルをロードできるようにします。
#ifndef DATACSV_H_
#define DATACSV_H_
#include <vector>
#include <string>
#include <fstream> // ファイル読み出しifstreamに必要
#include <string> //文字列に必要
#include <sstream> // 文字列ストリームに必要
#include <assert.h> // 読込み失敗表示用
//★CsvValue型をint型や文字列string型や小数float型として【カメレオン】のように使える。
// 【エクセルはマス目がカメレオン】整数も小数も文字列もありうる。
// それを解消して統一して扱える便利な★CsvValue型。
class CsvValue
{
public:
enum class Type : unsigned char
{
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型データを格納
};
union {
std::string stringData{ "" }; // 文字列string型データを格納
};
public:
CsvValue() = default;
// 3種類のコンストラクタ
CsvValue(float floatData) : type{ Type::Float }
{
this->floatData = floatData;
this->stringData = std::to_string(floatData);
}
CsvValue(const std::string& stringData) : type{ Type::String }
{
this->stringData = stringData;
}
~CsvValue() {}; // 仮想デストラクタ
void operator=(const int& intValue) // 代入演算子
{
this->intData = intValue;
}
void copy(const CsvValue& value)
{
type = value.type;
if (value.type == Type::Int) { this->intData = value.intData; this->stringData = std::to_string(value.intData); }
else if (value.type == Type::Float) { this->floatData = value.floatData; this->stringData = std::to_string(value.floatData); }
else if (value.type == Type::String) { this->stringData = value.stringData; }
}
void operator=(const CsvValue& value) { copy(value); } // 代入演算子
CsvValue(const CsvValue& value) { copy(value); } // コピーコンストラクタ
// 変換演算子によりCSVのセルをintとしてもstringとしても扱えるように
// https://programming-place.net/ppp/contents/cpp/language/019.html#conversion_op
// 3種類の読み取りオペレータ
operator int()
{
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::Int) return (float)intData;
else if (type == Type::String && intData == 0)
floatData = floatFromString(); // デフォルトで文字列として読込んだデータをfloatに変換
return floatData;
}
inline operator std::string() const
{
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;
}
};
// CSVの1行を[]オペレータでアクセスした際1行のサイズが足りない場合に自動resizeする機能を[]オペレータに実装する
struct CsvLine : public std::vector<CsvValue>
{
inline std::vector<CsvValue>& operator[](std::size_t index) {
if (index >= size()) { resize(index + 1); }
return (*this)[index]; // 読み書き
}
};
// CSVファイルを読込み幅や高さとデータ本体を保持するデータ型
struct DataCsv
{
enum class CsvType
{
IntMap,
CsvValue
};
CsvType type;
bool isInitialized = false; //[継承2重ロード対策]ベース基底でロードしたらtrueに
// 読込んだデータファイルの情報
std::string FilePath{ "" };
std::vector<CsvLine> Data;// csvデータ
// ★スムーズに[][]でアクセスできるように[]演算子を独自定義する
// https://www.hiramine.com/programming/c_cpp/operator.html
inline std::vector<CsvValue>& operator[](std::size_t index) {
if (index >= Data.size())
Data.resize(index + 1); // 範囲外アクセス時は自動リサイズ(使う人が大きな数字間違っていれるとやばいので注意)
return Data[index]; // 読取り
}
std::size_t size()
{ // size()関数の名前をvectorと被らせることで使う側はvectorインvectorのままのコードで使える
return Data.size();
}
// 初期化コンストラクタでファイル名を指定して初期化と同時にファイル読込
DataCsv(std::string FilePath = "", CsvType csvType = CsvType::IntMap) : type{csvType}, FilePath { FilePath }
{// csvファイルの読込み★【初期化と同時なのでファイルとデータ型が一心同体で使いやすい】
if (FilePath != "")
{
Load(FilePath, csvType); // ファイル読込み
}
};
virtual ~DataCsv()
{// 仮想デストラクタ
clear();// 2次元配列データのお掃除
};
// データをクリアしてメモリを節約する
virtual void clear()
{ // [確実にメモリを空にするには] http://vivi.dyndns.org/tech/cpp/vector.html#shrink_to_fit
std::vector<CsvLine>().swap(Data); // 空のテンポラリオブジェクトでリセット
isInitialized = false; //ロード済みフラグをOFF
}
// csvファイルの読み込み
virtual void Load(std::string filePath, CsvType csvType = CsvType::IntMap)
{
if (filePath == "" || isInitialized) return; //ファイル名がないもしくはロード済
this->FilePath = filePath; // ファイル名を保管
this->type = csvType;
Data.clear(); //データを一旦クリア
// 読み込むcsvファイルを開く(std::ifstreamのコンストラクタで開く)
std::ifstream ifs_csv_file(filePath);
std::string line; //1行単位の読込み文字列
int readWidth = 0, readHeight = 0; //読込みデータの幅と高さ
int maxWidth = 0; //今から読み込んだ時のMAXの幅
while (std::getline(ifs_csv_file, line)) // ファイルを行ごとに読み込む
{
CsvLine valuelist; // 1行の数字リスト
std::istringstream linestream(line); // 各行の文字列ストリーム
std::string splitted; // カンマ分割文字列
int widthCount = 0; //この行の幅をカウント
if (this->type == CsvType::IntMap)
{
while (std::getline(linestream, splitted, { ',' }))
{
std::istringstream ss; //文字列ストリームの初期化
ss = std::istringstream(splitted); //文字列ストリーム
int num; // 数字単体
ss >> num; // 文字列ストリームから数字への変換
valuelist.push_back(CsvValue(num)); // 数字をリスト(1行リスト)に追加
widthCount++; //この行の幅をカウントアップ
}
}
else if (this->type == CsvType::CsvValue)
{
while (std::getline(linestream, splitted, { ',' }))
{
if (splitted.size() > 1 && splitted[0] == '\"')
splitted = splitted.substr(1, splitted.size() - 2);
valuelist.emplace_back(splitted); // 数字を数字のリスト(1行リスト)に追加
widthCount++; //この行の幅をカウントアップ
}
}
if (widthCount > maxWidth) maxWidth = widthCount; //暫定Max幅を更新
// 1行分をvectorに追加
if (valuelist.size() != 0) Data.emplace_back(valuelist);
readHeight++; //マップの高さをカウントアップ
}
readWidth = maxWidth; //MAX幅の王者を確定
assert(readWidth > 0); // マップ幅がおかしい
assert(readHeight > 0); // マップ高さがおかしい
// コンストラクタで初期化でロードの場合とLoad関数経由で読む経路があるから
isInitialized = true; // 読込み初期化済みフラグをON
return;
}
};
#endif
Mapフォルダの中にGameSceneSetting.csvを新規作成
して、下記内容のcsvのデータで、シーン1というタグでGameSceneを初期生成するとき、
ステージ1というタグのついたワールドにMap/stage1.jsonから読みだしたマップを読み出し生成する、という情報を記述し保存しておきましょう。(マップのjson読み出しはのちの章でやる予定です)
"//","GameSceneでシーン名を指定するとその行以降をそのシーンで使うworldsとしてロードする",,,,
"GameScene","シーン1",,,,
"ステージ1","Map/stage1.json",,,,
GameScene.hを新規作成
して、ゲームを実際にUpdateしたりDrawしたりするためのシーンを定義しましょう(ワールドの定義もシーンの中にある)。
#ifndef GAMESCENE_H_
#define GAMESCENE_H_
#include <memory> // 共有ポインタ std::shared_ptrを使う
#include <unordered_map> // ワールドにタグをつけて複数のワールドを管理 複数あれば(扉→別マップへの移動→元のマップへ戻っても敵が消えずに残せる)
#include "Scene.h"
#include "DxLib.h"
#include "Input.h"
#include "Resource.h"
#include "Screen.h"
#include "Camera.h"
#include "DataCsv.h"
#include "SceneManager.h"
struct GameSceneSetting // ゲームシーン情報をGameSceneSetting.csvなどから読みだす
{
GameSceneSetting(const std::string& csvFileName = "")
{
if (csvFileName != "") Load(csvFileName);
}
~GameSceneSetting() {}; //デストラクタ
struct WorldTag_Path
{
WorldTag_Path() = default;
inline WorldTag_Path(const std::string& tag, const std::string& mapFilePath) : tag{ tag }, mapFilePath{ mapFilePath } {}
inline ~WorldTag_Path() {}
std::string tag{ "" }; // ワールドにつけるtag
std::string mapFilePath{ "" }; // マップのファイルパス
};
struct Info : public std::unordered_map<hash32, WorldTag_Path> // <ワールド名のタグ, ワールドのファイルパス>
{
Info() = default;
inline Info(const std::string& gameSceneTag) : gameSceneTag{ gameSceneTag } {};
~Info() {};
std::string gameSceneTag{ "" }; // シーン名のタグ
};
std::unordered_map<hash32, Info> info; //[辞書]シーン名のタグ → ワールド情報
bool isInitialized = false; //[継承2重ロード対策]
std::string FilePath{ "" }; // csvのファイルパス
Info& operator[](const std::string& gameSceneTag) {
assert(info.count(gameSceneTag) != 0 && "シーン情報が読み取れていません Map/GameSceneSettings.csv に情報があるか確認!");
return info[gameSceneTag]; // 読み書き
}
void Load(const std::string& csvFileName = "")
{
if (isInitialized) return; // 2重ロード対策
if (csvFileName != "") this->FilePath = csvFileName;
if (this->FilePath == "") return;
DataCsv Data; // マップ情報一覧のデータ
Data.Load(csvFileName, DataCsv::CsvType::CsvValue);// CSVファイルからデータをロード
std::string gameSceneTag; //ゲームシーン名
for (int j = 0; j < Data.size(); ++j)
{
if ((std::string)Data[j][0] == "//") continue; // 一番左の列が//なら読み飛ばしのコメント行
else if ((std::string)Data[j][0] == "GameScene")
{
gameSceneTag = Data[j][1]; //ゲームシーン名
info.emplace(std::piecewise_construct, std::forward_as_tuple(gameSceneTag), std::forward_as_tuple(gameSceneTag));
continue;
}
if (gameSceneTag == "") continue; //ステージファイル名が未確定
else if ((std::string)Data[j][0] != "")
info[gameSceneTag][(std::string)Data[j][0]] = std::move(WorldTag_Path(Data[j][0], Data[j][1])); //ワールド名 → ワールドのファイルのパスを読み取り
}
isInitialized = true;
}
};
class GameScene : public Scene
{
public:
static GameSceneSetting settings; // ゲームシーンの設定情報 デフォルトは{ "Map/GameSceneSetting.csv" } を読み取り
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::Instance(); //唯一のシーンマネージャへの参照(&)を得る
int screenHandle0{ -1 }; // 上画面スクリーンのハンドル
int screenHandle1{ -1 }; // 下画面スクリーンのハンドル
// タイルマップを読み込む 縦8マス、横8マス、縦32ピクセル、横32ピクセル
std::string mapChipPath{ "Map/mapchip.png" };
std::string playerImgPath1{ "Image/pikkoron.png" };
std::string playerImgPath2{ "Image/satan.png" };
protected:
std::unordered_map<hash32, std::shared_ptr<World>> worlds; // 複数ワールドの共有ポインタ辞書
public:
// ワールドをタグをつけて生成する
void CreateWorld(const std::string& worldTag) { if (worlds.count(worldTag) == 0) { worlds.emplace(worldTag, std::make_shared<World>(this, worldTag)); } }
// タグからワールドへのポインタリンクを得る
World* world(const std::string& worldTag) { auto itr = worlds.find(worldTag); return (itr != end(worlds)) ? itr->second.get() : nullptr; }
// 指定したゲームシーンのタグに関連づいたファイル名に対応したワールドを初期化する
bool LoadWorlds(const std::string& gameSceneTag);
// プレイヤはタイトル画面などでも初期選択できるようにあえてworld配下の管理ではなく
// GameScene配下の管理でゲーム開始時から直接生成できるようにここに定義
std::unordered_map<hash32, std::shared_ptr<Player>> players; // <タグ,プレイヤへの共有ポインタ>辞書[複数プレイヤ可能]
void ChangeWorld(const std::string& worldTag, std::shared_ptr<Player> pPlayer); // プレイヤのいるワールドを移動する
int score = 0; // プレイ中のゲームのスコア:短期的数値でシーン遷移ごとにリセットされる
// コンストラクタ
GameScene(const std::string& sceneTag = "") : Scene(sceneTag)
{
this->typeTag = "GameScene";
if (this->tag != "") // this->tag には : Scene(sceneTag) 経由で sceneTag が設定されている
LoadWorlds(this->tag); // GameSceneSetting.csv に設定したタグに対応したファイルをロード
}
// 初期化処理
void Initialize() override;
// 終了処理(大抵Initializeと同じリセットだがInitと区別して終了時だけやりたいリセットもある)
void Finalize() override
{
// ここにプレイ終了時のリセット処理(ゲームクリアやゲームオーバーなど)
}
// 更新処理
void Update() override;
// 描画処理
void Draw() override;
};
#endif
World.hを変更
して、AddPlayerでシーンにプレイヤ追加する関数などを定義しましょう。
#ifndef WORLD_H_
#define WORLD_H_
#include <list>
#include <memory>
#include <unordered_map>
#include <functional> // std::functionのラムダ式 で 関数を Draw関数の引数にして、カメラすべてに対して描画処理を発動させる
#include "MyHash.h" // ワールドのタグをhash32型で管理する
// [前方宣言] 宣言だけして、#includeはしてないので、ポインタだけはこのファイル内でつかえる
// #includeはしてないので、hpなどの変数には#include "~.h"しないとアクセスできないので循環インクルード防止のため.cppでインクルードしてください
class GameScene;
class Player;
class Bullet;
class Enemy;
class Camera;
// プレイヤや敵や弾などの共有ポインタを管理し、EraseRemoveIfで消して参照カウンタが0になったらそれらを消滅させるクラス(C#のガベージコレクタの代わり)
class World
{
public:
inline World(GameScene* pScene, const std::string& worldTag = "") : m_pScene{ pScene }, tag{ worldTag } {}
inline ~World() {}
protected: // public:にすると他で外部で後からタグやシーンを書き換えられると辞書での管理などの前提がおかしくなるのでprotected:する
hash32 tag{ "" }; // ワールド名のタグ
GameScene* m_pScene{ nullptr }; // ワールドを所有するシーン
public:
hash32 Tag() const { return tag; }
GameScene* scene() { return m_pScene; }
// ワールドを所有するシーンにプレイヤを追加する
void AddPlayer(const std::string& playerTag, std::shared_ptr<Player> pPlayer);
// ワールドにいるプレイヤを得る
std::vector<std::weak_ptr<Player>> GetPlayers();
std::unordered_map<hash32, std::shared_ptr<Player>> players; // プレイヤのタグ辞書
std::list<std::shared_ptr<Bullet>> bullets; // 弾のリスト
std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
// 削除処理を共通テンプレート関数にする
// [共通テンプレート関数]https://programming-place.net/ppp/contents/cpp/language/009.html#function_template
template <typename TypeT, class T_if>
void EraseRemoveIf(std::list<TypeT>& 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()の位置
};
std::unordered_map<hash32, std::shared_ptr<Camera>> cameras; // このワールドに存在するカメラの辞書<カメラにつけたタグ, カメラの共有ポインタ>
// 指定されたタグのカメラへの共有ポインタを得る カメラ辞書に存在しないタグでアクセスしたときにunordered_mapの特性で勝手に意図しないカメラができるのを防ぐ関数
std::shared_ptr<Camera> camera(const std::string& cameraTag) { auto itr = cameras.find(cameraTag); return (itr != end(cameras)) ? itr->second : nullptr; }
// このワールドに存在するカメラすべてに対して drawFuncで描きたい処理を渡して 全カメラのスクリーンに対して描画処理を走らせる
void Draw(std::function<void()> drawFunc) noexcept;
// カメラにタグをつけてワールドに追加 カメラにもワールドへのポインタリンクを張る
inline bool AddCamera(const std::string& cameraTag, std::shared_ptr<Camera> pCamera);
// 指定したタグのカメラをこのワールドから削除する カメラ側の worldへのリンクのポインタも nullptr にする
inline bool EraseCamera(const std::string& cameraTag);
// カメラを nowWorld から nextWorld に移動させる
static bool MoveCamera(const std::string& cameraTag, World* nowWorld, World* nextWorld);
};
#endif
World.cppを変更
して、シーンにプレイヤを追加するAddPlayer関数などを追加しましょう。
#include "World.h"
#include "Player.h"
#include "Camera.h"
#include "Screen.h"
#include "GameScene.h"
void World::AddPlayer(const std::string& playerTag, std::shared_ptr<Player> pPlayer)
{
if (m_pScene != nullptr)
{
m_pScene->players.emplace(playerTag, pPlayer); // シーンにプレイヤを追加する
}
}
// このワールドにいるプレイヤを得る
std::vector<std::weak_ptr<Player>> World::GetPlayers()
{
std::vector<std::weak_ptr<Player>> players;
for (auto&& pPlayer : players)
{
players.emplace_back(pPlayer);
}
return players;
}
// カメラにタグをつけてワールドに追加 カメラにもワールドへの生ポインタリンクを渡す
bool World::AddCamera(const std::string& cameraTag, std::shared_ptr<Camera> pCamera)
{
if (pCamera == nullptr) return false;
if (cameras.count(cameraTag) > 0) return false; // すでに同じタグのカメラがワールドにあったら false
if (pCamera->world() != nullptr) pCamera->world()->EraseCamera(cameraTag); // すでにカメラが別ワールドに配置されていたら別ワールドから削除
cameras.emplace(cameraTag, pCamera); // ワールドにカメラを追加する
pCamera->world(this); // カメラにもワールドへのポインタリンクを張る(カメラ側に渡すのは共有ポインタではない生ポインタだから共有デッドロックにはならない)
return true;
}
// 指定したタグのカメラをこのワールドから削除する カメラ側の worldへのリンクのポインタも nullptr にする
bool World::EraseCamera(const std::string& cameraTag)
{
auto itr = cameras.find(cameraTag); // 指定したタグのカメラを探す
if (itr == end(cameras)) return false; // 指定したタグのカメラがワールドにない
auto pCamera = itr->second;
pCamera->world(nullptr); // カメラ側の worldへのリンクのポインタも nullptrに
cameras.erase(cameraTag); // カメラを辞書から削除
return true;
}
// カメラを nowWorld から nextWorld に移動させる
bool World::MoveCamera(const std::string& cameraTag, World* nowWorld, World* nextWorld)
{
if (nowWorld == nullptr) return false;
auto itr = nowWorld->cameras.find(cameraTag); // タグでカメラを探す
if (itr == end(nowWorld->cameras)) return false; // タグに関連づいたカメラがなかった
auto targetCamera = itr->second; // カメラの共有ポインタをキープ(次の行でeraseしてもカメラの共有カウンタは0にはならずこのリンクのおかげでカメラはメモリに残存)
targetCamera->world(nullptr); // カメラ側の worldへのリンクのポインタを一旦 nullptrに
nowWorld->cameras.erase(cameraTag); // nowWorld からは カメラを消す
if (nextWorld == nullptr) return false;
return nextWorld->AddCamera(cameraTag, targetCamera); // 次のワールドにカメラを追加する
}
// このワールドに存在するカメラすべてに対して funcで描きたい処理を渡して 全カメラのスクリーンに対して描画処理を走らせる
void World::Draw(std::function<void()> drawFunc) noexcept
{
for (auto&& keyValue : cameras) // このワールドにあるすべてのカメラぶんループ
{
VECTOR_D beforeCamPos; // 設定変更前のカメラのパラメータを一旦、保管
double beforeVRot, beforeHRot, beforeTRot, beforeNear, beforeFar, beforeFov;
Camera::GetCameraSettings(&beforeCamPos, &beforeVRot, &beforeHRot, &beforeTRot, &beforeFov, &beforeNear, &beforeFar);
auto& camera = keyValue.second;
//camera->screenID
DxLib::SetCameraPositionAndAngle(camera->pos, camera->rot.V, camera->rot.H, camera->rot.T); // 各カメラの位置、回転を設定
Screen::Start(camera->screenHandle, true, camera->fovAngle); // カメラに関連付けられたスクリーンIDを描画先にする
{
drawFunc(); // 渡された描画処理を実行
}
Screen::End(true); // 描画先を元に戻す
// カメラの設定をもとに戻しておく
Camera::SetCameraSettings(&beforeCamPos, &beforeVRot, &beforeHRot, &beforeTRot, &beforeFov, &beforeNear, &beforeFar);
}
}
GameScene.cppを新規作成
して、ゲームをUpdateしたりDrawしたりする処理を実装しましょう。
#include "GameScene.h"
#include "Player.h"
GameSceneSetting GameScene::settings{ "Map/GameSceneSetting.csv" }; // ゲームシーンの設定情報
// 指定したゲームシーンのタグに関連づいたファイル名に対応したワールドを初期化する
bool GameScene::LoadWorlds(const std::string& gameSceneTag)
{
if (settings.info.count(gameSceneTag) == 0)
return false;
auto& info = settings[gameSceneTag];
for (auto&& tag_mapPath : info)
{
CreateWorld(tag_mapPath.second.tag); // ワールドをタグをつけて生成
}
return true;
}
void GameScene::ChangeWorld(const std::string& worldTag, std::shared_ptr<Player> pPlayer)
{
if (pPlayer == nullptr) return;
if (worlds.count(worldTag) == 0) return; // 存在しないタグだった
pPlayer->world(worlds[worldTag].get()); // ワールドへのリンクをすげ替える
}
void GameScene::Initialize()
{// Init処理
// Zバッファを有効にする [Zの深度]
//[参考]https://docs.google.com/presentation/d/1Z23t1yAS7uzPDVakgW_M02p20qt9DeTs4ku4IiMDLco/edit?usp=sharing
//[参考] https://dxlib.xsrv.jp/function/dxfunc_3d_draw.html#R14N12
DxLib::SetUseZBuffer3D(TRUE);
// Zバッファへの書き込みを有効にする
DxLib::SetWriteZBuffer3D(TRUE);
// 光の明暗計算を無効に
DxLib::SetUseLighting(FALSE);
// SetDrawScreen に引数として渡せる( 描画対象として使用できる )グラフィックハンドルを作成するかどうかを設定する
// ( TRUE:描画可能グラフィックハンドルを作成する FLASE:通常のグラフィックハンドルを作成する( デフォルト ) )
DxLib::SetDrawValidGraphCreateFlag(TRUE);
// サブのスクリーンを2つ生成 画面の全体の縦方向サイズの Height / 2 にして、上下分割で配置できるようにする
screenHandle0 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
screenHandle1 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
// スクリーンを作成したらデフォルト設定に戻しておかないと
DxLib::SetDrawValidGraphCreateFlag(FALSE);
Resource::MakeShared<Texture>(mapChipPath, 8, 8, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
Resource::MakeShared<Texture>(playerImgPath1, 3, 4, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
Resource::MakeShared<Texture>(playerImgPath2, 3, 4, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
// プレイヤの生成
if (pWorld != nullptr)
{
//pWorld->AddCamera("カメラ1", Camera::defaultCamera); // カメラ1というタグをデフォルトカメラにつけてワールドにデフォルトカメラを配置
pWorld->AddCamera("カメラ1", std::make_shared<Camera>(pWorld, Vector3{ 0,0,0 }));
auto pCamera1 = pWorld->camera("カメラ1"); // カメラ1 というタグのついたカメラへの共有ポインタを得る
pCamera1->SetScreenHandle(screenHandle0); // カメラから見える3Dを描く スクリーンのハンドル を関連付ける
auto pPlayer1 = std::make_shared<Player>(pWorld, Pad::Key, Vector3(90 + 256, 32, 95 + 256));
pWorld->AddPlayer("プレイヤ1", pPlayer1); // シーンにプレイヤを追加する
pPlayer1->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath1]);
pPlayer1->camera = pCamera1.get(); // カメラをプレイヤにリンクする
pWorld->AddCamera("カメラ2", std::make_shared<Camera>(pWorld, Vector3{ 0,0,0 }));
auto pCamera2 = pWorld->camera("カメラ2"); // プレイヤ2 というタグのついたカメラへの共有ポインタを得る
pCamera2->SetScreenHandle(screenHandle1); // カメラから見える3Dを描く スクリーンのハンドル を関連付ける
auto pPlayer2 = std::make_shared<Player>(pWorld, Pad::Two, Vector3(190, 32, 195));
pWorld->AddPlayer("プレイヤ2", pPlayer2); // シーンにプレイヤを追加する
pPlayer2->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath2]);
pPlayer2->camera = pCamera2.get(); // カメラをプレイヤにリンクする
}
};
void GameScene::Update()
{// 更新処理
Screen::ClearDrawScreen(screenHandle0); // 一旦キャンバスをきれいにまっさらに
Screen::ClearDrawScreen(screenHandle1); // 一旦キャンバスをきれいにまっさらに
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
for (const auto& player : players)
player.second->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
}
void GameScene::Draw()
{// 描画処理
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
for (const auto& player : players)
{
player.second->Draw(); // プレイヤの描画【忘れるとプレイヤ表示されない】
// プレイヤのいるワールドのカメラのみを描けば、プレイヤのいないワールドは描かずに済む
// 複数のカメラを 範囲 for文 で回す
for (auto&& camera : player.second->world()->cameras)
{ // ラムダ式[&](){ ~ }で{}の外側の変数すべてを & キャプチャで&参照として「Draw関数の内側へ引き連れて」処理できる
camera.second->Draw([&]()
{
// Draw関数() の 外側にある変数 mapChipPath も[&]効果で { } の内側で問題なくアクセスできている↓
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(40, 40, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 2);
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(0, 40, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 0);
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(0, 70, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 8);
if (camera.first == "カメラ1")
DxLib::DrawString(0, 0, "こちらがプレイヤ1", GetColor(255, 0, 0));
else if (camera.first == "カメラ2")
DxLib::DrawString(0, 0, "こちらはプレイヤ2", GetColor(0, 255, 0));
});
}
}
// スクリーン0の内容を画面に描く
DxLib::DrawGraph(0, 0, screenHandle0, FALSE);
DxLib::DrawLineBox(0, 0, Screen::Width, Screen::Height / 2, GetColor(255, 0, 0));
// スクリーン1の内容を画面に描く
DxLib::DrawGraph(0, Screen::Height / 2, screenHandle1, FALSE);
DxLib::DrawLineBox(0, Screen::Height / 2, Screen::Width, Screen::Height, GetColor(0, 255, 0));
}
Game.hを新規作成
して、各シーンで共通する処理をUpdateしたりDrawしたりする処理を実装しましょう。
#ifndef GAME_H_
#define GAME_H_
#include "Screen.h"
#include "SceneManager.h"
#include "Scene.h"
#include "MyRandom.h"
#include "Input.h"
class Game
{
public:
Game() {}; // 初期化コンストラクタ
~Game() {}; // 破棄処理デストラクタ
//★【仲介者パターン】ゲーム内のもの同士はマネージャを通して一元化してやり取り
SceneManager& sm = SceneManager::Instance(); //唯一のシーンマネージャへの参照(&)を得る
void Init()
{// Init処理
// 各種Init中に、ユーザーにお待ちいただく間、不安を和らげるためのメッセージをScreenFlipで表示してからInit開始
std::string loadingStr("ゲーム起動中... 必要なファイルをロードしています");
int strSize = DxLib::GetFontSize() * loadingStr.size();
DxLib::DrawString(Screen::Width / 2 - strSize / 2, Screen::Height / 2, loadingStr.c_str(), GetColor(255, 255, 255));
DxLib::ScreenFlip();
Input::Init(); // 入力機能の初期化
MyRandom::Init(); // 乱数のシードを現時点の時刻に連動して初期化
// ゲーム開始して最初のシーンに遷移
sm.LoadScene("GameScene", "シーン1");
};
void Update() {// 更新処理
Input::Update(); //【注意】これ忘れるとキー入力押してもキャラ動かない
// シーンの更新処理
sm.currentScene->Update();
// NextScene関数で次のシーン移動予約があったらシーンを移動する
sm.LoadScene(); // 引数無し呼び出しの場合、事前に NextScene関数で予約がない場合はスルーされる
}
void Draw()
{// 描画処理
// シーンの描画処理
sm.currentScene->Draw();
}
};
#endif
main.cppを変更
して、Gameクラスを通じてシーンをInit、Update、Drawしましょう。
#include "DxLib.h"
#include "Game.h"
#include "Screen.h"
#include "Camera.h"
#include "MyHash.h"
#include "Resource.h"
#include "MyDraw.h"
#include "Player.h"
#include "World.h"
// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// 画面モードの設定
DxLib::SetGraphMode(Screen::Width, Screen::Height, 32); // 画面サイズ Width×Heightのカラービット数32ビットで起動
DxLib::SetWindowSize(Screen::Width, Screen::Height);// ウィンドウサイズ Width×Height(こことSetGraphModeのサイズが異なると画像がゆがむ)
DxLib::ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
DxLib::SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
DxLib::SetMainWindowText("3Dタイルゲーム");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも
//↑ここまでの設定は↓下のDXライブラリ初期化より先にやらないとDxLib_Init()中は画面がフルスクリーンになって終わってからウィンドウサイズが変更になり見苦しい
// DXライブラリの初期化
if (DxLib_Init() < 0)
{
// DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
return -1;
}
// 初期化処理
Game game;
game.Init(); // gameのInit準備
DxLib::ScreenFlip();
// タイルマップを読み込む 縦8マス、横8マス、縦32ピクセル、横32ピクセル
std::string mapChipPath{ "Map/mapchip.png" };
Resource::MakeShared<Texture>(mapChipPath,8,8,32,32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
std::string playerImgPath1{ "Image/pikkoron.png" };
Resource::MakeShared<Texture>(playerImgPath1, 3, 4, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
std::string playerImgPath2{ "Image/satan.png" };
Resource::MakeShared<Texture>(playerImgPath2, 3, 4, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
// SetDrawScreen に引数として渡せる( 描画対象として使用できる )グラフィックハンドルを作成するかどうかを設定する
// ( TRUE:描画可能グラフィックハンドルを作成する FLASE:通常のグラフィックハンドルを作成する( デフォルト ) )
DxLib::SetDrawValidGraphCreateFlag(TRUE);
// サブのスクリーンを2つ生成 画面の全体の縦方向サイズの Height / 2 にして、上下分割で配置できるようにする
int screenHandle0 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
int screenHandle1 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
// スクリーンを作成したらデフォルト設定に戻しておかないと
DxLib::SetDrawValidGraphCreateFlag(FALSE);
// ワールドを生成
std::shared_ptr<World> world1 = std::make_shared<World>(nullptr, "ワールド1");
// カメラを2つ生成して、2つのカメラ視点を2つのスクリーンにそれぞれ関連付ける
world1->cameras.emplace("カメラ0", std::make_shared<Camera>(world1.get(), Vector3{ 20,40,-50 })); // カメラ0
world1->cameras.emplace("カメラ1", std::make_shared<Camera>(world1.get(), Vector3{ 80,30,-50 })); // カメラ1
world1->cameras["カメラ0"]->screenHandle = screenHandle0; // カメラ0 と スクリーン0 を関連付ける
world1->cameras["カメラ1"]->screenHandle = screenHandle1; // カメラ1 と スクリーン1 を関連付ける
world1->players.emplace("プレイヤ0", std::make_shared<Player>(world1.get(), Pad::Key, Vector3{ 20,30,-50 }));
world1->players.emplace("プレイヤ1", std::make_shared<Player>(world1.get(), Pad::Two, Vector3{ 80,30,0 }));
world1->players["プレイヤ0"]->SetCamera(world1->cameras["カメラ0"].get()); // .get()で共有ポインタstd::shared_ptrから生の*ポインタへ変換
world1->players["プレイヤ1"]->SetCamera(world1->cameras["カメラ1"].get());
world1->players["プレイヤ0"]->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath1]);
world1->players["プレイヤ1"]->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath2]);
Input::Init(); // 入力状態を初期化
// アニメーション(パラパラ漫画)するにはWhile文
while (DxLib::ProcessMessage() == 0)
{// ProcessMessage() == 0になるのは×ボタン押したときなど
game.Update(); //ゲームの更新処理
DxLib::ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
game.Draw(); // パラパラ漫画の描画処理
DxLib::ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
Screen::ClearDrawScreen(screenHandle0); // 一旦キャンバスをきれいにまっさらに
Screen::ClearDrawScreen(screenHandle1); // 一旦キャンバスをきれいにまっさらに
Input::Update(); // 入力状態を更新
// 光の明暗計算を無効に
DxLib::SetUseLighting(FALSE);
// Zバッファを有効にする [Zの深度]
//[参考] https://dxlib.xsrv.jp/function/dxfunc_3d_draw.html#R14N12
DxLib::SetUseZBuffer3D(TRUE);
// Zバッファへの書き込みを有効にする
DxLib::SetWriteZBuffer3D(TRUE);
// world1のプレイヤを更新する
for (auto& pair : world1->players)
{
auto pPlayer = pair.second;
pPlayer->Update();
}
// world1のプレイヤを描画する
for (auto& pair : world1->players)
{
auto pPlayer = pair.second;
pPlayer->Draw();
}
// 複数のカメラを 範囲 for文 で回す
for (auto&& camera : world1->cameras)
{ // ラムダ式[&](){ ~ }で{}の外側の変数すべてを & キャプチャで&参照として「Draw関数の内側へ引き連れて」処理できる
camera.second->Draw([&]()
{
// Draw関数() の 外側にある変数 mapChipPath も[&]効果で { } の内側で問題なくアクセスできている↓
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(40, 40, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 2);
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(0, 40, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 0);
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(0, 70, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 4);
if (camera.first == "カメラ0") //←"カメラ0"はビルド時に事前にハッシュ値が計算され、文字列ではない数値とおきかわっている
DxLib::DrawString(0, 0, "こちらがカメラ0", GetColor(255, 0, 0));
else if (camera.first == "カメラ1")
DxLib::DrawString(0, 0, "こちらはカメラ1", GetColor(0, 255, 0));
});
}
// スクリーン0の内容を画面に描く
DxLib::DrawGraph(0, 0, screenHandle0, FALSE);
DxLib::DrawLineBox(0, 0, Screen::Width, Screen::Height / 2, GetColor(255, 0, 0));
// スクリーン1の内容を画面に描く
DxLib::DrawGraph(0, Screen::Height / 2, screenHandle1, FALSE);
DxLib::DrawLineBox(0, Screen::Height / 2, Screen::Width, Screen::Height, GetColor(0, 255, 0));
DxLib::ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
}
// キー入力待ちをする
DxLib::WaitKey();
// DXライブラリの後始末
DxLib::DxLib_End();
// ソフトの終了
return 0;
}
MyTimer.hを新規作成
して、chrono経由で現在時刻などを計測できる機能をシングルトンとして準備しましょう。
#ifndef MYTIMER_H_
#define MYTIMER_H_
#include <chrono> // 精度の高い時刻を得る(C++ならWindows,Macなど色んな環境で使える)
#include <thread> // 処理休止sleep_forするために必要
#include <string> // 文字列string型に必要
#include <sstream> // ostringstreamに必要 標準出力(ostringstreamで文字列に標準出力を)
#include <iomanip> // 時間の文字列出力put_time()に必要
#include "Singleton.h"
// マルチプラットフォーム対応のchrono使用の時刻取り扱いクラス(C++11対応なら移植容易)
class MyTimer : public Singleton<MyTimer>
{
public:
friend class Singleton<MyTimer>; // Singleton でのインスタンス作成は許可
// 最初にsystem_clockとsteady_clockの両方で現在時間を取得
// ★system_clockは【WindowなどOSの時刻(ユーザーが変更したら巻き戻しもありうる)】
// ★steady_clockは【↑OSに依存しない時刻(不可逆なので時間計測に向いている)】
std::chrono::system_clock::time_point systemInitTime = std::chrono::system_clock::now();
std::chrono::steady_clock::time_point steadyInitTime = std::chrono::steady_clock::now();
std::chrono::steady_clock::time_point steadyNowTime = std::chrono::steady_clock::now();
std::chrono::system_clock::time_point systemTimeFromStart = UpdateSystemTimeFromStart();
// 時刻を保管する文字列【注意】UpdateSystemTimeFromStart()呼ばないうちは最新に更新されない
std::string timeStringSystemClock = GetTimeString("%Y年%m月%d日%H時%M分%S秒");
float timeScale = 1.0f;//[実験機能]main.cppで画面を描く速度と連動させてスローモーションや早送りも
bool isTimeScaleMode = false; //[実験機能]スローモーション、早送りモードをON OFF
// 起動時間の記録の初期化/リセット(Initし直せば再取得もできる)
inline void Init()
{
// 最初にsystem_clockとsteady_clockの両方で現在時間を取得(再取得も)
systemInitTime = std::chrono::system_clock::now();
steadyInitTime = std::chrono::steady_clock::now();
}
// 現時刻をOSに左右されず求める(計算結果はsystemTimeFromStartに保管)
inline std::chrono::system_clock::time_point UpdateSystemTimeFromStart()
{
steadyNowTime = std::chrono::steady_clock::now(); //現時刻を得る(OSの時刻じゃない方)
// 開始時のsteady_clock::nowとの差をとって、開始時のsystem_clock::nowに足す
// [参考] https://techblog.kayac.com/steady_clock
systemTimeFromStart =
systemInitTime + std::chrono::duration_cast<std::chrono::seconds>(steadyNowTime - steadyInitTime);
return systemTimeFromStart;
}
// 現時刻を特定の文字列フォーマットで得る(得たものはsteadyNowTimeとtimeStringSystemClockに保管)
// 出力は"%c"ならフォーマットは[Mon Nov 27 10:58:32 2017]
// "%Y年%m月%d日%H時%M分%S秒"なら[2020年12月11日14時30分15秒](デフォルト)
// [フォーマット例] https://cpprefjp.github.io/reference/chrono/local_time.html
inline std::string& GetTimeString(const char* timeStringFormat = "%Y年%m月%d日%H時%M分%S秒")
{
systemTimeFromStart = UpdateSystemTimeFromStart();//現時刻をOS時刻に左右されず得る
// 時刻タイプ型をsystem_clockのtime_point型からtime_t型に変換する。
std::time_t timeFromStart = std::chrono::system_clock::to_time_t(systemTimeFromStart);
// ローカルタイムを得る(ローカルタイムについては以下リンク参照)
// https://programming.pc-note.net/c/time.html
struct tm localTimeFromStart;
localtime_s(&localTimeFromStart, &timeFromStart);
//【ostringstream】は文字列ストリームに<<演算子などの方法でデータを流し込んでいく
// https://programming-place.net/ppp/contents/cpp/library/029.html#write_to_string_stream
std::ostringstream oss;
//↓この一行で文字列ストリームに流し込み
oss << std::put_time(&localTimeFromStart, timeStringFormat) << std::endl;
timeStringSystemClock = oss.std::ostringstream::str(); //str()で文字列string化して保管
return timeStringSystemClock; //現時刻を文字列として返す
}
// Initしてから現在までの経過時間を得る(単位Tick = 100ナノ秒 = 1/10000000秒)
inline long GetElapsedTicks()
{ // 時刻を更新
UpdateSystemTimeFromStart();
// Initしてから現在までの経過時間(ナノ秒)
auto durationFromInit = std::chrono::duration_cast<std::chrono::nanoseconds>(steadyNowTime - steadyInitTime);
// Tick数。1Tick = 100ナノ秒 = 1/10000000秒
return (long)(durationFromInit.count() / 100);
}
// 指定ナノ秒 プログラムを休止
inline void SleepForNanoSec(int waittime)
{ // 指定ナノ秒 プログラムを休止
std::this_thread::sleep_for(std::chrono::nanoseconds(waittime));
}
// 指定マイクロ秒 プログラムを休止
inline void SleepForMicroSec(int waittime)
{ // 指定ミリ秒 プログラムを休止
std::this_thread::sleep_for(std::chrono::microseconds(waittime));
}
// 指定ミリ秒 プログラムを休止
inline void SleepForMilliSec(int waittime)
{ // 指定ミリ秒 プログラムを休止
std::this_thread::sleep_for(std::chrono::milliseconds(waittime));
}
// 指定 秒 プログラムを休止
inline void SleepForSec(int waittime)
{ // 指定 秒 プログラムを休止
std::this_thread::sleep_for(std::chrono::seconds(waittime));
}
// 指定 分 プログラムを休止
inline void SleepForMinutes(int waittime)
{ // 指定 分 プログラムを休止
std::this_thread::sleep_for(std::chrono::minutes(waittime));
}
protected:
// 外部からのインスタンス作成は禁止
MyTimer() {};
//外部からのインスタンス破棄も禁止
virtual ~MyTimer() {};
};
#endif
main.cppを変更
して、MyTimerクラスでゲームの毎フレーム描画処理(GameDraw)を「スキップやウェイト待ち処理」などしてフレームレートを安定させましょう。
#include "DxLib.h"
#include "Game.h"
#include "MyTimer.h"
#include "Screen.h"
//[g_グローバル変数 と s_スタティックグローバル変数の違い] https://zenn.dev/melos/articles/1ddb67d024220f
// グローバル変数は「他のファイルからもアクセス可能な変数」になる
// staticグローバル変数は「他のファイルからはアクセスできないこのファイル限定呼び出し可能変数」になる
// FPS 60フレームを目標にして描画に時間がかかるときに描画をスキップして1秒あたりのフレームレートを安定させる
int g_targetFPS{ 60 }; // 目標のFPS(Frame Per Second, 1秒あたりのフレーム数)
static bool s_isEnableFrameSkip{ true }; // 高負荷時にフレームスキップするか(falseの場合は処理落ち(スロー))
double g_maxAllowSkipTime{ 0.2 }; // フレームスキップする最大間隔(秒)これ以上の間隔が空いた場合はスキップせずに処理落ちにする。
long g_intervalTicks = (long)(1.0 / g_targetFPS * 10000000); // フレーム間のTick数 1Tick = 100ナノ秒 = 1/10000000秒
int g_maxAllowSkipCount = (int)(g_targetFPS * g_maxAllowSkipTime); // フレームスキップを許す最大数
static long s_nextFrameTicks = g_intervalTicks; // 次のフレームの目標時刻
static long s_fpsTicks{ 0 }; // FPS計測のためのTicks
static int s_skipCount{ 0 }; // 何回連続でフレームスキップしたか
static int s_fpsFrameCount{ 0 }; // FPS計測のためのフレームカウント 60回数えるごとに、要した時間からFPSを算出する
static float s_currentFPS; // 現在のFPS (Frame per Second)
// ゲームを描く処理
static inline void GameDraw(Game& game)
{//↑staticグローバル関数は「他のファイルからはアクセスできないこのファイル限定呼び出し可能関数」になる
DxLib::ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
game.Draw(); // パラパラ漫画の描画処理
#ifdef DEBUG_DRAW_FPS
DxLib::DrawString(Screen::Width - 130, 30, (std::string("FPS:") + std::to_string(s_currentFPS)).c_str(), GetColor(255, 255, 255));
#endif
DxLib::ScreenFlip(); // 裏で描いておいたパラパラ漫画を表面に入れ替えフリップ
s_skipCount = 0; // フレームスキップのカウントをリセット
}
// 設定画面の【リンカー】→【システム】の【サブシステム】をWindows(SUBSYSTEM:WINDOWS)に設定するとWinMainからプログラムが開始する
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// [Windowsの機能]このプログラム実行の処理優先度を上げたいときに呼ぶ
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
// ★垂直同期とはモニターへの描画が完了するまで処理全体を停止し、
// 描画処理と更新処理の間隔を双方一定となるように処理の間隔を調整すること
// https://qiita.com/Seiten_Minagawa/items/615312ebe5e688ffee7f
// 画面リフレッシュレートと目標フレームレートが等しい場合は垂直同期を有効に、等しくない場合は垂直同期を無効にする
DxLib::SetWaitVSyncFlag( (DxLib::GetRefreshRate() == g_targetFPS) ? TRUE : FALSE ); //垂直同期を切るか?
//DxLib::SetWaitVSyncFlag(0); // 垂直同期を強制的に切りたい場合はコメントをはずす https://www.ay3s-room.com/entry/dxlib-framerate-controll
// 画面モードの設定
DxLib::SetGraphMode(Screen::Width, Screen::Height, 32); // 画面サイズ Width×Heightのカラービット数32ビットで起動
DxLib::SetWindowSize(Screen::Width, Screen::Height);// ウィンドウサイズ Width×Height(こことSetGraphModeのサイズが異なると画像がゆがむ)
DxLib::ChangeWindowMode(TRUE);//フルスクリーン表示かウィンドウ表示か
DxLib::SetMouseDispFlag(TRUE);// ここをFALSEにするとマウスカーソル非表示
DxLib::SetMainWindowText("3Dタイルゲーム");//この行でエラーになったら【設定】マルチバイト文字セットが間違ってるかも
DxLib::SetAlwaysRunFlag(TRUE); // ウィンドウが非アクティブでも動作させるか?
//↑ここまでの設定は↓下のDXライブラリ初期化より先にやらないとDxLib_Init()中は画面がフルスクリーンになって終わってからウィンドウサイズが変更になり見苦しい
// DXライブラリの初期化
if (DxLib_Init() < 0)
{
// DXの初期化にエラーが発生したらプログラム自体を終了(returnで)
return -1;
}
// 独自時刻クラスの参照を得る(シングルトン型)
MyTimer& timer = MyTimer::Instance();
timer.Init(); // 時刻計測開始
// 初期化処理
Game game;
game.Init(); // gameのInit準備
#ifdef _DEBUG // デバッグ用にgameのinit終了までの秒数を一瞬画面に出す(初期化ロード時間計測のため)
long initTime = timer.GetElapsedTicks(); // デバッグ用にInitの経過時間を取得
DxLib::DrawString(0, 0, std::to_string(initTime / 10000000.0).c_str(), GetColor(255, 255, 255));
#endif
DxLib::ScreenFlip();
timer.Init(); // 時刻計測開始時間リセット(game.Initにかかった時間は計測時間に含めないため)
// アニメーション(パラパラ漫画)するにはWhile文
while (DxLib::ProcessMessage() == 0)
{// ProcessMessage() == 0になるのは×ボタン押したときなど
#ifdef DEBUG_DRAW_FPS // ←#defineされていたら FPSを画面右上に描画する
// FPSの計測(FPS = 1秒あたりのパラパラ漫画枚数)
++s_fpsFrameCount; // フレーム数のカウントを +1
if (s_fpsFrameCount >= 60) // 60フレームを超えたら
{ // 経過時間 1Tick = 100ナノ秒 = 1/10000000秒 (elasped=経過)
long elapsedTicks = timer.GetElapsedTicks() - s_fpsTicks;
float elapsedSec = (float)elapsedTicks / 10000000; // 単位を秒(s:Second)に変換
s_currentFPS = s_fpsFrameCount / elapsedSec;// 現在のFPS
s_fpsFrameCount = 0; // 60枚数えたら0リセット
// 60枚数えたら現在のFPS(パラパラ速度)を記録
s_fpsTicks = timer.GetElapsedTicks();
}
#endif
game.Update(); //ゲームの更新処理
game.Draw(); // パラパラ漫画の描画処理
DxLib::ScreenFlip(); //隠れて裏側で描いておいた画像を表面に入れ替え
//[実験機能]スローモーション、早送り(time.timeScaleが 2で2倍速 0.5で1/2スロー)
if (timer.isTimeScaleMode && timer.timeScale > 0)
{ // フレームレートを再計算し早送りやスローモーション
g_targetFPS = (int)(60 * timer.timeScale); // 目標のFPS(Frame Per Second, 1秒あたりのフレーム数)
g_maxAllowSkipTime = 0.2; // フレームスキップする最大間隔(秒)。これ以上の間隔が空いた場合はスキップせずに処理落ちにする。
g_intervalTicks = (long)(1.0 / g_targetFPS * 10000000); // フレーム間のTick数。1Tick = 100ナノ秒 = 1/10000000秒
g_maxAllowSkipCount = (int)(g_targetFPS * g_maxAllowSkipTime);
}
// ★垂直同期がDX設定で効いているならDXライブラリにWait処理を任せられる(フレームスキップだけ自分でやる)
if (DxLib::GetWaitVSyncFlag() == TRUE && timer.isTimeScaleMode == false)
{ // ★フレームスキップとは描画処理が重い時にパラパラ漫画を1枚飛ばすか?
if (s_isEnableFrameSkip)
{ // ★余った時刻 = 次のパラパラ予定時刻 - 現在の時刻
long waitTicks = s_nextFrameTicks - timer.GetElapsedTicks();
if (waitTicks < 0) // 目標時刻をオーバーしている
{ //★あまりにも漫画のパラパラページ数を飛ばすとキャラがワープして見えるので限度をつけてる
if (s_skipCount < g_maxAllowSkipCount) // 連続フレームスキップ数が最大スキップ数を超えていなければ
{
++s_skipCount; // フレームスキップした数を +1(描画処理を飛ばした数)
}
else
{ // 最大スキップ数を超えてるので、フレームスキップしないで描画
s_nextFrameTicks = timer.GetElapsedTicks();
GameDraw(game); // ★限度越えでスロー描画処理に切替え
}
}
else
{ // 目標時刻OKなのでフレームスキップしないで描く
// ★DXライブラリが自動で液晶の60Hzとタイミング合わせて描画
GameDraw(game); // 描画処理
}
// 次のパラパラ予定時刻はIntervalTicks(単位:100ナノ秒)あと
s_nextFrameTicks += g_intervalTicks;
}
else
{ // ここのelseは何があってもフレームスキップしない設定の時に来る
GameDraw(game); // ★スロー描画処理
}
}
else
{ // ★垂直同期がDX設定で【効いてない】ならWait処理を自力でやる(フレームスキップも自分でやる)
// ↓こちらはフレームスキップ処理(waitTicks = 次のパラパラ予定時刻 - 現在の時刻)
long waitTicks = s_nextFrameTicks - timer.GetElapsedTicks(); // 余った時間(待機が必要な時間)
if (s_isEnableFrameSkip && waitTicks < 0)
{ // あまりにも漫画のパラパラページ数を飛ばしすぎるとキャラがワープして見えるので限度をつけてる
if (s_skipCount < g_maxAllowSkipCount)
{
++s_skipCount; // フレームスキップ(描画処理を飛ばす)
}
else // スキップ数の許容限度をオーバーしたら、FPSを落としてでもスローで無理してでも描く
{ // 次のパラパラ予定時刻を現時刻に設定するので次も絶対遅れてwaitTicks < 0になる
s_nextFrameTicks = timer.GetElapsedTicks();
GameDraw(game); //遅れつつもスローモーションで描いてゆく設定の時はここ(当然Updateも遅れていく)
}
}
else
{ // ★自力でWait処理
if (waitTicks > 20000) // あと2ミリ秒以上待つ必要がある
{ // ★自作TimeクラスのSleepで一定時間プログラム休止
// https://marycore.jp/prog/objective-c/sleep-process/#sleep_for
// Sleepは指定した時間でピッタリ戻ってくるわけではないので、
// 余裕を持って、「待たなければならない時間-2ミリ秒」Sleepする
int waitMillsec = (int)(waitTicks / 10000) - 2;
timer.SleepForMilliSec(waitMillsec);
}
// 時間が来るまで何もしないループを回して待機する
while (timer.GetElapsedTicks() < s_nextFrameTicks)
{ // 所定の時間になるまで空ループ
}
GameDraw(game); // 所定の時間になったら描画処理
}
// 次のパラパラ予定時刻はIntervalTicks(単位:100ナノ秒)あと
s_nextFrameTicks += g_intervalTicks;
}
}
// キー入力待ちをする
DxLib::WaitKey();
// DXライブラリの後始末
DxLib::DxLib_End();
// ソフトの終了
return 0;
}
main.cppを変更
して、#define DEBUG_DRAW_FPSで FPSを画面右上に表示させるマクロを発動させましょう。
#include "DxLib.h"
#include "Game.h"
#include "MyTimer.h"
#include "Screen.h"
#define DEBUG_DRAW_FPS // ここをコメントアウトすると画面にFPSを文字で表示しない
//[g_グローバル変数 と s_スタティックグローバル変数の違い] https://zenn.dev/melos/articles/1ddb67d024220f
// グローバル変数は「他のファイルからもアクセス可能な変数」になる
// staticグローバル変数は「他のファイルからはアクセスできないこのファイル限定呼び出し可能変数」になる
// FPS 60フレームを目標にして描画に時間がかかるときに描画をスキップして1秒あたりのフレームレートを安定させる
int g_targetFPS{ 60 }; // 目標のFPS(Frame Per Second, 1秒あたりのフレーム数)
static bool s_isEnableFrameSkip{ true }; // 高負荷時にフレームスキップするか(falseの場合は処理落ち(スロー))
double g_maxAllowSkipTime{ 0.2 }; // フレームスキップする最大間隔(秒)これ以上の間隔が空いた場合はスキップせずに処理落ちにする。
long g_intervalTicks = (long)(1.0 / g_targetFPS * 10000000); // フレーム間のTick数 1Tick = 100ナノ秒 = 1/10000000秒
int g_maxAllowSkipCount = (int)(g_targetFPS * g_maxAllowSkipTime); // フレームスキップを許す最大数
(中略)..........................
// ゲームを描く処理
static inline void GameDraw(Game& game)
{//↑staticグローバル関数は「他のファイルからはアクセスできないこのファイル限定呼び出し可能関数」になる
ClearDrawScreen();// 一旦キャンバスをきれいにまっさらに
game.Draw(); // パラパラ漫画の描画処理
#ifdef DEBUG_DRAW_FPS // game.Drawを描いたあとに上のレイヤにぬりつぶす形でFPSを文字で画面右上に表示
DxLib::DrawString(Screen::Width - 130, 30, (std::string("FPS:") + std::to_string(s_currentFPS)).c_str(), GetColor(255, 255, 255));
#endif
ScreenFlip(); // 裏で描いておいたパラパラ漫画を表面に入れ替えフリップ
s_skipCount = 0; // フレームスキップのカウントをリセット
}
(以下略).....
前提としてクラスQuaternionを別記事から作成しておきます
←クオータニオンで安定して回転できるようにする
Object.hを新規作成
して、継承するとshared_from_this_asでき、
Create関数経由で初期化とInitをセットでできて、インスタンスIDやタグを振って検索できるベース基底クラスを準備しましょう。
#ifndef OBJECT_H_
#define OBJECT_H_
#include <memory> // 共有ポインタを使う
#include <unordered_map>
#include "MyHash.h" // hash32でタグ→オブジェクトを辞書検索できるようにする
// Objectクラスのコンストラクタでは shared_from_this を使えないから
// Initをoverride継承して Initの中で shared_from_thisを必要とする初期化する際に渡すパラメタを
// このベース基底クラスを継承して定義する
struct InitParams
{
InitParams() = default;
// このクラスを継承したクラスで、初期化に必要なパラメタを渡す(初期化に必要なパラメタはそれぞれ違うので)
virtual ~InitParams() {}
};
// GameObjectなどをこの Object型を継承して新規生成することで シリアルIDを自動で振り、shared_from_this_as<Player>()などで 共有状態のthisを得られるようにする
class Object : public std::enable_shared_from_this<Object>
{ // [std::shared_ptrでthisポインタをつかうためには]↑ https://www.kabuku.co.jp/developers/cpp_enable_shared_from_this
// 通し番号(シリアルID)をstaticで管理してインスタンスが生成されるたびにシリアル番号を++して返す
static unsigned int newID() { static unsigned int _serialID_ = 0; return _serialID_++; }
protected: // protected:にしてクラス外からの直接のアクセスを阻む
const unsigned int m_InstanceID; // オブジェクトのインスタンスID (0 ~ 約42億までの番号)
hash32 m_Tag{ "" }; // オブジェクトに振ったタグ デフォルトの空文字""の場合は検索可能な辞書に登録しない
// コンストラクタはprotected:にしてあるので Create関数経由で初期化してください
Object() : m_InstanceID{ Object::newID() } { };
public:
unsigned int InstanceID() { return m_InstanceID; };
inline hash32 Tag() const { return m_Tag; }
// 仮想デストラクタ (辞書にタグが登録されていたら、そのタグを辞書からも消しておく)
virtual ~Object() { if (m_Tag != "") umTagObjects().erase(m_Tag); };
//[protectedなコンストラクタでもstd::make_sharedで呼ぶテク] https://gintenlabo.hatenablog.com/entry/20131211/1386771626
template<typename ObjectT>
struct ProtectedCall : public ObjectT
{ // ダミーとなる ProtectedCallクラスが public:だからそれを経由して間接的にコンストラクタがprotected:なクラスでもmake_sharedを呼ぶテク
template<class... ArgsT> // std::make_sharedはprotectedな継承クラスは呼べないのでそれを回避するテクニック
explicit ProtectedCall(ArgsT&&... args)
: ObjectT(std::forward<ArgsT>(args)...) { } //std::forwardで複数引数を完全転送で引き渡し https://proc-cpuinfo.fixstars.com/2016/03/c-html/
};
// コンストラクタでは shared_from_this を使えないから、Initをoverride継承して Initの中で shared_from_thisを必要とする初期化をする
virtual void Init(std::shared_ptr<InitParams> initParams = nullptr)
{
// overrideして shared_from_this_as が必要な パラメータの初期化を行ってください
}
// Objectを継承した型はこの create関数を経由して新規生成する
template <typename DerivedT, typename OutTypeT = DerivedT, typename ...ArgsT>
static auto Create(std::shared_ptr<InitParams> initParams, ArgsT&&...args)
{
auto pObject = std::static_pointer_cast<OutTypeT>(std::make_shared<ProtectedCall<DerivedT>>(std::forward<ArgsT>(args)...));
pObject->Init(initParams); // コンストラクタでは shared_from_this_as できないものをmake_sharedが終わったタイミングで一緒に初期化
return pObject;
}
// shared_from_this() を DerivedT型にキャストして返す [参考] https://qiita.com/terukazu/items/e4129105b12442e42651
template <typename DerivedT>
auto shared_from_this_as() noexcept {
return std::dynamic_pointer_cast<DerivedT>(Object::shared_from_this());
}
protected:
// <振ったタグ, オブジェクトへの弱共有リンク> の辞書を関数内staticで保持して検索できるようにしてある
static inline std::unordered_map<hash32, std::weak_ptr<Object>>& umTagObjects()
{ // 関数内staticで タグから オブジェクトへの弱共有ポインタを保持
static std::unordered_map<hash32, std::weak_ptr<Object>> _umTagObjects_;
return _umTagObjects_;
}
public:
// オブジェクトにタグを振って検索できるようにしておく (空文字 "" のタグが指定されたら辞書から消す)
template <typename DerivedT>
inline auto SetTag(const std::string& tag)
{
if (m_Tag != "" && tag == "") // 空文字 "" のタグが指定されたら辞書から消す
{
umTagObjects().erase(m_Tag); // 既存のタグが辞書に登録されてたら辞書から消す
m_Tag = ""; // タグを空文字へリセット
return shared_from_this_as<DerivedT>(); // メソッドチェーンで->で連鎖アクセスできるように thisを返す
}
m_Tag = tag; // タグを設定
umTagObjects().emplace(m_Tag, shared_from_this_as<DerivedT>()); // タグと弱共有リンクをペアで辞書に登録
return shared_from_this_as<DerivedT>(); // メソッドチェーンで->で連鎖アクセスできるように thisを返す
}
// オブジェクトに振っておいたタグで検索する
template <typename DerivedT>
static std::weak_ptr<DerivedT> FindWithTag(const std::string& tag)
{
auto found = umTagObjects().find(tag); // 辞書からtagと一致するものを見つける
if (found == end(umTagObjects())) // 結果がendのときは見つからなかった
return std::weak_ptr<DerivedT>(); // 空の弱共有ポインタを返す(空の弱共有ポインタはlock()してもnullptrになる)
if (found->second.lock() == nullptr)
{ // 辞書には found が見つかったが lock()したら nullptr のときはすでにリンク先本体が消されているから辞書からもeraseで消す
umTagObjects().erase(tag);
return std::weak_ptr<DerivedT>(); // 空の弱ポインタを返す(lock()してもnullptrになる)
}
return std::dynamic_pointer_cast<DerivedT>(found); // DerivedT型(継承先の型タイプ)に変換してreturn する
}
// 比較演算子 == ではインスタンスIDを比較する
inline bool operator == (const Object& other) const { return this->m_InstanceID == other.m_InstanceID; }
// 不一致演算子 != ではインスタンスIDを比較する
inline bool operator != (const Object& other) const { return !(*this == other); }
};
#endif
GameObject.hを新規作成
して、PlayerやZakoなどの基底ベースとなるクラスを準備しましょう。
#ifndef GAMEOBJECT_H_
#define GAMEOBJECT_H_
#include <memory> // 共有ポインタの定義のため
#include "Vector3.h"
#include "Quaternion.h"
#include "Object.h" // インスタンスIDや検索タグを持ち、shared_from_this_as<継承先クラス名>などができる
class World; // 前方宣言
// ゲーム上に表示される物体の基底クラス。
// プレイヤーや敵、アイテムなどはこのクラスを継承して作る。
class GameObject : public Object
{
protected:
// コンストラクタはprotected:にしてあるのでCreate関数経由で初期生成してください
GameObject(World* world = nullptr, Vector3 position = { 0,0,0 }, Quaternion rotation = { 0,0,0,1 }, Vector3 velocity = { 0,0,0 }, Vector3 force = { 0,0,0 })
: Object(), m_world{ world }, position{ position }, rotation{ rotation }, velocity{ velocity }, force{ force }
{
}
public:
// 仮想デストラクタ
virtual ~GameObject() {}
union { // ★共用体unionテクニック https://inemaru.hatenablog.com/entry/2016/03/02/005408
//★下記3つはメモリ上で共用状態になる(position、x,y,z、xyzどの名前から数値を変えたり読出してもメモリ上は同じ)
// 同じデータに3種類の名前を付けたイメージ、しかもVector3の機能や配列としてのアクセスの仕方もできて便利
struct { float x, y, z; }; // XYZ座標 [匿名共用体とは] https://code.i-harness.com/ja-jp/q/4d437c
Vector3 position; // XYZ座標
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で回せる
Quaternion rotation; // 回転クオータニオン
union {
struct { float vx, vy, vz; }; // XYZ方向の速度
Vector3 velocity; // XYZ方向の速度
std::array<float, 3> vxyz;
};
union {
struct { float vxForce, vyForce, vzForce; }; // XYZ方向にかかる力(Unityでいうと AddForce関数 )
Vector3 force; // XYZ方向の力(物理的には加速度:1フレームごとにvelocityの増える=加速する量)
std::array<float, 3> vxyzForce;
};
protected:
World* m_world; // GameObjectが配置されたワールドへのリンク
public:
// 現在いるワールドへのポインタを得る
virtual World* world() { return m_world; }
// 現在いるワールドのポインタを変更する
virtual void world(World* changeWorld) { m_world = changeWorld; }
bool isDead{ false }; // 死んだ(削除対象)フラグ
bool isStatic{ false }; // 動かない(静的オブジェクトか?)
bool isGround{ false }; // 地面についているか
bool isFlying{ false }; // 空をとんでいるか
hash32 typeTag{ "" }; // 小カテゴリ Zako0など個別のタイプ
hash32 baseTag{ "" }; // 大カテゴリ Enemyなど大まかなベースジャンル
// 更新処理[純粋仮想関数 = 0 なのでoverride必須の関数]
virtual void Update() = 0; // 純粋仮想関数 = 0 にすると【絶対 Update()関数はoverrideしなきゃいけない義務を付与できる 】
// 描画処理[純粋仮想関数 = 0 なのでoverride必須の関数]
virtual void Draw() = 0; // 純粋仮想関数 = 0 にすると【絶対 Draw()関数はoverrideしなきゃいけない義務を継承先クラスに付与 】
};
#endif
Player.hを変更
して、GameObjectを継承しましょう。
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Resource.h"
#include "GameObject.h"
#include "MyHash.h" // hash32型でプレイヤが現在いるワールドのタグを管理する
#include "Vector3.h"
class World; // 前方宣言
class Camera; // 前方宣言
class Player : public GameObject
{
protected:
// コンストラクタはprotected:にしてあるので、Create関数経由で初期生成してください
Player(World* world, Pad pad, Vector3 position, Quaternion rotation = { 0,0,0,1 }, Vector3 velocity = { 0,0,0 }, Vector3 force = { 0,0,0 })
: pad{ pad }, GameObject(world, position, rotation, velocity, force) m_world{ world }, position{ position }, velocity{ 0,0,0 }, force{0,0,0}
{
this->typeTag = "Player"; // タグは個別の"プレイヤ1"などに使い、タイプはtypeTagで判別する形にしてタイプのジャンルを区別
}
public:
// 仮想デストラクタ
virtual ~Player() {}
union { // ★共用体unionテクニック https://inemaru.hatenablog.com/entry/2016/03/02/005408
//★下記3つはメモリ上で共用状態になる(position、x,y,z、xyzどの名前から数値を変えたり読出してもメモリ上は同じ)
// 同じデータに3種類の名前を付けたイメージ、しかもVector3の機能や配列としてのアクセスの仕方もできて便利
struct { float x, y, z; }; // XYZ座標 [匿名共用体とは] https://code.i-harness.com/ja-jp/q/4d437c
Vector3 position; // XYZ座標
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で回せる
union {
struct { float vx, vy, vz; }; // XYZ方向の速度
Vector3 velocity; // XYZ方向の速度
std::array<float, 3> vxyz;
};
union {
struct { float vxForce, vyForce, vzForce; }; // XYZ方向にかかる力(Unityでいうと AddForce関数 )
Vector3 force; // XYZ方向の力(物理的には加速度:1フレームごとにvelocityの増える=加速する量)
std::array<float, 3> vxyzForce;
};
hash32 tag{ "" }; // プレイヤにつけるタグ
protected:
World* m_world; // 配置されたワールドへのリンク
public:
// 現在いるワールドへのポインタを得る
virtual World* world() { return m_world; }
// 現在いるワールドのポインタを変更する
virtual void world(World* changeWorld) { m_world = changeWorld; }
Pad pad; // 操作するコントローラー番号
hash32 worldTag{ "" }; // 現在いるワールドのタグ
float MoveSpeedMax = 6; // 移動速度Max値
float MoveSpeed = 0;//移動速度
float moveAngle = 0; // X軸→方向から何度か
float deltaAngle = 0; // プレイヤの動く角度の変化率、ハンドルを切ったらだんだんもとに戻る
int animCount = 0;
std::shared_ptr<Texture> image; // プレイヤの板ポリゴンで描くタイル分割画像への共有リンク
Camera* camera{ nullptr }; // プレイヤを追随するカメラ
void SetCamera(Camera* pCamera) { camera = pCamera; }
// 入力を受けての処理
virtual void HandleInput();
// 更新処理
virtual void Update() override;
// 描画処理
virtual void Draw() override;
};
#endif
Player.cppを変更
して、GameObjectを継承する記述にしましょう。
#include "Player.h"
#include <cmath> // 浮動小数点の余り%演算子がC++にないのでfmodを使うため
#include "MyMath.h"
#include "MyDraw.h"
#include "Camera.h"
#include "World.h"
#include <vector>
void Player::HandleInput()
{
if (Input::GetButton(pad, PAD_INPUT_LEFT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_A)) )
{
//vx = - MoveSpeed; // 左
deltaAngle += 1; //ハンドルを左に回す
}
if (Input::GetButton(pad, PAD_INPUT_RIGHT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_D)))
{
//vx = MoveSpeed; // 右
deltaAngle -= 1; //ハンドルを右に回す
}
if (Input::GetButton(pad, PAD_INPUT_UP)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_W)))
{
//vy = - MoveSpeed; // 上
MoveSpeed += 0.6f; //アクセルを踏む
if (MoveSpeed > MoveSpeedMax)
MoveSpeed = MoveSpeedMax;//Maxスピードで止める
}
else if (Input::GetButton(pad, PAD_INPUT_DOWN)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_S)))
{
//vy = MoveSpeed; // 下
MoveSpeed -= 0.6f; //ブレーキを踏む
if (MoveSpeed < -0.9f)
MoveSpeed = -0.9f;//ちょっとだけバックできるように
}
if (pad == Pad::Key || pad == Pad::One) // コントローラ1またはキーボード操作のプレイヤのときだけ
{
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGUP)) // キーボードのPageUp ボタンを押したときは
{
vy = 2.0f; // テスト用に空中の上方向に移動
}
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGDN)) // キーボードのPageDown ボタンを押したときは
{
vy = -2.0f; // テスト用に空中の下方向に移動
}
}
}
// 更新処理
void Player::Update()
{
deltaAngle *= 0.5F; // 車のハンドルの方向をだんだんニュートラルに戻す
MoveSpeed *= 0.9F; // 移動速度も減速する
// 入力を受けての処理
HandleInput();
moveAngle += deltaAngle; // ハンドルを回したぶんプレイヤの進行方向を変える
if (moveAngle < -180) moveAngle = 360 + moveAngle; // -180以下の角度にならないようにマイナスになったら+180度にループさせる
else if (moveAngle > +180) moveAngle = -360 + moveAngle; // +180以下の角度にならないようにマイナスになったら-180度にループさせる
rotation.SetRotation(VGet(0, (180 - moveAngle) * MyMath::Deg2Rad, 0)); // 前向きの3Dモデルなら180度回転させて奥方向の向きに設定
// ↓進行方向角度をX方向とZ方向の速度に変える
vx = (float)std::cos((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
vz = (float)std::sin((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
// 実際に位置を動かす
// まず横に移動する
x += vx;
// 次に縦に動かす
y += vy;
// 次にZ奥行き方向に動かす
z += vz;
vy += vyForce; // 勢い(力・フォースを加算
// カメラのプレイヤ追尾
float camDistance = 150; // プレイヤからカメラまでのY平面上の距離
float camHeight = 100; // プレイヤからカメラまでのY方向上の高さ
// moveAngleは2D画像のX方向右を0度とするから、-90度回すと画像を正面に捉える位置にカメラを置ける
float camAngleX = (float)std::cos((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
float camAngleZ = (float)std::sin((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
if (camera != nullptr)
{ // カメラの位置をプレイヤの進行方向と真逆に設定
camera->SetPosition(position + Vector3(camAngleX, camHeight, camAngleZ));
camera->LookAt(position); //カメラはプレイヤの方を見る
}
}
// 描画処理
void Player::Draw()
{
int animIndex = animCount / 20; // キャラのパラパラアニメの画像番号(最大3)
float abs_vx = (vx > 0) ? vx : -vx; // x方向絶対値
float abs_vz = (vz > 0) ? vz : -vz; // y方向絶対値
if (abs_vx > 1.00f || abs_vz > 1.00f)
animCount += 6; // キャラが地面のXとZ方向に少しでもスピードがあったらアニメを走らせる
if (animCount >= 60) animCount = 0; // アニメ時間を0へループ
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
for (auto&& keyValue : world()->cameras) // プレイヤのいるワールドにあるすべてのカメラぶんループ
{
auto& camera = keyValue.second;
camera->Draw([&]()
{
// 常にカメラ方向を向くビルボード回転の角度を得る
VECTOR_D angle = MyDraw::GetAngle(DxLib::GetCameraBillboardMatrixD());
angle.x = 0; angle.z = 0; // Y軸周りのビルボード回転だけ有効にする X軸,Z軸まわりの回転は 0 にする
int imgIndex = 9; // キャラチップ画像の起点番号
// ビルボードの角度 と キャラの向いている向き の 差 によって、別のプレイヤのカメラから見たキャラチップの向きの画像を切り替える
// -180 ~ +180 : angle.y moveAngle : -180 ~ +180 ↓
float difAngleY = angle.y * 180.0f / DX_PI_F + moveAngle;
imgIndex = (difAngleY < -135 || 135 < difAngleY) ? 0 // カメラ向きの画像
: (difAngleY < -45) ? 6 // 右 向きの画像
: (difAngleY > 45) ? 3 // 左 向きの画像
: 9; // 奥向きの画像
MyDraw::DrawDivRotaGraphF3D(MyDraw::Plane::Z, x, y, z, 1.0f, angle, *image, imgIndex + animIndex, TRUE);
});
}
}
GameScene.cppを変更
して、ObjectのCreate関数を呼び出す形でPlayerを新規生成する記述にしましょう。
#include "GameScene.h"
#include "Player.h"
(中略).................
void GameScene::Initialize()
{// Init処理
(中略).................
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
// プレイヤの生成
if (pWorld != nullptr)
{
//pWorld->AddCamera("カメラ1", Camera::defaultCamera); // プレイヤ1というタグをカメラにつけてワールドにカメラを配置
pWorld->AddCamera("カメラ1", std::make_shared<Camera>(pWorld, Vector3{ 0,0,0 }));
auto pCamera1 = pWorld->camera("カメラ1"); // カメラ1 というタグのついたカメラへの共有ポインタを得る
pCamera1->SetScreenHandle(screenHandle0); // カメラから見える3Dを描く スクリーンのハンドル を関連付ける
auto pPlayer1 = Object::Create<Player>(nullptr, pWorld, Pad::Key, Vector3(90 + 256, 32, 95 + 256));
pWorld->AddPlayer("プレイヤ1", pPlayer1); // シーンにプレイヤを追加する
pPlayer1->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath1]);
pPlayer1->camera = pCamera1.get(); // カメラをプレイヤにリンクする
pWorld->AddCamera("カメラ2", std::make_shared<Camera>(pWorld, Vector3{ 0,0,0 }));
auto pCamera2 = pWorld->camera("カメラ2"); // カメラ2 というタグのついたカメラへの共有ポインタを得る
pCamera2->SetScreenHandle(screenHandle1); // カメラから見える3Dを描く スクリーンのハンドル を関連付ける
auto pPlayer2 = Object::Create<Player>(nullptr, pWorld, Pad::Two, Vector3(190, 32, 195));
pWorld->AddPlayer("プレイヤ2", pPlayer2); // シーンにプレイヤを追加する
pPlayer2->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath2]);
pPlayer2->camera = pCamera2.get(); // カメラをプレイヤにリンクする
}
};
(以下、略)......
Componentの例
struct Component
{
GameObject* m_owner{ nullptr }; // オーナーのパラメータへアクセスするためのポインタ
Component(GameObject* owner) : m_owner{ owner } {}
void Update()
{
owner->x += 1; // ポインタ経由でオーナーの位置を+1ずつずらす
}
}
Playerの例
struct Player
{
std::shared_ptr<Component> m_component{ nullptr }; // コンポーネントへの共有カウンタ +1をキープして消えないように所持する
Player() : m_component{ this } {}
void Update()
{
m_component->Update(); // コンポーネントのUpdate経由でオーナーの位置を+1ずつずらす
}
}
Componentの例
struct Component
{
std::weak_ptr<GameObject> m_owner{ nullptr }; // オーナーのパラメータへアクセスするためのポインタ
Component(std::shared_ptr<GameObject> owner) : m_owner{ owner } {}
void Update()
{
auto pPlayer = owner.lock(); // 共有ポインタに戻す
if(pPlayer != nullptr) // ダングリング状態じゃないか?
pPlayer->x += 1; // ポインタ経由でオーナーの位置を+1ずつずらす
}
}
Player.hを変更
して、ジャンプと重力落下の処理を直打ちで追加します。
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Resource.h"
#include "GameObject.h"
class World; // 前方宣言
class Camera; // 前方宣言
class Player : public GameObject
{
protected:
// コンストラクタはprotected:にしてあるので、Create関数経由で初期生成してください
Player(World* world, Pad pad, Vector3 position, Quaternion rotation = { 0,0,0,1 }, Vector3 velocity = { 0,0,0 }, Vector3 force = { 0,0,0 })
: pad{ pad }, GameObject(world, position, rotation, velocity, force)
{
this->typeTag = "Player"; // タグは個別の"プレイヤ1"などに使い、タイプはtypeTagで判別する形にしてタイプのジャンルを区別
}
public:
// 仮想デストラクタ
virtual ~Player() {}
Pad pad; // 操作するコントローラー番号
hash32 worldTag{ "" }; // 現在いるワールドのタグ
float MoveSpeedMax = 6; // 移動速度Max値
float MoveSpeed = 0;//移動速度
float moveAngle = 0; // X軸→方向から何度か
float deltaAngle = 0; // プレイヤの動く角度の変化率、ハンドルを切ったらだんだんもとに戻る
int animCount = 0;
std::shared_ptr<Texture> image; // プレイヤの板ポリゴンで描くタイル分割画像への共有リンク
Camera* camera{ nullptr }; // プレイヤを追随するカメラ
void SetCamera(Camera* pCamera) { camera = pCamera; }
float yJumpStart{ 0.0f };
std::vector<float> vyJumpSpeed{ 13, 14, 15, 4 }; // ジャンプ開始の初期速度
std::vector<float> vyForceJump{ 0.5f, 0.4f, 0.3f, 0.1f }; // ジャンプ上昇にかかる力の初期値
std::vector<float> vyGravity{ 0.8f,0.8f,0.8f,0.8f }; // 重力の設定値
std::vector<float> vyDownSpeedMax{ 16, 16, 16, 8 }; // 降下スピードのリミット
// 入力を受けての処理
virtual void HandleInput();
// 更新処理
virtual void Update() override;
// 描画処理
virtual void Draw() override;
};
#endif
Player.cppを変更
して、GameObjectを継承する記述にしましょう。
#include "Player.h"
#include <cmath> // 浮動小数点の余り%演算子がC++にないのでfmodを使うため
#include "MyMath.h"
#include "MyDraw.h"
#include "Camera.h"
#include "World.h"
#include <vector>
void Player::HandleInput()
{
if (Input::GetButton(pad, PAD_INPUT_LEFT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_A)) )
{
//vx = - MoveSpeed; // 左
deltaAngle += 1; //ハンドルを左に回す
}
if (Input::GetButton(pad, PAD_INPUT_RIGHT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_D)))
{
//vx = MoveSpeed; // 右
deltaAngle -= 1; //ハンドルを右に回す
}
if (Input::GetButton(pad, PAD_INPUT_UP)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_W)))
{
//vy = - MoveSpeed; // 上
MoveSpeed += 0.6f; //アクセルを踏む
if (MoveSpeed > MoveSpeedMax)
MoveSpeed = MoveSpeedMax;//Maxスピードで止める
}
else if (Input::GetButton(pad, PAD_INPUT_DOWN)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_S)))
{
//vy = MoveSpeed; // 下
MoveSpeed -= 0.6f; //ブレーキを踏む
if (MoveSpeed < -0.9f)
MoveSpeed = -0.9f;//ちょっとだけバックできるように
}
if (pad == Pad::Key || pad == Pad::One) // コントローラ1またはキーボード操作のプレイヤのときだけ
{
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGUP)) // キーボードのPageUp ボタンを押したときは
{
vy = 2.0f; // テスト用に空中の上方向に移動
}
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGDN)) // キーボードのPageDown ボタンを押したときは
{
vy = -2.0f; // テスト用に空中の下方向に移動
}
}
}
// 更新処理
void Player::Update()
{
deltaAngle *= 0.5F; // 車のハンドルの方向をだんだんニュートラルに戻す
MoveSpeed *= 0.9F; // 移動速度も減速する
// 入力を受けての処理
HandleInput();
if (isGround)
{
if (Input::GetButtonDown(pad, PAD_INPUT_A) // Zキーを押した瞬間
|| (pad == Pad::Two && Input::GetButtonDown(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを押した瞬間
{
vy = vyJumpSpeed[0]; // ジャンプ方向はY上方向
vyForce = -vyForceJump[0];
yJumpStart = y;
isGround = false; // ジャンプ中はisGroundフラグがfalse
}
}
else if (vy <= 0) // 下落開始(vyが0未満のときはまだジャンプ中)
{
vyForce = -vyGravity[0]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
}
else if (Input::GetButtonUp(pad, PAD_INPUT_A) // Zキーを離した瞬間
|| (pad == Pad::Two && Input::GetButtonUp(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを離した瞬間
{
if (y - yJumpStart >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
vyForce = -vyGravity[0]; // ジャンプ上昇中(vy未満のとき)にボタンを離したら早めに重力をかけ始める
}
moveAngle += deltaAngle; // ハンドルを回したぶんプレイヤの進行方向を変える
if (moveAngle < -180) moveAngle = 360 + moveAngle; // -180以下の角度にならないようにマイナスになったら+180度にループさせる
else if (moveAngle > +180) moveAngle = -360 + moveAngle; // +180以下の角度にならないようにマイナスになったら-180度にループさせる
rotation.SetRotation(VGet(0, (180 - moveAngle) * MyMath::Deg2Rad, 0)); // 前向きの3Dモデルなら180度回転させて奥方向の向きに設定
// ↓進行方向角度をX方向とZ方向の速度に変える
vx = (float)std::cos((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
vz = (float)std::sin((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
// 実際に位置を動かす
// まず横に移動する
x += vx;
// 次に縦に動かす
y += vy;
// 次にZ奥行き方向に動かす
z += vz;
vy += vyForce; // 勢い(力・フォースを加算
if (y <= 0.0f) // 0.0fを地面として 0.0f以下になったら地面に着地したと判定する
{
y = 0.0f; // 地面に沿わせる
vy = -0.01f; // 下方向への速度をほぼ 0 にする
isGround = true;
}
if (vy <= -vyDownSpeedMax[0]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
vy = -vyDownSpeedMax[0];
// カメラのプレイヤ追尾
float camDistance = 150; // プレイヤからカメラまでのY平面上の距離
float camHeight = 100; // プレイヤからカメラまでのY方向上の高さ
// moveAngleは2D画像のX方向右を0度とするから、-90度回すと画像を正面に捉える位置にカメラを置ける
float camAngleX = (float)std::cos((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
float camAngleZ = (float)std::sin((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
if (camera != nullptr)
{ // カメラの位置をプレイヤの進行方向と真逆に設定
camera->SetPosition(position + Vector3(camAngleX, camHeight, camAngleZ));
camera->LookAt(position); //カメラはプレイヤの方を見る
}
}
// 描画処理
void Player::Draw()
{
int animIndex = animCount / 20; // キャラのパラパラアニメの画像番号(最大3)
float abs_vx = (vx > 0) ? vx : -vx; // x方向絶対値
float abs_vz = (vz > 0) ? vz : -vz; // y方向絶対値
if (abs_vx > 1.00f || abs_vz > 1.00f)
animCount += 6; // キャラが地面のXとZ方向に少しでもスピードがあったらアニメを走らせる
if (animCount >= 60) animCount = 0; // アニメ時間を0へループ
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
for (auto&& keyValue : world()->cameras) // プレイヤのいるワールドにあるすべてのカメラぶんループ
{
auto& camera = keyValue.second;
camera->Draw([&]()
{
// 常にカメラ方向を向くビルボード回転の角度を得る
VECTOR_D angle = MyDraw::GetAngle(DxLib::GetCameraBillboardMatrixD());
angle.x = 0; angle.z = 0; // Y軸周りのビルボード回転だけ有効にする X軸,Z軸まわりの回転は 0 にする
int imgIndex = 9; // キャラチップ画像の起点番号
// ビルボードの角度 と キャラの向いている向き の 差 によって、別のプレイヤのカメラから見たキャラチップの向きの画像を切り替える
// -180 ~ +180 : angle.y moveAngle : -180 ~ +180 ↓
float difAngleY = angle.y * 180.0f / DX_PI_F + moveAngle;
imgIndex = (difAngleY < -135 || 135 < difAngleY) ? 0 // カメラ向きの画像
: (difAngleY < -45) ? 6 // 右 向きの画像
: (difAngleY > 45) ? 3 // 左 向きの画像
: 9; // 奥向きの画像
MyDraw::DrawDivRotaGraphF3D(MyDraw::Plane::Z, x, y, z, 1.0f, angle, *image, imgIndex + animIndex, TRUE);
});
}
}
Component.hを新規作成
して、コンポーネントのベース基底となるクラスを作成しましょう。
#ifndef COMPONENT_H_
#define COMPONENT_H_
#include <unordered_map> // コンポーネントの辞書配列に使う
#include <typeindex> // intやComponentなどの「型」をsize_t型の IDの数値 へ typeid(int)などを使って変換する
#include "MyHash.h" // MyHashクラスで 言語 C++ C# など や ビルドするコンパイラ VisualStudioやgccなどに関係なく同じハッシュを計算
#include <string>
#include <memory> // 共有ポインタを使う
#include "Object.h" // コンポーネントもObjectクラスを継承してshared_from_thisできるようにする
class GameObject; //前方宣言(循環インクルード防止)
// 各コンポーネントのひな形
class Component : public Object
{
protected:
std::weak_ptr<GameObject> m_pOwner; // このコンポーネントを所有するPlayerクラスなどへの弱共有ポインタ(弱なので所有権を主張しない)
public:
// このコンポーネントを所有するプレイヤなどのオーナーへの共有ポインタを得る (オーナーがすでにメモリから削除済の場合は nullptr になる)
std::shared_ptr<GameObject> owner() const { auto pOwner = m_pOwner.lock(); return (pOwner != nullptr) ? pOwner : nullptr; }
std::type_index typeID = typeid(Component);
hash32 typeTag{ "" }; // 小カテゴリ コンポーネントの名前 TilemapColliderなどColliderを継承したコンポーネント本体の名前
hash32 baseTag{ "" }; // 大カテゴリ コンポーネントの種類(Collider)など https://docs.unity3d.com/ja/current/ScriptReference/Collider.html
hash32 statusTag{ "" }; // 自由に使えばよいタグ
bool isEnabled{ true }; // コンポーネントが有効かどうか?
protected:
// 初期化コンストラクタ : このコンポーネントの所有者ownerを内部に持つ
Component(std::weak_ptr<GameObject> pOwner) : m_pOwner{ pOwner }, Object()
{};
public:
// 下記関数をoverrideして実装して【コンポーネント志向】を実現する
//virtual void Initialize(GameObject* obj = nullptr) {}; // Init的な処理 BGMの変更とか
virtual void Update(GameObject* obj = nullptr) {}; // Update的な役
virtual void Draw(GameObject* obj = nullptr) {}; // Draw的な役
//virtual void Finalize(GameObject* obj = nullptr) {}; // Finalize的な役(ステートの終了時)
virtual ~Component() {}; // 仮想デストラクタ(無いとstringなど配列を内包するもののデストラクタが暗黙で呼ばれずメモリリーク)
};
#endif
PhysicsComponent.hを新規作成
して、物理系のPhysicsComponentクラスを継承してジャンプや落下のコンポーネント作成しましょう。
#ifndef PHYSICS_COMPONENT_H_
#define PHYSICS_COMPONENT_H_
#include <vector>
#include "Component.h"
#include "GameObject.h"
// 物理系のコンポーネントはこのクラスを継承して実装する
class PhysicsComponent : public Component
{
protected:
inline PhysicsComponent(std::shared_ptr<GameObject> pOwner) : Component(pOwner)
{
this->baseTag = "PhysicsComponent"; // 大まかなカテゴリーとしては物理系コンポーネント
};
public:
virtual ~PhysicsComponent() {};
virtual void Update(GameObject* obj = nullptr) override
{
// なにか物理系のコンポーネントで共通して更新したいことがあればここに実装
}
virtual void Draw(GameObject* obj = nullptr) override
{
// なにか物理系のコンポーネントで共通して描画したいことがあればここに実装
}
};
class GravityComponent : public PhysicsComponent
{
public:
inline GravityComponent(std::shared_ptr<GameObject> pOwner, const std::vector<float>& vyGravity, const std::vector<float>& vyDownSpeedMax)
: PhysicsComponent(pOwner), m_vyGravity{ vyGravity }, m_vyDownSpeedMax{ vyDownSpeedMax }
{
this->typeID = typeid(GravityComponent); //[環境によって差が出るので↓が無難か] https://qiita.com/nil_gawa/items/1fece3eee7ca1f88c71d
this->typeTag = "GravityComponent"; // 個別のカテゴリーとしては重力落下コンポーネント
};
virtual ~GravityComponent() {}
size_t m_idx{ 0 }; // 適用する設定配列の番号
void SetIndex(size_t idx) { m_idx = idx; }
std::vector<float> m_vyGravity; // 重力の設定値
float gravity() { return (m_idx < m_vyGravity.size()) ? m_vyGravity[m_idx] : 0.0f; }
std::vector<float> m_vyDownSpeedMax; // 降下スピードのリミット
float downSpeedMax() { return (m_idx < m_vyDownSpeedMax.size()) ? m_vyDownSpeedMax[m_idx] : 0.0f; }
virtual void Update(GameObject* obj = nullptr) override
{
if (!isEnabled) return; // コンポーネントが有効状態じゃないときは何もせずreturn
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタをロックして得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
if (pOwner->isGround == false && pOwner->vy <= 0) // 下落開始(vyが 0以上 のときはまだジャンプ中なので上昇を邪魔しないようにする)
{
pOwner->vyForce = -gravity(); // ジャンプ上昇力が0になって以降に初めて重力をオンにする(ジャンプの加速を邪魔しない工夫)
pOwner->OnFalling(); // 下落中の処理のコールバック関数を呼び返す
}
if (pOwner->vy <= -downSpeedMax()) // 無限に落下速度が加速しないようにリミットを働かせる(空気抵抗みたいなもの)
pOwner->vy = -downSpeedMax(); // downSpeedMax()以上の落下速度にならないようにする
}
};
class JumpComponent : public PhysicsComponent
{
public:
inline JumpComponent(std::shared_ptr<GameObject> pOwner, const std::vector<float>& vyJumpSpeed, const std::vector<float>& vyForceJump)
: PhysicsComponent(pOwner), m_vyJumpSpeed{ vyJumpSpeed }, m_vyForceJump{ vyForceJump }
{
this->typeID = typeid(JumpComponent); //[環境によって差が出るので↓が無難か] https://qiita.com/nil_gawa/items/1fece3eee7ca1f88c71d
this->typeTag = "JumpComponent"; // 個別のカテゴリーとしてはジャンプ上昇コンポーネント
};
virtual ~JumpComponent() {}
size_t m_idx{ 0 }; // 適用する設定配列の番号
void SetIndex(size_t idx) { m_idx = idx; }
float yJumpStart{ 0 }; // ジャンプした瞬間のyの位置
std::vector<float> m_vyJumpSpeed; // ジャンプの上昇スピードのリミット
float jumpSpeed() { return (m_idx < m_vyJumpSpeed.size()) ? m_vyJumpSpeed[m_idx] : 0.0f; }
std::vector<float> m_vyForceJump; // ジャンプ中の下方向の勢いの減速する力
float forceJump() { return (m_idx < m_vyForceJump.size()) ? m_vyForceJump[m_idx] : 0.0f; }
virtual void Update(GameObject* obj = nullptr) override
{
if (!isEnabled) return; // コンポーネントが有効状態じゃないときは何もせずreturn
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
if (pOwner->isGround == false && pOwner->vy > 0) // vyが0以上のときはまだジャンプ中
{
pOwner->OnJumping(); // 空中でジャンプの上昇中に呼ばれるコールバック関数を呼び返す
}
}
virtual void JumpStart(GameObject* obj = nullptr)
{
if (!isEnabled) return; // コンポーネントが有効状態じゃないときは何もせずreturn
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
if (pOwner->isGround)
{
pOwner->vy = jumpSpeed(); // ジャンプ方向はY上方向
pOwner->vyForce = -forceJump(); // ジャンプの勢いが弱まる力
yJumpStart = pOwner->y;
pOwner->OnStartJump(); // ジャンプスタート時の処理をコールバックで呼び返す
}
}
virtual void JumpCancel(GameObject* obj = nullptr)
{
if (!isEnabled) return; // コンポーネントが有効状態じゃないときは何もせずreturn
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
if (!pOwner->isGround && pOwner->vy >= 0) // vyが0以上のときはまだジャンプ上昇中
{
if (pOwner->y - yJumpStart >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけ始めない
{
pOwner->vyForce = -pOwner->gravity(); // ジャンプボタンを離したときには重力をかけ始める
}
}
}
};
#endif
GameObject.hを変更
して、OnStartJump()やOnFalling()やgravity()などジャンプ中や落下中に呼ばれるoverride元の関数を準備しましょう。
#ifndef GAMEOBJECT_H_
#define GAMEOBJECT_H_
#include <memory> // 共有ポインタの定義のため
#include "Vector3.h"
#include "Quaternion.h"
#include "Object.h" // インスタンスIDや検索タグを持ち、shared_from_this_as<継承先クラス名>などができる
class World; // 前方宣言
class Component; // 前方宣言
// ゲーム上に表示される物体の基底クラス。
// プレイヤーや敵、アイテムなどはこのクラスを継承して作る。
class GameObject : public Object
{
protected:
// コンストラクタはprotected:にしてあるのでCreate関数経由で初期生成してください
GameObject(World* world = nullptr, Vector3 position = { 0,0,0 }, Quaternion rotation = { 0,0,0,1 }, Vector3 velocity = { 0,0,0 }, Vector3 force = { 0,0,0 })
: Object(), m_world{ world }, position{ position }, rotation{ rotation }, velocity{ velocity }, force{ force }
{
}
public:
// 仮想デストラクタ
virtual ~GameObject() {}
union { // ★共用体unionテクニック https://inemaru.hatenablog.com/entry/2016/03/02/005408
//★下記3つはメモリ上で共用状態になる(position、x,y,z、xyzどの名前から数値を変えたり読出してもメモリ上は同じ)
// 同じデータに3種類の名前を付けたイメージ、しかもVector3の機能や配列としてのアクセスの仕方もできて便利
struct { float x, y, z; }; // XYZ座標 [匿名共用体とは] https://code.i-harness.com/ja-jp/q/4d437c
Vector3 position; // XYZ座標
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で回せる
Quaternion rotation; // 回転クオータニオン
union {
struct { float vx, vy, vz; }; // XYZ方向の速度
Vector3 velocity; // XYZ方向の速度
std::array<float, 3> vxyz;
};
union {
struct { float vxForce, vyForce, vzForce; }; // XYZ方向にかかる力(Unityでいうと AddForce関数 )
Vector3 force; // XYZ方向の力(物理的には加速度:1フレームごとにvelocityの増える=加速する量)
std::array<float, 3> vxyzForce;
};
// 重力を返す、重力を変えたいときはoverrideして処理を変えてください、重力を変えてない場合は 0.0f
virtual inline float gravity() { return 0.0f; }
protected:
World* m_world; // GameObjectが配置されたワールドへのリンク
public:
// 現在いるワールドへのポインタを得る
virtual World* world() { return m_world; }
// 現在いるワールドのポインタを変更する
virtual void world(World* changeWorld) { m_world = changeWorld; }
bool isDead{ false }; // 死んだ(削除対象)フラグ
bool isStatic{ false }; // 動かない(静的オブジェクトか?)
bool isGround{ false }; // 地面についているか
bool isFlying{ false }; // 空をとんでいるか
hash32 typeTag{ "" }; // 小カテゴリ Zako0など個別のタイプ
hash32 baseTag{ "" }; // 大カテゴリ Enemyなど大まかなベースジャンル
// 更新処理[純粋仮想関数 = 0 なのでoverride必須の関数]
virtual void Update() = 0; // 純粋仮想関数 = 0 にすると【絶対 Update()関数はoverrideしなきゃいけない義務を付与できる 】
// 描画処理[純粋仮想関数 = 0 なのでoverride必須の関数]
virtual void Draw() = 0; // 純粋仮想関数 = 0 にすると【絶対 Draw()関数はoverrideしなきゃいけない義務を継承先クラスに付与 】
// ジャンプ開始した直後に呼ばれる関数
virtual void OnStartJump()
{
// overrideしてジャンプした瞬間に音を鳴らしたりする処理などをオブジェクトの種類ごとに実装する
};
// 空中でジャンプの上昇中に呼ばれる関数
virtual void OnJumping()
{
// overrideして空中でジャンプの上昇中にエフェクトを出したりする処理などをオブジェクトの種類ごとに実装する
};
// 空中で落ちている最中に呼ばれる関数
virtual void OnFalling()
{
// overrideして空中で落ちている最中に音を鳴らしたり、エフェクトを出したりする処理などをオブジェクトの種類ごとに実装する
};
};
#endif
Player.hを変更
して、ジャンプと重力落下の初期化をコンポーネントへ置き換えます。
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Resource.h"
#include "GameObject.h"
#include "PhysicsComponent.h"
class World; // 前方宣言
class Camera; // 前方宣言
class Player : public GameObject
{
protected:
// コンストラクタはprotected:にしてあるので、Create関数経由で初期生成してください
Player(World* world, Pad pad, Vector3 position, Quaternion rotation = { 0,0,0,1 }, Vector3 velocity = { 0,0,0 }, Vector3 force = { 0,0,0 })
: pad{ pad }, GameObject(world, position, rotation, velocity, force)
{
this->typeTag = "Player"; // タグは個別の"プレイヤ1"などに使い、タイプはtypeTagで判別する形にしてタイプのジャンルを区別
}
public:
// 仮想デストラクタ
virtual ~Player() {}
Pad pad; // 操作するコントローラー番号
hash32 worldTag{ "" }; // 現在いるワールドのタグ
float MoveSpeedMax = 6; // 移動速度Max値
float MoveSpeed = 0;//移動速度
float moveAngle = 0; // X軸→方向から何度か
float deltaAngle = 0; // プレイヤの動く角度の変化率、ハンドルを切ったらだんだんもとに戻る
int animCount = 0;
std::shared_ptr<Texture> image; // プレイヤの板ポリゴンで描くタイル分割画像への共有リンク
Camera* camera{ nullptr }; // プレイヤを追随するカメラ
void SetCamera(Camera* pCamera) { camera = pCamera; }
// overrideして物理落下コンポーネントからの重力値を返す
virtual float gravity() override { return (gravityPhysics != nullptr) ? gravityPhysics->gravity() : 0.0f; }
protected:
std::shared_ptr<JumpComponent> jumpPhysics{ nullptr }; // ジャンプの物理処理コンポーネント
std::shared_ptr<GravityComponent> gravityPhysics{ nullptr }; // 落下の重力の物理処理コンポーネント
public:
virtual void Init(std::shared_ptr<InitParams> initParams = nullptr) override
{
std::vector<float> vyJumpSpeed{ 13, 14, 15, 4 }; // ジャンプ開始の初期速度
std::vector<float> vyForceJump{ 0.5f, 0.4f, 0.3f, 0.1f }; // ジャンプ上昇にかかる力の初期値
std::vector<float> vyGravity{ 0.8f,0.8f,0.8f,0.8f }; // 重力の設定値
std::vector<float> vyDownSpeedMax{ 16, 16, 16, 8 }; // 降下スピードのリミット
// shared_from_this()を必ず渡してセットで初期化して、オーナーがリンク切れのコンポーネントができないように気をつける
jumpPhysics = std::make_shared<JumpComponent>(shared_from_this_as<Player>(), vyJumpSpeed, vyForceJump);
gravityPhysics = std::make_shared<GravityComponent>(shared_from_this_as<Player>(), vyGravity, vyDownSpeedMax);
}
float yJumpStart{ 0.0f };
std::vector<float> vyJumpSpeed{ 13, 14, 15, 4 }; // ジャンプ開始の初期速度
std::vector<float> vyForceJump{ 0.5f, 0.4f, 0.3f, 0.1f }; // ジャンプ上昇にかかる力の初期値
std::vector<float> vyGravity{ 0.8f,0.8f,0.8f,0.8f }; // 重力の設定値
std::vector<float> vyDownSpeedMax{ 16, 16, 16, 8 }; // 降下スピードのリミット
// 入力を受けての処理
virtual void HandleInput();
// 更新処理
virtual void Update() override;
// 描画処理
virtual void Draw() override;
// ジャンプ開始した直後に呼ばれる関数
virtual void OnStartJump() override;
};
#endif
Player.cppを変更
して、コンポーネントにジャンプや落下の処理を任せましょう。
#include "Player.h"
#include <cmath> // 浮動小数点の余り%演算子がC++にないのでfmodを使うため
#include "MyMath.h"
#include "MyDraw.h"
#include "Camera.h"
#include "World.h"
#include <vector>
void Player::HandleInput()
{
if (Input::GetButton(pad, PAD_INPUT_LEFT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_A)) )
{
//vx = - MoveSpeed; // 左
deltaAngle += 1; //ハンドルを左に回す
}
if (Input::GetButton(pad, PAD_INPUT_RIGHT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_D)))
{
//vx = MoveSpeed; // 右
deltaAngle -= 1; //ハンドルを右に回す
}
if (Input::GetButton(pad, PAD_INPUT_UP)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_W)))
{
//vy = - MoveSpeed; // 上
MoveSpeed += 0.6f; //アクセルを踏む
if (MoveSpeed > MoveSpeedMax)
MoveSpeed = MoveSpeedMax;//Maxスピードで止める
}
else if (Input::GetButton(pad, PAD_INPUT_DOWN)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_S)))
{
//vy = MoveSpeed; // 下
MoveSpeed -= 0.6f; //ブレーキを踏む
if (MoveSpeed < -0.9f)
MoveSpeed = -0.9f;//ちょっとだけバックできるように
}
if (pad == Pad::Key || pad == Pad::One) // コントローラ1またはキーボード操作のプレイヤのときだけ
{
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGUP)) // キーボードのPageUp ボタンを押したときは
{
vy = 2.0f; // テスト用に空中の上方向に移動
}
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGDN)) // キーボードのPageDown ボタンを押したときは
{
vy = -2.0f; // テスト用に空中の下方向に移動
}
}
}
// ジャンプ開始した直後に呼ばれる関数
void Player::OnStartJump()
{
this->isGround = false; // ジャンプ中はisGroundフラグがfalse
}
// 更新処理
void Player::Update()
{
deltaAngle *= 0.5F; // 車のハンドルの方向をだんだんニュートラルに戻す
MoveSpeed *= 0.9F; // 移動速度も減速する
// 入力を受けての処理
HandleInput();
if (jumpPhysics != nullptr)
{
if (isGround)
{
if (Input::GetButtonDown(pad, PAD_INPUT_A) // Zキーを押した瞬間
|| (pad == Pad::Two && Input::GetButtonDown(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを押した瞬間
{
jumpPhysics->JumpStart(); // ジャンプのスタート処理を発動
}
}
else if (vy > 0 && Input::GetButtonUp(pad, PAD_INPUT_A) // Zキーを離した瞬間
|| (pad == Pad::Two && Input::GetButtonUp(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを離した瞬間
{
jumpPhysics->JumpCancel(); // ジャンプのキャンセル処理を発動
}
jumpPhysics->Update(); // ジャンプのコンポーネントを更新する
}
if (gravityPhysics != nullptr)
gravityPhysics->Update(); // 重力落下のコンポーネントを更新する
if (isGround)
{
if (Input::GetButtonDown(pad, PAD_INPUT_A) // Zキーを押した瞬間
|| (pad == Pad::Two && Input::GetButtonDown(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを押した瞬間
{
vy = vyJumpSpeed[0]; // ジャンプ方向はY上方向
vyForce = -vyForceJump[0];
yJumpStart = y;
isGround = false; // ジャンプ中はisGroundフラグがfalse
}
}
else if (vy <= 0) // 下落開始(vyが0未満のときはまだジャンプ中)
{
vyForce = -vyGravity[0]; // ジャンプ上昇力が0になって以降に初めて重力をオンにする
}
else if (Input::GetButtonUp(pad, PAD_INPUT_A) // Zキーを離した瞬間
|| (pad == Pad::Two && Input::GetButtonUp(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを離した瞬間
{
if (y - yJumpStart >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけない
vyForce = -vyGravity[0]; // ジャンプ上昇中(vy未満のとき)にボタンを離したら早めに重力をかけ始める
}
moveAngle += deltaAngle; // ハンドルを回したぶんプレイヤの進行方向を変える
if (moveAngle < -180) moveAngle = 360 + moveAngle; // -180以下の角度にならないようにマイナスになったら+180度にループさせる
else if (moveAngle > +180) moveAngle = -360 + moveAngle; // +180以下の角度にならないようにマイナスになったら-180度にループさせる
rotation.SetRotation(VGet(0, (180 - moveAngle) * MyMath::Deg2Rad, 0)); // 前向きの3Dモデルなら180度回転させて奥方向の向きに設定
// ↓進行方向角度をX方向とZ方向の速度に変える
vx = (float)std::cos((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
vz = (float)std::sin((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
// 実際に位置を動かす
// まず横に移動する
x += vx;
// 次に縦に動かす
y += vy;
// 次にZ奥行き方向に動かす
z += vz;
vy += vyForce; // 勢い(力・フォースを加算
if (y <= 0.0f) // 0.0fを地面として 0.0f以下になったら地面に着地したと判定する
{
y = 0.0f; // 地面に沿わせる
vy = -0.01f; // 下方向への速度をほぼ 0 にする
isGround = true;
}
if (vy <= -vyDownSpeedMax[0]) // 無限に落下速度が加速しないようにリミット(空気抵抗みたいなもの)
vy = -vyDownSpeedMax[0];
// カメラのプレイヤ追尾
float camDistance = 150; // プレイヤからカメラまでのY平面上の距離
float camHeight = 100; // プレイヤからカメラまでのY方向上の高さ
// moveAngleは2D画像のX方向右を0度とするから、-90度回すと画像を正面に捉える位置にカメラを置ける
float camAngleX = (float)std::cos((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
float camAngleZ = (float)std::sin((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
if (camera != nullptr)
{ // カメラの位置をプレイヤの進行方向と真逆に設定
camera->SetPosition(position + Vector3(camAngleX, camHeight, camAngleZ));
camera->LookAt(position); //カメラはプレイヤの方を見る
}
}
// 描画処理
void Player::Draw()
{
int animIndex = animCount / 20; // キャラのパラパラアニメの画像番号(最大3)
float abs_vx = (vx > 0) ? vx : -vx; // x方向絶対値
float abs_vz = (vz > 0) ? vz : -vz; // y方向絶対値
if (abs_vx > 1.00f || abs_vz > 1.00f)
animCount += 6; // キャラが地面のXとZ方向に少しでもスピードがあったらアニメを走らせる
if (animCount >= 60) animCount = 0; // アニメ時間を0へループ
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
for (auto&& keyValue : world()->cameras) // プレイヤのいるワールドにあるすべてのカメラぶんループ
{
auto& camera = keyValue.second;
camera->Draw([&]()
{
// 常にカメラ方向を向くビルボード回転の角度を得る
VECTOR_D angle = MyDraw::GetAngle(DxLib::GetCameraBillboardMatrixD());
angle.x = 0; angle.z = 0; // Y軸周りのビルボード回転だけ有効にする X軸,Z軸まわりの回転は 0 にする
int imgIndex = 9; // キャラチップ画像の起点番号
// ビルボードの角度 と キャラの向いている向き の 差 によって、別のプレイヤのカメラから見たキャラチップの向きの画像を切り替える
// -180 ~ +180 : angle.y moveAngle : -180 ~ +180 ↓
float difAngleY = angle.y * 180.0f / DX_PI_F + moveAngle;
imgIndex = (difAngleY < -135 || 135 < difAngleY) ? 0 // カメラ向きの画像
: (difAngleY < -45) ? 6 // 右 向きの画像
: (difAngleY > 45) ? 3 // 左 向きの画像
: 9; // 奥向きの画像
MyDraw::DrawDivRotaGraphF3D(MyDraw::Plane::Z, x, y, z, 1.0f, angle, *image, imgIndex + animIndex, TRUE);
});
}
}
GameObject.hを変更
して、AddComponentやGetComponentsなどのテンプレ処理を準備しましょう。
#ifndef GAMEOBJECT_H_
#define GAMEOBJECT_H_
#include <memory> // 共有ポインタの定義のため
#include "Vector3.h"
#include "Quaternion.h"
#include "Object.h" // インスタンスIDや検索タグを持ち、shared_from_this_as<継承先クラス名>などができる
class World; // 前方宣言
class Component; // 前方宣言
// ゲーム上に表示される物体の基底クラス。
// プレイヤーや敵、アイテムなどはこのクラスを継承して作る。
class GameObject : public Object
{
protected:
// コンストラクタはprotected:にしてあるのでCreate関数経由で初期生成してください
GameObject(World* world = nullptr, Vector3 position = { 0,0,0 }, Quaternion rotation = { 0,0,0,1 }, Vector3 velocity = { 0,0,0 }, Vector3 force = { 0,0,0 })
: Object(), m_world{ world }, position{ position }, rotation{ rotation }, velocity{ velocity }, force{ force }
{
}
public:
// 仮想デストラクタ
virtual ~GameObject() {}
union { // ★共用体unionテクニック https://inemaru.hatenablog.com/entry/2016/03/02/005408
//★下記3つはメモリ上で共用状態になる(position、x,y,z、xyzどの名前から数値を変えたり読出してもメモリ上は同じ)
// 同じデータに3種類の名前を付けたイメージ、しかもVector3の機能や配列としてのアクセスの仕方もできて便利
struct { float x, y, z; }; // XYZ座標 [匿名共用体とは] https://code.i-harness.com/ja-jp/q/4d437c
Vector3 position; // XYZ座標
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で回せる
Quaternion rotation; // 回転クオータニオン
union {
struct { float vx, vy, vz; }; // XYZ方向の速度
Vector3 velocity; // XYZ方向の速度
std::array<float, 3> vxyz;
};
union {
struct { float vxForce, vyForce, vzForce; }; // XYZ方向にかかる力(Unityでいうと AddForce関数 )
Vector3 force; // XYZ方向の力(物理的には加速度:1フレームごとにvelocityの増える=加速する量)
std::array<float, 3> vxyzForce;
};
// 重力を返す、重力を変えたいときはoverrideして処理を変えてください、重力を変えてない場合は 0.0f
virtual inline float gravity() { return 0.0f; }
protected:
World* m_world; // GameObjectが配置されたワールドへのリンク
public:
// 現在いるワールドへのポインタを得る
virtual World* world() { return m_world; }
// 現在いるワールドのポインタを変更する
virtual void world(World* changeWorld) { m_world = changeWorld; }
bool isDead{ false }; // 死んだ(削除対象)フラグ
bool isStatic{ false }; // 動かない(静的オブジェクトか?)
bool isGround{ false }; // 地面についているか
bool isFlying{ false }; // 空をとんでいるか
hash32 typeTag{ "" }; // 小カテゴリ Zako0など個別のタイプ
hash32 baseTag{ "" }; // 大カテゴリ Enemyなど大まかなベースジャンル
protected:
// コンポーネントの<ハッシュID, 共有所有ポインタ> の辞書でコンポーネントの共有カウンタ+1を保持する
std::unordered_map<size_t, std::shared_ptr<Component>> components;
public:
// コンポーネントを得る 例えば GetComponent<BoxCollider>でBoxColliderという名前のコンポーネントをcomponents辞書から得る
template<class ComponentT>
std::vector<std::weak_ptr<ComponentT>> GetComponents()
{ //★[ComponentTのハッシュコードを得るテク] https://cpprefjp.github.io/reference/typeindex/type_index/hash_code.html
size_t hashID = typeid(ComponentT).hash_code(); //[まずハッシュでID化]
// equal_range 関数を使って、指定したキーの最初と終端の位置を示すイテレータを std::pair<> で取得します。
auto range = components.equal_range(hashID); //https://ez-net.jp/article/9E/DFLX8hNG/G5J8DPmsltz4/
std::vector<std::weak_ptr<ComponentT>> returnList; // "ComponentT" 例."CircleCollider"など をキーとするコンポーネント一覧
for (auto ite = range.first; ite != range.second; ++ite)
returnList.emplace_back(ite->second); // 辞書で見つかったコンポーネントを返すリストに追加
return returnList;
}
// コンポーネントを追加する .AddComponent<BoxCollider>(...)で追加できる
template<class ComponentT, typename ...ArgsT>
std::weak_ptr<ComponentT> AddComponent(ArgsT&&...args)
{ //★[ComponentTのハッシュコードを得るテク] https://cpprefjp.github.io/reference/typeindex/type_index/hash_code.html
size_t hashID = typeid(ComponentT).hash_code(); //[まずハッシュでID化]
// shared_from_this()を必ず渡してセットで初期化して、オーナーがリンク切れのコンポーネントができないように気をつける
std::shared_ptr<ComponentT> newComponent = std::make_shared<ComponentT>(shared_from_this_as<GameObject>(), std::forward<ArgsT>(args)...);
// https://ez-net.jp/article/9E/DFLX8hNG/G5J8DPmsltz4/
// std::unrodered_multimap<> と同型の値を取る std::pair<> を作って insert 関数でcomponents辞書に追加
components.insert(std::pair<size_t, std::shared_ptr<ComponentT>>(hashID, newComponent));
return newComponent;
}
// コンポーネントを削除する
template<typename ComponentT>
bool RemoveComponent(std::shared_ptr<ComponentT> removeComponent)
{ //★[ComponentTのハッシュコードを得るテク] https://cpprefjp.github.io/reference/typeindex/type_index/hash_code.html
size_t hashID = typeid(ComponentT).hash_code(); //[まずハッシュでID化]
// equal_range 関数を使って、指定したキーの最初と終端の位置を示すイテレータを std::pair<> で取得します。
auto range = components.equal_range(hashID); //https://ez-net.jp/article/9E/DFLX8hNG/G5J8DPmsltz4/
//[イテレート中に探しながら消す] https://cpprefjp.github.io/reference/unordered_map/unordered_multimap/erase.html
for (auto ite = range.first; ite != range.second;)
if (ite->second == removeComponent)
{
components.erase(ite); // 削除された要素の次を指すイテレータが返される
return true; // 見つかって消せた場合は true
}
else
++ite; // 次の要素へイテレータを進める
return false; // 見つからずに消せなかった場合は false
}
// 更新処理[純粋仮想関数 = 0 なのでoverride必須の関数]
virtual void Update() = 0; // 純粋仮想関数 = 0 にすると【絶対 Update()関数はoverrideしなきゃいけない義務を付与できる 】
// 描画処理[純粋仮想関数 = 0 なのでoverride必須の関数]
virtual void Draw() = 0; // 純粋仮想関数 = 0 にすると【絶対 Draw()関数はoverrideしなきゃいけない義務を継承先クラスに付与 】
// ジャンプ開始した直後に呼ばれる関数
virtual void OnStartJump()
{
// overrideしてジャンプした瞬間に音を鳴らしたりする処理などをオブジェクトの種類ごとに実装する
};
// 空中でジャンプの上昇中に呼ばれる関数
virtual void OnJumping()
{
// overrideして空中でジャンプの上昇中にエフェクトを出したりする処理などをオブジェクトの種類ごとに実装する
};
// 空中で落ちている最中に呼ばれる関数
virtual void OnFalling()
{
// overrideして空中で落ちている最中に音を鳴らしたり、エフェクトを出したりする処理などをオブジェクトの種類ごとに実装する
};
};
#endif
Player.hを変更
して、UnityっぽくAddComponentでコンポーネントを追加し、プレイヤ側のstd::shared_ptrはstd::weak_ptrに変えておきましょう。
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Resource.h"
#include "GameObject.h"
#include "PhysicsComponent.h"
class World; // 前方宣言
class Camera; // 前方宣言
class Player : public GameObject
{
protected:
// コンストラクタはprotected:にしてあるので、Create関数経由で初期生成してください
Player(World* world, Pad pad, Vector3 position, Quaternion rotation = { 0,0,0,1 }, Vector3 velocity = { 0,0,0 }, Vector3 force = { 0,0,0 })
: pad{ pad }, GameObject(world, position, rotation, velocity, force)
{
this->typeTag = "Player"; // タグは個別の"プレイヤ1"などに使い、タイプはtypeTagで判別する形にしてタイプのジャンルを区別
}
public:
// 仮想デストラクタ
virtual ~Player() {}
Pad pad; // 操作するコントローラー番号
hash32 worldTag{ "" }; // 現在いるワールドのタグ
float MoveSpeedMax = 6; // 移動速度Max値
float MoveSpeed = 0;//移動速度
float moveAngle = 0; // X軸→方向から何度か
float deltaAngle = 0; // プレイヤの動く角度の変化率、ハンドルを切ったらだんだんもとに戻る
int animCount = 0;
std::shared_ptr<Texture> image; // プレイヤの板ポリゴンで描くタイル分割画像への共有リンク
Camera* camera{ nullptr }; // プレイヤを追随するカメラ
void SetCamera(Camera* pCamera) { camera = pCamera; }
// overrideして物理落下コンポーネントからの重力値を返す
virtual float gravity() override { auto pGravityPhysics = gravityPhysics.lock(); return (pGravityPhysics != nullptr) ? pGravityPhysics->gravity() : 0.0f; }
protected:
std::weak_ptr<JumpComponent> jumpPhysics{ nullptr }; // ジャンプの物理処理コンポーネント
std::weak_ptr<GravityComponent> gravityPhysics{ nullptr }; // 落下の重力の物理処理コンポーネント
public:
virtual void Init(std::shared_ptr<InitParams> initParams = nullptr) override
{
std::vector<float> vyJumpSpeed{ 13, 14, 15, 4 }; // ジャンプ開始の初期速度
std::vector<float> vyForceJump{ 0.5f, 0.4f, 0.3f, 0.1f }; // ジャンプ上昇にかかる力の初期値
std::vector<float> vyGravity{ 0.8f,0.8f,0.8f,0.8f }; // 重力の設定値
std::vector<float> vyDownSpeedMax{ 16, 16, 16, 8 }; // 降下スピードのリミット
// AddComponent内部でshared_from_this()を必ず渡してセットで初期化して、オーナーがリンク切れのコンポーネントができないように気をつける
jumpPhysics = AddComponent<JumpComponent>(vyJumpSpeed, vyForceJump);
gravityPhysics = AddComponent<GravityComponent>(vyGravity, vyDownSpeedMax);
}
// 入力を受けての処理
virtual void HandleInput();
// 更新処理
virtual void Update() override;
// 描画処理
virtual void Draw() override;
// ジャンプ開始した直後に呼ばれる関数
virtual void OnStartJump() override;
};
#endif
Player.cppを変更
して、std::shared_ptrからstd::weak_ptrに変えたので.lock()でロックして、
コンポーネントを使っている間(Update関数()の { ~ }のカッコが閉じるまで )はコンポーネントが消えないように +1 キープしながら使うようにしましょう。
#include "Player.h"
#include <cmath> // 浮動小数点の余り%演算子がC++にないのでfmodを使うため
#include "MyMath.h"
#include "MyDraw.h"
#include "Camera.h"
#include "World.h"
#include <vector>
void Player::HandleInput()
{
if (Input::GetButton(pad, PAD_INPUT_LEFT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_A)) )
{
//vx = - MoveSpeed; // 左
deltaAngle += 1; //ハンドルを左に回す
}
if (Input::GetButton(pad, PAD_INPUT_RIGHT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_D)))
{
//vx = MoveSpeed; // 右
deltaAngle -= 1; //ハンドルを右に回す
}
if (Input::GetButton(pad, PAD_INPUT_UP)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_W)))
{
//vy = - MoveSpeed; // 上
MoveSpeed += 0.6f; //アクセルを踏む
if (MoveSpeed > MoveSpeedMax)
MoveSpeed = MoveSpeedMax;//Maxスピードで止める
}
else if (Input::GetButton(pad, PAD_INPUT_DOWN)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_S)))
{
//vy = MoveSpeed; // 下
MoveSpeed -= 0.6f; //ブレーキを踏む
if (MoveSpeed < -0.9f)
MoveSpeed = -0.9f;//ちょっとだけバックできるように
}
if (pad == Pad::Key || pad == Pad::One) // コントローラ1またはキーボード操作のプレイヤのときだけ
{
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGUP)) // キーボードのPageUp ボタンを押したときは
{
vy = 2.0f; // テスト用に空中の上方向に移動
}
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGDN)) // キーボードのPageDown ボタンを押したときは
{
vy = -2.0f; // テスト用に空中の下方向に移動
}
}
}
// ジャンプ開始した直後に呼ばれる関数
void Player::OnStartJump()
{
this->isGround = false; // ジャンプ中はisGroundフラグがfalse
}
// 更新処理
void Player::Update()
{
deltaAngle *= 0.5F; // 車のハンドルの方向をだんだんニュートラルに戻す
MoveSpeed *= 0.9F; // 移動速度も減速する
// 入力を受けての処理
HandleInput();
auto pJumpPhysics = jumpPhysics.lock();
if (pJumpPhysics != nullptr)
{
if (isGround)
{
if (Input::GetButtonDown(pad, PAD_INPUT_A) // Zキーを押した瞬間
|| (pad == Pad::Two && Input::GetButtonDown(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを押した瞬間
{
pJumpPhysics->JumpStart(); // ジャンプのスタート処理を発動
}
}
else if (vy > 0 && Input::GetButtonUp(pad, PAD_INPUT_A) // Zキーを離した瞬間
|| (pad == Pad::Two && Input::GetButtonUp(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを離した瞬間
{
pJumpPhysics->JumpCancel(); // ジャンプのキャンセル処理を発動
}
pJumpPhysics->Update(); // ジャンプのコンポーネントを更新する
}
auto pGravityPhysics = gravityPhysics.lock();
if (pGravityPhysics != nullptr)
pGravityPhysics->Update(); // 重力落下のコンポーネントを更新する
moveAngle += deltaAngle; // ハンドルを回したぶんプレイヤの進行方向を変える
if (moveAngle < -180) moveAngle = 360 + moveAngle; // -180以下の角度にならないようにマイナスになったら+180度にループさせる
else if (moveAngle > +180) moveAngle = -360 + moveAngle; // +180以下の角度にならないようにマイナスになったら-180度にループさせる
rotation.SetRotation(VGet(0, (180 - moveAngle) * MyMath::Deg2Rad, 0)); // 前向きの3Dモデルなら180度回転させて奥方向の向きに設定
// ↓進行方向角度をX方向とZ方向の速度に変える
vx = (float)std::cos((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
vz = (float)std::sin((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
// 実際に位置を動かす
// まず横に移動する
x += vx;
// 次に縦に動かす
y += vy;
// 次にZ奥行き方向に動かす
z += vz;
vy += vyForce; // 勢い(力・フォースを加算
if (y <= 0.0f) // 0.0fを地面として 0.0f以下になったら地面に着地したと判定する
{
y = 0.0f; // 地面に沿わせる
vy = -0.01f; // 下方向への速度をほぼ 0 にする
isGround = true;
}
// カメラのプレイヤ追尾
float camDistance = 150; // プレイヤからカメラまでのY平面上の距離
float camHeight = 100; // プレイヤからカメラまでのY方向上の高さ
// moveAngleは2D画像のX方向右を0度とするから、-90度回すと画像を正面に捉える位置にカメラを置ける
float camAngleX = (float)std::cos((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
float camAngleZ = (float)std::sin((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
if (camera != nullptr)
{ // カメラの位置をプレイヤの進行方向と真逆に設定
camera->SetPosition(position + Vector3(camAngleX, camHeight, camAngleZ));
camera->LookAt(position); //カメラはプレイヤの方を見る
}
}
// 描画処理
void Player::Draw()
{
int animIndex = animCount / 20; // キャラのパラパラアニメの画像番号(最大3)
float abs_vx = (vx > 0) ? vx : -vx; // x方向絶対値
float abs_vz = (vz > 0) ? vz : -vz; // y方向絶対値
if (abs_vx > 1.00f || abs_vz > 1.00f)
animCount += 6; // キャラが地面のXとZ方向に少しでもスピードがあったらアニメを走らせる
if (animCount >= 60) animCount = 0; // アニメ時間を0へループ
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
for (auto&& keyValue : world()->cameras) // プレイヤのいるワールドにあるすべてのカメラぶんループ
{
auto& camera = keyValue.second;
camera->Draw([&]()
{
// 常にカメラ方向を向くビルボード回転の角度を得る
VECTOR_D angle = MyDraw::GetAngle(DxLib::GetCameraBillboardMatrixD());
angle.x = 0; angle.z = 0; // Y軸周りのビルボード回転だけ有効にする X軸,Z軸まわりの回転は 0 にする
int imgIndex = 9; // キャラチップ画像の起点番号
// ビルボードの角度 と キャラの向いている向き の 差 によって、別のプレイヤのカメラから見たキャラチップの向きの画像を切り替える
// -180 ~ +180 : angle.y moveAngle : -180 ~ +180 ↓
float difAngleY = angle.y * 180.0f / DX_PI_F + moveAngle;
imgIndex = (difAngleY < -135 || 135 < difAngleY) ? 0 // カメラ向きの画像
: (difAngleY < -45) ? 6 // 右 向きの画像
: (difAngleY > 45) ? 3 // 左 向きの画像
: 9; // 奥向きの画像
MyDraw::DrawDivRotaGraphF3D(MyDraw::Plane::Z, x, y, z, 1.0f, angle, *image, imgIndex + animIndex, TRUE);
});
}
}
Plane4.hとCollision3D.hは別記事を参考に作っておきます→
Vector3の記事の下のほうにPlane4.hとCollision3D.hがあります→Vector3に色々な関数を用意して3D空間の当たり判定をする
CellXYZ.hを新規作成
して、将棋などのようなマス目位置をハッシュ値変換できて辞書で管理できるクラスを準備しましょう。
#ifndef CELLXYZ_H_
#define CELLXYZ_H_
#include <unordered_map> // ハッシュ値計算 std::hashに必要
#include <cmath>
#include <algorithm> // std::maxに必要
// マス目のXとYとZを保持するデータ型
struct CellXYZ
{
int X = 0, Y = 0, Z = 0;
CellXYZ() = default;
inline ~CellXYZ() {};
inline CellXYZ(int X, int Y, int Z = 0) : X{ X }, Y{ Y }, Z{ Z } {};
// 連想配列mapのキーで使うため < 比較演算子(連想配列並べ替えのため)をカスタム定義 http://programming-tips.info/use_struct_as_key_of_map/cpp/index.html
inline bool operator < (const CellXYZ& other) const
{
if (this->Z < other.Z) return true; // Z優先並べ替え(std::map並び順ではZの大きいものが一番あとに[並び順]X行⇒Y列⇒Zのエクセル順)
else if (this->Z > other.Z) return false;
if (this->Y < other.Y) return true; // Zが同じ時はYを比較 Z Y の順に並べばCSVのように左上から列→行の順にならぶ
else if (this->Y > other.Y) return false;
if (this->X < other.X) return true; // Z,Yが同じ時はXを比較
else if (this->X > other.X) return false;
return false; // X と Y と Z がすべて等しい場合
}
// == 比較演算子カスタム定義 https://sygh.hatenadiary.jp/entry/2014/02/13/222356
inline bool operator == (const CellXYZ& other) const {
return this->X == other.X && this->Y == other.Y && this->Z == other.Z;
}
inline bool operator != (const CellXYZ& other) const {
return !(this->operator==(other));
}
inline size_t GetHashCode() const // X,Y,Zの値からハッシュ値を計算 https://sygh.hatenadiary.jp/entry/2014/02/13/222356
{
return
std::hash<decltype(X)>()(this->X) ^
std::hash<decltype(Y)>()(this->Y) ^
std::hash<decltype(Z)>()(this->Z);
}
struct Hash // ハッシュ値取得用の関数オブジェクト(std::unordered_map のキーで使うため) https://sygh.hatenadiary.jp/entry/2014/02/13/222356
{
size_t operator ()(const CellXYZ& cellXYZ) const
{
return cellXYZ.GetHashCode();
}
};
inline CellXYZ up() const { return CellXYZ(X, Y - 1, Z); }; // 上方向
inline CellXYZ down() const { return CellXYZ(X, Y + 1, Z); }; // 下方向
inline CellXYZ left() const { return CellXYZ(X - 1, Y, Z); }; // 左方向
inline CellXYZ right() const { return CellXYZ(X + 1, Y, Z); }; // 右方向
inline CellXYZ upZ() const { return CellXYZ(X, Y, Z - 1); }; // 上方向(Z方向)
inline CellXYZ downZ() const { return CellXYZ(X, Y, Z + 1); }; // 下方向(Z方向)
// 方向の種類:2D(4方向,8方向) 3D(6方向,26方向)
enum class DirType : unsigned char { Direction4, Direction8, Direction6, Direction26 };
//[enum定義:4方向]上下左右(反時計回り順:上→左→下→右)
enum class Direction4 : unsigned char { Up = 0, Left, Down, Right };
//[enum定義:8方向] 反時計回り順:上→左上→左→左下→下→右下→右→右上
enum class Direction8 : unsigned char { Up = 0, LeftUp, Left, LeftDown, Down, RightDown, Right, RightUp };
//[enum定義:6方向] 反時計回り順:上→左→下→右→右上→3D上→3D下
enum class Direction6 : unsigned char { Up = 0, Left, Down, Right, UpZ, DownZ };
//[enum定義:26方向] 反時計回り順:上→左上→左→左下→下→右下→右→右上→3D下→..反時計回り..→3D上→..反時計回り
enum class Direction26 : unsigned char {
Up = 0, LeftUp, Left, LeftDown, Down, RightDown, Right, RightUp,
DownZ, Up_DownZ, LeftUp_DownZ, Left_DownZ, LeftDown_DownZ, Down_DownZ, RightDown_DownZ, Right_DownZ, RightUp_DownZ,
UpZ, Up_UpZ, LeftUp_UpZ, Left_UpZ, LeftDown_UpZ, Down_UpZ, RightDown_UpZ, Right_UpZ, RightUp_UpZ,
};
//4方向[2D] [constexprで実行前に固定値化して高速化]
static constexpr const std::tuple<int, int, int> dir4[4] = { {0,-1,0},{-1,0,0},{0,+1,0},{+1,0,0} };
// マス目の上下左右 4方向 をvector配列として返す ★配列順 と enum定義順 を合わせてある
static inline std::vector<CellXYZ> directions4()
{ // moveで右辺値の所有権をそのまま { }の外へreturnで渡す https://cpprefjp.github.io/lang/cpp11/rvalue_ref_and_move_semantics.html
return std::move(std::vector<CellXYZ> {
CellXYZ(0, -1), // 上(0時方向)
CellXYZ(-1, 0), // 左(9時方向)
CellXYZ(0, +1), // 下(6時方向)
CellXYZ(+1, 0), // 右(3時方向)
});
}
// マス目の上下左右 4方向 をvector配列として返す ★配列順 と enum定義順 を合わせてある
inline std::vector<CellXYZ> getDirections4() const
{ // moveで右辺値の所有権をそのまま { }の外へreturnで渡す https://cpprefjp.github.io/lang/cpp11/rvalue_ref_and_move_semantics.html
return std::move(std::vector<CellXYZ> {
CellXYZ(X, Y - 1, Z), // 上(0時方向)
CellXYZ(X - 1, Y, Z), // 左(9時方向)
CellXYZ(X, Y + 1, Z), // 下(6時方向)
CellXYZ(X + 1, Y, Z) // 右(3時方向)
});
}
//8方向[2D] [constexprで実行前に固定値化して高速化]
static constexpr const std::tuple<int, int, int> dir8[8] = { {0,-1,0},{-1,-1,0},{-1,0,0},{-1,+1,0},{0,+1,0},{+1,+1,0},{+1,0,0},{+1,-1,0} };
// マス目の上下左右斜め 8方向 をvector配列として返す ★配列順 と enum定義順 を合わせてある
static inline std::vector<CellXYZ> directions8()
{ // moveで右辺値の所有権をそのまま { }の外へreturnで渡す https://cpprefjp.github.io/lang/cpp11/rvalue_ref_and_move_semantics.html
return std::move(std::vector<CellXYZ> {
CellXYZ(0, -1), // 上(12時 方向)
CellXYZ(-1, -1), //左上(10時半方向)
CellXYZ(-1, 0), // 左( 9時 方向)
CellXYZ(-1, +1), //左下( 7時半方向)
CellXYZ(0, +1), // 下( 6時 方向)
CellXYZ(+1, +1), //右下( 4時半方向)
CellXYZ(+1, 0), // 右( 3時 方向)
CellXYZ(+1, -1) //右上( 1時半方向)
});
}
// マス目の上下左右斜め 8方向 をvector配列として返す ★配列順 と enum定義順 を合わせてある
inline std::vector<CellXYZ> getDirections8() const
{ // moveで右辺値の所有権をそのまま { }の外へreturnで渡す https://cpprefjp.github.io/lang/cpp11/rvalue_ref_and_move_semantics.html
return std::move(std::vector<CellXYZ> {
CellXYZ(X, Y - 1, Z), // 上(12時 方向)
CellXYZ(X - 1, Y - 1, Z), //左上(10時半方向)
CellXYZ(X - 1, Y, Z), // 左( 9時 方向)
CellXYZ(X - 1, Y + 1, Z), //左下( 7時半方向)
CellXYZ(X, Y + 1, Z), // 下( 6時 方向)
CellXYZ(X + 1, Y + 1, Z), //右下( 4時半方向)
CellXYZ(X + 1, Y, Z), // 右( 3時 方向)
CellXYZ(X + 1, Y - 1, Z) //右上( 1時半方向)
});
}
//6方向[3D] [constexprで実行前に固定値化して高速化]
static constexpr const std::tuple<int, int, int> dir6[6] = { {0,-1,0},{-1,0,0},{0,+1,0},{+1,0,0},{0,0,-1},{0,0,+1} };
// マス目の[2D]上下左右、[3D]上下 6方向 をvector配列として返す ★配列順 と enum定義順 を合わせてある
static inline std::vector<CellXYZ> directions6()
{ // moveで右辺値の所有権をそのまま { }の外へreturnで渡す https://cpprefjp.github.io/lang/cpp11/rvalue_ref_and_move_semantics.html
return std::move(std::vector<CellXYZ> {
CellXYZ(0, -1), // 上(12時 方向)
CellXYZ(-1, 0), // 左( 9時 方向)
CellXYZ(0, +1), // 下( 6時 方向)
CellXYZ(+1, 0), // 右( 3時 方向)
CellXYZ(0, 0, -1), // 下方向(Z方向)
CellXYZ(0, 0, +1) // 上方向(Z方向)
});
}
// マス目の[2D]上下左右、[3D]上下 6方向 をvector配列として返す ★配列順 と enum定義順 を合わせてある
inline std::vector<CellXYZ> getDirections6() const
{ // moveで右辺値の所有権をそのまま { }の外へreturnで渡す https://cpprefjp.github.io/lang/cpp11/rvalue_ref_and_move_semantics.html
return std::move(std::vector<CellXYZ> {
CellXYZ(X, Y - 1, Z), // 上(12時 方向)
CellXYZ(X - 1, Y, Z), // 左( 9時 方向)
CellXYZ(X, Y + 1, Z), // 下( 6時 方向)
CellXYZ(X + 1, Y, Z), // 右( 3時 方向)
CellXYZ(X, Y, Z - 1), // 下方向(Z方向)
CellXYZ(X, Y, Z + 1) // 上方向(Z方向)
});
}
//26方向[3D] [constexprで実行前に固定値化して高速化]
static constexpr const std::tuple<int, int, int> dir26[26] = {
{0,-1, 0},{-1,-1, 0},{-1,0, 0},{-1,+1, 0},{0,+1, 0},{+1,+1, 0},{+1,0, 0},{+1,-1, 0},
{0,0,-1},{0,-1,-1},{-1,-1,-1},{-1,0,-1},{-1,+1,-1},{0,+1,-1},{+1,+1,-1},{+1,0,-1},{+1,-1,-1},
{0,0,+1},{0,-1,+1},{-1,-1,+1},{-1,0,+1},{-1,+1,+1},{0,+1,+1},{+1,+1,+1},{+1,0,+1},{+1,-1,+1},
};
// マス目の[2D]上下左右、[3D]上下 6方向 をvector配列として返す ★配列順 と enum定義順 を合わせてある
static inline std::vector<CellXYZ> directions26()
{ // moveで右辺値の所有権をそのまま { }の外へreturnで渡す https://cpprefjp.github.io/lang/cpp11/rvalue_ref_and_move_semantics.html
return std::move(std::vector<CellXYZ> {
CellXYZ(0, -1, 0), // 上(12時 方向)
CellXYZ(-1, -1, 0), //左上(10時半方向)
CellXYZ(-1, 0, 0), // 左( 9時 方向)
CellXYZ(-1, +1, 0), //左下( 7時半方向)
CellXYZ(0, +1, 0), // 下( 6時 方向)
CellXYZ(+1, +1, 0), //右下( 4時半方向)
CellXYZ(+1, 0, 0), // 右( 3時 方向)
CellXYZ(+1, -1, 0), //右上( 1時半方向)
CellXYZ(0, 0, -1), //[3D下]下方向(Z方向)
CellXYZ(0, -1, -1), //[3D下] 上(12時 方向)
CellXYZ(-1, -1, -1), //[3D下]左上(10時半方向)
CellXYZ(-1, 0, -1), //[3D下] 左( 9時 方向)
CellXYZ(-1, +1, -1), //[3D下]左下( 7時半方向)
CellXYZ(0, +1, -1), //[3D下] 下( 6時 方向)
CellXYZ(+1, +1, -1), //[3D下]右下( 4時半方向)
CellXYZ(+1, 0, -1), //[3D下] 右( 3時 方向)
CellXYZ(+1, -1, -1), //[3D下]右上( 1時半方向)
CellXYZ(0, 0, +1), //[3D上]上方向(Z方向)
CellXYZ(0, -1, +1), //[3D上] 上(12時 方向)
CellXYZ(-1, -1, +1), //[3D上]左上(10時半方向)
CellXYZ(-1, 0, +1), //[3D上] 左( 9時 方向)
CellXYZ(-1, +1, +1), //[3D上]左下( 7時半方向)
CellXYZ(0, +1, +1), //[3D上] 下( 6時 方向)
CellXYZ(+1, +1, +1), //[3D上]右下( 4時半方向)
CellXYZ(+1, 0, +1), //[3D上] 右( 3時 方向)
CellXYZ(+1, -1, +1) //[3D上]右上( 1時半方向)
});
}
// マス目の[2D]上下左右、[3D]下反時計周り 上反時計回り 26方向 をvector配列として返す ★配列順 と enum定義順 を合わせてある
inline std::vector<CellXYZ> getDirections26() const
{ // moveで右辺値の所有権をそのまま { }の外へreturnで渡す https://cpprefjp.github.io/lang/cpp11/rvalue_ref_and_move_semantics.html
return std::move(std::vector<CellXYZ> {
CellXYZ(X, Y - 1, Z), // 上(12時 方向)
CellXYZ(X - 1, Y - 1, Z), //左上(10時半方向)
CellXYZ(X - 1, Y, Z), // 左( 9時 方向)
CellXYZ(X - 1, Y + 1, Z), //左下( 7時半方向)
CellXYZ(X, Y + 1, Z), // 下( 6時 方向)
CellXYZ(X + 1, Y + 1, Z), //右下( 4時半方向)
CellXYZ(X + 1, Y, Z), // 右( 3時 方向)
CellXYZ(X + 1, Y - 1, Z), //右上( 1時半方向)
CellXYZ(X, Y, Z - 1), //[3D下]下方向(Z方向)
CellXYZ(X, Y - 1, Z - 1), //[3D下] 上(12時 方向)
CellXYZ(X - 1, Y - 1, Z - 1), //[3D下]左上(10時半方向)
CellXYZ(X - 1, Y, Z - 1), //[3D下] 左( 9時 方向)
CellXYZ(X - 1, Y + 1, Z - 1), //[3D下]左下( 7時半方向)
CellXYZ(X, Y + 1, Z - 1), //[3D下] 下( 6時 方向)
CellXYZ(X + 1, Y + 1, Z - 1), //[3D下]右下( 4時半方向)
CellXYZ(X + 1, Y, Z - 1), //[3D下] 右( 3時 方向)
CellXYZ(X + 1, Y - 1, Z - 1), //[3D下]右上( 1時半方向)
CellXYZ(X, Y, Z + 1), //[3D上]上方向(Z方向)
CellXYZ(X, Y - 1, Z + 1), //[3D上] 上(12時 方向)
CellXYZ(X - 1, Y - 1, Z + 1), //[3D上]左上(10時半方向)
CellXYZ(X - 1, Y, Z + 1), //[3D上] 左( 9時 方向)
CellXYZ(X - 1, Y + 1, Z + 1), //[3D上]左下( 7時半方向)
CellXYZ(X, Y + 1, Z + 1), //[3D上] 下( 6時 方向)
CellXYZ(X + 1, Y + 1, Z + 1), //[3D上]右下( 4時半方向)
CellXYZ(X + 1, Y, Z + 1), //[3D上] 右( 3時 方向)
CellXYZ(X + 1, Y - 1, Z + 1) //[3D上]右上( 1時半方向)
});
}
inline CellXYZ& operator += (const CellXYZ& other)
{
this->X = this->X + other.X;
this->Y = this->Y + other.Y;
this->Z = this->Z + other.Z;
return *this;
}
inline CellXYZ operator -= (const CellXYZ& other)
{
this->X = this->X - other.X;
this->Y = this->Y - other.Y;
this->Z = this->Z - other.Z;
return *this;
}
enum class DistType : unsigned char//[距離の種類] https://datachemeng.com/use_of_distance/
{
None = 0,
Manhattan, // マンハッタン距離(都市のブロック単位の距離、京都の碁盤の目の距離)
Euclidian, // ユークリッド距離(√直線距離:三平方の定理の距離)
Euclidian2, // ユークリッド距離(2乗直線距離:ルート計算を省いて高速化)
Chebyshev, // チェビシェフ距離(縦か横か高さの差のどれか一番大きい値)
};
// マス目距離を求める Aスターアルゴリズムのヒューリスティックコストなどに使われる
inline float distance(const CellXYZ& cell, DistType type = DistType::Manhattan)
{
if (type == DistType::Manhattan)
return (float)(std::abs(cell.X - X) + std::abs(cell.Y - Y) + std::abs(cell.Z - Z));
else if (type == DistType::Euclidian || type == DistType::Euclidian2)
{
float dist2 = (float)((cell.X - X) * (cell.X - X) + (cell.Y - Y) * (cell.Y - Y) + (cell.Z - Z) * (cell.Z - Z));
if (type == DistType::Euclidian2)
return dist2; // 2乗のまま返す(√計算コスト省く)
else
return std::sqrt(dist2); // √距離
}
else // <windows.h>をインクルードするとmaxという名前の関数マクロが定義され、std::max()と衝突してしまうので(std::max)(a, b);のように関数名をカッコで囲んで使用 https://cpprefjp.github.io/reference/algorithm/max.html
return (float)(std::max)({ std::abs(cell.X - X), std::abs(cell.Y - Y), std::abs(cell.Z - Z) }); // チェビシェフ距離 3変数のmax https://moxtsuan.hatenablog.com/entry/2014/12/01/235957
}
};
inline CellXYZ operator + (const CellXYZ& left, const CellXYZ& right)
{
return std::move(CellXYZ{ left.X + right.X, left.Y + right.Y, left.Z + right.Z });
}
inline CellXYZ operator - (const CellXYZ& left, const CellXYZ& right)
{
return std::move(CellXYZ{ left.X - right.X, left.Y - right.Y, left.Z - right.Z });
}
// CellXYZに追加機能として原点(0,0,0)からの距離を事前計算する機能を追加
struct CellDistXYZ
{
union {
struct { int X, Y, Z; };
CellXYZ XYZ;
};
int distpow2{ 0 }; // 原点(0,0,0)からの距離の2乗
operator CellXYZ& () { return XYZ; } // CellXYZ型への変換オペレータ
CellDistXYZ() = default;
inline ~CellDistXYZ() {};
inline CellDistXYZ(int X, int Y, int Z = 0) : X{ X }, Y{ Y }, Z{ Z }
{
distpow2 = X * X + Y * Y + Z * Z; // 原点(0,0,0)からの距離比較のため事前初期計算
};
// ★連想配列mapのキーで使うため < 比較演算子(連想配列並べ替えのため)をカスタム定義 http://programming-tips.info/use_struct_as_key_of_map/cpp/index.html
inline bool operator < (const CellDistXYZ& other) const {
//★原点からの距離distpow2順になるようにするとstd::mapにいれれば自動で原点からの距離順にならぶ(敵の近い順に使える)
if (this->distpow2 < other.distpow2) return true;
if (this->Z < other.Z) return true; // Z優先並べ替え(std::map並び順ではZの大きいものが一番あとに[並び順]X行⇒Y列⇒Zのエクセル順)
else if (this->Z > other.Z) return false;
if (this->Y < other.Y) return true; // Zが同じ時はYを比較 Z Y の順に並べばCSVのように左上から列→行の順にならぶ
else if (this->Y > other.Y) return false;
if (this->X < other.X) return true; // Z,Yが同じ時はXを比較
else if (this->X > other.X) return false;
return false; // X と Y と Z がすべて等しい場合
}
// 一致演算子 == std::unordered_map のキーで使うため == 比較演算子カスタム定義 https://sygh.hatenadiary.jp/entry/2014/02/13/222356
inline bool operator == (const CellXYZ& other) const {
return this->X == other.X && this->Y == other.Y && this->Z == other.Z;
}
// 不一致演算子 !=
inline bool operator != (const CellXYZ& other) const {
return !(this->operator==(other));
}
inline size_t GetHashCode() const { return XYZ.GetHashCode(); }
struct Hash // ハッシュ値取得用の関数オブジェクト(std::unordered_map のキーで使うため) https://sygh.hatenadiary.jp/entry/2014/02/13/222356
{
inline size_t operator ()(const CellDistXYZ& cell) const { return cell.XYZ.GetHashCode(); }
};
inline CellDistXYZ& operator += (const CellXYZ& other)
{
this->XYZ += other;
this->distpow2 = X * X + Y * Y + Z * Z; // 原点(0,0,0)からの距離比較のため
return *this;
}
inline CellDistXYZ& operator += (const CellDistXYZ& other)
{
this->XYZ += other.XYZ;
this->distpow2 = X * X + Y * Y + Z * Z; // 原点(0,0,0)からの距離比較のため
return *this;
}
inline CellDistXYZ& operator -= (const CellXYZ& other)
{
this->XYZ -= other;
this->distpow2 = X * X + Y * Y + Z * Z; // 原点(0,0,0)からの距離比較のため
return *this;
}
inline CellDistXYZ& operator -= (const CellDistXYZ& other)
{
this->XYZ -= other.XYZ;
this->distpow2 = X * X + Y * Y + Z * Z; // 原点(0,0,0)からの距離比較のため
return *this;
}
// ムーブコンストラクタ distpow2を再計算せずにそのまま代入 https://cpprefjp.github.io/lang/cpp11/rvalue_ref_and_move_semantics.html
inline CellDistXYZ(CellDistXYZ&& other) noexcept : XYZ{ other.XYZ }, distpow2{ other.distpow2 } {}
};
inline CellDistXYZ operator + (const CellDistXYZ& left, const CellDistXYZ& right)
{ // ムーブコンストラクタで { }内 の一時変数 を いちいち無駄にコピーせず { }外 へそのまま所有権ごと移動させるので { }内 のデータは消えずに { }外へ
return std::move(CellDistXYZ{ left.X + right.X,left.Y + right.Y,left.Z + right.Z });
}
inline CellDistXYZ operator - (const CellDistXYZ& left, const CellDistXYZ& right)
{ // ムーブコンストラクタで { }内 の一時変数 を いちいち無駄にコピーせず { }外 へそのまま所有権ごと移動させるので { }内 のデータは消えずに { }外へ
return std::move(CellDistXYZ{ left.X - right.X,left.Y - right.Y,left.Z - right.Z });
}
// 空間をグリッドでマス目として分割する
template<typename TypeT>
class CellGrid
{
public:
int cellWidth, cellHeight, cellDepth; // マス目セルのサイズ(当たり判定の半径よりは大きくないとあかん)
// CellXYZをキーにそのマス目にあるTypeTを得る辞書配列
std::unordered_map<CellXYZ, TypeT, CellXYZ::Hash> cellData;
inline CellGrid(int cellWidth = 64, int cellHeight = 64, int cellDepth = 64)
: cellWidth{ cellWidth }, cellHeight{ cellHeight }, cellDepth{ cellDepth }
{
clear();
}
virtual ~CellGrid()
{
clear();
}
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;
}
virtual int cellZ(float worldZ)
{ // マイナス方向にも対応
return (worldZ >= 0) ? (int)worldZ / cellDepth : (int)worldZ / cellDepth - 1;
}
virtual float centerX(int cellX)
{ // マイナス方向にも対応
return (cellX >= 0) ? (float)cellX * cellWidth + cellWidth / 2 : (float)(cellX + 1) * cellWidth - cellWidth / 2;
}
virtual float centerY(int cellY)
{ // マイナス方向にも対応
return (cellY >= 0) ? (float)cellY * cellHeight + cellHeight / 2 : (float)(cellY + 1) * cellHeight - cellHeight / 2;
}
virtual float centerZ(int cellZ)
{ // マイナス方向にも対応
return (cellZ >= 0) ? (float)cellZ * cellDepth + cellDepth / 2 : (float)(cellZ + 1) * cellDepth - cellDepth / 2;
}
inline TypeT top(float worldX, float worldY, float worldZ = 0) // マス目に入っている先頭の要素へのポインタを得る
{
return top(cellX(worldX), cellY(worldY), cellZ(worldZ));
}
inline TypeT top(int cellX, int cellY, int cellZ = 0) // マス目に入っている先頭の要素へのポインタを得る
{
return top(CellXYZ(cellX, cellY, cellZ));
}
inline TypeT top(const CellXYZ& searchXYZ) { // マス目に入っている先頭の要素を得る
if (cellData.count(searchXYZ) == 0) {
auto ret = cellData[searchXYZ]; //TypeTのデフォルトコンストラクタを走らせる
cellData.erase(searchXYZ); // 即消す
return ret;
}
return cellData[searchXYZ];
}
inline int cellID(int cellX, int cellY, int cellZ = 0) // マス目番号IDを返す(見つからない場合は-1が返る)
{
CellXYZ searchXYZ(cellX, cellY, cellZ);
if (cellData.count(searchXYZ) == 0) return -1; // 見つからない場合は-1が返る
return top(searchXYZ).posID;
}
inline int cellID(const CellXYZ& searchXYZ) // マス目番号IDを返す(見つからない場合は-1が返る)
{
if (cellData.count(searchXYZ) == 0) return -1; // 見つからない場合は-1が返る
return top(searchXYZ).posID;
}
inline TypeT& operator()(float worldX, float worldY, float worldZ = 0) { // マス目に入っている先頭の参照を得る
return cellData[CellXYZ(cellX(worldX), cellY(worldY), cellZ(worldZ))];
}
inline TypeT& operator()(int cellX, int cellY, int cellZ = 0) { // マス目に入っている先頭の参照を得る
return cellData[CellXYZ(cellX, cellY, cellZ)];
}
inline TypeT& operator[](const CellXYZ& searchXYZ) { // マス目に入っている先頭の参照を得る
return cellData[searchXYZ];
}
struct LineZ {//★ []オペレータの数珠つなぎで[][][]3つ四角で3次元配列のようにアクセスできるように
CellGrid* parent{ nullptr };
int X, Y;
inline LineZ(int X, int Y, CellGrid* parent) : X{ X }, Y{ Y }, parent{ parent } {};
inline TypeT& operator[](int Z) { // マス目に入っている先頭の参照を得る
return (*parent)[CellXYZ(X, Y, Z)];
}
inline operator TypeT& () { // [][]の2つ[]のときはZ無し辞書にアクセスして返す
return (*parent)[CellXYZ(X, Y)];
}
inline TypeT& operator = (TypeT other) { // 代入 [X][Y] = ~;の時の挙動
return (*parent)[CellXYZ(X, Y)] = other;
}
};
struct LineY {//★ []オペレータの数珠つなぎで[][][]3つ四角で3次元配列のようにアクセスできるように
CellGrid* parent{ nullptr };
int X;
inline LineY(int X, CellGrid* parent) : X{ X }, parent{ parent } {};
inline LineZ operator[](int Y) { // []オペレータの数珠つなぎで[][][]3つ四角で3次元配列のようにアクセスできるように
return LineZ(X, Y, parent);
}
};
inline LineY operator[](int X) { // マス目に入っている要素へ[][][]3つ四角でアクセスできるように
return LineY(X, this);
}
virtual void clear(CellXYZ* targetXYZ = nullptr)
{
if (targetXYZ == nullptr) {
std::vector<CellXYZ> cells;
for (auto&& cell : cellData)
cells.emplace_back(cell.first);
for (auto&& cell : cells)
clear(&cell);
cellData.clear(); //格子をクリア
//std::unordered_map<CellXYZ, TypeT, CellXYZ::Hash>().swap(cellData); // 空のテンポラリオブジェクトでリセット https://stackoverflow.com/questions/42114044/how-to-release-unordered-map-memory/42115076
}
// 継承してTypeTクラスがリンクリスト構造ならoverrideして数珠つなぎのリストをクリアしたほうがいい(そのままだと先頭しかclearされないから)
/*else if (targetXYZ != nullptr && cellData.count(*targetXYZ) != 0)
cellData[*targetXYZ]->clearLinkList();*/
};
virtual void Update() {}; // Update仮想関数
virtual void Draw() {}; // Draw仮想関数
// マス目の上下左右4方向をvector配列として返す
inline std::vector<CellXYZ> getDirections4(const CellXYZ& center, bool ifExists = false) // ifExists = trueにすると存在しているときだけget
{
std::vector<CellXYZ> directions;
for (auto&& cell : center.getDirections4())
{
if (ifExists == true && cellData.count(cell) > 0)
directions.emplace_back(cell);
}
return directions;
}
// マス目の上下左右斜め方向をvector配列として返す
inline std::vector<CellXYZ> getDirections8(const CellXYZ& center, bool ifExists = false) // ifExists = trueにすると存在しているときだけget
{
std::vector<CellXYZ> directions;
for (auto&& cell : center.getDirections8())
{
if (ifExists == true && cellData.count(cell))
directions.emplace_back(cell);
}
return directions;
}
//[3D] マス目の上下左右 と 3D上下 方向をvector配列として返す
inline std::vector<CellXYZ> getDirections6(const CellXYZ& center, bool ifExists = false) // ifExists = trueにすると存在しているときだけget
{
std::vector<CellXYZ> directions;
for (auto&& cell : center.getDirections6())
{
if (ifExists == true && cellData.count(cell))
directions.emplace_back(cell);
}
return directions;
}
//[3D] マス目の[2D]上下左右斜め 8方向 と [3D下]反時計回り9方向 と [3D上]反時計回り9方向 計26方向をvector配列として返す
inline std::vector<CellXYZ> getDirections26(const CellXYZ& center, bool ifExists = false) // ifExists = trueにすると存在しているときだけget
{
std::vector<CellXYZ> directions;
for (auto&& cell : center.getDirections26())
{
if (ifExists == true && cellData.count(cell))
directions.emplace_back(cell);
}
return directions;
}
};
#endif
GridObject.hを新規作成
して、マス目にいれるオブジェクトのクラスを準備しましょう。
#ifndef GRID_OBJECT_H_
#define GRID_OBJECT_H_
#include "CellXYZ.h"
#include "GameObject.h"
class GridObject; // 前方宣言
// GridObjectに特化したGridをCellGridを継承して定義
class Grid : public CellGrid<GridObject*>
{
public:
// 継承overrideしてGridObjectのリンクリスト構造を頭から最後まで全部クリアする
virtual void clear(CellXYZ* targetXYZ = nullptr) override;
Grid(int cellWidth = 64, int cellHeight = 64, int cellDepth = 64)
: CellGrid(cellWidth, cellHeight, cellDepth)
{
clear();
}
virtual ~Grid()
{
clear();
}
};
// GameProgramingPatternsの第20章:空間分割を参考に独自実装(コンポーネント化、マイナス空間も対応)
// https://www.amazon.co.jp/gp/offer-listing/4844338900/ref=tmm_pap_used_olp_0?ie=UTF8&condition=used
// グリッドに入れるオブジェクト、前と後のオブジェクトへのリンクを数珠繋ぎリンクリスト構造で持つことができる
class GridObject
{
public:
inline GridObject(Grid* pGrid = nullptr) : m_pGrid{ pGrid } {}
virtual ~GridObject() { erase(); }
union {
CellXYZ beforeXYZ{ 0,0,0 }; // 1フレーム前のマス目位置
struct { int X, Y, Z; };
};
// 前と次への双方向のポインタ矢印を持つ数珠繋ぎリンクリスト構造
GridObject* m_pPrev{ nullptr }; //前の点ノードに戻る矢印ポインタ
GridObject* m_pNext{ nullptr }; //次の点ノードに進む矢印ポインタ
Grid* m_pGrid{ nullptr }; // GridObject自身が入っているグリッドへのリンク
// override して グリッドの隣接グリッド探索などの処理を実装する
virtual void checkCell()
{
// override して グリッドの隣接グリッド探索などの処理を実装してください
}
// リンク構造のリセットとm_pGridへのリンクのクリア(ドミノ倒し的に全部リセット)
virtual void clearLinkList()
{
GridObject* next = m_pNext; // eraseでpNextのリンクが消える前にリンクを一時退避
erase();
m_pGrid = nullptr;
if (next != nullptr) next->clearLinkList(); // 最初に取っておいた次のリンク先も連鎖的にクリアさせる(ドミノ倒し的に全部リセット)
}
// グリッドにthisを追加 (m_pGridのnowXYZのマス目位置に追加)
virtual void add(CellXYZ* nowXYZ)
{
if (nowXYZ == nullptr) return;
if (m_pGrid == nullptr) return;
//↓リンクリスト構造のリストを使って先頭に追加する
m_pPrev = nullptr; //先頭だから自分より前はない
m_pNext = m_pGrid->top(*nowXYZ); // 次→grid配列の先頭に元から入っていたもの
(*m_pGrid)[*nowXYZ] = this; // grid配列の先頭をthisにすげかえる
if (m_pNext != nullptr) // 先頭に元から入っていたものがnullptrじゃなかったら
m_pNext->m_pPrev = this; // this←元からの先頭(先頭に割り込んだので元の先頭の人の前矢印をthisへリンク)
//↑ここまでの処理で数珠つなぎのリンクリスト構造になる
beforeXYZ = *nowXYZ; // 一つ前のx,y,zの位置を保存して次に備える
}
// グリッドからthisを削除
void erase(CellXYZ* nowXYZ = nullptr)
{
// リンクリスト構造からのリンク削除処理
if (m_pPrev != nullptr)
m_pPrev->m_pNext = m_pNext; // 前要素とのリンク削除
if (m_pNext != nullptr)
m_pNext->m_pPrev = m_pPrev; // 次要素とのリンク削除
// デバッグ表示
//if (nowXYZ == nullptr) printfDx("(%d,%d,%d)\n", beforeXYZ.X, beforeXYZ.Y, beforeXYZ.Z);
if (nowXYZ == nullptr) nowXYZ = &beforeXYZ;
//printfDx("(%d,%d,%d)\n", fromXYZ->X, fromXYZ->Y, fromXYZ->Z); // デバッグ表示
if (m_pGrid == nullptr) return;
if (m_pGrid->top(*nowXYZ) == this) // このグリッド(this)がリンクリスト構造の一番最初(top)だった場合はtopの置き換えが必要
{
if (m_pNext != nullptr) // リンクリスト構造の数珠つなぎの後ろ側(m_pNext)があるときには
(*m_pGrid)[*nowXYZ] = m_pNext; // gridのマス目配列の先頭を thisのかわりに pNextで上書きしてthisを上書きで消す
else // 数珠つなぎの後ろがいないときは
(*m_pGrid).cellData.erase(*nowXYZ); // マス目辞書配列の目次から削除(辞書の目次数が多いと重くなりやすいから)
}
}
virtual void Draw(GameObject* obj = nullptr)
{
#ifdef _DEBUG
if (m_pGrid == nullptr) return;
int cellX = beforeXYZ.X; int cellY = beforeXYZ.Y; int cellZ = beforeXYZ.Z;
float leftX = (float)cellX * m_pGrid->cellWidth;
float topY = (float)cellY * m_pGrid->cellHeight;
float leftTopZ = (float)cellZ * m_pGrid->cellDepth;
float rightX = (float)(cellX + 1) * m_pGrid->cellWidth;
float bottomY = (float)(cellY + 1) * m_pGrid->cellHeight;
float rightBottomZ = (float)(cellZ + 1) * m_pGrid->cellDepth;
unsigned int color = GetColor(0, 255, 255); // グリッドを 水色 で描画
// 四角Boxで GridObjectが存在するグリッド を描画
DxLib::DrawCube3D(VGet(leftX, topY, leftTopZ),
VGet(rightX, bottomY, rightBottomZ),
color, color, FALSE);
#endif
}
};
#endif
GridObject.cppを新規作成
して、グリッドのマス目辞書から指定したターゲットとなるGridObjectのリンクリスト数珠つなぎをclearLinkList()でリセットできるようにしましょう。
#include "GridObject.h"
// 継承overrideしてGridObjectのリンクリスト構造を頭から最後まで全部クリアする
void Grid::clear(CellXYZ* targetXYZ)
{
if (targetXYZ == nullptr) // ターゲットの指定がないときは辞書cellDataに入っているすべてをclearにかける
{
std::vector<CellXYZ> cells;
for (auto&& cell : cellData) // 一旦辞書cellDataに入っているマス目をすべてcellsに移す
cells.emplace_back(cell.first);
for (auto& targetCell : cells) // 辞書のすべてのマス目をループ
clear(&targetCell); // 辞書のすべてのマス目をtargetXYZにすえて、clearをひとつひとつ実行(辞書を全部クリア)
cellData.clear(); //辞書をクリアして目次が一つもない状態にする
}
else if (targetXYZ != nullptr && cellData.count(*targetXYZ) != 0) // ターゲットが辞書の目次で見つかったら
cellData[*targetXYZ]->clearLinkList(); // 辞書に入っているGridObjectのリンクリストの数珠つなぎをドミノ倒し的にクリア
}
GridComponent.hを新規作成
して、オーナーが今いるグリッドと隣接するグリッドのマス目辞書の先頭のGridObjectから数珠つなぎにチェックできるようにしましょう。
#ifndef GRIDCOMPONENT_H_
#define GRIDCOMPONENT_H_
#include "Component.h"
#include <list>
#include <memory> // 共有ポインタ std::shared_ptr を使用する
#include <unordered_map>
#include <string>
#include <assert.h> // OnCheck()を継承先でoverrideしていないことの警告用
#include "CellXYZ.h"
#include "GameObject.h"
#include "Camera.h"
#include "GridObject.h"
#include "Collider.h"
// GameProgramingPatternsの第20章:空間分割を参考に独自実装(コンポーネント化、マイナス空間も対応)
// https://www.amazon.co.jp/gp/offer-listing/4844338900/ref=tmm_pap_used_olp_0?ie=UTF8&condition=used
// 空間分割の機能をコンポーネントとして継承。[弾×敵]や[プレイヤ×敵]などを作り分けられる設計に
class GridComponent : public GridObject, public Component
{
public:
GridComponent(std::shared_ptr<GameObject> pOwner, const std::string& typeTag, CellXYZ::DirType dirType = CellXYZ::DirType::Direction8)
: Component(pOwner), dirType{ dirType },
GridObject((pOwner->world() == nullptr) ? nullptr : pOwner->world()->makeGrid()) // グリッドをmakeGridで初期生成
{
this->typeTag = typeTag;
this->baseTag = "GridComponent";
this->beforeXYZ = CellXYZ(m_pGrid->cellX(pOwner->x), m_pGrid->cellY(pOwner->y), m_pGrid->cellZ(pOwner->z));
add(); // 初期化と同時にgridに追加
};
virtual ~GridComponent() { erase(); }
//GridManager& grids = GridManager::GetInstance();
CellXYZ::DirType dirType; // 当たり判定でたどる周りのセルのタイプ: [2D]4方向,8方向,[3D]6方向,26方向
// 継承overrideしてコライダがあるときはそのリストを返す
virtual std::list<std::weak_ptr<Collider>>* colliders() { return nullptr; }
// グリッドにthisを追加
virtual void add(CellXYZ* nowXYZ = nullptr) override
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
CellXYZ nowCellXYZ(m_pGrid->cellX(pOwner->x), m_pGrid->cellY(pOwner->y), m_pGrid->cellZ(pOwner->z));
if (nowXYZ == nullptr) nowXYZ = &nowCellXYZ;
GridObject::add(nowXYZ); // 基底ベースクラスのaddを呼び出す
}
// 移動時のグリッドのマス目の隣への移動処理
void moveCell(CellXYZ* nowXYZ = nullptr)
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) // オーナーがすでにメモリから解放済のときは
{
erase(); // オーナーがすでにメモリから解放済のときは リンクリストから削除して return
return;
}
//移動先のセル(マス目)の指定 nowXYZ がnullptrで指定がなかったら現在オーナーのいる位置を nowXYZに指定する
CellXYZ nowCellXYZ(m_pGrid->cellX(pOwner->x), m_pGrid->cellY(pOwner->y), m_pGrid->cellZ(pOwner->z));
if (nowXYZ == nullptr) nowXYZ = &nowCellXYZ;
if (beforeXYZ == *nowXYZ) return; // 移動しても同一のマス目なら即return
erase(&beforeXYZ); // 前のマス目の位置でのリンクリストから一旦削除
add(nowXYZ); // 今のマス目位置(nowXYZ)の先頭に thisを割り込みでaddで追加する
}
// 隣接するグリッドのレンジ内にいてcheck関数がよばれたときに実行したい処理をoverrideして実装する
virtual void OnCheck(GridComponent* other)
{
assert("OnCheckを継承したクラスでoverrideして独自の当たり判定処理などを実装しないと意味ないよ(例.GridCollisionComponent.hを見てください)" == "");
}
// リンクリストの数珠つなぎを順にたどってOnCheck関数を呼ぶ
inline void checkLinkList(GridObject* other)
{
while (other != nullptr) // while文で数珠つなぎのm_pNextを次々とたどって最後にたどり着くと次がなくてnullptrになる
{
if (this != other) // otherが自分自身のときは自分自身どうしのチェックはしない
OnCheck((GridComponent*)other); // OnCheckを継承したクラスで書き分けることで[弾×敵]や[プレイヤ×敵]などを作り分けられる設計に
other = other->m_pNext; // リンクリスト構造の次をwhileで次々とたどる
}
}
// グリッドのマス目内と隣接するマス目内に格納されている要素(敵や弾)のチェック
// 実際のチェック処理はOnCheck関数でやる。隣接するマス目しか調べないので高速
void checkCell() override
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) // オーナーがすでにメモリから解放済のときは
{
erase(); // オーナーがすでにメモリから解放済のときは リンクリストから削除して return
return;
}
// オーナーのいるグリッドのマス目辞書から先頭を取り出し、その同一グリッドにいるオブジェクトをチェックする
auto center = (GridComponent*)m_pGrid->top(pOwner->x, pOwner->y, pOwner->z);
if (center == nullptr) return;
this->checkLinkList(center); // 中心centerの同一マスに他にもオブジェクトがある場合にそれらのオブジェクトをチェックする
int cellX = m_pGrid->cellX(pOwner->x);
int cellY = m_pGrid->cellY(pOwner->y);
int cellZ = m_pGrid->cellZ(pOwner->z);
size_t iSize = 4;
auto* dir = &CellXYZ::dir4[0]; //[2D] 隣接 4方向グリッド調べる constexpr で高速化を狙う
if (dirType == CellXYZ::DirType::Direction8) { iSize = 8; dir = &CellXYZ::dir8[0]; } //[2D] 隣接 8方向グリッド調べる
else if (dirType == CellXYZ::DirType::Direction6) { iSize = 6; dir = &CellXYZ::dir6[0]; } //[3D] 隣接 6方向グリッド調べる
else if (dirType == CellXYZ::DirType::Direction26) { iSize = 26; dir = &CellXYZ::dir26[0]; } //[3D] 隣接 26方向グリッド調べる
#define DIR_X( i ) std::get<0>(dir[i]) // std::get<0>(dir[i]) と書くより少し見やすく DIR_X(i) と書けるように
#define DIR_Y( i ) std::get<1>(dir[i])
#define DIR_Z( i ) std::get<2>(dir[i])
for (size_t i = 0; i < iSize; ++i) //隣接するグリッドを先頭から数珠つなぎにチェックして調べる
this->checkLinkList(m_pGrid->top(cellX + DIR_X(i), cellY + DIR_Z(i), cellZ + DIR_Y(i)));
#undef DIR_X
#undef DIR_Y
#undef DIR_Z
}
virtual void Update(GameObject* obj = nullptr) override
{
for (auto& colider : *colliders())
if (auto pCollider = colider.lock())
pCollider->UpdateIsCollision(); // 衝突情報を一旦リセット(m_isPrevCollisionとm_isCollisionなどをリセット)
moveCell(); // 所属するマス目セルを更新する(現在のオーナーがいるマス目位置のグリッドに移動)
};
virtual void Draw(GameObject* obj = nullptr) override
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
if (pOwner->world() != nullptr) // コンポーネントを所有するオーナーのいるワールドのカメラすべてに描く
pOwner->world()->Draw([&]()
{ // 四角Boxで GameObjectが存在するグリッド を描画
GridObject::Draw(obj);
});
};
};
#endif
Collider.hを新規作成
して、Collision3D.hのカプセルや球などの当たり判定を呼んで衝突を判定しましょう。
#ifndef COLLIDER_H_
#define COLLIDER_H_
#include "Component.h"
#include <memory> // 共有ポインタ std::shared_ptr を使用する
#include <unordered_set> // std::unordered_setで衝突している他のコライダのIDを保管してかぶらないようにする
#include "GameObject.h"
#include "Collision3D.h"
class Collider; // 前方宣言
//[衝突情報:参考] https://docs.unity3d.com/jp/2018.4/ScriptReference/Collision.html
class Collision
{ // 衝突はGameObjectに所有されているComponent同士の衝突により生成される 衝突後、発生元のGameObjectはCollisionと関係なく消えてしまうかもしれない
protected:
std::weak_ptr<GameObject> m_pGameObject; // 衝突を発生させたGameObjectへのリンクを保管する(GameObjectはすでに消えているかもしれないから弱リンクに)
public:
Collision(std::weak_ptr<GameObject> pGameObject) : m_pGameObject{ pGameObject } {}
class ContactPoint //[衝突点の情報] https://docs.unity3d.com/jp/2018.4/ScriptReference/ContactPoint.html
{
public:
ContactPoint() = default;
ContactPoint(Vector3 point_, Vector3 normal_, float separation_, std::weak_ptr<Collider> thisCollider_, std::weak_ptr<Collider> otherCollider_)
: point{ point_ }, normal{ normal_ }, separation{ separation_ }, thisCollider{ thisCollider_ }, otherCollider{ otherCollider_ } {}
virtual ~ContactPoint() {}
Vector3 point{ 0.0f,0.0f,0.0f }; // 接触点
Vector3 normal{ 0.0f,0.0f,0.0f }; // 接触点の法線
float separation{ 0.0f }; // 接触点でのコライダー間の距離
std::weak_ptr<Collider> thisCollider{ std::weak_ptr<Collider>() }; //[衝突元]接触したコライダー情報
std::weak_ptr<Collider> otherCollider{ std::weak_ptr<Collider>() }; //[衝突先] 接触している他のコライダー情報
};
std::vector<ContactPoint> contacts; // 全ての接触点の配列
// 基本的にthis側のコライダーのオーナーのGameObjectを返す
std::weak_ptr<GameObject> gameObject() { return m_pGameObject; };
};
class Collider; // 前方宣言
// 関数の前方宣言
inline bool isCollisionColliders(std::shared_ptr<Collider> pThis, std::shared_ptr<Collider> pOther,
std::shared_ptr<Collision>* ppThisResult = nullptr, std::shared_ptr<Collision>* ppOtherResult = nullptr);
class Collider : public Component
{
public:
// コライダの当たり判定のタイプ
enum class Type : unsigned char
{
//Point = 0, // 点
Sphere, // 球
Capsule, // カプセル
//Box, // ボックス
//Plane, // 平面
};
inline Collider(std::shared_ptr<GameObject> pOwner, Type type) : Component(pOwner), m_type{ type } {}
virtual ~Collider() {}
const Type m_type; // コライダのタイプ
inline operator Type () { return m_type; }
protected:
bool m_isPrevCollision = false; // 前回衝突したか?
bool m_isCollision = false; // 今回衝突したか?
public:
bool isTrigger = false; // コライダーがトリガーですり抜けるかどうか?
bool enabled = true; // コライダーが有効か?
unsigned int hitColor = DxLib::GetColor(255, 255, 255); // 描画するときの色
std::vector<Collision::ContactPoint> m_contacts; // 全ての接触点の配列
std::unordered_set<unsigned int> m_prevCollisionIDs; // 前のフレームで衝突した他のコライダのInstanceID
std::unordered_set<unsigned int> m_collisionIDs; // 接触している他のコライダのInstanceID
bool IsPrevCollision() { return m_isPrevCollision; }
void IsPrevCollision(bool isPrevCollision) { m_isPrevCollision = isPrevCollision; return; }
bool IsCollision() { return m_isCollision; }
//void IsCollision(bool isCollision) { m_isCollision = isCollision; return; }
// コライダのワールド座標位置(ownerをたどったワールド座標値)
inline Vector3 worldPos(Vector3 localPosition) const { return owner()->position + localPosition; }
// コライダの含まれるワールド位置のx,y,z最小値を求める(8分木のエリア分別などに使うため)
virtual Vector3 minXYZ() const = 0; // = 0で純粋仮想関数にして必ずoverride継承しないとエラーになるようにする
// コライダの含まれるワールド位置のx,y,z最大値を求める(8分木のエリア分別などに使うため)
virtual Vector3 maxXYZ() const = 0; // = 0で純粋仮想関数にして必ずoverride継承しないとエラーになるようにする
// 継承override してカプセルや球やボックスなどのコライダの判定を実装する デフォルトは isCollisionColliders関数を呼び出す
virtual inline bool isCollision(std::shared_ptr<Collider> pOther,
std::shared_ptr<Collision>* ppThisResult = nullptr, std::shared_ptr<Collision>* ppOtherResult = nullptr)
{
if (m_collisionIDs.count(pOther->InstanceID()) > 0)
return false; // すでに調べたコライダのIDのペアだったらfalse
// 継承override してカプセルや球やボックスなどのコライダの判定を実装する
return isCollisionColliders(shared_from_this_as<Collider>(), pOther, ppThisResult, ppOtherResult);
}
// 毎フレーム isPrev(=m_isCollision)とisCollision(=false)に更新してから当たり判定する
virtual void UpdateIsCollision() // 前フレームで当たってなくて今当たっていればOnCollisionEnterに使える
{
std::vector<Collision::ContactPoint>().swap(m_contacts); // 衝突点情報の配列をクリア
// 今のフレームで衝突しているIDを↓prevに移しておけば次のフレームでのOnCollisionEnterやOnCollisionExitを呼ぶタイミングの判定に使える
m_collisionIDs.swap(m_prevCollisionIDs);
std::unordered_set<unsigned int>().swap(m_collisionIDs); // 衝突していたID一覧は、一旦空()と入れ替えて今フレームの衝突検査に備える
m_isPrevCollision = m_isCollision; // 前のフレームで衝突したかを保管
m_isCollision = false; // 一旦、今のフレームでは衝突していない状態として設定して後で当たり判定して書換
}
};
class SphereCollider : public Collider
{
public:
SphereCollider(std::shared_ptr<GameObject> pOwner, Vector3 center, float radius, unsigned int hitColor_ = GetColor(255, 0, 0))
: Collider(pOwner, Type::Sphere), m_center{ center }, m_radius{ radius }
{
this->hitColor = hitColor_;
}
virtual ~SphereCollider() {}
protected:
Vector3 m_center{ 0.0f,0.0f,0.0f }; // コライダのローカル座標での中心位置
public:
// 球体の半径
float m_radius{ 0.0f };
// 球体のローカル座標での中心座標
inline Vector3 center() const { return m_center; }
// 球体のワールド座標での中心位置(ownerをたどったワールド座標値)
inline Vector3 worldCenter() const
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
return pOwner->position + center();
}
// 球体の含まれるワールド位置のx,y,z最小値を求める(8分木のエリア分別などに使うため)
inline Vector3 minXYZ() const override
{
Vector3 center = worldCenter();
return Vector3{ (m_radius < 0) ? center.x + m_radius : center.x - m_radius,
(m_radius < 0) ? center.y + m_radius : center.y - m_radius,
(m_radius < 0) ? center.z + m_radius : center.z - m_radius };
}
// 球体の含まれるワールド位置のx,y,z最大値を求める(8分木のエリア分別などに使うため)
inline Vector3 maxXYZ() const override
{
Vector3 center = worldCenter();
return Vector3{ (m_radius < 0) ? center.x - m_radius : center.x + m_radius,
(m_radius < 0) ? center.y - m_radius : center.y + m_radius,
(m_radius < 0) ? center.z - m_radius : center.z + m_radius };
}
// デバッグ用に球体コライダを描く
virtual void Draw(GameObject* obj = nullptr) override
{
#ifdef _DEBUG
if (owner() == nullptr) return; // このコンポーネントを所有しているオーナーがすでにメモリから解放されていたら return
Vector3 center = worldCenter();
DxLib::DrawSphere3D(center, m_radius, 2, (m_isCollision) ? hitColor : GetColor(255, 255, 255), GetColor(255, 255, 255), FALSE);
if (m_isCollision)
{
for (auto& contact : m_contacts) // 衝突点と法線を描く
{
DxLib::DrawSphere3D(contact.point, 1.0f, 3, hitColor, GetColor(255, 255, 255), TRUE);
DxLib::DrawLine3D(contact.point, contact.point + contact.normal, GetColor(255, 255, 0));
}
}
#endif
}
};
class CapsuleCollider : public Collider
{
public:
CapsuleCollider(std::shared_ptr<GameObject> pOwner, Vector3 center, float radius, float height, Axis3D direction = Axis3D::Y, unsigned int hitColor_ = GetColor(255, 0, 0))
: Collider(pOwner, Type::Capsule), m_center{ center }, m_radius{ radius }, m_height{ height }, m_direction{ direction }
{
this->hitColor = hitColor_;
}
virtual ~CapsuleCollider() {}
protected:
Vector3 m_center{ 0.0f,0.0f,0.0f }; // コライダのローカル座標での中心位置
float m_height{ 1.0f }; // コライダの高さ
Axis3D m_direction{ Axis3D::Y };
public:
// カプセルの半径
float m_radius{ 0.0f };
// カプセルのローカル座標での中心座標
inline Vector3 center() const { return m_center; }
// ワールド座標での中心位置(ownerをたどったワールド座標値)
inline Vector3 worldCenter() const
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
return pOwner->position + center();
}
// カプセルの高さ
inline Vector3 height() const { return m_height; }
// カプセルの向く方向軸(height高さの方向軸)
inline Axis3D direction() const { return m_direction; }
// カプセルの始点(pOwnerから見たローカル座標値)
inline Vector3 start() const
{
return m_center - Vector3{ (m_direction == Axis3D::X) ? m_height / 2.0f : 0.0f,
(m_direction == Axis3D::Y) ? m_height / 2.0f : 0.0f,
(m_direction == Axis3D::Z) ? m_height / 2.0f : 0.0f };
}
// カプセルの始点(ownerをたどったワールド座標値)
inline Vector3 worldStart() const
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
return pOwner->position + start();
}
// カプセルの終点(ownerをたどったワールド座標値)
inline Vector3 worldEnd() const
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
return pOwner->position + end();
}
// カプセルの終点(pOwnerから見たローカル座標値)
inline Vector3 end() const
{
return m_center + Vector3{ (m_direction == Axis3D::X) ? m_height / 2.0f : 0.0f,
(m_direction == Axis3D::Y) ? m_height / 2.0f : 0.0f,
(m_direction == Axis3D::Z) ? m_height / 2.0f : 0.0f };
}
// カプセルの含まれるワールド位置のx,y,z最小値を求める(8分木のエリア分別などに使うため)
inline Vector3 minXYZ() const override
{
Vector3 start = worldStart();
Vector3 end = worldEnd();
return Vector3{ (start.x < end.x) ? start.x - m_radius : end.x - m_radius,
(start.y < end.y) ? start.y - m_radius : end.y - m_radius,
(start.z < end.z) ? start.z - m_radius : end.z - m_radius };
}
// カプセルの含まれるワールド位置のx,y,z最大値を求める(8分木のエリア分別などに使うため)
inline Vector3 maxXYZ() const override
{
Vector3 start = worldStart();
Vector3 end = worldEnd();
return Vector3{ (start.x > end.x) ? start.x + m_radius : end.x + m_radius,
(start.y > end.y) ? start.y + m_radius : end.y + m_radius,
(start.z > end.z) ? start.z + m_radius : end.z + m_radius };
}
// デバッグ用にカプセルコライダを描く
virtual void Draw(GameObject* obj = nullptr) override
{
#ifdef _DEBUG
if (owner() == nullptr) return; // このコンポーネントを所有しているオーナーがすでにメモリから解放されていたら return
Vector3 start = worldStart();
Vector3 end = worldEnd();
DxLib::DrawCapsule3D(start, end, m_radius, 4, (m_isCollision) ? hitColor : GetColor(255, 255, 255), GetColor(255, 255, 255), FALSE);
if (m_isCollision)
{
for (auto& contact : m_contacts) // 衝突点と法線を描く
{
DxLib::DrawSphere3D(contact.point, 0.1f, 3, hitColor, GetColor(255, 255, 255), TRUE);
DxLib::DrawLine3D(contact.point, contact.point + contact.normal, GetColor(255, 255, 0));
}
}
#endif
}
};
// カプセル同士が衝突しているかを判定する(ppThisResultとppOtherResult:衝突したら衝突情報が返される)
inline bool isCollisionCapsule_Capsule(std::shared_ptr<Collider> pThis0, std::shared_ptr<Collider> pOther1,
std::shared_ptr<Collision>* ppThisResult = nullptr, std::shared_ptr<Collision>* ppOtherResult = nullptr)
{
auto gameObject0 = pThis0->owner(), gameObject1 = pOther1->owner();
if (gameObject0 == nullptr || gameObject1 == nullptr) return false;
if (gameObject0->isStatic || gameObject1->isStatic) return false; // どちらも静的オブジェクト(isStatic)の場合は当たり判定しない
float radius0 = 0, radius1 = 0;
Vector3 start0, end0, start1, end1, result0, result1, center0, center1;
// col0はthis
if (*pThis0 == Collider::Type::Capsule)
{
auto col0 = std::static_pointer_cast<CapsuleCollider>(pThis0);
start0 = col0->worldStart(), end0 = col0->worldEnd(), center0 = col0->worldCenter();
radius0 = col0->m_radius;
}
else if (*pThis0 == Collider::Type::Sphere)
{ // 球体のときはstartとendが両方、球の中心にすればカプセルの当たり判定を使いまわせる
auto col0 = std::static_pointer_cast<SphereCollider>(pThis0);
start0 = col0->worldCenter(), end0 = col0->worldCenter(), center0 = col0->worldCenter();
radius0 = col0->m_radius;
}
// col1はother
if (*pOther1 == Collider::Type::Capsule)
{
auto col1 = std::static_pointer_cast<CapsuleCollider>(pOther1);
start1 = col1->worldStart(), end1 = col1->worldEnd(), center1 = col1->worldCenter();
radius1 = col1->m_radius;
}
else if (*pOther1 == Collider::Type::Sphere)
{ // 球体のときはstartとendが両方、球の中心にすればカプセルの当たり判定を使いまわせる
auto col1 = std::static_pointer_cast<SphereCollider>(pOther1);
start1 = col1->worldCenter(), end1 = col1->worldCenter(), center1 = col1->worldCenter();
radius1 = col1->m_radius;
}
// Collision3D.hの当たり判定関数を使ってチェック
bool isCollision = isCollisionCapsule_Capsule(start0, end0, radius0, start1, end1, radius1, &result0, &result1);
if (!isCollision) return false;
// collisionの 法線ベクトル:norm と 距離:mag を求める
Vector3 v01 = result1 - result0; // 最短距離の2点 result0 から result1 へ向かうベクトル
float mag01 = v01.magnitude(); // result0とresult1の距離
Vector3 norm01 = v01.normalized(); // 正規化normalizeする
if (mag01 == 0.0f) // カプセルの軸が交差しているときはresult0とresult1が同じ点になっちゃう
{
norm01 = (center1 - center0).normalized(); // 中心同士の差ベクトルを法線に
if (center0 == center1) // 中心位置が完全一致するときは
norm01 = Vector3{ 1.0f,0.0f,0.0f }; // 仕方ないからx軸方向に引き離す(2DでもつじつまあうようにX軸で)
}
pThis0->m_collisionIDs.emplace(pOther1->InstanceID()); // 接触している other のIDを this側 に登録して同じペアの判定を2度しないようにする
pOther1->m_collisionIDs.emplace(pThis0->InstanceID()); // 接触している this のIDを other側 に登録して同じペアの判定を2度しないようにする
// 衝突点 pThisContactとpOtherContact を生成
Collision::ContactPoint thisContact{ result0, -norm01, mag01, pThis0, pOther1 };
Collision::ContactPoint otherContact{ result1, norm01, mag01, pOther1, pThis0 };
pThis0->m_contacts.emplace_back(thisContact); // コライダ0のm_contactsにも情報登録
pOther1->m_contacts.emplace_back(otherContact); // コライダ1のm_contactsにも情報登録
if (ppThisResult != nullptr)
{
*ppThisResult = std::make_shared<Collision>(gameObject0); // this側の衝突点をpResultでreturnする
(*ppThisResult)->contacts.emplace_back(thisContact); //[0] this側の衝突点の情報をcontacts配列に格納
(*ppThisResult)->contacts.emplace_back(otherContact); //[1] other側の衝突点の情報をcontacts配列に格納
}
if (ppOtherResult != nullptr)
{
*ppOtherResult = std::make_shared<Collision>(gameObject1); // other側の衝突点をpResultでreturnする
(*ppOtherResult)->contacts.emplace_back(otherContact); //[0] other側の衝突点の情報をcontacts配列に格納
(*ppOtherResult)->contacts.emplace_back(thisContact); //[1] this側の衝突点の情報をcontacts配列に格納
}
return true;
}
// コライダー同士の当たり判定 当たったら衝突情報が ppThisResult,ppOtherResult がnullptrじゃなければ代入されて返却
inline bool isCollisionColliders(std::shared_ptr<Collider> pThis, std::shared_ptr<Collider> pOther,
std::shared_ptr<Collision>* ppThisResult, std::shared_ptr<Collision>* ppOtherResult)
{
// 現状はカプセルと球だけの当たり判定しか実装していない
if ((*pThis == Collider::Type::Sphere || *pThis == Collider::Type::Capsule)
&& (*pOther == Collider::Type::Sphere || *pOther == Collider::Type::Capsule))
{ // カプセルの当たり判定で球の当たり判定も兼用する
return isCollisionCapsule_Capsule(pThis, pOther, ppThisResult, ppOtherResult);
}
// ここにカプセルと球以外の当たり判定も追加すればボックスなどとの当たり判定もできるようになる
return false;
}
// 衝突したカプセル同士が当たらないようにコライダがついているGameObjectを押し戻してずらして当たらない位置に移動
inline bool resolveCollisionCapsule_Capsule(const Collision::ContactPoint& thisContact, const Collision::ContactPoint& otherContact)
{
//[共有ポインタのキャスト] https://cpprefjp.github.io/reference/memory/shared_ptr/dynamic_pointer_cast.html
auto thisCollider = std::dynamic_pointer_cast<CapsuleCollider>(thisContact.thisCollider.lock());
auto otherCollider = std::dynamic_pointer_cast<CapsuleCollider>(otherContact.thisCollider.lock());
auto thisGameObject = thisCollider->owner();
auto otherGameObject = otherCollider->owner();
if (!thisGameObject || !otherGameObject || !thisCollider || !otherCollider)
return false; // どれかがnullptrのときはfalse
if (thisGameObject->isStatic && otherGameObject->isStatic)
return false; // どちらも静的オブジェクト(isStatic)の場合は当たり判定を押し戻ししない
Vector3 p0 = thisContact.point;
Vector3 p1 = otherContact.point;
if (p0 == p1) // カプセル0とカプセル1の軸が交差しているとき
{ // カプセルの中心位置を使って引き離す
p0 = thisCollider->worldPos(thisCollider->center());
p1 = otherCollider->worldPos(otherCollider->center());
}
// 押し戻しの量を計算
float overlap = (thisCollider->m_radius + otherCollider->m_radius) - (p1 - p0).magnitude() + 0.00001f;
// 押し戻し方向に移動
Vector3 correction = thisContact.normal * (overlap / 2.0f); // 2で割って両方±動いて当たらないようにする
if (otherGameObject->isStatic) // 片方がisStaticなら↓2倍動かしてもう片方はそのまま
thisGameObject->position += correction * 2.0f;
else if (thisGameObject->isStatic)
otherGameObject->position += -correction * 2.0f;
else
{ // 両方isStaticじゃないときは * 2.0fせずに両方移動させる
thisGameObject->position += correction;
otherGameObject->position += -correction;
}
return true;
}
#endif
GridCollidersComponent.hを新規作成
して、m_colliders変数に複数のコライダをLinkCollider関数でリンクして、OnCheck関数をoverrideしてコライダ同士の当たり判定をcppで実装できるようにしましょう。
#ifndef GRID_COLLIDERS_COMPONENT_H_
#define GRID_COLLIDERS_COMPONENT_H_
#include "GridComponent.h"
#include <list>
#include <memory> // 共有ポインタ std::shared_ptr を使用する
class Collider; // 前方宣言
class GridCollidersComponent : public GridComponent
{
friend Component;
std::list<std::weak_ptr<Collider>> m_colliders; // GridComponentを所有するGameObjectが持つコライダーのリスト
public:
GridCollidersComponent(std::shared_ptr<GameObject> pOwner, const std::string& typeTag, CellXYZ::DirType dirType = CellXYZ::DirType::Direction8)
: GridComponent(pOwner, typeTag, dirType)
{
}
// コライダのリスト(弱共有ポインタ)へのポインタを返す
virtual std::list<std::weak_ptr<Collider>>* colliders() override { return &m_colliders; }
// コライダの弱共有ポインタを グリッドで当たり判定する対象として リンクを リスト m_colliders に追加する
virtual void LinkCollider(std::weak_ptr<Collider> pColliderLink) { m_colliders.emplace_back(pColliderLink); }
// OnCheckをoverrideすることで[弾×敵]や[プレイヤ×敵]などの当たり判定処理をCollision3D.hの関数でする
virtual void OnCheck(GridComponent* other) override;
};
#endif
GridCollidersComponent.cppを新規作成
して、Collider.hのisCollision関数経由でCollision3D.hの各種当たり判定を衝突結果をGameObjectのOnCollision関数に渡しましょう。
#include "GridCollidersComponent.h"
#include "Collider.h"
void GridCollidersComponent::OnCheck(GridComponent* other)
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
// コンポーネントのオーナーが死んでたらスキップする
//if (pOwner->isDead) return;
//if (other->owner()->isDead) return;
for (auto& pCollider : this->m_colliders) // this側のコライダすべてをループ
{
auto collider = pCollider.lock(); // 弱共有ポインタをロックして共有ポインタに変換
if (collider == nullptr) continue; // 共有ポインタが削除されすでに解放済の場合
for (auto& pOtherCollider : *(other->colliders()))
{
auto otherCollider = pOtherCollider.lock(); // 弱共有ポインタをロックして共有ポインタに変換
if (otherCollider == nullptr) continue; // 共有ポインタが削除されすでに解放済の場合
std::shared_ptr<Collision> pThisResult{ nullptr }; // 衝突したら情報が入ってきてnullptrじゃなくなる
std::shared_ptr<Collision> pOtherResult{ nullptr }; // 衝突したら情報が入ってきてnullptrじゃなくなる
if (collider->isCollision(otherCollider, &pThisResult, &pOtherResult))
{
if (collider->m_prevCollisionIDs.count(otherCollider->InstanceID()) == 0)
{ //[OnEnter]前のフレームでcolliderがotherと衝突してなくて、今フレームで衝突開始したらOnEnterを呼ぶ
collider->owner()->OnCollisionEnter(pThisResult); // collider側の衝突開始したときの関数をコールバックで呼び返す
otherCollider->owner()->OnCollisionEnter(pOtherResult); // other側の衝突開始したときの関数をコールバックで呼び返す
}
//[OnCollision] 衝突していたらコールバックを呼び続ける
collider->owner()->OnCollision(pThisResult); // collider側の衝突したときの関数をコールバックで呼び返す
otherCollider->owner()->OnCollision(pOtherResult); // other側の衝突したときの関数をコールバックで呼び返す
}
else // 衝突していなかったら
{
//[OnExit]前のフレームでcolliderがotherと衝突していて今フレームで衝突しなくなったらOnExitを呼ぶ
if (collider->m_prevCollisionIDs.count(otherCollider->InstanceID()) > 0)
collider->owner()->OnCollisionExit(pThisResult); // collider側の衝突終了したときの関数をコールバックで呼び返す
}
}
}
}
GameObject.hを変更
して、OnCollisionなどの衝突判定のコールバック関数を準備しましょう。
#ifndef GAMEOBJECT_H_
#define GAMEOBJECT_H_
#include <memory> // 共有ポインタの定義のため
#include "Vector3.h"
#include "Quaternion.h"
#include "Object.h" // インスタンスIDや検索タグを持ち、shared_from_this_as<継承先クラス名>などができる
class World; // 前方宣言
class Component; // 前方宣言
class Collision; // 前方宣言
// ゲーム上に表示される物体の基底クラス。
// プレイヤーや敵、アイテムなどはこのクラスを継承して作る。
class GameObject : public Object
{
protected:
// コンストラクタはprotected:にしてあるのでCreate関数経由で初期生成してください
GameObject(World* world = nullptr, Vector3 position = { 0,0,0 }, Quaternion rotation = { 0,0,0,1 }, Vector3 velocity = { 0,0,0 }, Vector3 force = { 0,0,0 })
: Object(), m_world{ world }, position{ position }, rotation{ rotation }, velocity{ velocity }, force{ force }
{
}
public:
// 仮想デストラクタ
virtual ~GameObject() {}
union { // ★共用体unionテクニック https://inemaru.hatenablog.com/entry/2016/03/02/005408
//★下記3つはメモリ上で共用状態になる(position、x,y,z、xyzどの名前から数値を変えたり読出してもメモリ上は同じ)
// 同じデータに3種類の名前を付けたイメージ、しかもVector3の機能や配列としてのアクセスの仕方もできて便利
struct { float x, y, z; }; // XYZ座標 [匿名共用体とは] https://code.i-harness.com/ja-jp/q/4d437c
Vector3 position; // XYZ座標
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で回せる
Quaternion rotation; // 回転クオータニオン
union {
struct { float vx, vy, vz; }; // XYZ方向の速度
Vector3 velocity; // XYZ方向の速度
std::array<float, 3> vxyz;
};
union {
struct { float vxForce, vyForce, vzForce; }; // XYZ方向にかかる力(Unityでいうと AddForce関数 )
Vector3 force; // XYZ方向の力(物理的には加速度:1フレームごとにvelocityの増える=加速する量)
std::array<float, 3> vxyzForce;
};
// 重力を返す、重力を変えたいときはoverrideして処理を変えてください、重力を変えてない場合は 0.0f
virtual inline float gravity() { return 0.0f; }
protected:
World* m_world; // GameObjectが配置されたワールドへのリンク
public:
// 現在いるワールドへのポインタを得る
virtual World* world() { return m_world; }
// 現在いるワールドのポインタを変更する
virtual void world(World* changeWorld) { m_world = changeWorld; }
bool isDead{ false }; // 死んだ(削除対象)フラグ
bool isStatic{ false }; // 動かない(静的オブジェクトか?)
bool isGround{ false }; // 地面についているか
bool isJumping{ false }; // ジャンプ中か
bool isFlying{ false }; // 空をとんでいるか
float pushPower{ 5.0f }; // コライダで接触したときに押し返す力(相手側が強いと押される)
float mass{ 1.0f }; // 質量(重さ)コライダで接触したときに重さでforceを割り算すると動きにくくなる
hash32 typeTag{ "" }; // 小カテゴリ Zako0など個別のタイプ
hash32 baseTag{ "" }; // 大カテゴリ Enemyなど大まかなベースジャンル
protected:
// コンポーネントの<ハッシュID, 共有所有ポインタ> の辞書でコンポーネントの共有カウンタ+1を保持する
std::unordered_map<size_t, std::shared_ptr<Component>> components;
public:
// コンポーネントを得る 例えば GetComponent<BoxCollider>でBoxColliderという名前のコンポーネントをcomponents辞書から得る
template<class ComponentT>
std::vector<std::weak_ptr<ComponentT>> GetComponents()
{ //★[ComponentTのハッシュコードを得るテク] https://cpprefjp.github.io/reference/typeindex/type_index/hash_code.html
size_t hashID = typeid(ComponentT).hash_code(); //[まずハッシュでID化]
// equal_range 関数を使って、指定したキーの最初と終端の位置を示すイテレータを std::pair<> で取得します。
auto range = components.equal_range(hashID); //https://ez-net.jp/article/9E/DFLX8hNG/G5J8DPmsltz4/
std::vector<std::weak_ptr<ComponentT>> returnList; // "ComponentT" 例."CircleCollider"など をキーとするコンポーネント一覧
for (auto ite = range.first; ite != range.second; ++ite)
returnList.emplace_back(ite->second); // 辞書で見つかったコンポーネントを返すリストに追加
return returnList;
}
// コンポーネントを追加する .AddComponent<BoxCollider>(...)で追加できる
template<class ComponentT, typename ...ArgsT>
std::weak_ptr<ComponentT> AddComponent(ArgsT&&...args)
{ //★[ComponentTのハッシュコードを得るテク] https://cpprefjp.github.io/reference/typeindex/type_index/hash_code.html
size_t hashID = typeid(ComponentT).hash_code(); //[まずハッシュでID化]
// shared_from_this()を必ず渡してセットで初期化して、オーナーがリンク切れのコンポーネントができないように気をつける
std::shared_ptr<ComponentT> newComponent = std::make_shared<ComponentT>(shared_from_this_as<GameObject>(), std::forward<ArgsT>(args)...);
// https://ez-net.jp/article/9E/DFLX8hNG/G5J8DPmsltz4/
// std::unrodered_multimap<> と同型の値を取る std::pair<> を作って insert 関数でcomponents辞書に追加
components.insert(std::pair<size_t, std::shared_ptr<ComponentT>>(hashID, newComponent));
return newComponent;
}
// コンポーネントを削除する
template<typename ComponentT>
bool RemoveComponent(std::shared_ptr<ComponentT> removeComponent)
{ //★[ComponentTのハッシュコードを得るテク] https://cpprefjp.github.io/reference/typeindex/type_index/hash_code.html
size_t hashID = typeid(ComponentT).hash_code(); //[まずハッシュでID化]
// equal_range 関数を使って、指定したキーの最初と終端の位置を示すイテレータを std::pair<> で取得します。
auto range = components.equal_range(hashID); //https://ez-net.jp/article/9E/DFLX8hNG/G5J8DPmsltz4/
//[イテレート中に探しながら消す] https://cpprefjp.github.io/reference/unordered_map/unordered_multimap/erase.html
for (auto ite = range.first; ite != range.second;)
if (ite->second == removeComponent)
{
components.erase(ite); // 削除された要素の次を指すイテレータが返される
return true; // 見つかって消せた場合は true
}
else
++ite; // 次の要素へイテレータを進める
return false; // 見つからずに消せなかった場合は false
}
// 更新処理[純粋仮想関数 = 0 なのでoverride必須の関数]
virtual void Update() = 0; // 純粋仮想関数 = 0 にすると【絶対 Update()関数はoverrideしなきゃいけない義務を付与できる 】
// 描画処理[純粋仮想関数 = 0 なのでoverride必須の関数]
virtual void Draw() = 0; // 純粋仮想関数 = 0 にすると【絶対 Draw()関数はoverrideしなきゃいけない義務を継承先クラスに付与 】
// 衝突開始したときの関数 overrideして衝突したときの処理などをオブジェクトの種類ごとに実装する
virtual void OnCollisionEnter(std::shared_ptr<Collision> pCollision)
{
// overrideして衝突開始したときの処理などをオブジェクトの種類ごとに実装する
};
// 衝突したときの関数 overrideして衝突したときの処理などをオブジェクトの種類ごとに実装する
virtual void OnCollision(std::shared_ptr<Collision> pCollision)
{
// overrideして衝突したときの処理などをオブジェクトの種類ごとに実装する
};
// 衝突終了したときの関数 overrideして衝突したときの処理などをオブジェクトの種類ごとに実装する
virtual void OnCollisionExit(std::shared_ptr<Collision> pCollision)
{
// overrideして衝突終了したときの処理などをオブジェクトの種類ごとに実装する
};
// ジャンプ開始した直後に呼ばれる関数
virtual void OnStartJump()
{
// overrideしてジャンプした瞬間に音を鳴らしたりする処理などをオブジェクトの種類ごとに実装する
};
// 空中でジャンプの上昇中に呼ばれる関数
virtual void OnJumping()
{
// overrideして空中でジャンプの上昇中にエフェクトを出したりする処理などをオブジェクトの種類ごとに実装する
};
// 空中で落ちている最中に呼ばれる関数
virtual void OnFalling()
{
// overrideして空中で落ちている最中に音を鳴らしたり、エフェクトを出したりする処理などをオブジェクトの種類ごとに実装する
};
};
#endif
World.hを変更
して、makeGridで世界にグリッド分割情報を持たせられるようにしましょう(デフォルト設定で128×128×128の大きさのグリッドにする)。
#ifndef WORLD_H_
#define WORLD_H_
#include <list>
#include <memory>
#include <unordered_map>
#include <functional> // std::functionのラムダ式 で 関数を Draw関数の引数にして、カメラすべてに対して描画処理を発動させる
#include "MyHash.h" // ワールドのタグをhash32型で管理する
// [前方宣言] 宣言だけして、#includeはしてないので、ポインタだけはこのファイル内でつかえる
// #includeはしてないので、hpなどの変数には#include "~.h"しないとアクセスできないので循環インクルード防止のため.cppでインクルードしてください
class GameScene;
class Player;
class Bullet;
class Enemy;
class Camera;
class Grid;
// プレイヤや敵や弾などの共有ポインタを管理し、EraseRemoveIfで消して参照カウンタが0になったらそれらを消滅させるクラス(C#のガベージコレクタの代わり)
class World
{
public:
inline World(GameScene* pScene, const std::string& worldTag = "") : m_pScene{ pScene }, tag{ worldTag } {}
inline ~World() {}
protected: // public:にすると他で外部で後からタグやシーンを書き換えられると辞書での管理などの前提がおかしくなるのでprotected:する
hash32 tag{ "" }; // ワールド名のタグ
GameScene* m_pScene{ nullptr }; // ワールドを所有するシーン
public:
hash32 Tag() const { return tag; }
GameScene* scene() { return m_pScene; }
//std::unordered_map<hash32, std::shared_ptr<Player>> players; // プレイヤのタグ辞書
// ワールドを所有するシーンにプレイヤを追加する
void AddPlayer(const std::string& playerTag, std::shared_ptr<Player> pPlayer);
// ワールドにいるプレイヤを得る
std::vector<std::weak_ptr<Player>> GetPlayers();
protected:
std::shared_ptr<Grid> m_pGrid{ nullptr }; // 当たり判定を高速にするためグリッドで空間分割
public:
Grid* grid() { return m_pGrid.get(); } // グリッドを返す
Grid* makeGrid(int cellWidth = 128, int cellHeight = 128, int cellDepth = 128); // グリッドを生成する
std::unordered_map<hash32, std::shared_ptr<Player>> players; // プレイヤのタグ辞書
std::list<std::shared_ptr<Bullet>> bullets; // 弾のリスト
std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
// 削除処理を共通テンプレート関数にする
// [共通テンプレート関数]https://programming-place.net/ppp/contents/cpp/language/009.html#function_template
template <typename TypeT, class T_if>
void EraseRemoveIf(std::list<TypeT>& 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()の位置
};
std::unordered_map<hash32, std::shared_ptr<Camera>> cameras; // このワールドに存在するカメラの辞書<カメラにつけたタグ, カメラの共有ポインタ>
// 指定されたタグのカメラへの共有ポインタを得る カメラ辞書に存在しないタグでアクセスしたときにunordered_mapの特性で勝手に意図しないカメラができるのを防ぐ関数
std::shared_ptr<Camera> camera(const std::string& cameraTag) { auto itr = cameras.find(cameraTag); return (itr != end(cameras)) ? itr->second : nullptr; }
// このワールドに存在するカメラすべてに対して drawFuncで描きたい処理を渡して 全カメラのスクリーンに対して描画処理を走らせる
void Draw(std::function<void()> drawFunc) noexcept;
// カメラにタグをつけてワールドに追加 カメラにもワールドへのポインタリンクを張る
inline bool AddCamera(const std::string& cameraTag, std::shared_ptr<Camera> pCamera);
// 指定したタグのカメラをこのワールドから削除する カメラ側の worldへのリンクのポインタも nullptr にする
inline bool EraseCamera(const std::string& cameraTag);
// カメラを nowWorld から nextWorld に移動させる
static bool MoveCamera(const std::string& cameraTag, World* nowWorld, World* nextWorld);
};
#endif
World.cppを変更
して、ワールドごとにmakeGrid関数でGridをstd::make_sharedで生成する処理を記述しましょう。
#include "World.h"
#include "Player.h"
#include "Camera.h"
#include "Screen.h"
#include "GameScene.h"
#include "GridObject.h" // Gridをmake_sharedで生成するため
Grid* World::makeGrid(int cellWidth, int cellHeight, int cellDepth)
{
if (this->m_pGrid == nullptr) // グリッドが未生成の場合は生成
this->m_pGrid = std::make_shared<Grid>(cellWidth, cellHeight, cellDepth);
return this->m_pGrid.get();
}
void World::AddPlayer(const std::string& playerTag, std::shared_ptr<Player> pPlayer)
{
if (m_pScene != nullptr)
{
m_pScene->players.emplace(playerTag, pPlayer); // シーンにプレイヤを追加する
}
}
// このワールドにいるプレイヤを得る
std::vector<std::weak_ptr<Player>> World::GetPlayers()
{
std::vector<std::weak_ptr<Player>> players;
for (auto&& pPlayer : players)
{
players.emplace_back(pPlayer);
}
return players;
}
// カメラにタグをつけてワールドに追加 カメラにもワールドへの生ポインタリンクを渡す
bool World::AddCamera(const std::string& cameraTag, std::shared_ptr<Camera> pCamera)
{
if (pCamera == nullptr) return false;
if (cameras.count(cameraTag) > 0) return false; // すでに同じタグのカメラがワールドにあったら false
if (pCamera->world() != nullptr) pCamera->world()->EraseCamera(cameraTag); // すでにカメラが別ワールドに配置されていたら別ワールドから削除
cameras.emplace(cameraTag, pCamera); // ワールドにカメラを追加する
pCamera->world(this); // カメラにもワールドへのポインタリンクを張る(カメラ側に渡すのは共有ポインタではない生ポインタだから共有デッドロックにはならない)
return true;
}
// 指定したタグのカメラをこのワールドから削除する カメラ側の worldへのリンクのポインタも nullptr にする
bool World::EraseCamera(const std::string& cameraTag)
{
auto itr = cameras.find(cameraTag); // 指定したタグのカメラを探す
if (itr == end(cameras)) return false; // 指定したタグのカメラがワールドにない
auto pCamera = itr->second;
pCamera->world(nullptr); // カメラ側の worldへのリンクのポインタも nullptrに
cameras.erase(cameraTag); // カメラを辞書から削除
return true;
}
// カメラを nowWorld から nextWorld に移動させる
bool World::MoveCamera(const std::string& cameraTag, World* nowWorld, World* nextWorld)
{
if (nowWorld == nullptr) return false;
auto itr = nowWorld->cameras.find(cameraTag); // タグでカメラを探す
if (itr == end(nowWorld->cameras)) return false; // タグに関連づいたカメラがなかった
auto targetCamera = itr->second; // カメラの共有ポインタをキープ(次の行でeraseしてもカメラの共有カウンタは0にはならずこのリンクのおかげでカメラはメモリに残存)
targetCamera->world(nullptr); // カメラ側の worldへのリンクのポインタを一旦 nullptrに
nowWorld->cameras.erase(cameraTag); // nowWorld からは カメラを消す
if (nextWorld == nullptr) return false;
return nextWorld->AddCamera(cameraTag, targetCamera); // 次のワールドにカメラを追加する
}
// このワールドに存在するカメラすべてに対して funcで描きたい処理を渡して 全カメラのスクリーンに対して描画処理を走らせる
void World::Draw(std::function<void()> drawFunc) noexcept
{
for (auto&& keyValue : cameras) // このワールドにあるすべてのカメラぶんループ
{
VECTOR_D beforeCamPos; // 設定変更前のカメラのパラメータを一旦、保管
double beforeVRot, beforeHRot, beforeTRot, beforeNear, beforeFar, beforeFov;
Camera::GetCameraSettings(&beforeCamPos, &beforeVRot, &beforeHRot, &beforeTRot, &beforeFov, &beforeNear, &beforeFar);
auto& camera = keyValue.second;
//camera->screenID
DxLib::SetCameraPositionAndAngle(camera->pos, camera->rot.V, camera->rot.H, camera->rot.T); // 各カメラの位置、回転を設定
Screen::Start(camera->screenHandle, true, camera->fovAngle); // カメラに関連付けられたスクリーンIDを描画先にする
{
drawFunc(); // 渡された描画処理を実行
}
Screen::End(true); // 描画先を元に戻す
// カメラの設定をもとに戻しておく
Camera::SetCameraSettings(&beforeCamPos, &beforeVRot, &beforeHRot, &beforeTRot, &beforeFov, &beforeNear, &beforeFar);
}
}
Player.hを変更
して、球体のコライダとグリッドのコンポーネントと押し返す力の設定を追加しましょう。
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Resource.h"
#include "GameObject.h"
#include "PhysicsComponent.h"
#include "GridCollidersComponent.h"
#include "Collider.h"
class World; // 前方宣言
class Camera; // 前方宣言
class Player : public GameObject
{
protected:
// コンストラクタはprotected:にしてあるので、Create関数経由で初期生成してください
Player(World* world, Pad pad, Vector3 position, Quaternion rotation = { 0,0,0,1 }, Vector3 velocity = { 0,0,0 }, Vector3 force = { 0,0,0 })
: pad{ pad }, GameObject(world, position, rotation, velocity, force)
{
this->typeTag = "Player"; // タグは個別の"プレイヤ1"などに使い、タイプはtypeTagで判別する形にしてタイプのジャンルを区別
this->baseTag = "Player";
this->pushPower = 5.0f; // コライダで衝突したときに押し返す力
}
public:
// 仮想デストラクタ
virtual ~Player() {}
Pad pad; // 操作するコントローラー番号
hash32 worldTag{ "" }; // 現在いるワールドのタグ
float MoveSpeedMax = 6; // 移動速度Max値
float MoveSpeed = 0;//移動速度
float moveAngle = 0; // X軸→方向から何度か
float deltaAngle = 0; // プレイヤの動く角度の変化率、ハンドルを切ったらだんだんもとに戻る
int animCount = 0;
std::shared_ptr<Texture> image; // プレイヤの板ポリゴンで描くタイル分割画像への共有リンク
Camera* camera{ nullptr }; // プレイヤを追随するカメラ
void SetCamera(Camera* pCamera) { camera = pCamera; }
// overrideして物理落下コンポーネントからの重力値を返す
virtual float gravity() override { auto pGravityPhysics = gravityPhysics.lock(); return (pGravityPhysics != nullptr) ? pGravityPhysics->gravity() : 0.0f; }
protected:
std::weak_ptr<JumpComponent> jumpPhysics; // ジャンプの物理処理コンポーネント
std::weak_ptr<GravityComponent> gravityPhysics; // 落下の重力の物理処理コンポーネント
std::weak_ptr<GridCollidersComponent> gridCollision; // グリッドマス目分割コンポーネント
std::weak_ptr<SphereCollider> sphereCollider; // 球コライダのコンポーネント
public:
virtual void Init(std::shared_ptr<InitParams> initParams = nullptr) override
{
std::vector<float> vyJumpSpeed{ 13, 14, 15, 4 }; // ジャンプ開始の初期速度
std::vector<float> vyForceJump{ 0.5f, 0.4f, 0.3f, 0.1f }; // ジャンプ上昇にかかる力の初期値
std::vector<float> vyGravity{ 0.8f,0.8f,0.8f,0.8f }; // 重力の設定値
std::vector<float> vyDownSpeedMax{ 16, 16, 16, 8 }; // 降下スピードのリミット
// AddComponent内部でshared_from_this()を必ず渡してセットで初期化して、オーナーがリンク切れのコンポーネントができないように気をつける
jumpPhysics = AddComponent<JumpComponent>(vyJumpSpeed, vyForceJump);
gravityPhysics = AddComponent<GravityComponent>(vyGravity, vyDownSpeedMax);
// 半径16(直径32)の球体コライダ
sphereCollider = AddComponent<SphereCollider>(Vector3(0, 0, 0), 16);
gridCollision = AddComponent<GridCollidersComponent>("Player"); //とりあえずわかりやすいように"Player"というタグをつけておく(別に何でもいい)
gridCollision.lock()->LinkCollider(sphereCollider); // コライダをリンクする
}
// 入力を受けての処理
virtual void HandleInput();
// 更新処理
virtual void Update() override;
// 描画処理
virtual void Draw() override;
// ジャンプ開始した直後に呼ばれる関数
virtual void OnStartJump() override;
// コライダの衝突した際に呼ばれるコールバック関数
virtual void OnCollision(std::shared_ptr<Collision> pCollision) override;
};
#endif
Player.cppを変更
して、OnCollision関数のpCollisionから得た衝突した法線方向を使ってUpdate関数で力を加えましょう。
#include "Player.h"
#include <cmath> // 浮動小数点の余り%演算子がC++にないのでfmodを使うため
#include "MyMath.h"
#include "MyDraw.h"
#include "Camera.h"
#include "World.h"
#include <vector>
void Player::HandleInput()
{
if (Input::GetButton(pad, PAD_INPUT_LEFT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_A)) )
{
//vx = - MoveSpeed; // 左
deltaAngle += 1; //ハンドルを左に回す
}
if (Input::GetButton(pad, PAD_INPUT_RIGHT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_D)))
{
//vx = MoveSpeed; // 右
deltaAngle -= 1; //ハンドルを右に回す
}
if (Input::GetButton(pad, PAD_INPUT_UP)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_W)))
{
//vy = - MoveSpeed; // 上
MoveSpeed += 0.6f; //アクセルを踏む
if (MoveSpeed > MoveSpeedMax)
MoveSpeed = MoveSpeedMax;//Maxスピードで止める
}
else if (Input::GetButton(pad, PAD_INPUT_DOWN)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_S)))
{
//vy = MoveSpeed; // 下
MoveSpeed -= 0.6f; //ブレーキを踏む
if (MoveSpeed < -0.9f)
MoveSpeed = -0.9f;//ちょっとだけバックできるように
}
if (pad == Pad::Key || pad == Pad::One) // コントローラ1またはキーボード操作のプレイヤのときだけ
{
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGUP)) // キーボードのPageUp ボタンを押したときは
{
vy = 2.0f; // テスト用に空中の上方向に移動
}
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGDN)) // キーボードのPageDown ボタンを押したときは
{
vy = -2.0f; // テスト用に空中の下方向に移動
}
}
}
// ジャンプ開始した直後に呼ばれる関数
void Player::OnStartJump()
{
this->isGround = false; // ジャンプ中はisGroundフラグがfalse
}
// 更新処理
void Player::Update()
{
vxForce *= 0.9f; // かかっている力の減衰率
vzForce *= 0.9f; // かかっている力の減衰率
deltaAngle *= 0.5F; // 車のハンドルの方向をだんだんニュートラルに戻す
MoveSpeed *= 0.9F; // 移動速度も減速する
// 入力を受けての処理
HandleInput();
auto pJumpPhysics = jumpPhysics.lock();
if (pJumpPhysics != nullptr)
{
if (isGround)
{
if (Input::GetButtonDown(pad, PAD_INPUT_A) // Zキーを押した瞬間
|| (pad == Pad::Two && Input::GetButtonDown(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを押した瞬間
{
pJumpPhysics->JumpStart(); // ジャンプのスタート処理を発動
}
}
else if (vy > 0 && Input::GetButtonUp(pad, PAD_INPUT_A) // Zキーを離した瞬間
|| (pad == Pad::Two && Input::GetButtonUp(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを離した瞬間
{
pJumpPhysics->JumpCancel(); // ジャンプのキャンセル処理を発動
}
pJumpPhysics->Update(); // ジャンプのコンポーネントを更新する
}
auto pGravityPhysics = gravityPhysics.lock();
if (pGravityPhysics != nullptr)
pGravityPhysics->Update(); // 重力落下のコンポーネントを更新する
moveAngle += deltaAngle; // ハンドルを回したぶんプレイヤの進行方向を変える
if (moveAngle < -180) moveAngle = 360 + moveAngle; // -180以下の角度にならないようにマイナスになったら+180度にループさせる
else if (moveAngle > +180) moveAngle = -360 + moveAngle; // +180以下の角度にならないようにマイナスになったら-180度にループさせる
rotation.SetRotation(VGet(0, (180 - moveAngle) * MyMath::Deg2Rad, 0)); // 前向きの3Dモデルなら180度回転させて奥方向の向きに設定
// ↓進行方向角度をX方向とZ方向の速度に変える
vx = (float)std::cos((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
vz = (float)std::sin((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
vx += vxForce; // 力をかけて速度に加算する
vz += vzForce; // 力をかけて速度に加算する
// 実際に位置を動かす
// まず横に移動する
x += vx;
// 次に縦に動かす
y += vy;
// 次にZ奥行き方向に動かす
z += vz;
vy += vyForce; // 勢い(力・フォースを加算
if (y <= 0.0f) // 0.0fを地面として 0.0f以下になったら地面に着地したと判定する
{
y = 0.0f; // 地面に沿わせる
vy = -0.01f; // 下方向への速度をほぼ 0 にする
isGround = true;
}
if (auto pGridCollision = gridCollision.lock()) // lock()してnullptrじゃなければ
pGridCollision->Update(); // 所属するマス目セルを更新する
// カメラのプレイヤ追尾
float camDistance = 150; // プレイヤからカメラまでのY平面上の距離
float camHeight = 100; // プレイヤからカメラまでのY方向上の高さ
// moveAngleは2D画像のX方向右を0度とするから、-90度回すと画像を正面に捉える位置にカメラを置ける
float camAngleX = (float)std::cos((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
float camAngleZ = (float)std::sin((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
if (camera != nullptr)
{ // カメラの位置をプレイヤの進行方向と真逆に設定
camera->SetPosition(position + Vector3(camAngleX, camHeight, camAngleZ));
camera->LookAt(position); //カメラはプレイヤの方を見る
}
}
void Player::OnCollision(std::shared_ptr<Collision> pCollision)
{
if (isDead) return; // すでにisDeadなら return
auto otherCollider = pCollision->contacts[0].otherCollider.lock(); // コライダの弱共有ポインタをロック
if (otherCollider == nullptr) return; // すでにコライダが消去済だった
auto other = otherCollider->owner();
if (other->isDead) return; // other側がisDeadだったら処理をスルー
if (other->baseTag == "Enemy" || other->baseTag == "Player")
{
auto norm = pCollision->contacts[0].normal * other->pushPower; // pushPowerの力でotherに押し返される
force += -norm; // 衝突法線方向の力を加える
}
}
// 描画処理
void Player::Draw()
{
int animIndex = animCount / 20; // キャラのパラパラアニメの画像番号(最大3)
float abs_vx = (vx > 0) ? vx : -vx; // x方向絶対値
float abs_vz = (vz > 0) ? vz : -vz; // y方向絶対値
if (abs_vx > 1.00f || abs_vz > 1.00f)
animCount += 6; // キャラが地面のXとZ方向に少しでもスピードがあったらアニメを走らせる
if (animCount >= 60) animCount = 0; // アニメ時間を0へループ
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
for (auto&& keyValue : world()->cameras) // プレイヤのいるワールドにあるすべてのカメラぶんループ
{
auto& camera = keyValue.second;
camera->Draw([&]()
{
// 常にカメラ方向を向くビルボード回転の角度を得る
VECTOR_D angle = MyDraw::GetAngle(DxLib::GetCameraBillboardMatrixD());
angle.x = 0; angle.z = 0; // Y軸周りのビルボード回転だけ有効にする X軸,Z軸まわりの回転は 0 にする
int imgIndex = 9; // キャラチップ画像の起点番号
// ビルボードの角度 と キャラの向いている向き の 差 によって、別のプレイヤのカメラから見たキャラチップの向きの画像を切り替える
// -180 ~ +180 : angle.y moveAngle : -180 ~ +180 ↓
float difAngleY = angle.y * 180.0f / DX_PI_F + moveAngle;
imgIndex = (difAngleY < -135 || 135 < difAngleY) ? 0 // カメラ向きの画像
: (difAngleY < -45) ? 6 // 右 向きの画像
: (difAngleY > 45) ? 3 // 左 向きの画像
: 9; // 奥向きの画像
MyDraw::DrawDivRotaGraphF3D(MyDraw::Plane::Z, x, y, z, 1.0f, angle, *image, imgIndex + animIndex, TRUE);
#ifdef _DEBUG // デバッグのときだけ_DEBUGが定義され描かれる
if (auto pSphereCollider = sphereCollider.lock()) pSphereCollider->Draw(); // コライダを描く
#endif
});
}
}
GameScene.cppを変更
して、プレイヤのコライダと所属するグリッドをデバッグ表示する処理を実装しましょう。
#include "GameScene.h"
#include "Player.h"
GameSceneSetting GameScene::settings{ "Map/GameSceneSetting.csv" }; // ゲームシーンの設定情報
// 指定したゲームシーンのタグに関連づいたファイル名に対応したワールドを初期化する
bool GameScene::LoadWorlds(const std::string& gameSceneTag)
{
if (settings.info.count(gameSceneTag) == 0)
return false;
auto& info = settings[gameSceneTag];
for (auto&& tag_mapPath : info)
{
CreateWorld(tag_mapPath.second.tag); // ワールドをタグをつけて生成
}
return true;
}
void GameScene::ChangeWorld(const std::string& worldTag, std::shared_ptr<Player> pPlayer)
{
if (pPlayer == nullptr) return;
if (worlds.count(worldTag) == 0) return; // 存在しないタグだった
pPlayer->world(worlds[worldTag].get()); // ワールドへのリンクをすげ替える
}
void GameScene::Initialize()
{// Init処理
// Zバッファを有効にする [Zの深度]
//[参考]https://docs.google.com/presentation/d/1Z23t1yAS7uzPDVakgW_M02p20qt9DeTs4ku4IiMDLco/edit?usp=sharing
//[参考] https://dxlib.xsrv.jp/function/dxfunc_3d_draw.html#R14N12
DxLib::SetUseZBuffer3D(TRUE);
// Zバッファへの書き込みを有効にする
DxLib::SetWriteZBuffer3D(TRUE);
// 光の明暗計算を無効に
DxLib::SetUseLighting(FALSE);
// SetDrawScreen に引数として渡せる( 描画対象として使用できる )グラフィックハンドルを作成するかどうかを設定する
// ( TRUE:描画可能グラフィックハンドルを作成する FLASE:通常のグラフィックハンドルを作成する( デフォルト ) )
DxLib::SetDrawValidGraphCreateFlag(TRUE);
// サブのスクリーンを2つ生成 画面の全体の縦方向サイズの Height / 2 にして、上下分割で配置できるようにする
screenHandle0 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
screenHandle1 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
// スクリーンを作成したらデフォルト設定に戻しておかないと
DxLib::SetDrawValidGraphCreateFlag(FALSE);
Resource::MakeShared<Texture>(mapChipPath, 8, 8, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
Resource::MakeShared<Texture>(playerImgPath1, 3, 4, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
Resource::MakeShared<Texture>(playerImgPath2, 3, 4, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
// プレイヤの生成
if (pWorld != nullptr)
{
//pWorld->AddCamera("カメラ1", Camera::defaultCamera); // カメラ1というタグをデフォルトカメラにつけてワールドにデフォルトカメラを配置
pWorld->AddCamera("カメラ1", std::make_shared<Camera>(pWorld, Vector3{ 0,0,0 }));
auto pCamera1 = pWorld->camera("カメラ1"); // カメラ1 というタグのついたカメラへの共有ポインタを得る
pCamera1->SetScreenHandle(screenHandle0); // カメラから見える3Dを描く スクリーンのハンドル を関連付ける
auto pPlayer1 = Object::Create<Player>(nullptr, pWorld, Pad::Key, Vector3(90 + 256, 32, 95 + 256));
pWorld->AddPlayer("プレイヤ1", pPlayer1); // シーンにプレイヤを追加する
pPlayer1->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath1]);
pPlayer1->camera = pCamera1.get(); // カメラをプレイヤにリンクする
pWorld->AddCamera("カメラ2", std::make_shared<Camera>(pWorld, Vector3{ 0,0,0 }));
auto pCamera2 = pWorld->camera("カメラ2"); // プレイヤ2 というタグのついたカメラへの共有ポインタを得る
pCamera2->SetScreenHandle(screenHandle1); // カメラから見える3Dを描く スクリーンのハンドル を関連付ける
auto pPlayer2 = Object::Create<Player>(nullptr, pWorld, Pad::Two, Vector3(190, 32, 195));
pWorld->AddPlayer("プレイヤ2", pPlayer2); // シーンにプレイヤを追加する
pPlayer2->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath2]);
pPlayer2->camera = pCamera2.get(); // カメラをプレイヤにリンクする
}
};
void GameScene::Update()
{// 更新処理
Screen::ClearDrawScreen(screenHandle0); // 一旦キャンバスをきれいにまっさらに
Screen::ClearDrawScreen(screenHandle1); // 一旦キャンバスをきれいにまっさらに
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
for (const auto& player : players)
player.second->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
// グリッドを更新して隣接するグリッドに含まれる敵や弾の当たり判定を行う
for (const auto& world : worlds)
if (world.second->grid() != nullptr)
for (const auto& cell : world.second->grid()->cellData)
{
auto pGridObject = cell.second;
while (pGridObject != nullptr)
{ // while文で数珠つなぎのm_pNextを次々とたどって最後にたどり着くと次がなくてnullptrになる
pGridObject->checkCell();
pGridObject = pGridObject->m_pNext; // リンクリスト構造を次々とたどる
}
}
}
void GameScene::Draw()
{// 描画処理
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
for (const auto& player : players)
{
player.second->Draw(); // プレイヤの描画【忘れるとプレイヤ表示されない】
auto world = player.second->world(); // プレイヤのいるワールド
// プレイヤのいるワールドのカメラのみを描けば、プレイヤのいないワールドは描かずに済む
// 複数のカメラを 範囲 for文 で回す
for (auto&& camera : player.second->world()->cameras)
{ // ラムダ式[&](){ ~ }で{}の外側の変数すべてを & キャプチャで&参照として「Draw関数の内側へ引き連れて」処理できる
camera.second->Draw([&]()
{
// Draw関数() の 外側にある変数 mapChipPath も[&]効果で { } の内側で問題なくアクセスできている↓
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(40, 40, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 2);
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(0, 40, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 0);
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(0, 70, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 8);
if (camera.first == "カメラ1")
DxLib::DrawString(0, 0, "こちらがプレイヤ1", GetColor(255, 0, 0));
else if (camera.first == "カメラ2")
DxLib::DrawString(0, 0, "こちらはプレイヤ2", GetColor(0, 255, 0));
});
}
#ifdef _DEBUG // デバッグのときだけ_DEBUGが定義され描画される
if (world->grid() != nullptr)
for (const auto& cell : world->grid()->cellData)
cell.second->Draw(); // グリッドの描画
#endif
}
// スクリーン0の内容を画面に描く
DxLib::DrawGraph(0, 0, screenHandle0, FALSE);
DxLib::DrawLineBox(0, 0, Screen::Width, Screen::Height / 2, GetColor(255, 0, 0));
// スクリーン1の内容を画面に描く
DxLib::DrawGraph(0, Screen::Height / 2, screenHandle1, FALSE);
DxLib::DrawLineBox(0, Screen::Height / 2, Screen::Width, Screen::Height, GetColor(0, 255, 0));
}
GameScene.cppを変更
して、プレイヤ1とプレイヤ2に質量 mass (重さ)を設定しましょう。
#include "GameScene.h"
#include "Player.h"
(中略).........
void GameScene::Initialize()
{// Init処理
// Zバッファを有効にする [Zの深度]
//[参考]https://docs.google.com/presentation/d/1Z23t1yAS7uzPDVakgW_M02p20qt9DeTs4ku4IiMDLco/edit?usp=sharing
//[参考] https://dxlib.xsrv.jp/function/dxfunc_3d_draw.html#R14N12
DxLib::SetUseZBuffer3D(TRUE);
// Zバッファへの書き込みを有効にする
DxLib::SetWriteZBuffer3D(TRUE);
// 光の明暗計算を無効に
DxLib::SetUseLighting(FALSE);
// SetDrawScreen に引数として渡せる( 描画対象として使用できる )グラフィックハンドルを作成するかどうかを設定する
// ( TRUE:描画可能グラフィックハンドルを作成する FLASE:通常のグラフィックハンドルを作成する( デフォルト ) )
DxLib::SetDrawValidGraphCreateFlag(TRUE);
// サブのスクリーンを2つ生成 画面の全体の縦方向サイズの Height / 2 にして、上下分割で配置できるようにする
screenHandle0 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
screenHandle1 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
// スクリーンを作成したらデフォルト設定に戻しておかないと
DxLib::SetDrawValidGraphCreateFlag(FALSE);
Resource::MakeShared<Texture>(mapChipPath, 8, 8, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
Resource::MakeShared<Texture>(playerImgPath1, 3, 4, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
Resource::MakeShared<Texture>(playerImgPath2, 3, 4, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
// プレイヤの生成
if (pWorld != nullptr)
{
//pWorld->AddCamera("カメラ1", Camera::defaultCamera); // カメラ1というタグをデフォルトカメラにつけてワールドにデフォルトカメラを配置
pWorld->AddCamera("カメラ1", std::make_shared<Camera>(pWorld, Vector3{ 0,0,0 }));
auto pCamera1 = pWorld->camera("カメラ1"); // カメラ1 というタグのついたカメラへの共有ポインタを得る
pCamera1->SetScreenHandle(screenHandle0); // カメラから見える3Dを描く スクリーンのハンドル を関連付ける
auto pPlayer1 = Object::Create<Player>(nullptr, pWorld, Pad::Key, Vector3(90 + 256, 32, 95 + 256));
pWorld->AddPlayer("プレイヤ1", pPlayer1); // シーンにプレイヤを追加する
pPlayer1->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath1]);
pPlayer1->camera = pCamera1.get(); // カメラをプレイヤにリンクする
pPlayer1->mass = 100.0f; // 体重を100にして衝突されてもびくともしないようにする
pWorld->AddCamera("カメラ2", std::make_shared<Camera>(pWorld, Vector3{ 0,0,0 }));
auto pCamera2 = pWorld->camera("カメラ2"); // プレイヤ2 というタグのついたカメラへの共有ポインタを得る
pCamera2->SetScreenHandle(screenHandle1); // カメラから見える3Dを描く スクリーンのハンドル を関連付ける
auto pPlayer2 = Object::Create<Player>(nullptr, pWorld, Pad::Two, Vector3(190, 32, 195));
pWorld->AddPlayer("プレイヤ2", pPlayer2); // シーンにプレイヤを追加する
pPlayer2->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath2]);
pPlayer2->camera = pCamera2.get(); // カメラをプレイヤにリンクする
pPlayer2->mass = 1.0f; // 体重を1にして普通にふっとぶようにする
}
};
(以下略)....
Player.cppを変更
して、力 force の計算式を体重 mass で割り算してぶつかったときのふっとび具合に差をつけてみましょう。
#include "Player.h"
#include <cmath> // 浮動小数点の余り%演算子がC++にないのでfmodを使うため
#include "MyMath.h"
#include "MyDraw.h"
#include "Camera.h"
#include "World.h"
#include <vector>
(中略)........................
void Player::OnCollision(std::shared_ptr<Collision> pCollision)
{
if (isDead) return; // すでにisDeadなら return
auto otherCollider = pCollision->contacts[0].otherCollider.lock(); // コライダの弱共有ポインタをロック
if (otherCollider == nullptr) return; // すでにコライダが消去済だった
auto other = otherCollider->owner();
if (other->isDead) return; // other側がisDeadだったら処理をスルー
if (other->baseTag == "Enemy" || other->baseTag == "Player")
{
auto norm = pCollision->contacts[0].normal* other->pushPower; // pushPowerの力でotherに押し返される
force += -norm / mass; // 衝突法線方向の力を加える
}
}
(以下略).............
Player.cppを変更
して、力 force の計算式を「離散要素法:DEM」の計算式に変えてみましょう。
#include "Player.h"
#include <cmath> // 浮動小数点の余り%演算子がC++にないのでfmodを使うため
#include "MyMath.h"
#include "MyDraw.h"
#include "Camera.h"
#include "World.h"
#include <vector>
(中略)........................
void Player::OnCollision(std::shared_ptr<Collision> pCollision)
{
if (isDead) return; // すでにisDeadなら return
auto otherCollider = pCollision->contacts[0].otherCollider.lock(); // コライダの弱共有ポインタをロック
if (otherCollider == nullptr) return; // すでにコライダが消去済だった
auto other = otherCollider->owner();
if (other->isDead) return; // other側がisDeadだったら処理をスルー
if (other->baseTag == "Enemy" || other->baseTag == "Player")
{
//[離散要素法:DEM] https://qiita.com/konbraphat51/items/157e5803c514c60264d2
constexpr float k = 1.0f; // バネ係数(めり込みに対するバネ反発の掛け率)
constexpr float e = 0.1f; // はね返り係数(直前と比べどれくらいの速さで離れるか0.5だと半分の速さではね返る)
float log_e = std::log(e); // はね返り係数の自然対数log
constexpr float pi_2 = DX_PI_F * DX_PI_F; // πの2乗
float eta = -2.0f * log_e * std::sqrt(k * mass / (log_e * log_e + pi_2)); // 粘性係数η(イータ)
auto norm = pCollision->contacts[0].normal; // 衝突の法線 * other->pushPower; // pushPowerの力でotherに押し返される
auto v_norm = dot(other->velocity - velocity, norm) * norm; // [相対速度v] 法線norm方向のv = (v1 - v0)・norm [ドット積]
auto sep = pCollision->contacts[0].separation; // 衝突点どうしの距離(めりこみ距離)
auto F = -k * sep * norm - eta * v_norm; // F (法線方向の力) = -k × sep - η × v (相対速度:自分から見た相手otherの速度)
force += F / mass; // 衝突法線方向の力を加える
}
}
(以下略).............
PhysicsComponent.hを開いてみて
ください。#ifndef PHYSICS_COMPONENT_H_
#define PHYSICS_COMPONENT_H_
(中略)..................
class GravityComponent : public PhysicsComponent
{
public:
(中略)..................
virtual void Update(GameObject* obj = nullptr) override
{
(中略)..................
if (pOwner->isGround == false && pOwner->vy <= 0) // 下落開始(vyが 0以上 のときはまだジャンプ中なので上昇を邪魔しないようにする)
{
pOwner->vyForce = -gravity(); // ジャンプ上昇力が0になって以降に初めて重力をオンにする(ジャンプの加速を邪魔しない工夫)
pOwner->OnFalling(); // 下落中の処理のコールバック関数を呼び返す
}
if (pOwner->vy <= -downSpeedMax()) // 無限に落下速度が加速しないようにリミットを働かせる(空気抵抗みたいなもの)
pOwner->vy = -downSpeedMax(); // downSpeedMax()以上の落下速度にならないようにする
}
};
PhysicsComponent.hを変更
して、変数m_vyForceと関数vyForce()を定義してコンポーネントごとに力を分離管理できるようにしましょう。
#ifndef PHYSICS_COMPONENT_H_
#define PHYSICS_COMPONENT_H_
#include <vector>
#include "Component.h"
#include "GameObject.h"
// 物理系のコンポーネントはこのクラスを継承して実装する
class PhysicsComponent : public Component
{
protected:
inline PhysicsComponent(std::shared_ptr<GameObject> pOwner) : Component(pOwner)
{
this->baseTag = "PhysicsComponent"; // 大まかなカテゴリーとしては物理系コンポーネント
};
public:
virtual ~PhysicsComponent() {};
virtual void Update(GameObject* obj = nullptr) override
{
// なにか物理系のコンポーネントで共通して更新したいことがあればここに実装
}
virtual void Draw(GameObject* obj = nullptr) override
{
// なにか物理系のコンポーネントで共通して描画したいことがあればここに実装
}
};
class GravityComponent : public PhysicsComponent
{
public:
inline GravityComponent(std::shared_ptr<GameObject> pOwner, const std::vector<float>& vyGravity, const std::vector<float>& vyDownSpeedMax)
: PhysicsComponent(pOwner), m_vyGravity{ vyGravity }, m_vyDownSpeedMax{ vyDownSpeedMax }
{
this->typeID = typeid(GravityComponent); //[環境によって差が出るので↓が無難か] https://qiita.com/nil_gawa/items/1fece3eee7ca1f88c71d
this->typeTag = "GravityComponent"; // 個別のカテゴリーとしては重力落下コンポーネント
};
virtual ~GravityComponent() {}
protected:
float m_vyForce{ 0.0f }; // オーナーのvyForceとは独立した「このコンポーネントのy方向の力」
public:
float vyForce() { return m_vyForce; }
size_t m_idx{ 0 }; // 適用する設定配列の番号
void SetIndex(size_t idx) { m_idx = idx; }
std::vector<float> m_vyGravity; // 重力の設定値
float gravity() { return (m_idx < m_vyGravity.size()) ? m_vyGravity[m_idx] : 0.0f; }
std::vector<float> m_vyDownSpeedMax; // 降下スピードのリミット
float downSpeedMax() { return (m_idx < m_vyDownSpeedMax.size()) ? m_vyDownSpeedMax[m_idx] : 0.0f; }
virtual void Update(GameObject* obj = nullptr) override
{
if (!isEnabled) return; // コンポーネントが有効状態じゃないときは何もせずreturn
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタをロックして得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
if (pOwner->isGround == false && pOwner->vy <= 0) // 下落開始(vyが 0以上 のときはまだジャンプ中なので上昇を邪魔しないようにする)
|| pOwner->isJumping == false && pOwner->vy > 0)// 上方向にぶっ飛ばされたとき
{
float vyMax_2 = downSpeedMax() * downSpeedMax(); // 最大スピードの2乗 を空気抵抗 kに使う
//[空気抵抗の物理] https://hunikablog.com/2020/02/02/equation-of-motion-resistance-proportional-to-the-square-of-speed/
// 空気抵抗係数 k https://www.natural-science.or.jp/article/20100829232938.php
float k = pOwner->mass * gravity() / vyMax_2; // k = m * g / vmaxの2乗 にすれば vmaxが収束速度になる
if (pOwner->vy <= 0) k = -k; // 下方向への空気抵抗は逆になる
// ジャンプ上昇力が0になって以降に初めて重力をオンにする(ジャンプの加速を邪魔しない工夫)
m_vyForce = -gravity() - k / pOwner->mass * pOwner->vy * pOwner->vy; // 空気抵抗は - k / m * vyの2乗
if (pOwner->vy > 0) pOwner->isGround = false; // ふっ飛ばされた場合はisGroundがtrueなのにvy > 0の状態になったりするから
pOwner->OnFalling(); // 下落中の処理のコールバック関数を呼び返す
}
if (pOwner->isGround)
m_vyForce = 0.0f; // 着地したらm_vyForceを0にリセットしないと次回ジャンプ時にJump側と重力がダブっちゃう
if (pOwner->vy <= -downSpeedMax()) // 無限に落下速度が加速しないようにリミットを働かせる(空気抵抗みたいなもの)
pOwner->vy = -downSpeedMax(); // downSpeedMax()以上の落下速度にならないようにする
}
};
class JumpComponent : public PhysicsComponent
{
public:
inline JumpComponent(std::shared_ptr<GameObject> pOwner, const std::vector<float>& vyJumpSpeed, const std::vector<float>& vyForceJump)
: PhysicsComponent(pOwner), m_vyJumpSpeed{ vyJumpSpeed }, m_vyForceJump{ vyForceJump }
{
this->typeID = typeid(JumpComponent); //[環境によって差が出るので↓が無難か] https://qiita.com/nil_gawa/items/1fece3eee7ca1f88c71d
this->typeTag = "JumpComponent"; // 個別のカテゴリーとしてはジャンプ上昇コンポーネント
};
virtual ~JumpComponent() {}
protected:
float m_vyForce{ 0.0f }; // オーナーのvyForceとは独立したこのコンポーネントのy方向の力
public:
float vyForce() { return m_vyForce; }
size_t m_idx{ 0 }; // 適用する設定配列の番号
void SetIndex(size_t idx) { m_idx = idx; }
float yJumpStart{ 0 }; // ジャンプした瞬間のyの位置
std::vector<float> m_vyJumpSpeed; // ジャンプの上昇スピードのリミット
float jumpSpeed() { return (m_idx < m_vyJumpSpeed.size()) ? m_vyJumpSpeed[m_idx] : 0.0f; }
std::vector<float> m_vyForceJump; // ジャンプ中の下方向の勢いの減速する力
float forceJump() { return (m_idx < m_vyForceJump.size()) ? m_vyForceJump[m_idx] : 0.0f; }
virtual void Update(GameObject* obj = nullptr) override
{
if (!isEnabled) return; // コンポーネントが有効状態じゃないときは何もせずreturn
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
if (pOwner->isJumping && pOwner->vy <= jumpSpeed() // jumpSpeedを超える場合はふっとばされている状態
&& pOwner->isGround == false && pOwner->vy > 0) // vyが0以上のときはまだジャンプ中
{
pOwner->OnJumping(); // 空中でジャンプの上昇中に呼ばれるコールバック関数を呼び返す
// ジャンプ上昇中にも空気抵抗を効かせるとき
float vyMax_2 = jumpSpeed() * jumpSpeed(); // jumpSpeed()の2乗を収束速度にする
float vyGravity = forceJump(); // ジャンプ中はforceJump()のゆるい重力でジャンプにふわっと感を出す
float k = pOwner->mass * vyGravity / vyMax_2; // k = m * g / vmaxの2乗 にすれば vmaxが収束速度になる
// 空気抵抗は - k / m * vyの2乗
m_vyForce = -vyGravity - k / pOwner->mass * pOwner->vy * pOwner->vy;
}
else // vy < 0になってジャンプ中じゃなくなり、落下状況になった
{
m_vyForce = 0.0f;
pOwner->isJumping = false; // ジャンプ状態フラグをfalseにする
}
}
virtual void JumpStart(GameObject* obj = nullptr)
{
if (!isEnabled) return; // コンポーネントが有効状態じゃないときは何もせずreturn
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
if (pOwner->isGround)
{
pOwner->vy = jumpSpeed(); // ジャンプ方向はY上方向
m_vyForce = -forceJump(); // ジャンプの勢いが弱まる力
yJumpStart = pOwner->y;
pOwner->isJumping = true; // isJumpingフラグがtrueになるのはJumpStart()関数経由が正規ルート
pOwner->OnStartJump(); // ジャンプスタート時の処理をコールバックで呼び返す
}
}
virtual void JumpCancel(GameObject* obj = nullptr)
{
if (!isEnabled) return; // コンポーネントが有効状態じゃないときは何もせずreturn
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
if (!pOwner->isGround && pOwner->vy >= 0) // vyが0以上のときはまだジャンプ上昇中
{
if (pOwner->y - yJumpStart >= 1) // ジャンプ位置から1ピクセルを超えないタイミングでボタンを離しても重力はかけ始めない
{
m_vyForce = -pOwner->gravity(); // ジャンプボタンを離したときには重力をかけ始める
pOwner->isJumping = false; // falseにしてジャンプ状態フラグをオフにする
}
}
}
};
#endif
Player.cppを変更
して、OnCollision関数のpCollisionから得た衝突した法線方向を使ってUpdate関数で力を加えましょう。
#include "Player.h"
#include <cmath> // 浮動小数点の余り%演算子がC++にないのでfmodを使うため
#include "MyMath.h"
#include "MyDraw.h"
#include "Camera.h"
#include "World.h"
#include <vector>
(中略).......................
// 更新処理
void Player::Update()
{
vxForce *= 0.9f; // かかっている力の減衰率
vyForce *= 0.9f; // かかっている力の減衰率
vzForce *= 0.9f; // かかっている力の減衰率
deltaAngle *= 0.5F; // 車のハンドルの方向をだんだんニュートラルに戻す
MoveSpeed *= 0.9F; // 移動速度も減速する
// 入力を受けての処理
HandleInput();
auto pJumpPhysics = jumpPhysics.lock();
if (pJumpPhysics != nullptr)
{
if (isGround)
{
if (Input::GetButtonDown(pad, PAD_INPUT_A) // Zキーを押した瞬間
|| (pad == Pad::Two && Input::GetButtonDown(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを押した瞬間
{
pJumpPhysics->JumpStart(); // ジャンプのスタート処理を発動
}
}
else if (vy > 0 && Input::GetButtonUp(pad, PAD_INPUT_A) // Zキーを離した瞬間
|| (pad == Pad::Two && Input::GetButtonUp(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを離した瞬間
{
pJumpPhysics->JumpCancel(); // ジャンプのキャンセル処理を発動
}
pJumpPhysics->Update(); // ジャンプのコンポーネントを更新する
}
auto pGravityPhysics = gravityPhysics.lock();
if (pGravityPhysics != nullptr)
pGravityPhysics->Update(); // 重力落下のコンポーネントを更新する
moveAngle += deltaAngle; // ハンドルを回したぶんプレイヤの進行方向を変える
if (moveAngle < -180) moveAngle = 360 + moveAngle; // -180以下の角度にならないようにマイナスになったら+180度にループさせる
else if (moveAngle > +180) moveAngle = -360 + moveAngle; // +180以下の角度にならないようにマイナスになったら-180度にループさせる
rotation.SetRotation(VGet(0, (180 - moveAngle) * MyMath::Deg2Rad, 0)); // 前向きの3Dモデルなら180度回転させて奥方向の向きに設定
// ↓進行方向角度をX方向とZ方向の速度に変える
vx = (float)std::cos((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
vz = (float)std::sin((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
float vyForceJump = (pJumpPhysics != nullptr) ? pJumpPhysics->vyForce() : 0.0f;
float vyForceGravity = (pGravityPhysics != nullptr) ? pGravityPhysics->vyForce() : 0.0f;
vx += vxForce; // 力をかけて速度に加算する
vy += vyForce + vyForceJump + vyForceGravity; // 勢い(力・フォースを加算
vz += vzForce; // 力をかけて速度に加算する
// 実際に位置を動かす
// まず横に移動する
x += vx;
// 次に縦に動かす
y += vy;
// 次にZ奥行き方向に動かす
z += vz;
vy += vyForce; // 勢い(力・フォースを加算
if (y <= 0.0f) // 0.0fを地面として 0.0f以下になったら地面に着地したと判定する
{
y = 0.0f; // 地面に沿わせる
vy = -0.01f; // 下方向への速度をほぼ 0 にする
isGround = true;
}
if (auto pGridCollision = gridCollision.lock()) // lock()してnullptrじゃなければ
pGridCollision->Update(); // 所属するマス目セルを更新する
// カメラのプレイヤ追尾
float camDistance = 150; // プレイヤからカメラまでのY平面上の距離
float camHeight = 100; // プレイヤからカメラまでのY方向上の高さ
// moveAngleは2D画像のX方向右を0度とするから、-90度回すと画像を正面に捉える位置にカメラを置ける
float camAngleX = (float)std::cos((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
float camAngleZ = (float)std::sin((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
if (camera != nullptr)
{ // カメラの位置をプレイヤの進行方向と真逆に設定
camera->SetPosition(position + Vector3(camAngleX, camHeight, camAngleZ));
camera->LookAt(position); //カメラはプレイヤの方を見る
}
}
(中略).......................
マップに並べられる「タイル側の情報」を持つ.tsx形式のファイル→
mapchip.tsx
マップにタイルやオブジェクトを並べた「マップ側のステージ情報」を持つ.json形式のファイル→
stage1.json
マップに関して色々「エクセルで設定したい追加情報」を持つ.csv形式のファイル→
MapSetting.csv
敵(Zako0)の画像.png形式のファイル→
zako0.png (GIMPの書き出し元データzako0.xcf)
敵(CharaMELで制作)の分割画像.png形式のファイル→
skullman.png
アイテム(エネルギー弾)の画像.png形式のファイル→
bullet.png (GIMPの書き出し元データbullet.xcf)
アイテム(お金:1円)の画像.png形式のファイル→
yen1.png
アイテム(お金:10円)の画像.png形式のファイル→
yen10.png
DataXML.hを新規作成
して、基底ベースとなるDataXMLクラスを作成します。
#ifndef DATA_XML_H_
#include <string>
#include <sstream> // std::istringstream で >>で 文字列を数値に変換する
#include "tinyxml2.h" //[軽量シンプルなXML読込パーサー] https://github.com/leethomason/tinyxml2
struct DataXML
{
enum class XMLType
{
XML,
TSX,
};
XMLType type;
bool isInitialized = false; //[継承2重ロード対策]ベース基底でロードしたらtrueに
// 読込んだデータファイルの情報
std::string FilePath{ "" }; // ファイルの保存パス
tinyxml2::XMLDocument Data; // XMLのデータ本体
// 初期化コンストラクタでファイル名を指定して初期化と同時にファイル読込
DataXML(const std::string& FilePath = "", XMLType xmlType = XMLType::XML) : type{ xmlType } {}
virtual ~DataXML() { clear(); } // 仮想デストラクタ
// データをクリアしてメモリを節約する
virtual void clear()
{
Data.Clear(); // データ本体をクリア
isInitialized = false; //ロード済みフラグをOFF
}
virtual void Load(const std::string& filePath, XMLType xmlType = XMLType::XML)
{
// ここには未実装 継承overrideして個別の読み取り処理を実装してください
}
};
#endif DATA_XML_H_
DataTsx.hを新規作成
して、基底ベースとなるDataXMLクラスを継承overrideして.tsx形式を読み取る処理を作成します。
#ifndef DATA_TSX_H_
#define DATA_TSX_H_
#include "DataXML.h"
#include "MyHash.h" // hash32でタグの高速化
#include "Resource.h" // タイルのテクスチャ画像ファイル読込み
#include "json.hpp" // jsonを使用
using json = nlohmann::json;
// .tsxファイルの情報
struct TsxInfo
{
static std::vector<std::pair<int, std::weak_ptr<Texture>>> id_images; // タイルのgid → タイルの<id,画像の弱リンク> への辞書
// tilewidth:幅, tileheight:高さ, tilecount:タイルの数, columns:1列のタイルの数
int tilewidth{ 32 }, tileheight{ 32 }, tilecount{ 64 }, columns{ 8 };
int imageWidth{ 256 }, imageHeight{ 256 }; // 画像ファイルの imageWidth:幅 imageHeight:高さ
std::string name{ "" }; // タイルの管理名
std::string imagePath{ "" }; // タイルの画像ファイルパス
std::vector<unsigned int> gid; // タイルのID → gid の配列辞書
};
// TSXファイルを読込むデータ型
struct DataTsx : public DataXML
{
TsxInfo tsxInfo; //.tsxファイルの情報
// 初期化コンストラクタでファイル名を指定して初期化と同時にファイル読込
DataTsx(const std::string& FilePath = "") : DataXML(FilePath, XMLType::TSX)
{
if (FilePath != "") // .tsxファイルの読込み
Load(FilePath); // ファイル読込み
}
virtual ~DataTsx() { clear(); } // 仮想デストラクタ
virtual void Load(const std::string& filePath, XMLType xmlType = XMLType::TSX) override
{
if (filePath == "" || isInitialized) return; //ファイル名がないもしくはロード済
this->FilePath = filePath; // ファイル名を保管
Data.Clear();
if (Data.LoadFile(FilePath.c_str()) != 0) return; // ファイルパスからデータ読み込み
#if 0 // TSX形式のXMLのフォーマット例
< tileset version = "1.10" tiledversion = "1.10.2" name = "mapchip" tilewidth = "32" tileheight = "32" tilecount = "64" columns = "8" >
<image source = "mapchip.png" width = "256" height = "256" / >
< / tileset>
#endif
//[tinyXMLの使い方] https://w.atwiki.jp/bokuyo/pages/186.html
tinyxml2::XMLElement * tileset = Data.FirstChildElement("tileset"); // 最初の要素を得る
if (tileset == nullptr) return;
const char* pName = tileset->Attribute("name"); // 名前
if (nullptr != pName) tsxInfo.name = std::string(pName);
const char* pTilewidth = tileset->Attribute("tilewidth"); // 幅
if (nullptr != pTilewidth) { std::istringstream ss(pTilewidth); ss >> tsxInfo.tilewidth; }
const char* pTileheight = tileset->Attribute("tileheight"); // 高さ
if (nullptr != pTileheight) { std::istringstream ss(pTileheight); ss >> tsxInfo.tileheight; }
const char* pTilecount = tileset->Attribute("tilecount"); // タイル総数
if (nullptr != pTilecount) { std::istringstream ss(pTilecount); ss >> tsxInfo.tilecount; }
const char* pColumns = tileset->Attribute("columns"); // 1列のタイル数
if (nullptr != pColumns) { std::istringstream ss(pColumns); ss >> tsxInfo.columns; }
tinyxml2::XMLElement* image = tileset->FirstChildElement("image"); // "tileset"内の"image"要素を得る
if (image != nullptr)
{
const char* pImagePath = image->Attribute("source"); // タイル画像パス
if (nullptr != pImagePath) tsxInfo.imagePath = std::string(pImagePath);
const char* pImageWidth = image->Attribute("width"); // タイル画像の幅
if (nullptr != pImageWidth) { std::istringstream ss(pImageWidth); ss >> tsxInfo.imageWidth; }
const char* pImageHeight = image->Attribute("height"); // タイル画像の高さ
if (nullptr != pImageHeight) { std::istringstream ss(pImageHeight); ss >> tsxInfo.imageHeight; }
}
isInitialized = true; //ロード済みフラグをON
}
};
#if 0
struct Tileset // 塗りのパレットとなっている画像ファイルなどのタイル情報
{ // 各パレットの0から始まるが他パレットとかぶるとどっちの番号か確定しないので
int firstgid = -1; // 各パレットのfirstgid=最初の0にあたる番号
std::string source{ "" }; // 塗りのパレットとなっている"mapchip.tsx"など
Tileset(int firstgid, std::string source) : firstgid{ firstgid }, source{ source } {};
~Tileset() {};
};
#endif
// あるgidに対して log N の速度で その firstgid や そのgidを含むタイルファイル名を 計算できる
struct Tilesets : std::map<unsigned int, std::string>
{
static std::unordered_map<hash32, TsxInfo> umTsxInfo; // <ファイルパス, .tsxの情報> の static 辞書
static std::vector<unsigned int> firstgidLog; // 過去に一度計算したfirstgid→1回計算済のfirstgid の static 配列
Tilesets() = default;
inline ~Tilesets() { clear(); }
void LoadTsx(json* pData, std::string* jsonFolderPath)
{ // データの中身を事前処理する
assert((*pData).count("tilesets") != 0 && "jsonにtileset情報が見つからない");
int i = 0;
for (auto& tileset : (*pData)["tilesets"]) {
std::string tsxPath = tileset["source"];
if (jsonFolderPath != nullptr) tsxPath = *jsonFolderPath + tsxPath;
//auto pos = tileName.find(".", 0);
//if (pos != std::string::npos) tileName = tileName.substr(0, pos); // mapchip.tsxのドット.までの文字列mapchip
int firstgid = tileset["firstgid"]; // .tsxファイルの最初のタイルのgid番号
this->emplace(firstgid, tsxPath); // this すなわち std::mapの辞書 にfirstgid と タイルのパス を登録
if (umTsxInfo.count(tsxPath) == 0)
{
DataTsx tsxData{ tsxPath }; // .tsxからタイルに関する情報をロード
std::string mapChipPath = tsxData.tsxInfo.imagePath; // マップチップのパス
if (jsonFolderPath != nullptr) // jsonファイルと同一フォルダを探す
mapChipPath = *jsonFolderPath + tsxData.tsxInfo.imagePath;
// .tsxファイルの情報をもとにテクスチャファイルをロード
Resource::MakeShared<Texture>(mapChipPath, tsxData.tsxInfo.columns,
tsxData.tsxInfo.tilecount / tsxData.tsxInfo.columns,
tsxData.tsxInfo.tilewidth, tsxData.tsxInfo.tileheight)->Load();
//[辞書]TsxInfo::id_imagesのタイルのgidぶんのページ数が足りなければresizeでページを増やしておく
if (firstgid + tsxData.tsxInfo.tilecount >= TsxInfo::id_images.size())
TsxInfo::id_images.resize(firstgid + tsxData.tsxInfo.tilecount + 1);
tsxData.tsxInfo.gid.resize(tsxData.tsxInfo.tilecount);
auto pMapChip = std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]);
for (int id = 0, iSize = pMapChip->m_handles.size(); id < iSize; ++id)
{
TsxInfo::id_images[firstgid + id] = { id, pMapChip }; // TsxInfoの辞書にロードしたidと画像の弱リンクを登録
tsxData.tsxInfo.gid[id] = firstgid + id; // gidを辞書に登録
}
//umTsxInfo.erase(tilePath); // すでに辞書に読込み済データが有ったら削除
umTsxInfo.emplace(tsxPath, tsxData.tsxInfo); // DataTsx::umTsxInfoの辞書に登録
}
}
}
void clear() { std::map<unsigned int, std::string>().swap(*this); }
#if 0
Tileset* operator[] (std::string tsxFilename) { // []オペレータで["mapchip"]タイル名でアクセス
if (listDic.count(tsxFilename) == 0) return nullptr;
return &list[listDic[tsxFilename]]; // 書き込み
}
#endif
// 例.入力gid:129 ファイル:mapchip1.tsx(0~128番) mapchip2.tsx(0~128番)ならば
// gid=129なら id は 129 - 128 = 1で mapchip2 の1番のタイルだと求めるために firstgidは128を返したい
inline unsigned int firstgid(unsigned int gid)
{
if (gid >= firstgidLog.size()) { firstgidLog.resize(gid + 1, -1); }
// すでに1回計算済のfirstgidがあればログ辞書↓からその計算済のログを返せば高速
if (firstgidLog[gid] != -1) return firstgidLog[gid]; // std::vectorなら O( 1 )の計算速度
// lower_boundで2分探索すれば、std::map<int,string>のsize()に対して ★log N の速度で探索できる
auto it = std::prev(this->lower_bound(gid + 1)); // [参考] https://rsk0315.hatenablog.com/?page=1604038627
if (it == this->end() || it->first == 0) return firstgidLog[gid] = 1; // 範囲例外のfirstgidは 1
return firstgidLog[gid] = it->first;
}
// gid から ローカルな id に変換する
inline int id(unsigned int gid) { return gid - firstgid(gid); }
// gidの最大値(最後尾のfirstgidに.tsxファイルのtilecountを足す)を求める
inline unsigned int gidMax()
{
if (this->size() == 0) return 1;
auto pair = this->end(); //辞書の最後から一つ後 end()を得る
--pair;// end()からひとつ前
if (umTsxInfo.count(pair->second) == 0) return 1;
return pair->first + umTsxInfo[pair->second].tilecount; // gidの最大値を求める
}
inline std::string tilepath(unsigned int gid) const
{ // lower_boundで2分探索すれば、std::map<int,string>のsize()に対して log N の速度で探索できる
auto it = std::prev(this->lower_bound(gid + 1)); // [参考] https://rsk0315.hatenablog.com/?page=1604038627
if (it == this->end() || it->first == 0) return "";
return it->second;
}
#if 0
int operator()(std::string gid) const { // ★()読み取りの処理を定義
std::priority_queue<int, std::vector<int>, std::greater<int>> priQue;// 昇順 (デフォルトはstd::less<TypeT>)
for (auto& tileset : list)
priQue.push(tileset.firstgid);
int firstgid = 1;
while (!priQue.empty())
{
int topId = priQue.top();
priQue.pop(); // 一番トップを削除する(取り出す) 計算速度 log N
if (gid < topId) { break; }
firstgid = topId;
}
return firstgid;
}
#endif
};
#endif
DataTsx.cppを新規作成
して.hのstatic関係の定義を作成します。
#include "DataTsx.h"
std::vector<std::pair<int, std::weak_ptr<Texture>>> TsxInfo::id_images; // タイルのgid → タイルの<id,画像の弱リンク> への辞書
std::unordered_map<hash32, TsxInfo> Tilesets::umTsxInfo; // <ファイルパス, tsxの情報> の static 辞書
std::vector<unsigned int> Tilesets::firstgidLog; // <過去に一度計算したfirstgid, 1回計算済のfirstgid> の static 辞書
DataJson.hを新規作成
して.json形式をnloman-jsonで読み取るための基底ベースクラスDataJsonを作成します。
#ifndef DATAJSON_H_
#define DATAJSON_H_
#include <fstream> // ファイル読み出しifstreamに必要
#include <string> //文字列に必要
#include <sstream> // 文字列ストリームに必要
#include "json.hpp" // jsonを使用
using json = nlohmann::json;
// .json形式のファイルをnlohman::jsonで読み取るための基底ベースクラス
// このクラスを継承overrideして個別のフォーマットに合わせたファイルの読み取りを実装する
class DataJson
{
public:
bool isInitialized = false; //[継承2重ロード対策]ベース基底でロードしたらtrueに
// 読込んだデータファイルの情報
std::string FilePath{ "" };
json Data;// jsonデータ
virtual std::size_t size()
{
return Data.size();
}
// 初期化コンストラクタでファイル名を指定して初期化と同時にファイル読込
DataJson(const std::string& FilePath = "") :FilePath{ FilePath }
{// jsonファイルの読込み
if (FilePath != "") Load(FilePath); // ファイル読込み
};
virtual ~DataJson()
{// 仮想デストラクタ
Data.clear();// 2次元配列データのお掃除
isInitialized = false; //ロード済みフラグをOFF
};
// データをクリアしてメモリを節約する
virtual void clear()
{ // データをクリアしてメモリを節約する
Data.clear();// 2次元配列データのお掃除
}
// jsonファイルの読み込み
virtual void Load(const std::string& filePath)
{
if (filePath == "" || isInitialized) return; //ファイル名がないもしくはロード済
FilePath = filePath; // ファイル名を保管
Data.clear(); //データを一旦クリア
std::ifstream loadStream(FilePath); // ファイルをロード
loadStream >> Data; // jsonデータ型に変換
// コンストラクタの初期化でロードの場合とLoad関数経由で読む経路があるから
isInitialized = true; // 読込み初期化済みフラグをON
return;
}
// jsonファイルのセーブ
virtual void Save(const std::string& filePath = "")
{
std::string savePath = (filePath != "") ? filePath : this->FilePath;
if (savePath == "") return; // セーブするファイル名指定が空の場合
std::ofstream saveStream(FilePath); // ファイルの書き込みストリーム
saveStream << Data; // jsonデータをセーブ
}
};
#endif
TMXInfo.hを新規作成
してタイルマップエディタ(.tmx形式)のレイヤーをまたぐ共通情報を読み取る処理を準備します。
#ifndef TMXINFO_H_
#define TMXINFO_H_
#include <string>
#include <assert.h> // データ読込み失敗表示用
#include "json.hpp" // jsonを使用
using json = nlohmann::json;
struct TMXInfo // TMX形式から書き出されたjsonの持つ基本情報
{
int width{ 0 }, height{ 0 }, tilewidth{ 0 }, tileheight{ 0 };
int nextlayerid{ 1 }, nextobjectid{ 1 }, compressionlevel{ -1 };
bool infinite{ false };
json* pJson{ nullptr };
std::string version{ "1.10" }, tiledversion{ "1.10.2" };
inline TMXInfo(json* pJson = nullptr) : pJson{ pJson }
{
if (pJson == nullptr) return;
*this << *pJson; // jsonからデータをロード
}
~TMXInfo() {}
inline void operator << (const json& Data)
{ // TMX形式から出力されたjsonデータの中身を読み取る
assert(Data.count("width") != 0 && "jsonに width が見つからない");
width = Data["width"];
assert(Data.count("height") != 0 && "jsonに height が見つからない");
height = Data["height"];
assert(Data.count("tilewidth") != 0 && "jsonに tilewidth が見つからない");
tilewidth = Data["tilewidth"];
assert(Data.count("tileheight") != 0 && "jsonに tileheight が見つからない");
tileheight = Data["tileheight"];
nextlayerid = Data["nextlayerid"];
nextobjectid = Data["nextobjectid"];
compressionlevel = Data["compressionlevel"];
infinite = Data["infinite"];
version = Data["version"];
tiledversion = Data["tiledversion"];
}
};
struct TMXLayerInfo
{
enum class Type : short { None, TMXLayer, ObjectGroup, Group };
int x{ 0 }, y{ 0 }, Width{ 0 }, Height{ 0 }, gid{ 0 };
float opecity = 1.0f;
Type type{ Type::None };
bool visible{ true };
json* pLayer{ nullptr };
std::string name{ "" }; // レイヤの名前
inline TMXLayerInfo(json* pLayer = nullptr) : pLayer{ pLayer }
{
if (pLayer == nullptr) return;
*this << *pLayer;
}
~TMXLayerInfo() {}
inline void operator << (const json& layerData)
{
if (layerData["type"] == "tilelayer") type = Type::TMXLayer;
else if (layerData["type"] == "objectgroup") type = Type::ObjectGroup;
else if (layerData["type"] == "group") type = Type::Group;
if (layerData.count("x") > 0) x = layerData["x"];
if (layerData.count("y") > 0) y = layerData["y"];
if (layerData.count("width") > 0) Width = layerData["width"];
if (layerData.count("height") > 0) Height = layerData["height"];
if (layerData.count("opecity") > 0) opecity = layerData["opecity"];
if (layerData.count("visible") > 0) visible = layerData["visible"];
gid = layerData["id"];
name = layerData["name"];
}
};
#endif
MapRange.hを新規作成
して、出現レンジを表すクラスを作成します。
#ifndef MAP_RANGE_H_
#define MAP_RANGE_H_
#include <vector>
#include <map>
#include <unordered_map>
#include "CellXYZ.h" // マス目XYZを辞書配列のキーとするためのクラス
// 敵を出現(スポーン)させるかを判定するマス目処理クラス
class MapRange
{
public:
// 敵の出現などの射程のレンジ【注意!単位はマス目1マス】
int CellRangeX{ 30 }, CellRangeY{ 30 }, CellRangeZ{ 0 };
enum class Type : unsigned char { // 敵の新規出現レンジのタイプ
Rect, // 四角の出現レンジ
Ellipse, // 楕円の出現レンジ
//Ellipsoid, // 3D楕円体の出現レンジ https://ja.wikipedia.org/wiki/%E6%A5%95%E5%86%86%E4%BD%93
};
Type type = Type::Rect;
bool isFirstAccess = true; // 初期位置のときにはレンジ内の敵を全部出す
std::unordered_map<CellXYZ, bool, CellXYZ::Hash> umRange; // 辞書:umRangeOpened[{10,5}]==trueなら出現範囲
MapRange() = default;
MapRange(int SpawnRangeX, int SpawnRangeY, Type type = Type::Rect)
: CellRangeX{ SpawnRangeX }, CellRangeY{ SpawnRangeY }, type{ type }
{
InitRange(type, SpawnRangeX, SpawnRangeY);
}
// 3D版のコンストラクタ
MapRange(int SpawnRangeX, int SpawnRangeY, int SpawnRangeZ, Type type = Type::Rect)
: CellRangeX{ SpawnRangeX }, CellRangeY{ SpawnRangeY }, CellRangeZ{ SpawnRangeZ }, type{ type }
{
InitRange(type, SpawnRangeX, SpawnRangeY);
}
virtual ~MapRange() {} // 仮想デストラクタ
// 敵出現射程の辞書初期化【先に計算して辞書化】すると計算が必要なくなり【高速化する】
//【注意!XとYの単位はマス目】
virtual void InitRange(Type type, int rangeCellX, int rangeCellY, int rangeCellZ = 0)
{
this->CellRangeX = rangeCellX; this->CellRangeY = rangeCellY; this->CellRangeZ = rangeCellZ; this->type = type;
umRange.clear();//一旦辞書をクリアするのでゲーム中の再設定も可(だが処理時間はかかる)
if (type == Type::Ellipse || type == Type::Rect)
{
// 敵出現射程の辞書初期化
// ★ 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
if (type == Type::Ellipse)
{ // 楕円の出現レンジにしたいときは
float theta = (float)std::atan2(y, x); // ★[逆三角関数]
float cos_t = std::cos(theta), 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ならば楕円の内側
{ //楕円の内側なら辞書SpawnDic[{ x,y }] = true;として登録
if (rangeCellZ == 0)
umRange[CellXYZ(x, y)] = true; //【例】SpawnDic[{ 3,2 }] = true;
else
for (int z = -rangeCellZ; z < rangeCellZ; ++z)
umRange[CellXYZ(x, y, z)] = true;
}
}
else if (type == Type::Rect)
{ //【★四角形の出現レンジにしたいときはこちら】
if (rangeCellZ == 0)
umRange[CellXYZ(x, y)] = true;
else
for (int z = -rangeCellZ; z < rangeCellZ; ++z)
umRange[CellXYZ(x, y, z)] = true;
}
}
}
}
}
virtual std::vector<CellXYZ> Spawn(int deltaCellX, int deltaCellY, int deltaCellZ = 0)
{
std::vector<CellXYZ> spawnCells; // 新しく開くマス目(敵を新規生成するマス目)
for (auto iter = umRange.begin(); iter != umRange.end(); iter++)
{ //★例 iter->firstが"3,1" iter->secondが[trueかfalse]の射程マスかどうかのペア
if (iter->second == true)
{ // 例.(3,1)のマス目が出現射程マスか[trueかfalse]か?
CellXYZ xyz = iter->first; // iter->firstに(x,y)=(3,1)が入っている時
// 例.↑(3,1)のマス目xy.X = 3, xy.Y = 1
// 例.deltaCellX = 1 deltaCellY = 2のとき
int searchX = xyz.X + deltaCellX; //例. 3+1 = 4 移動先は4
int searchY = xyz.Y + deltaCellY; //例. 1+2 = 3 移動先は3
int searchZ = xyz.Z + deltaCellZ;
//例.↓もし(4,3)が未登録もしくはfalseなら【新規出現マス!】
CellXYZ searchXYZ(searchX, searchY, searchZ);
if (isFirstAccess)
spawnCells.emplace_back(searchXYZ); // 初回アクセスのときはレンジ内の範囲は全部返す
else if (umRange.count(searchXYZ) == 1)
{ // 例.辞書に↑(4,3)登録があるならcount( (4,3) )は 1
//★例.(4,3)がtrueなら↓【被りマス】移動前にすでに敵の出現判定【済】
if (umRange[searchXYZ] == true) continue; // 敵を出現させず別マスの判定を続ける
}
else
spawnCells.emplace_back(searchXYZ); // 新しく開くマス目(敵を新規生成するマス目)
}
}
if (isFirstAccess) isFirstAccess = false; // 初回アクセスフラグをオフに
return spawnCells;
}
};
#endif
MapSpawnGrid.hを新規作成
して、マップをScroll関数でスクロールさせたときに敵を出現スポーンさせるため、スクロールで新たに開かれたグリッドを配列で返すクラスを作成します。
#ifndef MAP_SPAWN_GRID_H_
#define MAP_SPAWN_GRID_H_
#include <functional> // スポーン処理をラムダ式で Scroll関数の引数にできるように
#include "MapRange.h"
#include "Vector3.h"
#include "GridObject.h"
class MapSpawnGrid : public Grid
{
public:
inline MapSpawnGrid(int cellWidth = 64, int cellHeight = 64, int cellDepth = 64)
: Grid( cellWidth, cellHeight, cellDepth )
{
}
virtual ~MapSpawnGrid() {}
// グリッドをスクロールして敵などのオブジェクトをスポーンさせる
virtual void Scroll(Vector3 prevPos, Vector3 delta, MapRange& spawnRange, std::function<void(int,int,int)> spawnFunc)
{
// マス目を求める
int nextCellX = cellX(prevPos.x + delta.x);
int prevCellX = cellX(prevPos.x);
int nextCellZ = cellY(prevPos.y + delta.y); // マス目なので Z と Y 入れ替え
int prevCellZ = cellY(prevPos.y);
int nextCellY = cellZ(prevPos.z + delta.z); // マス目なので Z と Y 入れ替え
int prevCellY = cellZ(prevPos.z);
bool isMoveCell = false; // マス目移動が発生するか?発生しないならオブジェクト出現検索処理を回避したい
if (nextCellX != prevCellX || nextCellY != prevCellY || nextCellZ != prevCellZ)
{
isMoveCell = true;
}
// マス目移動以外のときはオブジェクト出現検索処理を省いて高速化
if (isMoveCell)
{ // 移動が1マス幅を超えればオブジェクト新規出現判定
int deltaCellX = nextCellX - prevCellX; // セル(マス目)単位での移動数(マス数)
int deltaCellY = nextCellY - prevCellY; // セル(マス目)単位での移動数(マス数)
int deltaCellZ = nextCellZ - prevCellZ; // セル(マス目)単位での移動数(マス数)
//printfDx("\n%d %d", deltaCellX, deltaCellY);
std::vector<CellXYZ> spawnCells = spawnRange.Spawn(deltaCellX, deltaCellY, deltaCellZ);
for (const auto& spawnCell : spawnCells)
{
int gridX = 0, gridY = 0, gridZ = 0;
// 現状ポジションを基準に
gridX = spawnCell.X + prevCellX; // セル(マス目)単位でのX位置
gridY = spawnCell.Y + prevCellY; // セル(マス目)単位でのY位置
gridZ = spawnCell.Z + prevCellZ;// セル(マス目)単位でのZ位置
spawnFunc(gridX, gridY, gridZ); // スポーン処理の関数を呼び出し
}
}
}
};
#endif
TMXJson.hを新規作成
して、タイルマップエディタのステージデータを読み取る「クラス群」を作成します。
#ifndef TMX_JSON_H_
#define TMX_JSON_H_
#include <memory>
#include <vector>
#include <assert.h> // マップ読込み失敗表示用
#include "DataJson.h"
#include "DataTsx.h"
#include "DataCsv.h"
#include "CellXYZ.h" // 敵の配置など、地面のタイルと違って隙間なく埋まっていないタイプのデータはstd::unordered_mapで扱いデータ量を減らす
#include "TMXInfo.h" // .tmx形式の基本データ構造
#include "MyHash.h"
#include "MapSpawnGrid.h"
#include "GridObject.h"
// タイルマップエディタのTMX形式に由来するマップのタイルデータの番号 gid を表すクラス
// ただの数字だけじゃなく、数字を書き換えたときにおおもとのjsonのデータも連動して書き換えるためjson*のリンクも保持できる
class TMX_gid
{
public:
int gid = 0; // 例.129 のとき Tilesetsで求めるfirstgidが128なら ローカルなidは1番
unsigned int firstgid = 1; // gid =0 firstgid = 1ならば 0 - 1 = -1でデフォルトは-1になる
json* pJsonTarget; // 元データのjsonを直で書き換えるためのポインタ参照
Tilesets*& tilesets; // firstgidを求めるのに必要
public:
// コンストラクタ
inline TMX_gid(int gid, json* pJsonTarget, Tilesets*& tilesets)
: gid{ gid }, pJsonTarget{ pJsonTarget }, tilesets{ tilesets }
{
if (tilesets != nullptr)
this->firstgid = tilesets->firstgid(gid); // TMXのjsonはタイルごとに最初のgidもマス目ごとに保管しないとidかぶって保存されるとjsonとしてセーブするときに困る
}
inline ~TMX_gid() {}; // デストラクタ
void operator=(const int& intValue) // int型の id の代入演算子
{
if (intValue <= -1) {
if (pJsonTarget != nullptr) *pJsonTarget = 0;
this->gid = 0;
return;
}
if (pJsonTarget != nullptr)
*pJsonTarget = intValue + firstgid;
this->gid = intValue + firstgid;
}
void operator=(const unsigned int& uintValue) // unsigned int つまり gidの 代入演算子
{
if (pJsonTarget != nullptr)
*pJsonTarget = uintValue;
if (tilesets != nullptr)
firstgid = tilesets->firstgid((int)uintValue);
this->gid = uintValue;
}
inline void copy(const TMX_gid& value)
{
this->tilesets = value.tilesets;
this->pJsonTarget = value.pJsonTarget;
this->gid = value.gid;
this->firstgid = value.firstgid;
}
inline void operator=(const TMX_gid& value) { copy(value); } // 代入演算子
// コピーコンストラクタ
inline TMX_gid(const TMX_gid& other)
: pJsonTarget{ other.pJsonTarget }, gid{ other.gid }, firstgid{ other.firstgid }, tilesets{ other.tilesets }
{ }
inline operator int() // int型 つまり id の変換オペレータ
{
//★gidを引くことでjsonの保存形式の数字からcsvの0から始まる出力に合わせる
return (gid == 0) ? -1 : gid - firstgid;
}
inline operator unsigned int() // unsigned int つまり gidの 変換オペレータ
{
return gid;
}
inline operator json* () const // おおもとのjsonへのリンクを得る
{
return pJsonTarget;
}
};
// タイルの縦×横のCSV形式のデータの1行を表すためにstd::vector形式を継承したLineを定義
// []オペレータで配列サイズが足りないとき = マップの範囲外に新たにタイル設置した際に配列サイズを増やす機能を持つ
struct TMXLine_gid : public std::vector<TMX_gid>
{
Tilesets*& tilesets; // firstgidのためにTilesetsのリンクが必要だからTMX_gidに引き渡すための変数定義
inline TMXLine_gid(Tilesets*& tilesets) : tilesets{ tilesets } {}
inline TMX_gid& operator[] (size_t x) // 書き込み
{
if (x >= size()) // ★xが範囲外のときは拡張(注意:でかい数字を入れるとメモリパニック:使う側の性善説に基づく)
this->resize(x + 1, { 0, nullptr, tilesets });
return this->at(x);
}
};
struct TMXCsv; // 前方宣言
// タイルマップ辞書構造(3D:z方向あり)何もない空中をデータとしてあらかじめ敷き詰めるのはメモリのムダなので辞書構造に
struct TMXterrains : public std::unordered_map<int, std::shared_ptr<TMXCsv>>
{
TMXCsv& operator[](int z)
{
if (this->count(z) == 0)
this->emplace(z, std::make_shared<TMXCsv>());
return *this->at(z); // 読取り
}
#if 0
inline json& operator() (int x, int y, int z)
{
if (this->count(z) == 0)
this->emplace(z, std::make_shared<TMXCsv>());
return (*this->at(z))(x, y); // 読取り
}
#endif
};
// TMX由来の.jsonファイルのオブジェクトごとに設定されているプロパティ
struct TMXProperty
{
enum class Type : unsigned char { Float, Int, String };
Type type = Type::Int;
union {
int intValue{ 0 };
float floatValue;
std::string stringValue;
};
std::string name{ "" };
TMXProperty() = default;
TMXProperty(const std::string& name, int value) : name{ name }, intValue{ value }, type{ Type::Int } {}
TMXProperty(const std::string& name, float value) : name{ name }, floatValue{ value }, type{ Type::Float } {}
TMXProperty(const std::string& name, const std::string& value) : name{ name }, stringValue{ value }, type{ Type::String } {}
~TMXProperty() {}
inline operator int() { return intValue; }
inline operator float() { return floatValue; }
inline operator std::string() { return stringValue; }
inline operator Type() { return type; }
};
// Objectクラスを継承overrideしたGameObjectでTMXのjsonからプロパティデータを受け取って
// 各GameObjectのInit関数での初期化処理を行う
struct InitTMXProperty : public InitParams
{
// Initが終わったら破棄される一時的な変数なので 生ポインタで tmxのプロパティデータを受け取る
std::unordered_map<hash32, TMXProperty>* properties;
inline InitTMXProperty(std::unordered_map<hash32, TMXProperty>* tmxProperties = nullptr)
: properties{ tmxProperties }, InitParams() {}
virtual ~InitTMXProperty() {}
};
// TMXのオブジェクトレイヤの敵やライトやアイテムデータをグリッドで出現スポーンするためGridObjectを継承
struct TMXobject : public GridObject
{
// 出現させるスポーンのタイプ
enum class SpawnType : unsigned char { Light, Enemy, Item, Object };
inline TMXobject(Grid* pGrid = nullptr, SpawnType spawnType = SpawnType::Object) : spawnType{ spawnType }, GridObject(pGrid) {};
virtual ~TMXobject() {}
float x{ 0 }, y{ 0 }, z{ 0 };
float rotation{ 0 };
SpawnType spawnType = SpawnType::Object;
bool visible{ true };
bool point{ true };
//int spawnID{ -1 }; // 敵の種類ID や オブジェクトの種類IDなど
std::string objectName{ "" };
std::string baseType{ "" }; // Factory.cppで生成する際に使う「基底ベースクラス名」
std::string type{ "" }; // Factory.cppで生成する際に使う「クラス名」
std::unordered_map<hash32, TMXProperty> properties;
// TMX形式のjsonから読み取ったプロパティの辞書を
// Object.hのInitParamsをoverride継承したInitTMXProperty型として返す
// これをFactory.h の MakeSharedObject関数に渡せれば、
// ★各GameObjectのInit関数の初期化にjsonから読み取ったデータを渡せる
inline std::shared_ptr<InitTMXProperty> GetInitProperty()
{
return std::make_shared<InitTMXProperty>(&properties);
}
// jsonから << オペレータでデータを読み取り
inline void operator << (json& objectData)
{
objectName = objectData["name"];
type = objectData["type"];
visible = objectData["visible"];
point = objectData["point"];
rotation = objectData["rotation"];
x = objectData["x"], y = objectData["y"], z = 0;
for (auto&& propertyData : objectData["properties"])
{
std::string propertyName = propertyData["name"];
std::string propertyType = propertyData["type"];
if (propertyName == "Z" && propertyType == "float") z = propertyData["value"];
else if (propertyName == "z" && propertyType == "float") z = propertyData["value"];
//else if (propertyName == "spawnID") spawnID = propertyData["value"];
else if (propertyType == "float")
{
float value = propertyData["value"];
properties.emplace(std::piecewise_construct, std::forward_as_tuple(propertyName), std::forward_as_tuple(propertyName, value));
}
else if (propertyType == "int")
{
int value = (int)propertyData["value"];
properties.emplace(std::piecewise_construct, std::forward_as_tuple(propertyName), std::forward_as_tuple(propertyName, value));
}
else
{
std::string value = propertyData["value"];
properties.emplace(std::piecewise_construct, std::forward_as_tuple(propertyName), std::forward_as_tuple(propertyName, value));
}
}
}
};
// 敵の出現のタイルマップのようにほとんどのデータが空白(-1)なのに
// -1でデータを埋めるのはメモリの無駄なのでstd::unordered_mapを内部に持つCellGridを継承して[{0,4,5}] X=0,y=4,Z=5 の辞書配列でデータを保管できる
struct TMXobjects : public MapSpawnGrid
{
TMXobjects(int cellWidth = 64, int cellHeight = 64, int cellDepth = 64)
: MapSpawnGrid(cellWidth, cellHeight, cellDepth) {}
virtual ~TMXobjects() {}
TMXobject::SpawnType spawnType = TMXobject::SpawnType::Object;
std::string GroupName{ "" }; // レイヤ名(例."Object0") 複数レイヤz=0~... を一つのCellGrid上のデータCellGrid.dataのunordered_mapで管理する
std::string BaseName{ "" }; // レイヤ名の先頭(例."Object0"のObject) ★Factory.cppの基底ベースクラスの判定にも使う
//std::unordered_map<int, int> umObjectsCount; // <レイヤ番号, そのレイヤ階層にあるオブジェクト数>
std::shared_ptr<MapRange> pSpawnRange{ nullptr };
std::unordered_map<hash32, TMXProperty> properties; // ユーザの定義した追加プロパティ
std::list<std::shared_ptr<TMXobject>> data; // オブジェクトのデータの実体をキープする共有リスト
// データを消す
void Erase(TMXobject* targetObj)
{
auto it = data.begin();
while (it != data.end()) // リストを回している途中でデータを消すには https://qiita.com/satoruhiga/items/fa6eae09c9d89bd48b5d
if (it->get() == targetObj)
it = data.erase(it);
else ++it;
}
//int Width{ 0 }, Height{ 0 };
// プロパティを読み取り、グリッドのセルのサイズやスポーンのレンジ設定を読み取る
inline void LoadProperty(json& objectsData)
{
assert(objectsData.count("objects") != 0 && "jsonの objectgroup に objects が見つからない");
if (objectsData.count("properties") == 0) return; // ユーザーの追加定義したプロパティがなければ終了
// ユーザーの追加定義したプロパティがあれば読み取る
for (auto&& propertyData : objectsData["properties"])
{
std::string propertyName = propertyData["name"];
std::string propertyType = propertyData["type"];
if (propertyType == "float")
{
float value = propertyData["value"];
properties.emplace(std::piecewise_construct, std::forward_as_tuple(propertyName), std::forward_as_tuple(propertyName, value));
}
else if (propertyType == "int")
{
int value = propertyData["value"];
properties.emplace(std::piecewise_construct, std::forward_as_tuple(propertyName), std::forward_as_tuple(propertyName, value));
}
else
{
std::string value = propertyData["value"];
properties.emplace(std::piecewise_construct, std::forward_as_tuple(propertyName), std::forward_as_tuple(propertyName, value));
}
}
// グリッドのサイズの設定が追加プロパティにユーザが定義していたら読み取る なければMapSetting.csvの設定が優先される
if (properties.count("CellSizeX") > 0) this->cellWidth = properties["CellSizeX"];
if (properties.count("CellSizeY") > 0) this->cellHeight = properties["CellSizeY"];
if (properties.count("CellSizeZ") > 0) this->cellDepth = properties["CellSizeZ"];
// スポーンのレンジ設定がプロパティに追加ユーザが定義していたら読み取る なければMapSetting.csvの設定が優先される
if (properties.count("SpawnRangeX") > 0)
{
int SpawnRangeX{ 10 }, SpawnRangeY{ 10 }, SpawnRangeZ{ 1 };
MapRange::Type spawnType = MapRange::Type::Ellipse;
if (properties.count("SpawnType") > 0) { std::string str = properties["SpawnType"]; spawnType = (str == "Ellipse") ? MapRange::Type::Ellipse : MapRange::Type::Rect; }
if (properties.count("SpawnRangeX") > 0) SpawnRangeX = properties["SpawnRangeX"];
if (properties.count("SpawnRangeY") > 0) SpawnRangeY = properties["SpawnRangeY"];
if (properties.count("SpawnRangeZ") > 0) SpawnRangeZ = properties["SpawnRangeZ"];
pSpawnRange = std::make_shared<MapRange>(SpawnRangeX, SpawnRangeY, SpawnRangeZ, spawnType);
pSpawnRange->InitRange(spawnType, SpawnRangeX, SpawnRangeY, SpawnRangeZ); // 各マップごとに違うスポーンのレンジ設定があればInitしておく
}
}
// jsonのオブジェクトレイヤからデータを << オペレータで読み取り
inline void operator << (json& objectsData)
{
//assert(GroupName != "" && "レイヤをフィルタするグループ名:GroupNameを設定してから << 読込してください。");
assert(objectsData.count("objects") != 0 && "jsonの objectgroup に objects が見つからない");
// プロパティを読み取り、グリッドのセルのサイズやスポーンのレンジ設定を読み取る
LoadProperty(objectsData); // グリッドのセルのサイズによっては下で使う cellX() cellY() cellZ() 関数の結果が変わってくるので先に "properties"を読み取り
for (auto&& object : objectsData["objects"])
{
std::shared_ptr<TMXobject> pTmxObject = std::make_shared<TMXobject>(this, spawnType); // this は Grid* 型 のリンクでオブジェクトからグリッドへのリンクを張る
*pTmxObject << object; // jsonから << でデータを読み取り
data.emplace_back(pTmxObject); // データのキープはこちらの data で行う
}
}
// baseClassName : Factory.cppで生成するときの「基底ベースクラス名」を設定する
// タイルマップエディタとのスケールの違いの比率を計算して補正した位置のグリッドへ data から objectをaddする
inline void InitObjects(const std::string& baseClassName, int tilewidth, int tileheight)
{
float ratioX = (float)cellWidth / tilewidth;
float ratioY = (float)cellHeight / tileheight; // タイルマップエディタとのスケールの違いの比率を計算
float ratioZ = (float)cellDepth / tilewidth;
for (auto& pTmxObject : data)
{
pTmxObject->baseType = baseClassName; // Factory.cppで生成するときの「基底ベースクラス名」を設定
CellXYZ gridPos{ cellX(pTmxObject->x * ratioX),cellY(pTmxObject->y * ratioY),cellZ(pTmxObject->z * ratioZ) };
pTmxObject->add(&gridPos); // グリッドのgridPosのマス目位置に オブジェクトへのリンクを追加
}
}
};
class TMXJson; // 前方宣言
// タイルマップのステージの.json形式のdata:[1,1,1, ....]部分からマップのタイル番号を読み取りgid_Dataに保持するクラス
struct TMXCsv : public DataCsv
{
Tilesets* tilesets{ nullptr };
size_t Width{ 0 }, Height{ 0 };
int z{ 0 }; // z方向(マップ上の上y方向)
inline std::size_t size() { return Height; }
json* pData{ nullptr }; // jsonのおおもとデータへのリンク
//TMXCsvLine tmxCsvLine{ Width, pData, tilesets };
std::vector<TMXLine_gid> gid_Data; // タイルのデータはこの配列に保持される
std::string LayerName{ "" };
inline TMXCsv(const std::string& FilePath = "")
: DataCsv() { if (FilePath != "") this->FilePath = FilePath; }
virtual ~TMXCsv() {}
inline TMXLine_gid& operator[] (size_t y)
{
if (y >= gid_Data.size())
gid_Data.resize(y + 1, tilesets); // yが範囲外のときは拡張(使う側の性善説に基づく)
return gid_Data[y];
}
// jsonのレイヤからデータを読み取る
inline void operator << (json& layerData)
{
assert(tilesets != nullptr && "tilesetsがnullptrです。tilesetsに=でタイルセットへのリンクを張ってから << 読込してください。");
assert(layerData.count("data") != 0 && "jsonのレイヤーにdataが見つからない");
assert(layerData.count("width") != 0 && "jsonのレイヤーにwidthが見つからない");
assert(layerData.count("height") != 0 && "jsonのレイヤーにheightが見つからない");
//int dataWidth = layerData["width"], dataHeight = layerData["height"];
Width = layerData["width"];
Height = layerData["height"];
assert(Width > 0 && "jsonのレイヤーのwidthが0以下、データがおかしい");
assert(Height > 0 && "jsonのレイヤーのheightが0以下、データがおかしい");
int commaCount = 0; //カンマの数をカウント
int widthCount = 0; //行の数字の数をカウント
int readHeight = 0; //行数をカウント
pData = &layerData["data"];
TMXLine_gid valuelist{ tilesets }; // tilesetsへの参照を渡す
for (auto&& gid : *pData)
{ //幅をカウントアップしlineWidthと一致したら1行ぶんemplace_backする方法で1行区切る(layerのdataには改行がないので)
++widthCount;
++commaCount;
//int firstgid = tilesets->firstgid((int)gid);
valuelist.emplace_back(TMX_gid((int)gid, &gid, tilesets)); // 数字をリスト(1行リスト)に追加
if (widthCount >= Width)
{
// 1行分をvectorに追加
if (valuelist.size() != 0) gid_Data.emplace_back(valuelist);
valuelist.clear();//1行push_backできたら次の行読みの為クリア
++readHeight; //マップの高さをカウントアップ
widthCount = 0;//次の行のカウントを再び始めるのでカウント幅を0に
}
}
this->LayerName = layerData["name"];
this->isInitialized = true;
}
inline json& operator () (int x, int y)
{
assert(0 <= x && x < Width && 0 <= y && y < Height && "x y のアクセス位置が範囲外です。");
return (*pData)[x + y * Width];
}
};
// タイルマップエディタに特化したJSON読み書きクラス
class TMXJson : public DataJson
{
public:
// 初期化コンストラクタでファイル名を指定して初期化と同時にファイル読込
TMXJson(const std::string& FilePath = "") :DataJson(FilePath)
{ // jsonファイルの読込みは継承もとのDataJsonで終わっている前提でデータの中身を事前処理する
if (FilePath == "") return;
this->Load(FilePath);
};
virtual ~TMXJson() {}
TMXInfo tmxInfo; // .tmx形式の基本データ
std::string folderPath{ "" }; // jsonの入っているフォルダ
Tilesets tilesets; // 塗りのパレットタイル情報のリスト
struct Group : std::list<int>
{
int gid;
std::list<Group> children;
inline Group(int _id = 0) : gid{ _id } {}
inline ~Group() {}
};
Group groups; // 複数のレイヤを子に持てるフォルダの gid の 親子構造を保持する
TMXterrains terrains; // z方向にも複数タイルを保管できる辞書
TMXobjects lights; //[光源のスポーン] CellGridを継承して辞書配列ベースで 空白 -1 を保管せずにデータを管理
TMXobjects enemies; //[敵のスポーン] CellGridを継承して辞書配列ベースで 空白 -1 を保管せずにデータを管理
TMXobjects items; //[アイテムのスポーン] CellGridを継承して辞書配列ベースで 空白 -1 を保管せずにデータを管理
TMXobjects objects; //[その他のスポーン] CellGridを継承して辞書配列ベースで 空白 -1 を保管せずにデータを管理
void SetGridSize(Vector3 gridSize)
{
lights.cellWidth = gridSize.x; lights.cellHeight = gridSize.y; lights.cellDepth = gridSize.z;
enemies.cellWidth = gridSize.x; enemies.cellHeight = gridSize.y; enemies.cellDepth = gridSize.z;
items.cellWidth = gridSize.x; items.cellHeight = gridSize.y; items.cellDepth = gridSize.z;;
objects.cellWidth = gridSize.x; objects.cellHeight = gridSize.y; objects.cellDepth = gridSize.z;
}
std::size_t size() { return terrains.size(); }
// [] オペレータでアクセスされたらterrainsを返す
inline TMXCsv& operator[] (int z)
{
return terrains[z];
}
// filePathにあるjson形式のファイルをタイルマップのTMXの形式として読み取る
virtual void Load(const std::string& filePath)
{
if (filePath == "") return; //ファイル名がない
this->FilePath = filePath;
// [親フォルダを抽出] https://qiita.com/takano_tak/items/acf34b4a30cb974bab65
size_t path_i = FilePath.find_last_of("\\/");
folderPath = FilePath.substr(0, path_i + 1);
DataJson::Load(FilePath); // 基底クラスDataJsonでjsonをロード
assert(Data.count("type") != 0 && "jsonファイルの中身が空かタイルマップの.tmx由来じゃないかも");
assert(Data["type"] == "map" && "jsonファイルがタイルマップの.tmx由来じゃないかも");
tilesets.LoadTsx(&Data, &folderPath); // .tsxからタイル情報を取得
tmxInfo << Data; // データの中身を事前読み取り
SetGridSize({ (float)tmxInfo.tilewidth,(float)tmxInfo.tileheight,(float)tmxInfo.tilewidth });
assert(Data.count("layers") != 0 && "jsonにlayers情報が見つからない");
LoadTMXLayer(filePath, Data, groups);
}
protected:
void LoadTMXLayer(const std::string& filePath, json& layerData, Group& groupNode, int terrainNum = 0, int objectsNum = 0, int AStarNum = 0)
{ // 各レイヤをループ
for (auto& layer : layerData["layers"])
{
if (layer["type"] == "tilelayer" || layer["type"] == "objectgroup" || layer["type"] == "group")
{
if (layer["type"] == "tilelayer")
{
//[文字列発見] https://cpprefjp.github.io/reference/string/basic_string/find.html
std::string layerName = layer["name"];
if (layerName.find("AStar") != std::string::npos)
{ // "AStar..."てレイヤーなので強制的にAStarNumで0123..と番号をつけなおす
layer["name"] = "AStar" + std::to_string(AStarNum++);
}
}
}
else
assert("対応していない形式(tilelayer,objectgroup,group以外)のレイヤーが見つかりました(タイルエディタで未対応形式のレイヤー作ってない?)" == "");
if (layer["type"] == "group") // レイヤのタイプが groupのとき
{
groupNode.children.emplace_back((int)layer["id"]);
LoadTMXLayer(filePath, layer, groupNode.children.back(), terrainNum, objectsNum, AStarNum);
}
else if (layer["type"] == "objectgroup") // レイヤのタイプが objectgroupのとき
{
groupNode.emplace_back((int)layer["id"]);
//bool isTerrain = false;
objects.GroupName = layer["name"];
if (objects.GroupName.find("Enemy") != std::string::npos)
{
enemies.spawnType = TMXobject::SpawnType::Enemy;
enemies << layer; // 敵のスポーンデータを読み取り
enemies.InitObjects("Enemy", tmxInfo.tilewidth, tmxInfo.tileheight);
}
else if (objects.GroupName.find("Light") != std::string::npos)
{
lights.spawnType = TMXobject::SpawnType::Light;
lights << layer; // 光源のデータを読み取り
lights.InitObjects("Light", tmxInfo.tilewidth, tmxInfo.tileheight);
}
else if (objects.GroupName.find("Item") != std::string::npos)
{
items.spawnType = TMXobject::SpawnType::Item;
items << layer; // アイテムのデータを読み取り
items.InitObjects("Item", tmxInfo.tilewidth, tmxInfo.tileheight);
}
else
{
objects.spawnType = TMXobject::SpawnType::Object;
objects << layer; // その他オブジェクトのデータを読み取り
objects.InitObjects("Object", tmxInfo.tilewidth, tmxInfo.tileheight);
}
}
else if (layer["type"] == "tilelayer")
{
assert(layer.count("width") != 0 && layer.count("height") != 0 && "json(TMX)のレイヤーのwidthかheightの情報がない??");
groupNode.emplace_back((int)layer["id"]);
bool isTerrain = false;
std::string layerName = layer["name"];
{
auto pos = layerName.find("terrain");
if (pos != std::string::npos)
{
size_t endPos = std::string("terrain").size();
std::string numStr = layerName.substr(pos + endPos, layerName.size() - 1);
std::istringstream ss(numStr); //文字列ストリーム
int numZ; ss >> numZ;
if (pos != std::string::npos)
{
auto& terrainZ = terrains[numZ];
terrainZ.tilesets = &(this->tilesets);
terrainZ.FilePath = this->FilePath;
terrainZ.z = numZ;
terrainZ << layer; // layer["data"]を <<で読み取る
isTerrain = true;
}
}
}
}
}
}
};
#endif
CollisionParams.hを新規作成
して、jsonから衝突関連のパラメータを読み取れるようにしましょう。
#ifndef COLLISIONPARAMS_H_
#define COLLISIONPARAMS_H_
#include "TMXJson.h"
#include "Object.h"
struct CollisionParams : public InitParams
{ //[離散要素法] https://qiita.com/konbraphat51/items/157e5803c514c60264d2
float k = 10.0f; // バネ係数(めり込みに対するバネ反発の掛け率)
float e = 0.3f; // 跳ね返り係数(直前と比べどれくらいの速さで離れるか 0)
inline float log_e() { return std::log(e); };
const float pi_2 = DX_PI_F * DX_PI_F;
// 粘性係数
inline float eta(float mass) { return -2.0f * log_e() * std::sqrt(k * mass / (log_e() * log_e() + pi_2)); }
float pushPower{ 5.0f }; // kやeの設定がなかった場合はデフォルトのpushPowerで法線方向に押し返す
bool isDEMcollision = false; //[離散要素法:DEM]のパラメータを使用するか(falseのときはpushPowerで単純な押し戻し)
inline CollisionParams(std::shared_ptr<InitParams> initParams = nullptr)
{
if (initParams == nullptr) return;
auto tmxParams = std::static_pointer_cast<InitTMXProperty>(initParams);
if (tmxParams->properties == nullptr) return;
auto tmxProperties = tmxParams->properties;
// tmxのjsonの追加プロパティからデータを読み取る
auto itr = tmxProperties->find("collision_e");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Float && "json の collision_e のタイプが Floatでない!jsonのプロパティを見直してください");
e = itr->second.floatValue;
isDEMcollision = true;
}
itr = tmxProperties->find("collision_k");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Float && "json の collision_k のタイプが Floatでない!jsonのプロパティを見直してください");
k = itr->second.floatValue;
isDEMcollision = true;
}
itr = tmxProperties->find("pushPower");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Float && "json の pushPower のタイプが Floatでない!jsonのプロパティを見直してください");
pushPower = itr->second.floatValue;
}
}
~CollisionParams() {}
};
#endif
HitPart.hを新規作成
して、タイルなどに当たった箇所を表すデータ定義を作成します。
#ifndef HIT_PART_H_
#define HIT_PART_H_
#include <string>
// タイルなどに当たった箇所を表すenum と Bit演算 HitBit型(int)を使う
typedef int HitBit; // 当たった箇所を表すビット特性 Left | Top なら 左上部分が当たった の特性を表す
enum class HitPart : HitBit // 当たった箇所を表すビット
{
None = 0,
Left = 0b0000'0000'0000'0000'0000'0000'0000'0001,
Right = 0b0000'0000'0000'0000'0000'0000'0000'0010,
Top = 0b0000'0000'0000'0000'0000'0000'0000'0100,
Bottom = 0b0000'0000'0000'0000'0000'0000'0000'1000,
XMiddle = 0b0000'0000'0000'0000'0000'0000'0001'0000,
YMiddle = 0b0000'0000'0000'0000'0000'0000'0010'0000,
Forward = 0b0000'0000'0000'0000'0000'0000'0100'0000,
Back = 0b0000'0000'0000'0000'0000'0000'1000'0000,
ZMiddle = 0b0000'0000'0000'0000'0000'0001'0000'0000,
};
// 文字列 stringType から HitPart型に変換する
inline HitPart enumHitPart(const std::string& stringType)
{
return
(stringType == "Left") ? HitPart::Left :
(stringType == "Right") ? HitPart::Right :
(stringType == "Top") ? HitPart::Top :
(stringType == "Bottom") ? HitPart::Bottom :
(stringType == "XMiddle") ? HitPart::XMiddle :
(stringType == "YMiddle") ? HitPart::YMiddle :
(stringType == "Forward") ? HitPart::Forward :
(stringType == "Back") ? HitPart::Back :
(stringType == "ZMiddle") ? HitPart::ZMiddle :
HitPart::None;
}
// [ビット論理和 演算子 | のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr HitBit operator | (HitPart left, HitPart right) { return static_cast<HitBit>(left) | static_cast<HitBit>(right); }
// [ビット論理和 演算子 | のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr HitBit operator | (HitBit left, HitPart right) { return left | static_cast<HitBit>(right); }
// [ビット論理和 演算子 | のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr HitBit operator | (HitPart left, HitBit right) { return static_cast<HitBit>(left) | right; }
// [ビット論理積 演算子 & のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr HitBit operator & (HitPart left, HitPart right) { return static_cast<HitBit>(left) & static_cast<HitBit>(right); }
// [ビット論理積 演算子 & のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr HitBit operator & (HitBit left, HitPart right) { return left & static_cast<HitBit>(right); }
// [ビット論理積 演算子 & のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr HitBit operator & (HitPart left, HitBit right) { return static_cast<HitBit>(left) & right; }
// [ビット排他的論理和 演算子 ^ のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr HitBit operator ^ (HitPart left, HitPart right) { return static_cast<HitBit>(left) ^ static_cast<HitBit>(right); }
// [ビット排他的論理和 演算子 ^ のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr HitBit operator ^ (HitBit left, HitPart right) { return left ^ static_cast<HitBit>(right); }
// [ビット排他的論理和 演算子 ^ のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr HitBit operator ^ (HitPart left, HitBit right) { return static_cast<HitBit>(left) ^ right; }
// [一致演算子 == ]のオーバーロード HitPart型を HitBit型に変換して比較
inline constexpr bool operator == (HitPart left, HitBit right) { return static_cast<HitBit>(left) == right; }
// [一致演算子 == ]のオーバーロード HitPart型を HitBit型に変換して比較
inline constexpr bool operator == (HitBit left, HitPart right) { return left == static_cast<HitBit>(right); }
// [不一致演算子 != ]のオーバーロード HitPart型を HitBit型に変換して比較
inline constexpr bool operator != (HitPart left, HitBit right) { return !(left == right); }
// [不一致演算子 != ]のオーバーロード HitPart型を HitBit型に変換して比較
inline constexpr bool operator != (HitBit left, HitPart right) { return !(left == right); }
#endif
Tile.hを新規作成
して、壁、床、壊せるなどのビット特性を表現する定義と設定を「Map/MapSetting.csv」から読み出すクラスを作成します。
#ifndef TILE_H_
#define TILE_H_
#include <string> //文字列に必要
#include <unordered_map>
#include <vector>
#include "DataCsv.h"
#include "DataTsx.h"
#include "MyDraw.h"
#include "MyHash.h" // hash32でタグを高速化
#include "HitPart.h" // タイルに当たった箇所を表すenum と Bit演算 HitBit型(int)を使う
typedef int TileBit; // タイルのビット特性 Wall | Bounce なら はずむ壁 の特性を持つ
enum class TileType : TileBit //★ブロックの特性を32桁のビットで表現
{
None = 0,
Wall = 0b0000'0000'0000'0000'0000'0000'0000'0001,
Damage = 0b0000'0000'0000'0000'0000'0000'0000'0010,
Bounce = 0b0000'0000'0000'0000'0000'0000'0000'0100, // 叩くと弾むブロック
Change = 0b0000'0000'0000'0000'0000'0000'0000'1000, // 叩くとブロックの種類が変わる
Switch = 0b0000'0000'0000'0000'0000'0000'0001'0000, // スイッチを叩くと赤ブロックや青ブロックの無効⇔有効が切り替え
Goal = 0b0000'0000'0000'0000'0000'0000'0010'0000,
Floor = 0b0000'0000'0000'0000'0000'0000'0100'0000,
};
// 文字列 stringType から TileType型に変換する
inline TileType enumTileBit(const std::string& stringType)
{
return
(stringType == "Wall") ? TileType::Wall :
(stringType == "Damage") ? TileType::Damage :
(stringType == "Bounce") ? TileType::Bounce :
(stringType == "Change") ? TileType::Change :
(stringType == "Switch") ? TileType::Switch :
(stringType == "Goal") ? TileType::Goal :
(stringType == "Floor") ? TileType::Floor :
TileType::None;
}
// [ビット論理和 演算子 | のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr TileBit operator | (TileType left, TileType right) { return static_cast<TileBit>(left) | static_cast<TileBit>(right); }
// [ビット論理和 演算子 | のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr TileBit operator | (TileBit left, TileType right) { return left | static_cast<TileBit>(right); }
// [ビット論理和 演算子 | のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr TileBit operator | (TileType left, TileBit right) { return static_cast<TileBit>(left) | right; }
// [ビット論理積 演算子 & のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr TileBit operator & (TileType left, TileType right) { return static_cast<TileBit>(left) & static_cast<TileBit>(right); }
// [ビット論理積 演算子 & のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr TileBit operator & (TileBit left, TileType right) { return left & static_cast<TileBit>(right); }
// [ビット論理積 演算子 & のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr TileBit operator & (TileType left, TileBit right) { return static_cast<TileBit>(left) & right; }
// [ビット排他的論理和 演算子 ^ のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr TileBit operator ^ (TileType left, TileType right) { return static_cast<TileBit>(left) ^ static_cast<TileBit>(right); }
// [ビット排他的論理和 演算子 ^ のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr TileBit operator ^ (TileBit left, TileType right) { return left ^ static_cast<TileBit>(right); }
// [ビット排他的論理和 演算子 ^ のオーバーロードでenum型を暗黙変換] https://marycore.jp/prog/cpp/enum-bitmask-bitwise-operation/
inline constexpr TileBit operator ^ (TileType left, TileBit right) { return static_cast<TileBit>(left) ^ right; }
// [一致演算子 == ]のオーバーロード HitPart型を HitBit型に変換して比較
inline constexpr bool operator == (TileType left, TileBit right) { return static_cast<TileBit>(left) == right; }
// [一致演算子 == ]のオーバーロード HitPart型を HitBit型に変換して比較
inline constexpr bool operator == (TileBit left, TileType right) { return left == static_cast<TileBit>(right); }
// [不一致演算子 != ]のオーバーロード HitPart型を HitBit型に変換して比較
inline constexpr bool operator != (TileType left, TileBit right) { return !(left == right); }
// [不一致演算子 != ]のオーバーロード HitPart型を HitBit型に変換して比較
inline constexpr bool operator != (TileBit left, TileType right) { return !(left == right); }
struct Tile // タイル情報
{
static constexpr unsigned int None = 0; // 何もないことを表す gid
Tile() = default;
inline Tile(int id, float cost = 1.0f, TileBit typeBit = (TileBit)TileType::None, const std::string& typeTag = "", const CsvValue& value = CsvValue())
: id{ id }, cost{ cost }, typeBit{ typeBit }, typeTag{ typeTag }, value{ value } {};
~Tile() {};// デストラクタ
int id = -1; // タイルid
float cost = 1; // AIの経路判定で使う移動コスト
TileBit typeBit = (TileBit)TileType::None; // タイルのタイプ
int changeID{ 0 }; // 変化先ID
hash32 typeTag{ "" }; // タイルのタグ
CsvValue value{ "" }; // 値(ダメージ値など 文字列も使用できる)
std::vector<int> syncIDs; // 連動するID (sync:シンクロ)ブロックを叩いたら連動して他も変わるなどに使う
};
// "Map/MapSettings.csv"から読み出すタイルの情報
struct TileInfo
{
protected:
TileBit noneBit{ 0 }; // id = -1 のときのBit
// <gid→idに対応したタイプのbit> へ変換するvector型の辞書
std::vector<TileBit> idTypeBits;
public:
std::unordered_map<int, Tile> tiles; // タイル情報
inline Tile& operator[](int id) {
return tiles[id]; // &参照で読み書き
}
inline TileBit& idTypeBit(int id)
{
if (id >= (int)idTypeBits.size()) idTypeBits.resize(id + 1, 0);
return (id < 0) ? noneBit : idTypeBits[id];
}
// id→ビットのタイプ の辞書に |= でbitTypeをビット論理和して、Wall:壁 や Bounce:はずむ などの特性を追加する
TileBit SetTypeBit(int id, TileBit bitType) { return idTypeBit(id) |= bitType; }
// id→ビットのタイプ の辞書で & ビット論理積で 指定したidが Wall:壁 や Bounce:はずむ などの特性を持つかを判定する
inline bool isType(int id, TileType bitType) { return idTypeBit(id) & bitType; }
// id→ビットのタイプ の辞書で & ビット論理積で 指定したidが Wall:壁 や Bounce:はずむ などの特性を持つかを判定し一致したBitを返す(複数が1のビットもありうる)
inline TileBit matchTypes(int id, TileBit bitType) { return idTypeBit(id) & bitType; }
inline bool IsWall(int id) {
return isType(id, TileType::Wall);
}
};
// "Map/MapSettings.csv"からタイルの設定情報を読み出すクラス
struct TileSetting
{
TileSetting(const std::string& csvFileName = "")
{
if (csvFileName != "") Load(csvFileName);
}
~TileSetting() {} //デストラクタ
//[辞書].tsxファイル名→タイル情報
std::unordered_map<hash32, std::shared_ptr<TileInfo>> info;
inline std::shared_ptr<TileInfo> operator[](const std::string& tsxPath) {
return info[tsxPath]; // 読み書き
}
bool isInitialized = false; //[継承2重ロード対策]
std::string FilePath{ "" }; // csvのファイルパス
DataCsv Data; // マップ情報一覧のデータ
// 例.csvのセルに"Wall | Bounce | Change"と書かれたいたら TileType::Wall Bounce Changeが1となったビットを返す
TileBit bitFromCsvCell(const std::string& csvCellString)
{
TileBit returnBit = (TileBit)TileType::None;
if (csvCellString == "") return returnBit;
std::istringstream ss(csvCellString); // 文字列ストリーム
std::string typeString; // | での分割文字列
while (std::getline(ss, typeString, { '|' }))
{ // [std::isspaceで空白除去] https://b.0218.jp/20150625194056.html
typeString.erase(std::remove_if(typeString.begin(), typeString.end(), std::isspace), typeString.end());
returnBit |= (TileBit)enumTileBit(typeString); // ビット論理和でタイルのビット特性を付加していく 例. Wall | Bounce はずむ壁
}
return returnBit;
}
// 例..csvのセルに"12 | 13 | 15"と書かれていたら配列に[12][13][15]を入れて返す
std::vector<int> intFromCsvCell(const std::string& csvCellString)
{
std::vector<int> returnInts;
if (csvCellString == "") return returnInts;
std::istringstream ss(csvCellString); // 文字列ストリーム
std::string intString; // | での分割文字列
while (std::getline(ss, intString, { '|' }))
{ // [std::isspaceで空白除去] https://b.0218.jp/20150625194056.html
intString.erase(std::remove_if(intString.begin(), intString.end(), std::isspace), intString.end());
returnInts.emplace_back((int)std::stol(intString)); // stolで文字列→数値に
}
return returnInts;
}
void Load(const std::string& csvFileName = "")
{
if (isInitialized) return; // 2重ロード対策
if (csvFileName != "") this->FilePath = csvFileName;
if (this->FilePath == "") return;
Data.Load(this->FilePath, DataCsv::CsvType::CsvValue);// CSVファイルからデータをロード
std::string tsxFile; //tsxファイル名
for (int j = 0; j < Data.size(); ++j)
{
//std::string test = (std::string)Data[j][0];
if ((std::string)Data[j][0] == "//") continue; // 一番左の列が//なら読み飛ばしのコメント行
else if ((std::string)Data[j][0] == "TsxFile")
{
tsxFile = Data[j][1]; //tsxファイル名
info.emplace(tsxFile, std::make_shared<TileInfo>());
continue;
}
if (tsxFile == "") continue; //ステージファイル名が未確定
else if (Data[j].size() > 1 && (std::string)Data[j][1] != "")
{
int tileID = Data[j][1]; // タイルID
info[tsxFile]->tiles[tileID].id = tileID; // タイルIDを辞書登録
if ((std::string)Data[j][2] != "")
info[tsxFile]->tiles[tileID].cost = Data[j][2]; //コスト登録
info[tsxFile]->tiles[tileID].typeTag = (std::string)Data[j][3]; //タグ登録
if ((std::string)Data[j][4] != "") // タイプ登録 None,Wall,Floor..など
info[tsxFile]->tiles[tileID].typeBit = info[tsxFile]->SetTypeBit(tileID, bitFromCsvCell(Data[j][4]));
if (Data[j].size() > 5) // 値(ダメージ値など)
info[tsxFile]->tiles[tileID].value = Data[j][5];
if (Data[j].size() > 6) // 変化先ID
info[tsxFile]->tiles[tileID].changeID = Data[j][6];
if (Data[j].size() > 7) // 連動ID
info[tsxFile]->tiles[tileID].syncIDs = std::move(intFromCsvCell(Data[j][7]));
}
}
isInitialized = true;
}
};
#endif
Map.hを新規作成
して、タイルマップエディタで作成したステージを表現するMapクラスを作成してタイルマップを3D表示させます(現状当たり判定と敵の出現はまだない)。
#ifndef MAP_H_
#define MAP_H_
#include <vector> // C#におけるListリストの機能
#include <unordered_map> // 辞書型連想配列に必要
#include <memory> // メモリを扱う【共有ポインタに必要】
#include <limits> //intやfloatなどの無限大∞に使う https://cpprefjp.github.io/reference/limits/numeric_limits/max.html
#include <fstream> // ファイル読み出しifstreamに必要
#include <string> //文字列に必要
#include <sstream> // 文字列ストリームに必要
#include <assert.h> // マップ読込み失敗表示用
#include "Screen.h"
#include "MyMath.h"
#include "MyHash.h"
#include "MyDraw.h"
#include "DataCsv.h"
#include "TMXJson.h"
#include "Singleton.h"
#include "GameObject.h"
#include "World.h"
#include "Camera.h"
#include "MapRange.h" // マス目のレンジ表現するクラス
#include "Tile.h"
#include "Vector3.h"
class AI_Map; // 前方宣言
// csvから読み取るマップの基本情報
struct MapInfo
{
MapInfo() = default;
inline MapInfo(unsigned int CellSize, int SpawnRangeX, int SpawnRangeY, MapRange::Type type = MapRange::Type::Rect)
: CellSize{ CellSize }, spawnRange{ MapRange(SpawnRangeX,SpawnRangeY,type) } {};
~MapInfo() {};
unsigned int CellSize{ 1 }; // マップの1マスのピクセル数
MapRange spawnRange; // 敵の新規出現レンジ
};
struct MapSetting // マップ情報をMapSetting.csvなどから読みだす
{
MapSetting(const std::string& csvFileName = "")
{
if (csvFileName != "")
Load(csvFileName);
}
~MapSetting() {}; //デストラクタ
std::string FilePath{ "" }; // csvのファイルパス
std::unordered_map<hash32, std::shared_ptr<MapInfo>> info; //[辞書]stageファイル名→マップ情報
MapInfo& operator[](const std::string& stageFilePath) {
assert(info.count(stageFilePath) != 0 && "マップのステージ情報が読み取れていません Map/MapSettings.csv にステージ情報があるか確認!");
return *info[stageFilePath]; // 読み書き
}
void Load(const std::string& csvFileName = "")
{
if (csvFileName != "") this->FilePath = csvFileName;
if (this->FilePath == "") return;
// マップ情報のデータ CSVファイルからデータをロード
DataCsv Data{ this->FilePath, DataCsv::CsvType::CsvValue };
std::string stageFile; //ステージファイル名
for (int j = 0; j < Data.size(); ++j)
{
if ((std::string)Data[j][0] == "//")
continue; // 一番左の列が//なら読み飛ばしのコメント行
else if ((std::string)Data[j][0] == "File")
{
stageFile = Data[j][1]; //ステージファイル名
info.emplace(stageFile, std::make_shared<MapInfo>());
continue;
}
if (stageFile == "") continue; //ステージファイル名が未確定
else if ((std::string)Data[j][0] == "CellSize")
info[stageFile]->CellSize = (int)Data[j][1]; //マス目サイズ
else if ((std::string)Data[j][0] == "SpawnRange")
{ // 敵出現レンジ
MapRange::Type rangeType = ((std::string)Data[j][3] == "Ellipse") ? MapRange::Type::Ellipse : MapRange::Type::Rect; //敵出現のタイプ
info[stageFile]->spawnRange.InitRange(rangeType, Data[j][1].intFromString(), Data[j][2].intFromString()); //敵出現のタイプを初期化設定
}
}
}
};
// ステージのタイルマップのクラス
class Map
{
public:
// コンストラクタ
Map(World* world, const std::string& filePath = "")
: m_world{ world }
{
if (filePath == "") return;
Load(filePath);
}
// デストラクタ
~Map()
{ // マップデータのお掃除
enemyData.clear();
//terrains.clear(); //★地形のお掃除
}
static MapSetting settings; // マップに関する設定 Map.cpp で初期読込み デフォルトは"Map/MapSetting.csv" を読み込む
static TileSetting tileSettings; // タイルに関する設定 Map.cpp で初期読込み デフォルトは"Map/MapSetting.csv" を読み込む
// タイルのチップ画像のパスとidからTileの情報とタイルのgid情報を得る
static Tile GetTile(const std::string& mapChipPath, int id, unsigned int& gid)
{
auto pos = mapChipPath.find(".", 0);
std::string tsxPath{ "" };
if (pos != std::string::npos) tsxPath = mapChipPath.substr(0, pos) + ".tsx";
auto found = Tilesets::umTsxInfo.find(tsxPath);
if (found != end(Tilesets::umTsxInfo) && found->second.gid.size() > id)
gid = found->second.gid[id]; // 辞書で id から gidに変換しておく
else return Tile(); // id が-1のときは見つからなかったとき
pos = tsxPath.find("/", 0);
if (pos != std::string::npos) tsxPath = tsxPath.substr(pos + 1, tsxPath.size() - 1);
return Map::tileSettings.info[tsxPath]->tiles[id];
}
protected:
World* m_world{ nullptr }; // マップに配置する敵などのポインタを保持するワールド
public:
// 現在いるワールドへのポインタを得る
World* world() { return m_world; }
// 現在いるワールドのポインタを変更する
void world(World* changeWorld) { m_world = changeWorld; }
std::shared_ptr<AI_Map> aiMap{ nullptr }; // AIの経路探索に使うAIマップ
// 読込んだデータファイルの情報
std::string FilePath{ "" };
protected:
unsigned int m_CellSize{ 1 }; // マップの1マスのピクセル数 1のときはtmxに設定されたサイズが採用される
unsigned int m_HalfCellSize{ 1 }; // マス目サイズの 1/2
public:
inline unsigned int CellSize() { return m_CellSize; }
inline unsigned int HalfCellSize() { return m_HalfCellSize; }
inline void SetCellSize(unsigned int cellSize) { m_CellSize = cellSize; m_HalfCellSize = m_CellSize / 2; }
MapRange spawnRange; // 敵の新規出現レンジをプレイヤから半径X,Yの楕円にする
inline float centerX(int cellX) const { return (cellX >= 0) ? (float)((cellX * m_CellSize) + m_HalfCellSize) : (float)((cellX + 1) * m_CellSize) - m_HalfCellSize; } //マス目サイズ/2ずらし
inline float centerY(int cellY) const { return (cellY >= 0) ? (float)((cellY * m_CellSize) + m_HalfCellSize) : (float)((cellY + 1) * m_CellSize) - m_HalfCellSize; } //マス目サイズ/2ずらし
inline float centerZ(int cellZ) const { return (cellZ >= 0) ? (float)((cellZ * m_CellSize) + m_HalfCellSize) : (float)((cellZ + 1) * m_CellSize) - m_HalfCellSize; } //マス目サイズ/2ずらし
inline int cellX(float worldX) const { return (worldX >= 0) ? (int)worldX / m_CellSize : (int)worldX / m_CellSize - 1; } // マイナス方向にも対応
inline int cellY(float worldY) const { return (worldY >= 0) ? (int)worldY / m_CellSize : (int)worldY / m_CellSize - 1; } // マイナス方向にも対応
inline int cellZ(float worldZ) const { return (worldZ >= 0) ? (int)worldZ / m_CellSize : (int)worldZ / m_CellSize - 1; } // マイナス方向にも対応
TMXCsv enemyData;// 敵配置データ
TMXJson tmxData; // タイルマップエディタのjsonデータ
protected:
// <gid→idに対応したタイプのbit> へ変換するvector型の辞書
std::vector<TileBit> gidTypeBits{ 0 };
public:
inline TileBit& gidTypeBit(unsigned int gid)
{
if (gid >= (int)gidTypeBits.size()) gidTypeBits.resize(gid + 1, 0);
return (gid < 0) ? gidTypeBits[0] : gidTypeBits[gid];
}
// id→ビットのタイプ の辞書に |= でbitTypeをビット論理和して、Wall:壁 や Bounce:はずむ などの特性を追加する
TileBit SetTypeBit(unsigned int gid, TileBit bitType) { return gidTypeBit(gid) |= bitType; }
// id→ビットのタイプ の辞書で & ビット論理積で 指定したidが Wall:壁 や Bounce:はずむ などの特性を持つかを判定する
inline bool isType(unsigned int gid, TileType bitType) { return gidTypeBit(gid) & bitType; }
// id→ビットのタイプ の辞書で & ビット論理積で 指定したidが Wall:壁 や Bounce:はずむ などの特性を持つかを判定し一致したBitを返す(複数が1のビットもありうる)
inline TileBit matchTypes(unsigned int gid, TileBit bitType) { return gidTypeBit(gid) & bitType; }
// tilesetsの.tsxのfirstgidをたどって、SetTypeBitでビット辞書に
// csvのtileSettingsから読み取っておいたビット設定を登録
bool InitTypeBit(const Tilesets& tilesets)
{
assert(tilesets.size() > 0 && "ステージのjsonファイルにタイルセット設定が一つもありません");
if (tilesets.size() == 0) return false;
for (auto& pair : tilesets)
{
auto firstgid = pair.first;
auto& tsxFilePath = pair.second;
auto& tsxInfo = tilesets.umTsxInfo.at(tsxFilePath);
// [親フォルダを抽出] https://qiita.com/takano_tak/items/acf34b4a30cb974bab65
size_t path_i = tsxFilePath.find_last_of("\\/");
if (path_i + 1 >= tsxFilePath.size()) continue;
auto tsxFile = tsxFilePath.substr(path_i + 1);
auto csvInfo = tileSettings.info.find(tsxFile);
if (csvInfo == end(tileSettings.info)) continue; // csv側にタイルの情報がなかった
// gidが0 idが-1 のデフォルトのタイルのビット特性を設定
SetTypeBit(0, csvInfo->second->idTypeBit(-1));
int id = 0; // gidじゃなく0から始まるローカルなid
for (auto gid = firstgid, iEnd = firstgid + tsxInfo.tilecount; gid < iEnd; ++gid)
{ // このMapのgidのビット辞書にcsvのtileSettingsから読み取っておいたビット設定を登録
SetTypeBit(gid, csvInfo->second->idTypeBit(id));
++id; // ローカルなidも次に進める
}
}
}
// マップファイルのロード
void Load(const std::string& filePath)
{
this->FilePath = filePath;
// ★jsonファイルの読み込み
tmxData.Load(filePath);
// tilesetsの.tsxのfirstgidをたどって、SetTypeBitでビット辞書に
// csvのtileSettingsから読み取っておいたビット設定を登録
InitTypeBit(tmxData.tilesets);
auto info = settings[filePath];
if (info.spawnRange.umRange.size() != 0)
spawnRange = info.spawnRange;
else
spawnRange.InitRange(MapRange::Type::Ellipse, 10, 10);// 敵の新規出現レンジをプレイヤから半径X,Yの楕円にする
if (info.CellSize != 1)
{
SetCellSize(info.CellSize); // csvに書かれているCellSizeを設定
tmxData.SetGridSize({(float)info.CellSize ,(float)info.CellSize ,(float)info.CellSize });
}
else // if (info.CellSize == 1)
SetCellSize(tmxData.tmxInfo.tilewidth); // tmxのタイルの幅を取得
}
// 指定された座標(ワールド座標)の地形データを取得する
unsigned int GetTerrain(float worldX, float worldY, float worldZ = (std::numeric_limits<float>::min)())
{
if (worldZ != (std::numeric_limits<float>::min)())
{
float tmpY = worldY;
worldY = worldZ; //【YとZを変換】Zの入力があるときはZをYとして扱う
worldZ = tmpY; //【YとZを変換】
}
// 負の座標が指定された場合は、何も無いものとして扱う
if (worldX < 0 || worldY < 0)
return Tile::None;
// マップ座標系(二次元配列の行と列)に変換する
int mapX = (int)(worldX / m_CellSize);
int mapY = (int)(worldY / m_CellSize);
int mapZ = (int)(worldZ / m_CellSize);
if (tmxData.terrains.count(mapZ) == 0) // zの階層がまだなかったとき
return Tile::None; // 配列の範囲外は、何も無いものとして扱う
auto& terrainZ = tmxData.terrains[mapZ]; // 辞書からZに対応するデータへの参照を得る
if (mapY >= terrainZ.size() || mapX >= terrainZ[mapY].size())
return Tile::None; // 配列の範囲外は、何も無いものとして扱う
return terrainZ[mapY][mapX]; // 二次元配列から地形IDを取り出して返却する
}
// 指定された座標(ワールド座標)の地形データを書き換える return には書き換える前のgidが返る
bool SetTerrain(unsigned int gid, float worldX, float worldY, float worldZ = (std::numeric_limits<float>::min)())
{
if (worldZ != (std::numeric_limits<float>::min)())
{
float tmpY = worldY;
worldY = worldZ; //【YとZを変換】Zの入力があるときはZをYとして扱う
worldZ = tmpY; //【YとZを変換】
}
// 負の座標が指定された場合は、書き換えられない
if (worldX < 0 || worldY < 0)
return false;
// マップ座標系(二次元配列の行と列)に変換する
int mapX = (int)(worldX / m_CellSize);
int mapY = (int)(worldY / m_CellSize);
int mapZ = (int)(worldZ / m_CellSize);
// 二次元配列の範囲外は、書き換えられない
//if (mapY >= terrain.size() || mapX >= terrain[mapY].size())
// return;
bool isNewZ = false;
if (tmxData.terrains.count(mapZ) == 0) isNewZ = true; // 新しい z の階層の場合
auto& terrainZ = tmxData.terrains[mapZ]; // 辞書からZに対応するデータへの参照を得る
if (isNewZ) // 新しい z の階層の場合
{
terrainZ.z = mapZ;
terrainZ.tilesets = &(tmxData.tilesets); // 新しい z の階層のtilesetsを設定する
terrainZ.Width = tmxData.tmxInfo.width; terrainZ.Height = tmxData.tmxInfo.height;
auto& terrinZYX = terrainZ[mapY][mapX];
terrinZYX.tilesets = &(tmxData.tilesets);
terrinZYX = gid; // 地形データ書き換え
}
else
terrainZ[mapY][mapX] = gid; // 地形データ書き換え
return true;
}
// gidに応じてタイルを描き分ける
void DrawTile(unsigned int gid, float worldX, float worldY, float worldZ, float ExRate)
{
auto texture = TsxInfo::id_images[gid].second.lock();
int id = TsxInfo::id_images[gid].first;
if (isType(gid, TileType::Floor)) // 板ポリゴンで床を描く
MyDraw::DrawDivRotaFloorF3D(MyDraw::Plane::Y, worldX, worldY, worldZ, ExRate, 0, *texture, id);
else if (isType(gid, TileType::Wall)) //壁のときはボックスを描く
MyDraw::DrawDivBoxF3D(worldX, worldY, worldZ, ExRate, *texture, id);
}
// 地形をタイルで描く
void DrawTerrain()
{
for (auto&& terrainN : tmxData.terrains)
{
auto& terrainZ = *terrainN.second;
int cellZ = terrainN.first;
for (int cellY = 0, ySize = terrainZ.size(); cellY < ySize; cellY++)
{
for (int cellX = 0, xSize = terrainZ[cellY].size(); cellX < ySize; cellX++)
{
float x = centerX(cellX); //マス目サイズ/2ずらし
float y = centerY(cellY); //マス目サイズ/2ずらし
float z = centerZ(cellZ); //マス目サイズ/2ずらし
int id = -1;
unsigned int gid = 0;
if (cellY < terrainZ.size() && cellX < terrainZ[cellY].size())
{
gid = terrainZ[cellY][cellX];
}
if (0 <= gid && gid < TsxInfo::id_images.size())
{
auto pTileImage = TsxInfo::id_images[gid].second.lock();
if (pTileImage == nullptr) continue;
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
for (auto&& keyValue : world()->cameras) // ワールドにあるすべてのカメラぶんループ
{
auto& camera = keyValue.second;
//auto stepXYZs = MyMath::CrossGridVec(camera->pos - camera->m_targetXYZ, Vector3{ CellSize, CellSize, CellSize });
camera->Draw([&]()
{
float ExRate = (float)m_CellSize / (float)pTileImage->m_XSize;
DrawTile(gid, x, z, y, ExRate); // idに応じてタイルを描き分ける
});
}
}
}
}
}
}
// 指定された座標(ワールド座標)の地形が壁か調べる
inline bool IsWall(float worldX, float worldY, float worldZ = (std::numeric_limits<float>::min)())
{
auto terrainID = GetTerrain(worldX, worldY, worldZ); // 指定された座標の地形のIDを取得
return IsWall(terrainID);
}
//あるIDが壁かどうかだけ調べる
inline bool IsWall(unsigned int gid)
{ // 地形が壁ならtrue、違うならfalseを返却する
return isType(gid, TileType::Wall);
}
// 指定された座標worldPosの地形が 壁 や ダメージブロックかなどをチェックする関数
bool checkID(TileBit checkBit, float worldX, float worldY, float worldZ,
int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, unsigned int* pTerrainID = nullptr, TileBit* pMatchBits = nullptr)
{
unsigned int terrainID; // 指定された座標の地形のID
if (worldZ != (std::numeric_limits<float>::min)())
{ //【YとZを変換】Zの入力があるときはZをYとして扱う タイルマップの X,Yは3DではYがZ:奥行に対応するから
float tmpY = worldY;
worldY = worldZ;
worldZ = tmpY; //【YとZを変換】
}
if (worldX < 0 || worldY < 0) return false; // 負の座標が指定された場合
// マップ座標系(二次元配列の行と列)に変換する
int mapX = cellX(worldX), mapY = cellY(worldY), mapZ = cellZ(worldZ);
if (tmxData.terrains.count(mapZ) == 0) // zの階層がまだなかったときは 0
terrainID = 0; // 範囲外のときは 0
else // zの階層がすでにあったとき
{ // 二次元配列の範囲内か判定
auto& terrainZ = tmxData.terrains[mapZ];
if (mapY >= terrainZ.size() || mapX >= terrainZ[mapY].size())
terrainID = 0; // 範囲外のときは 0
else
terrainID = terrainZ[mapY][mapX]; // 配列から地形IDを取り出す
}
if (pTerrainID != nullptr)
*pTerrainID = terrainID;
TileBit matchBits = matchTypes(terrainID, checkBit); // ビット論理積 & で地形のchekckBitを確かめる
bool isCheckBit = matchBits != (TileBit)TileType::None;
if (isCheckBit && pMapX != nullptr && pMapY != nullptr && pMapZ != nullptr)
*pMapX = mapX, * pMapY = mapY, * pMapZ = mapZ; // ブロックのマス目のセル番号をポインタに記録(nullptrの場合は記録しない)
if (pMatchBits != nullptr)
*pMatchBits = matchBits; // マッチしたビットをポインタ先に代入して返す
return isCheckBit;
}
};
#endif
Map.cppを新規作成
して、static変数の初期化でcsvなどをロードしましょう。
#include "Map.h"
MapSetting Map::settings{ "Map/MapSetting.csv" }; // マップ一覧情報
TileSetting Map::tileSettings{ "Map/MapSetting.csv" }; // タイル情報
World.hを変更
して、Mapの前方宣言と共有ポインタとマップをロードする関数を宣言しましょう。
#ifndef WORLD_H_
#define WORLD_H_
#include <list>
#include <memory>
#include <unordered_map>
#include <functional> // std::functionのラムダ式 で 関数を Draw関数の引数にして、カメラすべてに対して描画処理を発動させる
#include "MyHash.h" // ワールドのタグをhash32型で管理する
// [前方宣言] 宣言だけして、#includeはしてないので、ポインタだけはこのファイル内でつかえる
// #includeはしてないので、hpなどの変数には#include "~.h"しないとアクセスできないので循環インクルード防止のため.cppでインクルードしてください
class GameScene;
class Player;
class Bullet;
class Enemy;
class Camera;
class Grid;
class Map;
// プレイヤや敵や弾などの共有ポインタを管理し、EraseRemoveIfで消して参照カウンタが0になったらそれらを消滅させるクラス(C#のガベージコレクタの代わり)
class World
{
public:
inline World(GameScene* pScene, const std::string& worldTag = "") : m_pScene{ pScene }, tag{ worldTag } {}
inline ~World() {}
protected: // public:にすると他で外部で後からタグやシーンを書き換えられると辞書での管理などの前提がおかしくなるのでprotected:する
hash32 tag{ "" }; // ワールド名のタグ
GameScene* m_pScene{ nullptr }; // ワールドを所有するシーン
public:
hash32 Tag() const { return tag; }
GameScene* scene() { return m_pScene; }
//std::unordered_map<hash32, std::shared_ptr<Player>> players; // プレイヤのタグ辞書
// ワールドを所有するシーンにプレイヤを追加する
void AddPlayer(const std::string& playerTag, std::shared_ptr<Player> pPlayer);
// ワールドにいるプレイヤを得る
std::vector<std::weak_ptr<Player>> GetPlayers();
bool LoadMap(const std::string& mapFilePath); // ワールドのマップをロード
std::shared_ptr<Map> map{ nullptr }; // マップ(共有ポインタ) マップのメモリ解放は World と運命をともにして消える
protected:
std::shared_ptr<Grid> m_pGrid{ nullptr }; // 当たり判定を高速にするためグリッドで空間分割
public:
Grid* grid() { return m_pGrid.get(); } // グリッドを返す
Grid* makeGrid(int cellWidth = 128, int cellHeight = 128, int cellDepth = 128); // グリッドを生成する
std::unordered_map<hash32, std::shared_ptr<Player>> players; // プレイヤのタグ辞書
std::list<std::shared_ptr<Bullet>> bullets; // 自機弾のリスト
std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
// 削除処理を共通テンプレート関数にする
// [共通テンプレート関数]https://programming-place.net/ppp/contents/cpp/language/009.html#function_template
template <typename TypeT, class T_if>
void EraseRemoveIf(std::list<TypeT>& 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()の位置
};
std::unordered_map<hash32, std::shared_ptr<Camera>> cameras; // このワールドに存在するカメラの辞書<カメラにつけたタグ, カメラの共有ポインタ>
// 指定されたタグのカメラへの共有ポインタを得る カメラ辞書に存在しないタグでアクセスしたときにunordered_mapの特性で勝手に意図しないカメラができるのを防ぐ関数
std::shared_ptr<Camera> camera(const std::string& cameraTag) { auto itr = cameras.find(cameraTag); return (itr != end(cameras)) ? itr->second : nullptr; }
// このワールドに存在するカメラすべてに対して drawFuncで描きたい処理を渡して 全カメラのスクリーンに対して描画処理を走らせる
void Draw(std::function<void()> drawFunc) noexcept;
// カメラにタグをつけてワールドに追加 カメラにもワールドへのポインタリンクを張る
inline bool AddCamera(const std::string& cameraTag, std::shared_ptr<Camera> pCamera);
// 指定したタグのカメラをこのワールドから削除する カメラ側の worldへのリンクのポインタも nullptr にする
inline bool EraseCamera(const std::string& cameraTag);
// カメラを nowWorld から nextWorld に移動させる
static bool MoveCamera(const std::string& cameraTag, World* nowWorld, World* nextWorld);
};
#endif
World.cppを変更
して、Mapをstd::make_sharedで生成する処理を記述しましょう。
#include "World.h"
#include "Player.h"
#include "Camera.h"
#include "Screen.h"
#include "GameScene.h"
#include "GridObject.h" // Gridをmake_sharedで生成するため
#include "Map.h" // Mapをmake_sharedで生成するため
Grid* World::makeGrid(int cellWidth, int cellHeight, int cellDepth)
{
if (this->m_pGrid == nullptr) // グリッドが未生成の場合は生成
this->m_pGrid = std::make_shared<Grid>(cellWidth, cellHeight, cellDepth);
return this->m_pGrid.get();
}
void World::AddPlayer(const std::string& playerTag, std::shared_ptr<Player> pPlayer)
{
if (m_pScene != nullptr)
{
m_pScene->players.emplace(playerTag, pPlayer); // シーンにプレイヤを追加する
}
}
// このワールドにいるプレイヤを得る
std::vector<std::weak_ptr<Player>> World::GetPlayers()
{
std::vector<std::weak_ptr<Player>> players;
for (auto&& pPlayer : players)
{
players.emplace_back(pPlayer);
}
return players;
}
// ワールドのマップをロード
bool World::LoadMap(const std::string& mapFilePath)
{
map = std::make_shared<Map>(this, mapFilePath); // マップをファイルから読み込む
return map->tmxData.isInitialized; // 読込に成功したかを返す
}
// カメラにタグをつけてワールドに追加 カメラにもワールドへの生ポインタリンクを渡す
bool World::AddCamera(const std::string& cameraTag, std::shared_ptr<Camera> pCamera)
{
if (pCamera == nullptr) return false;
if (cameras.count(cameraTag) > 0) return false; // すでに同じタグのカメラがワールドにあったら false
if (pCamera->world() != nullptr) pCamera->world()->EraseCamera(cameraTag); // すでにカメラが別ワールドに配置されていたら別ワールドから削除
cameras.emplace(cameraTag, pCamera); // ワールドにカメラを追加する
pCamera->world(this); // カメラにもワールドへのポインタリンクを張る(カメラ側に渡すのは共有ポインタではない生ポインタだから共有デッドロックにはならない)
return true;
}
// 指定したタグのカメラをこのワールドから削除する カメラ側の worldへのリンクのポインタも nullptr にする
bool World::EraseCamera(const std::string& cameraTag)
{
auto itr = cameras.find(cameraTag); // 指定したタグのカメラを探す
if (itr == end(cameras)) return false; // 指定したタグのカメラがワールドにない
auto pCamera = itr->second;
pCamera->world(nullptr); // カメラ側の worldへのリンクのポインタも nullptrに
cameras.erase(cameraTag); // カメラを辞書から削除
return true;
}
// カメラを nowWorld から nextWorld に移動させる
bool World::MoveCamera(const std::string& cameraTag, World* nowWorld, World* nextWorld)
{
if (nowWorld == nullptr) return false;
auto itr = nowWorld->cameras.find(cameraTag); // タグでカメラを探す
if (itr == end(nowWorld->cameras)) return false; // タグに関連づいたカメラがなかった
auto targetCamera = itr->second; // カメラの共有ポインタをキープ(次の行でeraseしてもカメラの共有カウンタは0にはならずこのリンクのおかげでカメラはメモリに残存)
targetCamera->world(nullptr); // カメラ側の worldへのリンクのポインタを一旦 nullptrに
nowWorld->cameras.erase(cameraTag); // nowWorld からは カメラを消す
if (nextWorld == nullptr) return false;
return nextWorld->AddCamera(cameraTag, targetCamera); // 次のワールドにカメラを追加する
}
// このワールドに存在するカメラすべてに対して funcで描きたい処理を渡して 全カメラのスクリーンに対して描画処理を走らせる
void World::Draw(std::function<void()> drawFunc) noexcept
{
for (auto&& keyValue : cameras) // このワールドにあるすべてのカメラぶんループ
{
VECTOR_D beforeCamPos; // 設定変更前のカメラのパラメータを一旦、保管
double beforeVRot, beforeHRot, beforeTRot, beforeNear, beforeFar, beforeFov;
Camera::GetCameraSettings(&beforeCamPos, &beforeVRot, &beforeHRot, &beforeTRot, &beforeFov, &beforeNear, &beforeFar);
auto& camera = keyValue.second;
//camera->screenID
DxLib::SetCameraPositionAndAngle(camera->pos, camera->rot.V, camera->rot.H, camera->rot.T); // 各カメラの位置、回転を設定
Screen::Start(camera->screenHandle, true, camera->fovAngle); // カメラに関連付けられたスクリーンIDを描画先にする
{
drawFunc(); // 渡された描画処理を実行
}
Screen::End(true); // 描画先を元に戻す
// カメラの設定をもとに戻しておく
Camera::SetCameraSettings(&beforeCamPos, &beforeVRot, &beforeHRot, &beforeTRot, &beforeFov, &beforeNear, &beforeFar);
}
}
GameScene.cppを変更
して、タイルマップ読み込む処理と3D表示で描く処理を実装しましょう。
#include "GameScene.h"
#include "Player.h"
#include "Map.h"
GameSceneSetting GameScene::settings{ "Map/GameSceneSetting.csv" }; // ゲームシーンの設定情報
// 指定したゲームシーンのタグに関連づいたファイル名に対応したワールドを初期化する
bool GameScene::LoadWorlds(const std::string& gameSceneTag)
{
if (settings.info.count(gameSceneTag) == 0)
return false;
auto& info = settings[gameSceneTag];
for (auto&& tag_mapPath : info)
{
CreateWorld(tag_mapPath.second.tag); // ワールドをタグをつけて生成
// ワールドのマップをロード
worlds[tag_mapPath.second.tag]->LoadMap(tag_mapPath.second.mapFilePath);
}
return true;
}
void GameScene::ChangeWorld(const std::string& worldTag, std::shared_ptr<Player> pPlayer)
{
if (pPlayer == nullptr) return;
if (worlds.count(worldTag) == 0) return; // 存在しないタグだった
pPlayer->world(worlds[worldTag].get()); // ワールドへのリンクをすげ替える
}
void GameScene::Initialize()
{// Init処理
// Zバッファを有効にする [Zの深度]
//[参考]https://docs.google.com/presentation/d/1Z23t1yAS7uzPDVakgW_M02p20qt9DeTs4ku4IiMDLco/edit?usp=sharing
//[参考] https://dxlib.xsrv.jp/function/dxfunc_3d_draw.html#R14N12
DxLib::SetUseZBuffer3D(TRUE);
// Zバッファへの書き込みを有効にする
DxLib::SetWriteZBuffer3D(TRUE);
// 光の明暗計算を無効に
DxLib::SetUseLighting(FALSE);
// SetDrawScreen に引数として渡せる( 描画対象として使用できる )グラフィックハンドルを作成するかどうかを設定する
// ( TRUE:描画可能グラフィックハンドルを作成する FLASE:通常のグラフィックハンドルを作成する( デフォルト ) )
DxLib::SetDrawValidGraphCreateFlag(TRUE);
// サブのスクリーンを2つ生成 画面の全体の縦方向サイズの Height / 2 にして、上下分割で配置できるようにする
screenHandle0 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
screenHandle1 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
// スクリーンを作成したらデフォルト設定に戻しておかないと
DxLib::SetDrawValidGraphCreateFlag(FALSE);
Resource::MakeShared<Texture>(mapChipPath, 8, 8, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
Resource::MakeShared<Texture>(playerImgPath1, 3, 4, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
Resource::MakeShared<Texture>(playerImgPath2, 3, 4, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
// プレイヤの生成
if (pWorld != nullptr)
{
//pWorld->AddCamera("カメラ1", Camera::defaultCamera); // カメラ1というタグをデフォルトカメラにつけてワールドにデフォルトカメラを配置
pWorld->AddCamera("カメラ1", std::make_shared<Camera>(pWorld, Vector3{ 0,0,0 }));
auto pCamera1 = pWorld->camera("カメラ1"); // カメラ1 というタグのついたカメラへの共有ポインタを得る
pCamera1->SetScreenHandle(screenHandle0); // カメラから見える3Dを描く スクリーンのハンドル を関連付ける
auto pPlayer1 = Object::Create<Player>(nullptr, pWorld, Pad::Key, Vector3(90 + 256, 32, 95 + 256));
pWorld->AddPlayer("プレイヤ1", pPlayer1); // シーンにプレイヤを追加する
pPlayer1->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath1]);
pPlayer1->camera = pCamera1.get(); // カメラをプレイヤにリンクする
pPlayer1->mass = 100.0f; // 体重を100にして衝突されてもびくともしないようにする
pWorld->AddCamera("カメラ2", std::make_shared<Camera>(pWorld, Vector3{ 0,0,0 }));
auto pCamera2 = pWorld->camera("カメラ2"); // プレイヤ2 というタグのついたカメラへの共有ポインタを得る
pCamera2->SetScreenHandle(screenHandle1); // カメラから見える3Dを描く スクリーンのハンドル を関連付ける
auto pPlayer2 = Object::Create<Player>(nullptr, pWorld, Pad::Two, Vector3(190, 32, 195));
pWorld->AddPlayer("プレイヤ2", pPlayer2); // シーンにプレイヤを追加する
pPlayer2->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath2]);
pPlayer2->camera = pCamera2.get(); // カメラをプレイヤにリンクする
pPlayer2->mass = 1.0f; // 体重を1にして普通にふっとぶようにする
}
};
void GameScene::Update()
{// 更新処理
Screen::ClearDrawScreen(screenHandle0); // 一旦キャンバスをきれいにまっさらに
Screen::ClearDrawScreen(screenHandle1); // 一旦キャンバスをきれいにまっさらに
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
for (const auto& player : players)
player.second->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
// グリッドを更新して隣接するグリッドに含まれる敵や弾の当たり判定を行う
for (const auto& world : worlds)
if (world.second->grid() != nullptr)
for (const auto& cell : world.second->grid()->cellData)
{
auto pGridObject = cell.second;
while (pGridObject != nullptr)
{ // while文で数珠つなぎのm_pNextを次々とたどって最後にたどり着くと次がなくてnullptrになる
pGridObject->checkCell();
pGridObject = pGridObject->m_pNext; // リンクリスト構造を次々とたどる
}
}
}
void GameScene::Draw()
{// 描画処理
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
std::unordered_set<World*> drawEndSet; // すでに描いたWorldをポインタのsetでかぶらないように判定する
for (const auto& player : players)
{
player.second->Draw(); // プレイヤの描画【忘れるとプレイヤ表示されない】
auto world = player.second->world(); // プレイヤのいるワールド
if (drawEndSet.count(world) > 0) continue; // すでに描いたWorldは描かずスキップする
drawEndSet.emplace(world); // 描いたWorldのポインタ住所をsetに登録する
world->map->DrawTerrain(); // マップの描画
// プレイヤのいるワールドのカメラのみを描けば、プレイヤのいないワールドは描かずに済む
// 複数のカメラを 範囲 for文 で回す
for (auto&& camera : player.second->world()->cameras)
{ // ラムダ式[&](){ ~ }で{}の外側の変数すべてを & キャプチャで&参照として「Draw関数の内側へ引き連れて」処理できる
camera.second->Draw([&]()
{
// Draw関数() の 外側にある変数 mapChipPath も[&]効果で { } の内側で問題なくアクセスできている↓
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(40, 40, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 2);
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(0, 40, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 0);
// 3Dで6枚のタイル(12ポリゴン)でボックスを描く
MyDraw::DrawDivBoxF3D(0, 70, 40, 1.0, *std::dynamic_pointer_cast<Texture>(Resource::files[mapChipPath]), 8);
if (camera.first == "カメラ1")
DxLib::DrawString(0, 0, "こちらがプレイヤ1", GetColor(255, 0, 0));
else if (camera.first == "カメラ2")
DxLib::DrawString(0, 0, "こちらはプレイヤ2", GetColor(0, 255, 0));
});
}
#ifdef _DEBUG // デバッグのときだけ_DEBUGが定義され描画される
if (world->grid() != nullptr)
for (const auto& cell : world->grid()->cellData)
cell.second->Draw(); // グリッドの描画
#endif
}
// スクリーン0の内容を画面に描く
DxLib::DrawGraph(0, 0, screenHandle0, FALSE);
DxLib::DrawLineBox(0, 0, Screen::Width, Screen::Height / 2, GetColor(255, 0, 0));
// スクリーン1の内容を画面に描く
DxLib::DrawGraph(0, Screen::Height / 2, screenHandle1, FALSE);
DxLib::DrawLineBox(0, Screen::Height / 2, Screen::Width, Screen::Height, GetColor(0, 255, 0));
}
HitBoxComponent.hを新規作成
して、コンポーネントのオーナーの当たり判定のボックスの上下左右奥手前を取得できる機能を実装しましょう。
#ifndef HITBOX_COMPONENT_H_
#define HITBOX_COMPONENT_H_
#include "Component.h"
#include "GameObject.h"
#include "Vector3.h"
#include "MyDraw.h"
class HitBoxComponent : public Component
{
public:
HitBoxComponent(std::shared_ptr<GameObject> pOwner, Vector3 boxSize, Vector3 offsetLeftBottomBack, Vector3 offsetRightTopForward)
: Component(pOwner), m_BoxSize{ boxSize }, m_OffsetLeftBottomBack{ offsetLeftBottomBack }, m_OffsetRightTopForward{ offsetRightTopForward }
{
StorePostionAndHitBox(); // ヒットボックスの現在の境界を一旦 prev に 初期保管
}
virtual ~HitBoxComponent() {}
Vector3 m_BoxSize; // ボックスの幅X、高さY、奥行Z
union {
Vector3 m_OffsetLeftBottomBack; // ヒットボックスのオフセット(X左、Y底、Z手前)
struct { float hitboxOffsetLeft, hitboxOffsetBottom, hitboxOffsetBack; };
};
union {
Vector3 m_OffsetRightTopForward; // ヒットボックスのオフセット(X右、Y上、Z奥)
struct { float hitboxOffsetRight, hitboxOffsetTop, hitboxOffsetForward; };
};
Vector3 m_PrevPos; // 1フレーム前の位置 (動く雲に乗るなどに使う)
union {
Vector3 m_PrevLeftBottomBack; // 1フレーム前のボックスの境目位置 (動く雲に乗るなどに使う)
struct { float prevLeft, prevBottom, prevBack; };
};
union {
Vector3 m_PrevRightTopForward; // 1フレーム前のボックスの境目位置 (動く雲に乗るなどに使う)
struct { float prevRight, prevTop, prevForward; };
};
// 当たり判定の左端を取得
virtual inline float GetLeft()
{
return (owner()->x - m_BoxSize.x / 2) + hitboxOffsetLeft;
}
// 左端を指定することにより位置を設定する
virtual inline void SetLeft(float left)
{
owner()->x = left - hitboxOffsetLeft + m_BoxSize.x / 2;
}
// 右端を取得
virtual inline float GetRight()
{
return (owner()->x - m_BoxSize.x / 2) + m_BoxSize.x - hitboxOffsetRight;
}
// 右端を指定することにより位置を設定する
virtual inline void SetRight(float right)
{
owner()->x = right + hitboxOffsetRight - m_BoxSize.x + m_BoxSize.x / 2;
}
// 上端を取得
virtual inline float GetTop()
{
return (owner()->y - m_BoxSize.y / 2) + m_BoxSize.y - hitboxOffsetTop;
}
// 上端を指定することにより位置を設定する
virtual inline void SetTop(float top)
{
owner()->y = top + hitboxOffsetTop - m_BoxSize.y + m_BoxSize.y / 2;
}
// 下端を取得する
virtual inline float GetBottom()
{
return (owner()->y - m_BoxSize.y / 2) + hitboxOffsetBottom;
}
// 下端を指定することにより位置を設定する
virtual inline void SetBottom(float bottom)
{
owner()->y = bottom - hitboxOffsetBottom + m_BoxSize.y / 2;
}
// 当たり判定のZ手前側を取得
virtual inline float GetBack()
{
return (owner()->z - m_BoxSize.z / 2) + hitboxOffsetBack;
}
// Z手前側を指定することにより位置を設定する
virtual inline void SetBack(float back)
{
owner()->z = back - hitboxOffsetBack + m_BoxSize.z / 2;
}
// Z奥行き方向位置を取得
virtual inline float GetForward()
{
return (owner()->z - m_BoxSize.z / 2) + m_BoxSize.z - hitboxOffsetForward;
}
// Z奥行き方向位置を指定することにより位置を設定する
virtual inline void SetForward(float forward)
{
owner()->z = forward + hitboxOffsetForward - m_BoxSize.z + m_BoxSize.z / 2;
}
// 雲に乗る系のための1フレーム前処理関数群
// 1フレーム前からの移動量(x方向)
virtual inline float GetDeltaX()
{
return owner()->x - m_PrevPos.x;
}
// 1フレーム前からの移動量(y方向)
virtual inline float GetDeltaY()
{
return owner()->y - m_PrevPos.y;
}
// 1フレーム前からの移動量(z方向)
virtual inline float GetDeltaZ()
{
return owner()->z - m_PrevPos.z;
}
// 1フレーム前の X 左端を取得する
virtual inline float GetPrevLeft()
{
return prevLeft;
}
// 1フレーム前の X 右端を取得する
virtual inline float GetPrevRight()
{
return prevRight;
}
// 1フレーム前の Y 上端を取得する
virtual inline float GetPrevTop()
{
return prevTop;
}
// 1フレーム前の Y 下端を取得する
virtual inline float GetPrevBottom()
{
return prevBottom;
}
// 1フレーム前の Z 前方を取得する
virtual inline float GetPrevForward()
{
return prevForward;
}
// 1フレーム前の Z 後方を取得する
virtual inline float GetPrevBack()
{
return prevBack;
}
// 1フレーム前の場所と当たり判定を記憶する
virtual inline void StorePostionAndHitBox()
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
m_PrevPos = pOwner->position;
prevLeft = GetLeft();
prevRight = GetRight();
prevTop = GetTop();
prevBottom = GetBottom();
prevForward = GetForward();
prevBack = GetBack();
}
// 当たり判定を描画(デバッグ用)
virtual void Draw(GameObject* obj = nullptr) override
{
#ifdef _DEBUG
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
// 赤で当たり判定の四角Boxを描画
MyDraw::DrawLineBox(Vector3(GetLeft(), GetBottom(), GetBack()), Vector3(GetRight(), GetTop(), GetForward()), GetColor(255, 0, 0));
#endif
}
};
#endif
MapHitBoxComponent.hを新規作成
して、マップのタイルから壁や床などのビットを取得して当たり判定と押し戻しする機能を実装しましょう。
#ifndef MAP_HITBOX_COMPONENT_H_
#define MAP_HITBOX_COMPONENT_H_
#include "HitBoxComponent.h"
#include "Tile.h" // Tile::isType(terrainID, checkBit)関数を使って ビット論理積 & で地形のchekckBitを確かめる
#include "Map.h"
class MapHitBoxComponent : public HitBoxComponent
{
public:
MapHitBoxComponent(std::shared_ptr<GameObject> pOwner, Vector3 boxSize, Vector3 offsetLeftBottomBack, Vector3 offsetRightTopForward)
: HitBoxComponent(pOwner, boxSize, offsetLeftBottomBack, offsetRightTopForward) {}
virtual ~MapHitBoxComponent() {}
#ifdef _DEBUG
// デバッグ用に最後に当たったXYZの位置などを記録する
float debugHitX = 0.0f, debugHitY = 0.0f, debugHitZ = 0.0f;
HitBit debugHitPart = (HitBit)HitPart::None;
#endif
// 当たり判定を描画(デバッグ用)
virtual void Draw(GameObject* obj = nullptr) override
{
HitBoxComponent::Draw(obj); // 継承もとのDrawも呼び出す
#ifdef _DEBUG
// ヒットしたパートを青四角で描画
if (debugHitPart & HitPart::Left || debugHitPart & HitPart::Right)
MyDraw::DrawLineBox(Vector3(debugHitX - 1, debugHitY - m_BoxSize.y / 2, debugHitZ - m_BoxSize.z / 2),
Vector3(debugHitX + 1, debugHitY + m_BoxSize.y / 2, debugHitZ + m_BoxSize.z / 2), GetColor(0, 0, 255));
if (debugHitPart & HitPart::Bottom || debugHitPart & HitPart::Top)
MyDraw::DrawLineBox(Vector3(debugHitX - m_BoxSize.x / 2, debugHitY - 1, debugHitZ - m_BoxSize.z / 2),
Vector3(debugHitX + m_BoxSize.x / 2, debugHitY + 1, debugHitZ + m_BoxSize.z / 2), GetColor(0, 0, 255));
if (debugHitPart & HitPart::Back || debugHitPart & HitPart::Forward)
MyDraw::DrawLineBox(Vector3(debugHitX - m_BoxSize.x / 2, debugHitY - m_BoxSize.y / 2, debugHitZ - 1),
Vector3(debugHitX + m_BoxSize.x / 2, debugHitY + m_BoxSize.y / 2, debugHitZ + 1), GetColor(0, 0, 255));
#endif
}
// ヒットボックスの当たり判定の隅の座標(±0.01fで1ピクセル隣)を取得
virtual inline bool GetHitBorder(float* pXLeft, float* pXMiddle, float* pXRight, float* pYBottom, float* pYMiddle, float* pYTop, float* pZBack, float* pZMiddle, float* pZForward)
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return false; // オーナーがすでにメモリから解放済のときは return
// 当たり判定の隅の座標(±0.01fで1ピクセル隣)を取得
float xLeft = GetLeft();
float xRight = GetRight();
if (pXLeft != nullptr) *pXLeft = xLeft + 0.01f;
if (pXMiddle != nullptr) *pXMiddle = xLeft + (xRight - xLeft) / 2; // xの中間点
if (pXRight != nullptr) *pXRight = xRight - 0.01f;
float yBottom = GetBottom();
float yTop = GetTop();
if (pYBottom != nullptr) *pYBottom = yBottom; // 底はisGround状態を維持するため +0.01fしない
if (pYMiddle != nullptr) *pYMiddle = yBottom + (yTop - yBottom) / 2; // yの中間点
if (pYTop != nullptr) *pYTop = yTop - 0.01f;
float zBack = GetBack();
float zForward = GetForward();
if (pZBack != nullptr) *pZBack = zBack + 0.01f;
if (pZMiddle != nullptr) *pZMiddle = zBack + (zForward - zBack) / 2; // zの中間点
if (pZForward != nullptr) *pZForward = zForward - 0.01f;
return true;
}
virtual void OnHitLeft(HitBit hitPartBitX, TileBit checkBit, float xLeft, float xMiddle, float xRight, float yBottom, float yMiddle, float yTop, float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
pOwner->OnHitTile(HitPart::Left, hitPartBitX, checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
//★[C++は小数点の % 余り演算がない!] https://marycore.jp/prog/c-lang/modulo-floating-point-number/
float wallRight = xLeft - (float)std::fmod(xLeft, pOwner->map()->CellSize()) + pOwner->map()->CellSize(); // 壁の右端
SetLeft(wallRight); // キャラの左端を壁の右端に沿わす
pOwner->vx = 0;
#ifdef _DEBUG
debugHitPart = (HitBit)HitPart::Left; debugHitX = xLeft; debugHitY = yMiddle; debugHitZ = zMiddle;
#endif
}
virtual void OnHitRight(HitBit hitPartBitX, TileBit checkBit, float xLeft, float xMiddle, float xRight, float yBottom, float yMiddle, float yTop, float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
pOwner->OnHitTile(HitPart::Right, hitPartBitX, checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
float wallLeft = xRight - (float)std::fmod(xRight, pOwner->map()->CellSize()); // 壁の左端
SetRight(wallLeft); // キャラの右端を壁の左端に沿わす
pOwner->vx = 0;
#ifdef _DEBUG
debugHitPart = (HitBit)HitPart::Right; debugHitX = xRight; debugHitY = yMiddle; debugHitZ = zMiddle;
#endif
}
// X方向[左端、右端]が checkBit かどうかのビット論理積 & をとり、OnHitLeft か OnHitRight をコールバックする
inline HitBit hitCheckX(TileBit checkBit, unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return (HitBit)HitPart::None; // オーナーがすでにメモリから解放済のときは return
if (pOwner->map() == nullptr) return (HitBit)HitPart::None;
// 当たり判定の隅の座標(±0.01fで1ピクセル隣)を取得
float xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward;
GetHitBorder(&xLeft, &xMiddle, &xRight, &yBottom, &yMiddle, &yTop, &zBack, &zMiddle, &zForward);
HitBit hitPartBitX = hitcheckX(checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
//★ 左端が壁にめりこんでいるか?
if (HitPart::Left & hitPartBitX)
{ // 左端がぶつかったときのコールバック関数を呼ぶ
OnHitLeft(hitPartBitX, checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
}
// 右端が壁にめりこんでいるか?
else if (HitPart::Right & hitPartBitX)
{ // 左端がぶつかったときのコールバック関数を呼ぶ
OnHitRight(hitPartBitX, checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
}
return hitPartBitX;
}
virtual void OnHitBottom(HitBit hitPartBitY, TileBit checkBit, float xLeft, float xMiddle, float xRight, float yBottom, float yMiddle, float yTop, float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
pOwner->OnHitTile(HitPart::Bottom, hitPartBitY, checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
float yBottomHit = yBottom; // めり込んだy位置
float pushUpY = pOwner->map()->CellSize(); // Y方向押し上げ高さ
if (pMatchBits != nullptr && (*pMatchBits) & (TileBit)TileType::Floor) // 床にめり込んだ場合
{
yBottomHit = yBottom + pOwner->map()->CellSize();
pushUpY = 0; // 床の場合はマス目サイズCellSizeぶん上にはあげない
}
float wallTop = yBottomHit - (float)std::fmod(yBottomHit, pOwner->map()->CellSize()) + pushUpY; // 壁や床の上側
SetBottom(wallTop); // キャラの足元を壁のトップや床に沿わす
pOwner->vy = -0.01f;
pOwner->isGround = true;
pOwner->isFlying = false;
#ifdef _DEBUG
debugHitPart = (HitBit)HitPart::Bottom; debugHitY = yBottom; debugHitX = xMiddle; debugHitZ = zMiddle;
#endif
}
virtual void OnHitTop(HitBit hitPartBitY, TileBit checkBit, float xLeft, float xMiddle, float xRight, float yBottom, float yMiddle, float yTop, float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr); // オーナーがすでにメモリから解放済のときは return
pOwner->OnHitTile(HitPart::Top, hitPartBitY, checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
float wallBottom = yTop - (float)std::fmod(yTop, pOwner->map()->CellSize()); // 壁の下側
SetTop(wallBottom); // キャラのトップを壁の下側に沿わす
pOwner->vy = 0;
#ifdef _DEBUG
debugHitPart = (HitBit)HitPart::Top; debugHitY = yTop; debugHitX = xMiddle; debugHitZ = zMiddle;
#endif
}
// 空中にいるときのコールバック
virtual void OnAir(HitBit hitPartBitY, TileBit checkBit, float xLeft, float xMiddle, float xRight, float yBottom, float yMiddle, float yTop, float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
pOwner->isGround = false; // 空中にいるので isGround を false に
}
// Y方向[下端、上端]が checkBit かどうかのビット論理積 & をとり、OnHitBottom か OnHitTop をコールバックする
inline HitBit hitCheckY(TileBit checkBit, unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return (HitBit)HitPart::None; // オーナーがすでにメモリから解放済のときは return
if (pOwner->map() == nullptr) return (HitBit)HitPart::None;
auto pMap = pOwner->map();
// 当たり判定の隅の座標(±0.01fで1ピクセル隣)を取得
float xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward;
GetHitBorder(&xLeft, &xMiddle, &xRight, &yBottom, &yMiddle, &yTop, &zBack, &zMiddle, &zForward);
#if 0
if (checkBit & (TileBit)TileType::Floor)
{ // 床かをチェックする場合は現在より一つ上のCellSizeぶん上のタイルが床なら一つ下に壁があるときに上方向に押し戻したのと同じような形になる
if ((int)HitPart::Bottom & hitPartFloorBitY)
{ // 下端がぶつかったときのコールバック関数を呼ぶ 床の場合
OnHitBottom(hitPartFloorBitY, checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop,
zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, &matchFloorBit); // matchFloorBitには TileType::Floor のビットだけが1になったビットが渡される
if (pMatchBits != nullptr) *pMatchBits = (TileBit)TileType::Floor; // ずらしてヒット判定したのであてになるのは TileType::Floor のビットのみなのでそれを |= ビット論理和する
return hitPartFloorBitY;
}
}
#endif
TileBit matchFloorBit = (TileBit)TileType::None;
// 床にめりこんでいるか(床は特殊で一つ上のマス目が床であれば キャラが床より下にめりこんでいるということなので、CellSizeぶん↓上のマス目が床か調べ、下のマス目のTopに沿わせる)
HitBit hitPartFloorBitY = hitcheckBottomY((TileBit)TileType::Floor, xLeft, xMiddle, xRight, yBottom + pMap->CellSize(), yMiddle + pMap->CellSize(), yTop + pMap->CellSize(),
zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, &matchFloorBit); // matchFloorBit は床にめり込んでいたらTileBit::Floorの 1の部分が1になっている
// 下側が床にめりこんでいるか?
if (HitPart::Bottom & hitPartFloorBitY)
{
OnHitBottom(hitPartFloorBitY, checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop,
zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, &matchFloorBit); // ←matchFloorBit経由でFloorの 1の部分を床の押し戻しかの判定用に渡す
if (pMatchBits != nullptr) *pMatchBits |= (TileBit)TileType::Floor; // ずらしてヒット判定したのであてになるのは TileType::Floor のビットのみなのでそれを |= ビット論理和する
return hitPartFloorBitY;
}
// 下側が「床以外」にめりこんでいるか★↓ ^ 排他的論理和をFloorに対してとると、checkBitのFloorのビットが1だったら打ち消すことができる
HitBit hitPartBitY = hitcheckY(checkBit ^ (TileBit)TileType::Floor, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
// 下側が床以外にめり込んでいるか?
if (HitPart::Bottom & hitPartBitY)
{ // 下端がぶつかったときのコールバック関数を呼ぶ
OnHitBottom(hitPartBitY, checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
}
// 上側が床以外にめりこんでいるか?
else if (HitPart::Top & hitPartBitY)
{ // 上端がぶつかったときのコールバック関数を呼ぶ
OnHitTop(hitPartBitY, checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
}
else // 空中にいるときのコールバック関数をよぶ
OnAir(hitPartBitY, checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
return hitPartBitY;
}
virtual void OnHitBack(HitBit hitPartBitZ, TileBit checkBit, float xLeft, float xMiddle, float xRight, float yBottom, float yMiddle, float yTop, float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
if (pOwner->map() == nullptr) return;
auto pMap = pOwner->map();
pOwner->OnHitTile(HitPart::Back, hitPartBitZ, checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
float wallForward = zBack - (float)std::fmod(zBack, pMap->CellSize()) + pMap->CellSize(); // 壁の奥行き方向
SetBack(wallForward); // キャラの背中を壁のフォワードに沿わす
pOwner->vz = 0;
#ifdef _DEBUG
debugHitPart = (HitBit)HitPart::Back; debugHitZ = zBack; debugHitX = xMiddle; debugHitY = yMiddle;
#endif
}
virtual void OnHitForward(HitBit hitPartBitZ, TileBit checkBit, float xLeft, float xMiddle, float xRight, float yBottom, float yMiddle, float yTop, float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return; // オーナーがすでにメモリから解放済のときは return
if (pOwner->map() == nullptr) return;
auto pMap = pOwner->map();
pOwner->OnHitTile(HitPart::Forward, hitPartBitZ, checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
float wallBack = zForward - (float)std::fmod(zForward, pMap->CellSize()); // 壁の手前側
SetForward(wallBack); // キャラのおでこを壁のバックに沿わす
pOwner->vz = 0;
#ifdef _DEBUG
debugHitPart = (HitBit)HitPart::Forward; debugHitZ = zForward; debugHitX = xMiddle; debugHitY = yMiddle;
#endif
}
// Z方向[z手前、Z奥]が checkBit かどうかのビット論理積 & をとり、OnHitBack か OnHitForward をコールバックする
inline HitBit hitCheckZ(TileBit checkBit, unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return (HitBit)HitPart::None; // オーナーがすでにメモリから解放済のときは return
//if (pOwner->map() == nullptr) return (HitBit)HitPart::None;
//auto pMap = pOwner->map();
// 当たり判定の隅の座標(±0.01fで1ピクセル隣)を取得
float xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward;
GetHitBorder(&xLeft, &xMiddle, &xRight, &yBottom, &yMiddle, &yTop, &zBack, &zMiddle, &zForward);
HitBit hitPartBitZ = hitcheckZ(checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
// 手前側が壁にめりこんでいるか?
if (HitPart::Back & hitPartBitZ)
{ // Z手前 がぶつかったときのコールバック関数を呼ぶ
OnHitBack(hitPartBitZ, checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
}
// 奥行き側が壁にめりこんでいるか?
else if (HitPart::Forward & hitPartBitZ)
{ // Z奥 がぶつかったときのコールバック関数を呼ぶ
OnHitForward(hitPartBitZ, checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
}
return hitPartBitZ;
}
// 指定された座標worldPosの地形が 壁 や ダメージブロックかなどをチェックする関数
inline bool checkID(TileBit checkBit, float worldX, float worldY, float worldZ,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
unsigned int terrain_gid; // 指定された座標の地形のID
if (worldZ != (std::numeric_limits<float>::min)())
{ //【YとZを変換】Zの入力があるときはZをYとして扱う タイルマップの X,Yは3DではYがZ:奥行に対応するから
float tmpY = worldY;
worldY = worldZ;
worldZ = tmpY; //【YとZを変換】
}
if (worldX < 0 || worldY < 0) return false; // 負の座標が指定された場合
auto pOwner = owner(); // コンポーネントを所有するオーナーへの共有ポインタを得る
if (pOwner == nullptr) return false; // オーナーがすでにメモリから解放済のときは return
if (pOwner->map() == nullptr) return false;
Map* map = pOwner->map();
// マップ座標系(二次元配列の行と列)に変換する
int mapX = map->cellX(worldX), mapY = map->cellY(worldY), mapZ = map->cellZ(worldZ);
if (map->tmxData.terrains.count(mapZ) == 0) // zの階層がまだなかったときは 0
terrain_gid = 0; // 範囲外のときは0
else // zの階層がすでにあったとき
{ // 二次元配列の範囲内か判定
auto& terrainZ = map->tmxData.terrains[mapZ];
if (mapY >= terrainZ.size() || mapX >= terrainZ[mapY].size())
terrain_gid = 0; // 範囲外のときは0
else
terrain_gid = terrainZ[mapY][mapX]; // 配列から地形IDを取り出す
}
TileBit matchBits = map->matchTypes(terrain_gid, checkBit); // ビット論理積 & で地形のchekckBitを確かめる
bool isCheckBit = matchBits != TileType::None;
if (isCheckBit && pMapX != nullptr && pMapY != nullptr && pMapZ != nullptr)
*pMapX = mapX, * pMapY = mapY, * pMapZ = mapZ; // ブロックのマス目のセル番号をポインタに記録(nullptrの場合は記録しない)
if (isCheckBit && pMatchBits != nullptr && matchBits & (TileBit)TileType::Floor)
*pMatchBits |= matchBits; // マッチしたビットをポインタにビット論理和して返す
if (isCheckBit && pTerrainID != nullptr)
*pTerrainID = terrain_gid; // チェックビット条件に当てはまったらその地形IDを返す
return isCheckBit;
}
// [X方向左]地形の ダメージブロックなどにぶつかったかをチェックする関数
inline HitBit hitcheckLeftX(TileBit checkBit,
float xLeft, float xMiddle, float xRight,
float yBottom, float yMiddle, float yTop,
float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
// 左端がめりこんでいるか?
if (checkID(checkBit, xLeft, yTop, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::Top | HitPart::Back; // 左上手前がめりこんでいる
if (checkID(checkBit, xLeft, yTop, zMiddle, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::Top | HitPart::ZMiddle; // 左上中がめりこんでいる
if (checkID(checkBit, xLeft, yTop, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::Top | HitPart::Forward; // 左上奥がめりこんでいる
if (checkID(checkBit, xLeft, yMiddle, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::YMiddle | HitPart::Back; // 左中手前がめりこんでいる
if (checkID(checkBit, xLeft, yMiddle, zMiddle, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::YMiddle | HitPart::ZMiddle; // 左中中がめりこんでいる
if (checkID(checkBit, xLeft, yMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::YMiddle | HitPart::Forward; // 左中奥がめりこんでいる
if (checkID(checkBit, xLeft, yBottom, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::Bottom | HitPart::Back; // 左下手前がめりこんでいる
if (checkID(checkBit, xLeft, yBottom, zMiddle, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::Bottom | HitPart::ZMiddle; // 左下中がめりこんでいる
if (checkID(checkBit, xLeft, yBottom, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::Bottom | HitPart::Forward; // 左下奥がめりこんでいる
return (HitBit)HitPart::None;
}
// [X方向右]地形の ダメージブロックなどにぶつかったかをチェックする関数
inline HitBit hitcheckRightX(TileBit checkBit,
float xLeft, float xMiddle, float xRight,
float yBottom, float yMiddle, float yTop,
float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
// 右端がめりこんでいるか?
if (checkID(checkBit, xRight, yTop, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::Top | HitPart::Back; // 右上手前がめりこんでいる
if (checkID(checkBit, xRight, yTop, zMiddle, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::Top | HitPart::ZMiddle; // 右上中がめりこんでいる
if (checkID(checkBit, xRight, yTop, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::Top | HitPart::Forward; // 右上奥がめりこんでいる
if (checkID(checkBit, xRight, yMiddle, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::YMiddle | HitPart::Back; // 右中手前がめりこんでいる
if (checkID(checkBit, xRight, yMiddle, zMiddle, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::YMiddle | HitPart::ZMiddle; // 右中中がめりこんでいる
if (checkID(checkBit, xRight, yMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::YMiddle | HitPart::Forward; // 右中奥がめりこんでいる
if (checkID(checkBit, xRight, yBottom, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::Bottom | HitPart::Back; // 右下手前がめりこんでいる
if (checkID(checkBit, xRight, yBottom, zMiddle, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::Bottom | HitPart::ZMiddle; // 右下中がめりこんでいる
if (checkID(checkBit, xRight, yBottom, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::Bottom | HitPart::Forward; // 右下奥がめりこんでいる
return (HitBit)HitPart::None;
}
// [X方向左右]地形の ダメージブロックなどにぶつかったかをチェックする関数
inline HitBit hitcheckX(TileType checkBit, float xLeft, float xMiddle, float xRight, float yBottom, float yMiddle, float yTop, float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{ // 下の関数にTileBit型をキャスト変換して取次ぎ
return hitcheckX((TileBit)checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
}
// [X方向左右]地形の ダメージブロックなどにぶつかったかをチェックする関数
inline HitBit hitcheckX(TileBit checkBit,
float xLeft, float xMiddle, float xRight,
float yBottom, float yMiddle, float yTop,
float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
// 左端がめりこんでいるか?
HitBit hitPartLeftBit = hitcheckLeftX(checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
if (hitPartLeftBit != HitPart::None)
return hitPartLeftBit;
// 右端がめりこんでいるか?
HitBit hitPartRightBit = hitcheckRightX(checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
if (hitPartRightBit != HitPart::None)
return hitPartRightBit;
return (HitBit)HitPart::None;
}
// [Y方向下]地形の ダメージブロックなどにぶつかったかをチェックする関数
inline HitBit hitcheckBottomY(TileBit checkBit,
float xLeft, float xMiddle, float xRight,
float yBottom, float yMiddle, float yTop,
float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
// 足元が壁にめりこんでいるか?
if (checkID(checkBit, xLeft, yBottom, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::Bottom | HitPart::Back; // 左下手前がめりこんでいる
if (checkID(checkBit, xLeft, yBottom, zMiddle, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::Bottom | HitPart::ZMiddle; // 左下中がめりこんでいる
if (checkID(checkBit, xLeft, yBottom, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::Bottom | HitPart::Forward; // 左下奥がめりこんでいる
if (checkID(checkBit, xMiddle, yBottom, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::XMiddle | HitPart::Bottom | HitPart::Back; // 中下手前がめりこんでいる
if (checkID(checkBit, xMiddle, yBottom, zMiddle, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::XMiddle | HitPart::Bottom | HitPart::ZMiddle; // 中下中がめりこんでいる
if (checkID(checkBit, xMiddle, yBottom, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::XMiddle | HitPart::Bottom | HitPart::Forward; // 中下奥がめりこんでいる
if (checkID(checkBit, xRight, yBottom, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::Bottom | HitPart::Back; // 右下手前がめりこんでいる
if (checkID(checkBit, xRight, yBottom, zMiddle, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::Bottom | HitPart::ZMiddle; // 右下中がめりこんでいる
if (checkID(checkBit, xRight, yBottom, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::Bottom | HitPart::Forward; // 右下奥がめりこんでいる
return (HitBit)HitPart::None;
}
// [Y方向下]地形の ダメージブロックなどにぶつかったかをチェックする関数
inline HitBit hitcheckTopY(TileBit checkBit,
float xLeft, float xMiddle, float xRight,
float yBottom, float yMiddle, float yTop,
float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
// 頭の部分がめりこんでいるか?
if (checkID(checkBit, xLeft, yTop, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::Top | HitPart::Back; // 左上手前がめりこんでいる
if (checkID(checkBit, xLeft, yTop, zMiddle, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::Top | HitPart::ZMiddle; // 左上中がめりこんでいる
if (checkID(checkBit, xLeft, yTop, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::Top | HitPart::Forward; // 左上奥がめりこんでいる
if (checkID(checkBit, xMiddle, yTop, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::XMiddle | HitPart::Top | HitPart::Back; // 中上手前がめりこんでいる
if (checkID(checkBit, xMiddle, yTop, zMiddle, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::XMiddle | HitPart::Top | HitPart::ZMiddle; // 中上中がめりこんでいる
if (checkID(checkBit, xMiddle, yTop, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::XMiddle | HitPart::Top | HitPart::Forward; // 中上奥がめりこんでいる
if (checkID(checkBit, xRight, yTop, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::Top | HitPart::Back; // 右上手前がめりこんでいる
if (checkID(checkBit, xRight, yTop, zMiddle, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::Top | HitPart::ZMiddle; // 右上中がめりこんでいる
if (checkID(checkBit, xRight, yTop, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::Top | HitPart::Forward; // 右上奥がめりこんでいる
return (HitBit)HitPart::None;
}
// [Y方向上下]地形の ダメージブロックなどにぶつかったかをチェックする関数
inline HitBit hitcheckY(TileType checkBit, float xLeft, float xMiddle, float xRight, float yBottom, float yMiddle, float yTop, float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{ // 下の関数にTileBit型をキャスト変換して取次ぎ
return hitcheckY((TileBit)checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
}
// [Y方向上下]地形の ダメージブロックなどにぶつかったかをチェックする関数
HitBit hitcheckY(TileBit checkBit,
float xLeft, float xMiddle, float xRight,
float yBottom, float yMiddle, float yTop,
float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
// 下端がめりこんでいるか?
HitBit hitPartBottomBit = hitcheckBottomY(checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
if (hitPartBottomBit != HitPart::None)
return hitPartBottomBit;
// 上端がめりこんでいるか?
HitBit hitPartTopBit = hitcheckTopY(checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
if (hitPartTopBit != HitPart::None)
return hitPartTopBit;
return (HitBit)HitPart::None;
}
#if 0
// [Y方向フロア床]地形の Floor:床にぶつかったかをチェックする関数(一つ上のタイルが床:Floorならtrue)
HitBit hitcheckFloorY(//Tile::Bit checkBit,
float xLeft, float xMiddle, float xRight,
float yBottom, float yMiddle, float yTop,
float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
// 一つ上のタイルが Floor:床 で 下端がめりこんでいるか?
HitBit hitPartBottomBit = hitcheckBottomY((int)TileType::Floor, xLeft, xMiddle, xRight, yBottom + CellSize, yMiddle + CellSize, yTop + CellSize, zBack, zMiddle, zForward,
pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
if (hitPartBottomBit != HitPart::None)
return hitPartBottomBit;
return (HitBit)HitPart::None;
}
#endif
// [Z方向手前]地形の ダメージブロックなどにぶつかったかをチェックする関数
inline HitBit hitcheckBackZ(TileBit checkBit,
float xLeft, float xMiddle, float xRight,
float yBottom, float yMiddle, float yTop,
float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
// 手前の部分がめりこんでいるか?
if (checkID(checkBit, xLeft, yTop, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::Top | HitPart::Back; // 左上手前がめりこんでいる
if (checkID(checkBit, xMiddle, yTop, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::XMiddle | HitPart::Top | HitPart::Back; // 中上手前がめりこんでいる
if (checkID(checkBit, xRight, yTop, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::Top | HitPart::Back; // 右上手前がめりこんでいる
if (checkID(checkBit, xLeft, yMiddle, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::YMiddle | HitPart::Back; // 左中手前がめりこんでいる
if (checkID(checkBit, xMiddle, yMiddle, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::XMiddle | HitPart::YMiddle | HitPart::Back; // 中中手前がめりこんでいる
if (checkID(checkBit, xRight, yMiddle, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::YMiddle | HitPart::Back; // 右中手前がめりこんでいる
if (checkID(checkBit, xLeft, yBottom, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::Bottom | HitPart::Back; // 左下手前がめりこんでいる
if (checkID(checkBit, xMiddle, yBottom, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::XMiddle | HitPart::Bottom | HitPart::Back; // 中下手前がめりこんでいる
if (checkID(checkBit, xRight, yBottom, zBack, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::Bottom | HitPart::Back; // 右下手前がめりこんでいる
return (HitBit)HitPart::None;
}
// [Z方向奥]地形の ダメージブロックなどにぶつかったかをチェックする関数
inline HitBit hitcheckForwardZ(TileBit checkBit,
float xLeft, float xMiddle, float xRight,
float yBottom, float yMiddle, float yTop,
float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
// 奥が壁にめりこんでいるか?
if (checkID(checkBit, xLeft, yTop, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::Top | HitPart::Forward; // 左上奥がめりこんでいる
if (checkID(checkBit, xMiddle, yTop, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::XMiddle | HitPart::Top | HitPart::Forward; // 中上奥がめりこんでいる
if (checkID(checkBit, xRight, yTop, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::Top | HitPart::Forward; // 右上奥がめりこんでいる
if (checkID(checkBit, xLeft, yMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::YMiddle | HitPart::Forward; // 左中奥がめりこんでいる
if (checkID(checkBit, xMiddle, yMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::XMiddle | HitPart::YMiddle | HitPart::Forward; // 中中奥がめりこんでいる
if (checkID(checkBit, xRight, yMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::YMiddle | HitPart::Forward; // 右中奥がめりこんでいる
if (checkID(checkBit, xLeft, yBottom, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Left | HitPart::Bottom | HitPart::Forward; // 左下奥がめりこんでいる
if (checkID(checkBit, xMiddle, yBottom, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::XMiddle | HitPart::Bottom | HitPart::Forward; // 中下奥がめりこんでいる
if (checkID(checkBit, xRight, yBottom, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits))
return HitPart::Right | HitPart::Bottom | HitPart::Forward; // 右下奥がめりこんでいる
return (HitBit)HitPart::None;
}
// [Z方向手前奥]地形の ダメージブロックなどにぶつかったかをチェックする関数
inline HitBit hitcheckZ(TileType checkBit, float xLeft, float xMiddle, float xRight, float yBottom, float yMiddle, float yTop, float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{ // 下の関数にTileBit型をキャスト変換して取次ぎ
return hitcheckZ((TileBit)checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
}
// [Z方向手前奥]地形の ダメージブロックなどにぶつかったかをチェックする関数
HitBit hitcheckZ(TileBit checkBit,
float xLeft, float xMiddle, float xRight,
float yBottom, float yMiddle, float yTop,
float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
// 手前がめりこんでいるか?
HitBit hitPartBackBit = hitcheckBackZ(checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
if (hitPartBackBit != HitPart::None)
return hitPartBackBit;
// 奥がめりこんでいるか?
HitBit hitPartForwardBit = hitcheckForwardZ(checkBit, xLeft, xMiddle, xRight, yBottom, yMiddle, yTop, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
if (hitPartForwardBit != HitPart::None)
return hitPartForwardBit;
return (HitBit)HitPart::None;
}
// 地形の ダメージブロックなどにぶつかったかをチェックする一時関数
HitBit hitcheck(TileBit checkBit,
float xLeft, float xMiddle, float xRight,
float yTop, float yMiddle, float yBottom,
float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
return hitcheckX(checkBit, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits)
| hitcheckY(checkBit, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits)
| hitcheckZ(checkBit, xLeft, xMiddle, xRight, yTop, yMiddle, yBottom, zBack, zMiddle, zForward, pTerrainID, pMapX, pMapY, pMapZ, pMatchBits);
}
};
#endif
GameObject.hを変更
して、OnCollisionなどの衝突判定のコールバック関数を準備しましょう。
#ifndef GAMEOBJECT_H_
#define GAMEOBJECT_H_
#include <memory> // 共有ポインタの定義のため
#include "Vector3.h"
#include "Quaternion.h"
#include "Tile.h" // OnHitTileのコールバックに必要
#include "Object.h" // インスタンスIDや検索タグを持ち、shared_from_this_as<継承先クラス名>などができる
#include "World.h"
class World; // 前方宣言
class Map; // 前方宣言
class Component; // 前方宣言
class Collision; // 前方宣言
// ゲーム上に表示される物体の基底クラス。
// プレイヤーや敵、アイテムなどはこのクラスを継承して作る。
class GameObject : public Object
{
protected:
// コンストラクタはprotected:にしてあるのでCreate関数経由で初期生成してください
GameObject(World* world = nullptr, Vector3 position = { 0,0,0 }, Quaternion rotation = { 0,0,0,1 }, Vector3 velocity = { 0,0,0 }, Vector3 force = { 0,0,0 })
: Object(), m_world{ world }, position{ position }, rotation{ rotation }, velocity{ velocity }, force{ force }
{
}
public:
// 仮想デストラクタ
virtual ~GameObject() {}
union { // ★共用体unionテクニック https://inemaru.hatenablog.com/entry/2016/03/02/005408
//★下記3つはメモリ上で共用状態になる(position、x,y,z、xyzどの名前から数値を変えたり読出してもメモリ上は同じ)
// 同じデータに3種類の名前を付けたイメージ、しかもVector3の機能や配列としてのアクセスの仕方もできて便利
struct { float x, y, z; }; // XYZ座標 [匿名共用体とは] https://code.i-harness.com/ja-jp/q/4d437c
Vector3 position; // XYZ座標
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で回せる
Quaternion rotation; // 回転クオータニオン
union {
struct { float vx, vy, vz; }; // XYZ方向の速度
Vector3 velocity; // XYZ方向の速度
std::array<float, 3> vxyz;
};
union {
struct { float vxForce, vyForce, vzForce; }; // XYZ方向にかかる力(Unityでいうと AddForce関数 )
Vector3 force; // XYZ方向の力(物理的には加速度:1フレームごとにvelocityの増える=加速する量)
std::array<float, 3> vxyzForce;
};
// 重力を返す、重力を変えたいときはoverrideして処理を変えてください、重力を変えてない場合は 0.0f
virtual inline float gravity() { return 0.0f; }
protected:
World* m_world; // GameObjectが配置されたワールドへのリンク
public:
// 現在いるワールドへのポインタを得る
virtual World* world() { return m_world; }
// 現在いるワールドのポインタを変更する
virtual void world(World* changeWorld) { m_world = changeWorld; }
// GameObjectが現在いるマップへのポインタを返す
Map* map() { return m_world->map.get(); }
bool isDead{ false }; // 死んだ(削除対象)フラグ
bool isStatic{ false }; // 動かない(静的オブジェクトか?)
bool isGround{ false }; // 地面についているか
bool isJumping{ false }; // ジャンプ中か
bool isFlying{ false }; // 空をとんでいるか
float pushPower{ 5.0f }; // コライダで接触したときに押し返す力(相手側が強いと押される)
float mass{ 1.0f }; // 質量(重さ)コライダで接触したときに重さでforceを割り算すると動きにくくなる
hash32 typeTag{ "" }; // 小カテゴリ Zako0など個別のタイプ
hash32 baseTag{ "" }; // 大カテゴリ Enemyなど大まかなベースジャンル
protected:
// コンポーネントの<ハッシュID, 共有所有ポインタ> の辞書でコンポーネントの共有カウンタ+1を保持する
std::unordered_map<size_t, std::shared_ptr<Component>> components;
public:
// コンポーネントを得る 例えば GetComponent<BoxCollider>でBoxColliderという名前のコンポーネントをcomponents辞書から得る
template<class ComponentT>
std::vector<std::weak_ptr<ComponentT>> GetComponents()
{ //★[ComponentTのハッシュコードを得るテク] https://cpprefjp.github.io/reference/typeindex/type_index/hash_code.html
size_t hashID = typeid(ComponentT).hash_code(); //[まずハッシュでID化]
// equal_range 関数を使って、指定したキーの最初と終端の位置を示すイテレータを std::pair<> で取得します。
auto range = components.equal_range(hashID); //https://ez-net.jp/article/9E/DFLX8hNG/G5J8DPmsltz4/
std::vector<std::weak_ptr<ComponentT>> returnList; // "ComponentT" 例."CircleCollider"など をキーとするコンポーネント一覧
for (auto ite = range.first; ite != range.second; ++ite)
returnList.emplace_back(ite->second); // 辞書で見つかったコンポーネントを返すリストに追加
return returnList;
}
// コンポーネントを追加する .AddComponent<BoxCollider>(...)で追加できる
template<class ComponentT, typename ...ArgsT>
std::weak_ptr<ComponentT> AddComponent(ArgsT&&...args)
{ //★[ComponentTのハッシュコードを得るテク] https://cpprefjp.github.io/reference/typeindex/type_index/hash_code.html
size_t hashID = typeid(ComponentT).hash_code(); //[まずハッシュでID化]
// shared_from_this()を必ず渡してセットで初期化して、オーナーがリンク切れのコンポーネントができないように気をつける
std::shared_ptr<ComponentT> newComponent = std::make_shared<ComponentT>(shared_from_this_as<GameObject>(), std::forward<ArgsT>(args)...);
// https://ez-net.jp/article/9E/DFLX8hNG/G5J8DPmsltz4/
// std::unrodered_multimap<> と同型の値を取る std::pair<> を作って insert 関数でcomponents辞書に追加
components.insert(std::pair<size_t, std::shared_ptr<ComponentT>>(hashID, newComponent));
return newComponent;
}
// コンポーネントを削除する
template<typename ComponentT>
bool RemoveComponent(std::shared_ptr<ComponentT> removeComponent)
{ //★[ComponentTのハッシュコードを得るテク] https://cpprefjp.github.io/reference/typeindex/type_index/hash_code.html
size_t hashID = typeid(ComponentT).hash_code(); //[まずハッシュでID化]
// equal_range 関数を使って、指定したキーの最初と終端の位置を示すイテレータを std::pair<> で取得します。
auto range = components.equal_range(hashID); //https://ez-net.jp/article/9E/DFLX8hNG/G5J8DPmsltz4/
//[イテレート中に探しながら消す] https://cpprefjp.github.io/reference/unordered_map/unordered_multimap/erase.html
for (auto ite = range.first; ite != range.second;)
if (ite->second == removeComponent)
{
components.erase(ite); // 削除された要素の次を指すイテレータが返される
return true; // 見つかって消せた場合は true
}
else
++ite; // 次の要素へイテレータを進める
return false; // 見つからずに消せなかった場合は false
}
// 更新処理[純粋仮想関数 = 0 なのでoverride必須の関数]
virtual void Update() = 0; // 純粋仮想関数 = 0 にすると【絶対 Update()関数はoverrideしなきゃいけない義務を付与できる 】
// 描画処理[純粋仮想関数 = 0 なのでoverride必須の関数]
virtual void Draw() = 0; // 純粋仮想関数 = 0 にすると【絶対 Draw()関数はoverrideしなきゃいけない義務を継承先クラスに付与 】
// 衝突開始したときの関数 overrideして衝突したときの処理などをオブジェクトの種類ごとに実装する
virtual void OnCollisionEnter(std::shared_ptr<Collision> pCollision)
{
// overrideして衝突開始したときの処理などをオブジェクトの種類ごとに実装する
};
// 衝突したときの関数 overrideして衝突したときの処理などをオブジェクトの種類ごとに実装する
virtual void OnCollision(std::shared_ptr<Collision> pCollision)
{
// overrideして衝突したときの処理などをオブジェクトの種類ごとに実装する
};
// 衝突終了したときの関数 overrideして衝突したときの処理などをオブジェクトの種類ごとに実装する
virtual void OnCollisionExit(std::shared_ptr<Collision> pCollision)
{
// overrideして衝突終了したときの処理などをオブジェクトの種類ごとに実装する
};
// ジャンプ開始した直後に呼ばれる関数
virtual void OnStartJump()
{
// overrideしてジャンプした瞬間に音を鳴らしたりする処理などをオブジェクトの種類ごとに実装する
};
// 空中でジャンプの上昇中に呼ばれる関数
virtual void OnJumping()
{
// overrideして空中でジャンプの上昇中にエフェクトを出したりする処理などをオブジェクトの種類ごとに実装する
};
// 空中で落ちている最中に呼ばれる関数
virtual void OnFalling()
{
// overrideして空中で落ちている最中に音を鳴らしたり、エフェクトを出したりする処理などをオブジェクトの種類ごとに実装する
};
// タイルに当たったときのコールバック
virtual void OnHitTile(HitPart hitPart, HitBit hitPartBit, TileBit checkBit, float xLeft, float xMiddle, float xRight,
float yBottom, float yMiddle, float yTop, float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
// 継承 overrideしてタイルに当たったときの処理を実装する
}
};
#endif
Player.hを変更
して、タイルマップと当たり判定をするコンポーネントの初期 Init 設定を追加しましょう。
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Resource.h"
#include "GameObject.h"
#include "PhysicsComponent.h"
#include "MapHitBoxComponent.h"
#include "GridCollidersComponent.h"
#include "Collider.h"
class World; // 前方宣言
class Camera; // 前方宣言
class Player : public GameObject
{
protected:
// コンストラクタはprotected:にしてあるので、Create関数経由で初期生成してください
Player(World* world, Pad pad, Vector3 position, Quaternion rotation = { 0,0,0,1 }, Vector3 velocity = { 0,0,0 }, Vector3 force = { 0,0,0 })
: pad{ pad }, GameObject(world, position, rotation, velocity, force)
{
this->typeTag = "Player"; // タグは個別の"プレイヤ1"などに使い、タイプはtypeTagで判別する形にしてタイプのジャンルを区別
this->baseTag = "Player";
this->pushPower = 5.0f; // コライダで衝突したときに押し返す力
}
public:
// 仮想デストラクタ
virtual ~Player() {}
Pad pad; // 操作するコントローラー番号
hash32 worldTag{ "" }; // 現在いるワールドのタグ
float MoveSpeedMax = 6; // 移動速度Max値
float MoveSpeed = 0;//移動速度
float moveAngle = 0; // X軸→方向から何度か
float deltaAngle = 0; // プレイヤの動く角度の変化率、ハンドルを切ったらだんだんもとに戻る
int animCount = 0;
std::shared_ptr<Texture> image; // プレイヤの板ポリゴンで描くタイル分割画像への共有リンク
Camera* camera{ nullptr }; // プレイヤを追随するカメラ
void SetCamera(Camera* pCamera) { camera = pCamera; }
// overrideして物理落下コンポーネントからの重力値を返す
virtual float gravity() override { auto pGravityPhysics = gravityPhysics.lock(); return (pGravityPhysics != nullptr) ? pGravityPhysics->gravity() : 0.0f; }
protected:
std::weak_ptr<JumpComponent> jumpPhysics; // ジャンプの物理処理コンポーネント
std::weak_ptr<GravityComponent> gravityPhysics; // 落下の重力の物理処理コンポーネント
// マップ地形に対するヒットボックスの当たり判定と押し戻しをするコンポーネント
std::weak_ptr<MapHitBoxComponent> mapHitBox;
std::weak_ptr<GridCollidersComponent> gridCollision; // グリッドマス目分割コンポーネント
std::weak_ptr<SphereCollider> sphereCollider; // 球コライダのコンポーネント
public:
virtual void Init(std::shared_ptr<InitParams> initParams = nullptr) override
{
std::vector<float> vyJumpSpeed{ 13, 14, 15, 4 }; // ジャンプ開始の初期速度
std::vector<float> vyForceJump{ 0.5f, 0.4f, 0.3f, 0.1f }; // ジャンプ上昇にかかる力の初期値
std::vector<float> vyGravity{ 0.8f,0.8f,0.8f,0.8f }; // 重力の設定値
std::vector<float> vyDownSpeedMax{ 16, 16, 16, 8 }; // 降下スピードのリミット
// AddComponent内部でshared_from_this()を必ず渡してセットで初期化して、オーナーがリンク切れのコンポーネントができないように気をつける
jumpPhysics = AddComponent<JumpComponent>(vyJumpSpeed, vyForceJump);
gravityPhysics = AddComponent<GravityComponent>(vyGravity, vyDownSpeedMax);
// 半径16(直径32)の球体コライダ
sphereCollider = AddComponent<SphereCollider>(Vector3(0, 0, 0), 16);
gridCollision = AddComponent<GridCollidersComponent>("Player"); //とりあえずわかりやすいように"Player"というタグをつけておく(別に何でもいい)
gridCollision.lock()->LinkCollider(sphereCollider); // コライダをリンクする
float mapHitBoxSize = sphereCollider.lock()->m_radius * 2.0f; // 球体コライダの直径に合わす
Vector3 boxSize{ mapHitBoxSize,mapHitBoxSize,mapHitBoxSize };
Vector3 offsetLeftBottomBack{ 3, 0, 3 }; // ボックスの端を内側にどれだけ削るか(キャラの画像に合わせて小さくできる)
Vector3 offsetRightTopForward{ 3, 8, 3 }; // ボックスの端を内側にどれだけ削るか(頭を8削る)
mapHitBox = AddComponent<MapHitBoxComponent>(boxSize, offsetLeftBottomBack, offsetRightTopForward);
}
// 入力を受けての処理
virtual void HandleInput();
// 更新処理
virtual void Update() override;
// 描画処理
virtual void Draw() override;
// ジャンプ開始した直後に呼ばれる関数
virtual void OnStartJump() override;
// コライダの衝突した際に呼ばれるコールバック関数
virtual void OnCollision(std::shared_ptr<Collision> pCollision) override;
};
#endif
Player.cppを変更
して、コンポーネントを使ってタイルマップと当たり判定をしましょう。
#include "Player.h"
#include <cmath> // 浮動小数点の余り%演算子がC++にないのでfmodを使うため
#include "MyMath.h"
#include "MyDraw.h"
#include "Camera.h"
#include "World.h"
#include <vector>
void Player::HandleInput()
{
if (Input::GetButton(pad, PAD_INPUT_LEFT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_A)) )
{
//vx = - MoveSpeed; // 左
deltaAngle += 1; //ハンドルを左に回す
}
if (Input::GetButton(pad, PAD_INPUT_RIGHT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_D)))
{
//vx = MoveSpeed; // 右
deltaAngle -= 1; //ハンドルを右に回す
}
if (Input::GetButton(pad, PAD_INPUT_UP)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_W)))
{
//vy = - MoveSpeed; // 上
MoveSpeed += 0.6f; //アクセルを踏む
if (MoveSpeed > MoveSpeedMax)
MoveSpeed = MoveSpeedMax;//Maxスピードで止める
}
else if (Input::GetButton(pad, PAD_INPUT_DOWN)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_S)))
{
//vy = MoveSpeed; // 下
MoveSpeed -= 0.6f; //ブレーキを踏む
if (MoveSpeed < -0.9f)
MoveSpeed = -0.9f;//ちょっとだけバックできるように
}
if (pad == Pad::Key || pad == Pad::One) // コントローラ1またはキーボード操作のプレイヤのときだけ
{
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGUP)) // キーボードのPageUp ボタンを押したときは
{
vy = 2.0f; // テスト用に空中の上方向に移動
}
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGDN)) // キーボードのPageDown ボタンを押したときは
{
vy = -2.0f; // テスト用に空中の下方向に移動
}
}
}
// ジャンプ開始した直後に呼ばれる関数
void Player::OnStartJump()
{
this->isGround = false; // ジャンプ中はisGroundフラグがfalse
}
// 更新処理
void Player::Update()
{
vxForce *= 0.9f; // かかっている力の減衰率
vyForce *= 0.9f; // かかっている力の減衰率
vzForce *= 0.9f; // かかっている力の減衰率
deltaAngle *= 0.5F; // 車のハンドルの方向をだんだんニュートラルに戻す
MoveSpeed *= 0.9F; // 移動速度も減速する
// 入力を受けての処理
HandleInput();
auto pJumpPhysics = jumpPhysics.lock();
if (pJumpPhysics != nullptr)
{
if (isGround)
{
if (Input::GetButtonDown(pad, PAD_INPUT_A) // Zキーを押した瞬間
|| (pad == Pad::Two && Input::GetButtonDown(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを押した瞬間
{
pJumpPhysics->JumpStart(); // ジャンプのスタート処理を発動
}
}
else if (vy > 0 && Input::GetButtonUp(pad, PAD_INPUT_A) // Zキーを離した瞬間
|| (pad == Pad::Two && Input::GetButtonUp(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを離した瞬間
{
pJumpPhysics->JumpCancel(); // ジャンプのキャンセル処理を発動
}
pJumpPhysics->Update(); // ジャンプのコンポーネントを更新する
}
auto pGravityPhysics = gravityPhysics.lock();
if (pGravityPhysics != nullptr)
pGravityPhysics->Update(); // 重力落下のコンポーネントを更新する
moveAngle += deltaAngle; // ハンドルを回したぶんプレイヤの進行方向を変える
if (moveAngle < -180) moveAngle = 360 + moveAngle; // -180以下の角度にならないようにマイナスになったら+180度にループさせる
else if (moveAngle > +180) moveAngle = -360 + moveAngle; // +180以下の角度にならないようにマイナスになったら-180度にループさせる
rotation.SetRotation(VGet(0, (180 - moveAngle) * MyMath::Deg2Rad, 0)); // 前向きの3Dモデルなら180度回転させて奥方向の向きに設定
// ↓進行方向角度をX方向とZ方向の速度に変える
vx = (float)std::cos((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
vz = (float)std::sin((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
float vyForceJump = (pJumpPhysics != nullptr) ? pJumpPhysics->vyForce() : 0.0f;
float vyForceGravity = (pGravityPhysics != nullptr) ? pGravityPhysics->vyForce() : 0.0f;
vx += vxForce; // 力をかけて速度に加算する
vy += vyForce + vyForceJump + vyForceGravity; // 勢い(力・フォースを加算
vz += vzForce; // 力をかけて速度に加算する
// 実際に位置を動かす
auto pMapHitBox = mapHitBox.lock();
// まず横に移動する
x += vx;
if (pMapHitBox != nullptr)
pMapHitBox->hitCheckX((TileBit)TileType::Wall);
// 次に縦に動かす
y += vy;
if (pMapHitBox != nullptr)
pMapHitBox->hitCheckY((TileBit)TileType::Wall | (TileBit)TileType::Floor);
// 次にZ奥行き方向に動かす
z += vz;
if (pMapHitBox != nullptr)
pMapHitBox->hitCheckZ((TileBit)TileType::Wall);
if (y <= 0.0f) // 0.0fを地面として 0.0f以下になったら地面に着地したと判定する
{
y = 0.0f; // 地面に沿わせる
vy = -0.01f; // 下方向への速度をほぼ 0 にする
isGround = true;
}
if (auto pGridCollision = gridCollision.lock()) // lock()してnullptrじゃなければ
pGridCollision->Update(); // 所属するマス目セルを更新する
// カメラのプレイヤ追尾
float camDistance = 150; // プレイヤからカメラまでのY平面上の距離
float camHeight = 100; // プレイヤからカメラまでのY方向上の高さ
// moveAngleは2D画像のX方向右を0度とするから、-90度回すと画像を正面に捉える位置にカメラを置ける
float camAngleX = (float)std::cos((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
float camAngleZ = (float)std::sin((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
if (camera != nullptr)
{ // カメラの位置をプレイヤの進行方向と真逆に設定
camera->SetPosition(position + Vector3(camAngleX, camHeight, camAngleZ));
camera->LookAt(position); //カメラはプレイヤの方を見る
}
}
void Player::OnCollision(std::shared_ptr<Collision> pCollision)
{
if (isDead) return; // すでにisDeadなら return
auto otherCollider = pCollision->contacts[0].otherCollider.lock(); // コライダの弱共有ポインタをロック
if (otherCollider == nullptr) return; // すでにコライダが消去済だった
auto other = otherCollider->owner();
if (other->isDead) return; // other側がisDeadだったら処理をスルー
if (other->baseTag == "Enemy" || other->baseTag == "Player")
{
//[離散要素法:DEM] https://qiita.com/konbraphat51/items/157e5803c514c60264d2
constexpr float k = 1.0f; // バネ係数(めり込みに対するバネ反発の掛け率)
constexpr float e = 0.1f; // はね返り係数(直前と比べどれくらいの速さで離れるか0.5だと半分の速さではね返る)
float log_e = std::log(e); // はね返り係数の自然対数log
constexpr float pi_2 = DX_PI_F * DX_PI_F; // πの2乗
float eta = -2.0f * log_e * std::sqrt(k * mass / (log_e * log_e + pi_2)); // 粘性係数η(イータ)
auto norm = pCollision->contacts[0].normal; // 衝突の法線 * other->pushPower; // pushPowerの力でotherに押し返される
auto v_norm = dot(other->velocity - velocity, norm) * norm; // [相対速度v] 法線norm方向のv = (v1 - v0)・norm [ドット積]
auto sep = pCollision->contacts[0].separation; // 衝突点どうしの距離(めりこみ距離)
auto F = -k * sep * norm - eta * v_norm; // F (法線方向の力) = -k × sep - η × v (相対速度:自分から見た相手otherの速度)
force += F / mass; // 衝突法線方向の力を加える
}
}
// 描画処理
void Player::Draw()
{
int animIndex = animCount / 20; // キャラのパラパラアニメの画像番号(最大3)
float abs_vx = (vx > 0) ? vx : -vx; // x方向絶対値
float abs_vz = (vz > 0) ? vz : -vz; // y方向絶対値
if (abs_vx > 1.00f || abs_vz > 1.00f)
animCount += 6; // キャラが地面のXとZ方向に少しでもスピードがあったらアニメを走らせる
if (animCount >= 60) animCount = 0; // アニメ時間を0へループ
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
for (auto&& keyValue : world()->cameras) // プレイヤのいるワールドにあるすべてのカメラぶんループ
{
auto& camera = keyValue.second;
camera->Draw([&]()
{
// 常にカメラ方向を向くビルボード回転の角度を得る
VECTOR_D angle = MyDraw::GetAngle(DxLib::GetCameraBillboardMatrixD());
angle.x = 0; angle.z = 0; // Y軸周りのビルボード回転だけ有効にする X軸,Z軸まわりの回転は 0 にする
int imgIndex = 9; // キャラチップ画像の起点番号
// ビルボードの角度 と キャラの向いている向き の 差 によって、別のプレイヤのカメラから見たキャラチップの向きの画像を切り替える
// -180 ~ +180 : angle.y moveAngle : -180 ~ +180 ↓
float difAngleY = angle.y * 180.0f / DX_PI_F + moveAngle;
imgIndex = (difAngleY < -135 || 135 < difAngleY) ? 0 // カメラ向きの画像
: (difAngleY < -45) ? 6 // 右 向きの画像
: (difAngleY > 45) ? 3 // 左 向きの画像
: 9; // 奥向きの画像
MyDraw::DrawDivRotaGraphF3D(MyDraw::Plane::Z, x, y, z, 1.0f, angle, *image, imgIndex + animIndex, TRUE);
#ifdef _DEBUG // デバッグのときだけ_DEBUGが定義され描かれる
if (auto pSphereCollider = sphereCollider.lock()) pSphereCollider->Draw(); // コライダを描く
if (auto pMapHitBox = mapHitBox.lock()) pMapHitBox->Draw(); // ヒットボックスを描く
#endif
});
}
}
GridSteps.hを新規作成
して、当たり判定がすり抜けないように細切れのグリッドの数値を計算する機能を準備しましょう。
#ifndef GRID_STEPS_H_
#define GRID_STEPS_H_
#include "Vector3.h"
// 超高速すぎて当たり判定をすり抜けないように gridSize で分割して
// グリッドを飛び越えずに1つ1つ 細かい step で進んですり抜けないように div 回分進んで、
// 最後に余りの mod ぶん進む と vec と同じぶん、進んだことになり、当たり判定もすり抜けずにすむ
struct GridSteps
{
int div; // stepを何回ぶん繰り返すと vec と等しくなるか
Vector3 step; //[ステップ] vec = step * div + mod で細かく div 回進むようにする
Vector3 mod; //[余り] vec = step * div + mod で細かく div 回進んで最後に 余りmodぶん進む
inline GridSteps(const Vector3& vec, const Vector3& gridSize, int divLimit = 1000)
{
float vx = vec.x, vy = vec.y, vz = vec.z;
float xGridSize = gridSize.x, yGridSize = gridSize.y, zGridSize = gridSize.z;
// ブロックなどのグリッドを貫通するほどの速さのvxがあるとき
// グリッドを1つ1つまたぐ分割されたスピートを vx の割り算とその余りで求める
float vxAbs = (vx < 0) ? -vx : vx, vyAbs = (vy < 0) ? -vy : vy, vzAbs = (vz < 0) ? -vz : vz;
float signX = (vx < 0) ? -1 : +1, signY = (vy < 0) ? -1 : +1, signZ = (vz < 0) ? -1 : +1;
float vxDiv = vxAbs / xGridSize; // vxから±を消した絶対値 を グリッドのサイズで割ると必要ステップ数
float vyDiv = (yGridSize == 0) ? 1.0f : vyAbs / yGridSize;
float vzDiv = (zGridSize == 0) ? 1.0f : vzAbs / zGridSize;
if (vxDiv <= 1.0f && vyDiv <= 1.0f && vzDiv <= 1.0f)
{
this->div = 1;
this->step = vec; // 1マスしか進まない場合は速度をそのまま返す
return;
}
if (vxDiv > divLimit) vxDiv = divLimit;
if (vyDiv > divLimit) vyDiv = divLimit; // divが大きくなりすぎるとStepが∞に近づくのでリミットをかける
if (vzDiv > divLimit) vzDiv = divLimit;
float vxMod = std::fmod(vxAbs, xGridSize); // グリッドのサイズで速度を割った余りmod
float vyMod = (yGridSize == 0) ? 0 : std::fmod(vyAbs, yGridSize);
float vzMod = (zGridSize == 0) ? 0 : std::fmod(vzAbs, zGridSize);
// ∞=infや計算不能 nan を !isfiniteで 検出したら 0 にする https://qiita.com/N700A/items/36b9383eaf923f4bf6ae
if (vxMod > xGridSize || !isfinite(vxMod)) vxMod = 0;
if (vyMod > yGridSize || !isfinite(vyMod)) vyMod = 0; // 速度が∞に近づくと余りも∞になるので 0 にする
if (vzMod > zGridSize || !isfinite(vzMod)) vzMod = 0;
int maxDiv; // 通過する区切りの回数が一番多いもの(= maxDiv)に合わせて進むx,y,zの1回のステップを決める
float xStep = xGridSize, yStep = yGridSize, zStep = zGridSize;
if (vxDiv >= vyDiv && vxDiv >= vzDiv) // 1番割り算したDivの区切りの多いものにあわせて他はその区切りで割るので
maxDiv = (int)vxDiv, yStep = vyAbs / maxDiv, zStep = vzAbs / maxDiv, vyMod = 0, vzMod = 0; // 他の余りは 0 にする
else if (vyDiv >= vzDiv && vyDiv >= vxDiv)
maxDiv = (int)vyDiv, xStep = vxAbs / maxDiv, zStep = vzAbs / maxDiv, vxMod = 0, vzMod = 0;
else
maxDiv = (int)vzDiv, xStep = vxAbs / maxDiv, yStep = vyAbs / maxDiv, vxMod = 0, vyMod = 0;
// ステップの回数 div,1回のステップ step, 最後のステップのつじつまを合わせる余り mod を返す
this->div = maxDiv;
this->step = Vector3{ signX * xStep, signY * yStep, signZ * zStep };
this->mod = Vector3{ signX * vxMod, signY * vyMod, signZ * vzMod };
}
};
#endif
Player.cppを変更
して、GridStepsで細切れに移動してすり抜けないように当たり判定をしましょう。
#include "Player.h"
#include <cmath> // 浮動小数点の余り%演算子がC++にないのでfmodを使うため
#include "MyMath.h"
#include "MyDraw.h"
#include "Camera.h"
#include "World.h"
#include "GridSteps.h"
#include <vector>
void Player::HandleInput()
{
if (Input::GetButton(pad, PAD_INPUT_LEFT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_A)) )
{
//vx = - MoveSpeed; // 左
deltaAngle += 1; //ハンドルを左に回す
}
if (Input::GetButton(pad, PAD_INPUT_RIGHT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_D)))
{
//vx = MoveSpeed; // 右
deltaAngle -= 1; //ハンドルを右に回す
}
if (Input::GetButton(pad, PAD_INPUT_UP)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_W)))
{
//vy = - MoveSpeed; // 上
MoveSpeed += 0.6f; //アクセルを踏む
if (MoveSpeed > MoveSpeedMax)
MoveSpeed = MoveSpeedMax;//Maxスピードで止める
}
else if (Input::GetButton(pad, PAD_INPUT_DOWN)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_S)))
{
//vy = MoveSpeed; // 下
MoveSpeed -= 0.6f; //ブレーキを踏む
if (MoveSpeed < -0.9f)
MoveSpeed = -0.9f;//ちょっとだけバックできるように
}
if (pad == Pad::Key || pad == Pad::One) // コントローラ1またはキーボード操作のプレイヤのときだけ
{
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGUP)) // キーボードのPageUp ボタンを押したときは
{
vy = 2.0f; // テスト用に空中の上方向に移動
}
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGDN)) // キーボードのPageDown ボタンを押したときは
{
vy = -2.0f; // テスト用に空中の下方向に移動
}
}
}
// ジャンプ開始した直後に呼ばれる関数
void Player::OnStartJump()
{
this->isGround = false; // ジャンプ中はisGroundフラグがfalse
}
// 更新処理
void Player::Update()
{
vxForce *= 0.9f; // かかっている力の減衰率
vyForce *= 0.9f; // かかっている力の減衰率
vzForce *= 0.9f; // かかっている力の減衰率
deltaAngle *= 0.5F; // 車のハンドルの方向をだんだんニュートラルに戻す
MoveSpeed *= 0.9F; // 移動速度も減速する
// 入力を受けての処理
HandleInput();
auto pJumpPhysics = jumpPhysics.lock();
if (pJumpPhysics != nullptr)
{
if (isGround)
{
if (Input::GetButtonDown(pad, PAD_INPUT_A) // Zキーを押した瞬間
|| (pad == Pad::Two && Input::GetButtonDown(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを押した瞬間
{
pJumpPhysics->JumpStart(); // ジャンプのスタート処理を発動
}
}
else if (vy > 0 && Input::GetButtonUp(pad, PAD_INPUT_A) // Zキーを離した瞬間
|| (pad == Pad::Two && Input::GetButtonUp(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを離した瞬間
{
pJumpPhysics->JumpCancel(); // ジャンプのキャンセル処理を発動
}
pJumpPhysics->Update(); // ジャンプのコンポーネントを更新する
}
auto pGravityPhysics = gravityPhysics.lock();
if (pGravityPhysics != nullptr)
pGravityPhysics->Update(); // 重力落下のコンポーネントを更新する
moveAngle += deltaAngle; // ハンドルを回したぶんプレイヤの進行方向を変える
if (moveAngle < -180) moveAngle = 360 + moveAngle; // -180以下の角度にならないようにマイナスになったら+180度にループさせる
else if (moveAngle > +180) moveAngle = -360 + moveAngle; // +180以下の角度にならないようにマイナスになったら-180度にループさせる
rotation.SetRotation(VGet(0, (180 - moveAngle) * MyMath::Deg2Rad, 0)); // 前向きの3Dモデルなら180度回転させて奥方向の向きに設定
// ↓進行方向角度をX方向とZ方向の速度に変える
vx = (float)std::cos((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
vz = (float)std::sin((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
float vyForceJump = (pJumpPhysics != nullptr) ? pJumpPhysics->vyForce() : 0.0f;
float vyForceGravity = (pGravityPhysics != nullptr) ? pGravityPhysics->vyForce() : 0.0f;
vx += vxForce; // 力をかけて速度に加算する
vy += vyForce + vyForceJump + vyForceGravity; // 勢い(力・フォースを加算
vz += vzForce; // 力をかけて速度に加算する
// 実際に位置を動かす
auto pMapHitBox = mapHitBox.lock();
float cellHalf = (map() != nullptr) ? (float)map()->CellSize() / 2.0f : 16.0f; // マップのセルのサイズの半分
// 速すぎて当たり判定がすり抜けないように細切れのステップのグリッドで移動させつつ当たり判定する
Vector3 gridStepSize = (pMapHitBox == nullptr) ? Vector3{ cellHalf, cellHalf, cellHalf } : pMapHitBox->m_BoxSize / 2;
GridSteps vSteps{ velocity, gridStepSize }; // 細切れのステップの数値を計算
#ifdef _DEBUG
if (vSteps.div >= 1000) // 分割が1000を超える場合は物理式かどこかで∞=inf や 計算不可能数=nanが発生してそう
{ // vx,vy,vzに原因がありそうなのでデバッグ表示
if (!isfinite(vx)) printfDx(("vx:∞かnan x:" + std::to_string(x) + "\n").c_str());
if (!isfinite(vy)) printfDx(("vy:∞かnan y:" + std::to_string(y) + "\n").c_str());
if (!isfinite(vz)) printfDx(("vz:∞かnan z:" + std::to_string(z) + "\n").c_str());
}
#endif
bool isCollisionX{ false }, isCollisionY{ false }, isCollisionZ{ false }, isCollisionCollider{ false };
for (int i = 1, iSize = vSteps.div + 1; i <= iSize; ++i)
{
auto& vStep = (i < iSize) ? vSteps.step : vSteps.mod;
// まず横に移動する
if (pMapHitBox == nullptr)
x += vx;
else if (isCollisionX == false) // まだ前のステップでX方向の衝突がないなら
{
x += vStep.x;
isCollisionX = pMapHitBox->hitCheckX((TileBit)TileType::Wall);
}
// 次に縦に動かす
if (pMapHitBox == nullptr)
y += vy;
else if (isCollisionY == false) // まだ前のステップでY方向の衝突がないなら
{
y += vStep.y;
isCollisionY = pMapHitBox->hitCheckY((TileBit)TileType::Wall | (TileBit)TileType::Floor);
}
// 次にZ奥行き方向に動かす
if (pMapHitBox == nullptr)
z += vz;
else if (isCollisionZ == false) // まだ前のステップでZ方向の衝突がないなら
{
z += vStep.z;
isCollisionZ = pMapHitBox->hitCheckZ((TileBit)TileType::Wall);
}
if (auto pGridCollision = gridCollision.lock())
{
pGridCollision->Update(); // 所属するマス目セルを更新する
for (auto& collider : *pGridCollision->colliders())
isCollisionCollider |= collider.lock()->IsCollision();
}
// 衝突があったら、速すぎてすり抜けちゃう前に今のステップでbreakで抜ける
if (isCollisionCollider)
break; // ぶつかっているのにさらに次のステップの速度も足すとすり抜けちゃう
}
// まず横に移動する
x += vx;
if (pMapHitBox != nullptr)
pMapHitBox->hitCheckX((TileBit)TileType::Wall);
// 次に縦に動かす
y += vy;
if (pMapHitBox != nullptr)
pMapHitBox->hitCheckY((TileBit)TileType::Wall | (TileBit)TileType::Floor);
// 次にZ奥行き方向に動かす
z += vz;
if (pMapHitBox != nullptr)
pMapHitBox->hitCheckZ((TileBit)TileType::Wall);
if (auto pGridCollision = gridCollision.lock()) // lock()してnullptrじゃなければ
pGridCollision->Update(); // 所属するマス目セルを更新する
// カメラのプレイヤ追尾
float camDistance = 150; // プレイヤからカメラまでのY平面上の距離
float camHeight = 100; // プレイヤからカメラまでのY方向上の高さ
// moveAngleは2D画像のX方向右を0度とするから、-90度回すと画像を正面に捉える位置にカメラを置ける
float camAngleX = (float)std::cos((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
float camAngleZ = (float)std::sin((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
if (camera != nullptr)
{ // カメラの位置をプレイヤの進行方向と真逆に設定
camera->SetPosition(position + Vector3(camAngleX, camHeight, camAngleZ));
camera->LookAt(position); //カメラはプレイヤの方を見る
}
}
void Player::OnCollision(std::shared_ptr<Collision> pCollision)
{
if (isDead) return; // すでにisDeadなら return
auto otherCollider = pCollision->contacts[0].otherCollider.lock(); // コライダの弱共有ポインタをロック
if (otherCollider == nullptr) return; // すでにコライダが消去済だった
auto other = otherCollider->owner();
if (other->isDead) return; // other側がisDeadだったら処理をスルー
if (other->baseTag == "Enemy" || other->baseTag == "Player")
{
//[離散要素法:DEM] https://qiita.com/konbraphat51/items/157e5803c514c60264d2
constexpr float k = 1.0f; // バネ係数(めり込みに対するバネ反発の掛け率)
constexpr float e = 0.1f; // はね返り係数(直前と比べどれくらいの速さで離れるか0.5だと半分の速さではね返る)
float log_e = std::log(e); // はね返り係数の自然対数log
constexpr float pi_2 = DX_PI_F * DX_PI_F; // πの2乗
float eta = -2.0f * log_e * std::sqrt(k * mass / (log_e * log_e + pi_2)); // 粘性係数η(イータ)
auto norm = pCollision->contacts[0].normal; // 衝突の法線 * other->pushPower; // pushPowerの力でotherに押し返される
auto v_norm = dot(other->velocity - velocity, norm) * norm; // [相対速度v] 法線norm方向のv = (v1 - v0)・norm [ドット積]
auto sep = pCollision->contacts[0].separation; // 衝突点どうしの距離(めりこみ距離)
auto F = -k * sep * norm - eta * v_norm; // F (法線方向の力) = -k × sep - η × v (相対速度:自分から見た相手otherの速度)
force += F / mass; // 衝突法線方向の力を加える
}
}
// 描画処理
void Player::Draw()
{
int animIndex = animCount / 20; // キャラのパラパラアニメの画像番号(最大3)
float abs_vx = (vx > 0) ? vx : -vx; // x方向絶対値
float abs_vz = (vz > 0) ? vz : -vz; // y方向絶対値
if (abs_vx > 1.00f || abs_vz > 1.00f)
animCount += 6; // キャラが地面のXとZ方向に少しでもスピードがあったらアニメを走らせる
if (animCount >= 60) animCount = 0; // アニメ時間を0へループ
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
for (auto&& keyValue : world()->cameras) // プレイヤのいるワールドにあるすべてのカメラぶんループ
{
auto& camera = keyValue.second;
camera->Draw([&]()
{
// 常にカメラ方向を向くビルボード回転の角度を得る
VECTOR_D angle = MyDraw::GetAngle(DxLib::GetCameraBillboardMatrixD());
angle.x = 0; angle.z = 0; // Y軸周りのビルボード回転だけ有効にする X軸,Z軸まわりの回転は 0 にする
int imgIndex = 9; // キャラチップ画像の起点番号
// ビルボードの角度 と キャラの向いている向き の 差 によって、別のプレイヤのカメラから見たキャラチップの向きの画像を切り替える
// -180 ~ +180 : angle.y moveAngle : -180 ~ +180 ↓
float difAngleY = angle.y * 180.0f / DX_PI_F + moveAngle;
imgIndex = (difAngleY < -135 || 135 < difAngleY) ? 0 // カメラ向きの画像
: (difAngleY < -45) ? 6 // 右 向きの画像
: (difAngleY > 45) ? 3 // 左 向きの画像
: 9; // 奥向きの画像
MyDraw::DrawDivRotaGraphF3D(MyDraw::Plane::Z, x, y, z, 1.0f, angle, *image, imgIndex + animIndex, TRUE);
#ifdef _DEBUG // デバッグのときだけ_DEBUGが定義され描かれる
if (auto pSphereCollider = sphereCollider.lock()) pSphereCollider->Draw(); // コライダを描く
if (auto pMapHitBox = mapHitBox.lock()) pMapHitBox->Draw(); // ヒットボックスを描く
#endif
});
}
}
Factory.hを新規作成
して、ザコやアイテムなど「生成する処理を集約して呼び出す工場の窓口」を準備しましょう。
#ifndef FACTORY_H_
#define FACTORY_H_
#include <string>
#include <memory> // 共有ポインタを使う
#include <unordered_map>
#include "Vector3.h"
#include "Quaternion.h"
#include <assert.h>
#include <list>
#include "Object.h"
class GameObject; // 前方宣言でクラス宣言だけしてインクルードしない(.hではポインタしか使ってないから)
class World;
class Enemy;
class Bullet;
class Light;
class Item;
// 無名名前空間で区切ることで関数名など被らないように(★constructorとかよくある関数名で不安なので)
namespace // 無名名前空間(他ファイルから参照できない変数や関数を宣言) https://marycore.jp/prog/cpp/unnamed-namespace/
{
//template <class classT>
//void* Constructor(World* world, Vector3 position, Quaternion rotation)
//{ // ★classTをZako1とすると new classT(.. は初期化コンストラクタへの住所ポインタ(void*)型を返す
// return (void*) new classT(world, position, rotation); // newの生成コンストラクタへのポインタを辞書に入れる場合
//}
template <class classT>
std::shared_ptr<Object> Constructor(std::shared_ptr<InitParams> initParams, World* world, Vector3 position, Quaternion rotation)
{
return Object::Create<classT>(initParams, world, position, rotation);
//return std::make_shared<classT>(world, position, rotation); // make_sharedの生成コンストラクタへのポインタを辞書に入れる場合
}
//[関数へのポインタの応用:コンストラクタも関数の一種なので] https://blog.cryolite.net/entry/01000226/p1
//typedef void* (*constructorT)(World*, Vector3, Quaternion);
typedef std::shared_ptr<Object>(*constructorT)(std::shared_ptr<InitParams>, World*, Vector3, Quaternion);
typedef std::unordered_map<std::string, constructorT> ConstructorDictionary;//クラス名⇒初期化コンストラクタへの辞書
//★<クラス名→コンストラクタ>を呼び出す辞書★★★★★★★★★★
ConstructorDictionary g_Constructors; // クラス名⇒コンストラクタへの辞書配列
//★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
// クラス辞書に任意のclassTの初期化コンストラクタを登録する
template <class classT>
void RegisterConstructor(std::string const& className)
{ // insertでクラス辞書配列にクラス名とその初期化コンストラクタのアドレスのペアを登録する
g_Constructors.insert(std::make_pair(className, &Constructor<classT>));
}
std::shared_ptr<Object> Construct(std::string const& className, std::shared_ptr<InitParams> initParams, World* world, Vector3 position, Quaternion rotation = { 0,0,0,1 })
{ // classNameでクラス辞書をfind検索する
ConstructorDictionary::iterator ite = g_Constructors.find(className);
if (ite == g_Constructors.end()) // .end()なら辞書の最後なので見つからなかったってこと
{
assert("登録されていないクラスを初期化しようとしました!ちゃんと Factory.cpp で REGISTER_CONSTRUCTORしてください" == "");
return 0;
}
//辞書のsecondには初期化コンストラクタが入っているのでそれを返す⇒returnと同時に初期化コンストラクタが駆動する
return ite->second(initParams, world, position, rotation); // _VSTD::forward<_Args>(__args)...));
}
}
// ★工場ファクトリーパターン(プレイヤーや敵、アイテムなどのメモリ上の実体の初期化コンストラクトやmake_sharedを作成を一手に任せる)
// 【効果】新規生成処理の経路を共通の窓口にすることで色んなとこに散らばった生成処理の経路を一本にまとめる
class Factory
{
public:
//friend Object; // フレンド宣言でObjectクラスのprivateな関数にもアクセスできるようにする
// 初回だけ Zako0 などのクラスのコンストラクタを辞書に登録を1回だけする
// class_name に対応するコンストラクタが辞書登録済かもチェック
static void Init(const std::string& class_name);
// class_nameに応じて敵などのオブジェクトを生成してreturnで返す
template <typename TypeT> //[templateを.hと.cppに分けて書くのが難しい] https://qiita.com/i153/items/38f9688a9c80b2cb7da7
static std::shared_ptr<TypeT> ConstructByClassName(const std::string& class_name, std::shared_ptr<InitParams> initParams, World* world, Vector3 position, Quaternion rotation = { 0,0,0,1 });
//IDに応じて作り分ける役目を工場でMapなどの代わりに請け負う
template<typename TypeT>
static std::shared_ptr<TypeT> MakeSharedObject(const std::string& class_name, std::shared_ptr<InitParams> initParams, World* world, Vector3 position, Quaternion rotation = { 0,0,0,1 })
{
// class_nameの名前のコンストラクタがあるかチェックし、初回だけ Zako0 などのクラスのコンストラクタを辞書に登録を1回だけする
Init(class_name);
// class_name に応じて敵などのオブジェクトを生成してreturnで返す
auto pObject = ConstructByClassName<TypeT>(class_name, initParams, world, position, rotation);
return pObject; // 共有ポインタに変換して回し読み状態で返す
}
};
#endif
Factory.cppを新規作成
して、Zako0やItemなどの「文字列 → Zako0などのコンストラクタへの辞書」でZako0などの実体を生成できるようにしましょう。
#include "Factory.h"
#include <unordered_map> //★ #文字列 マクロで<クラス名→コンストラクタ>をビルドする瞬間に 辞書に登録
// new や std::make_shared1 や Createで初期生成するには
// #includeしてコンストラクタを呼び出せる必要が出てくる
#include "Enemy.h"
#include "Zako0.h"
//#include "Zako1.h"
//#include "Zako2.h"
//#include "Zako3.h"
//#include "Boss.h"
//#include "Item.h"
//#include "ItemBlock.h"
//#include "ItemBullet.h"
//#include "ItemMoney.h"
//#include "Bullet.h"
#include "World.h"
//【上級1】★以下サイトを参考に【文字列"Zako1"とかからクラスを初期化するテクニック】
// https://stackoverflow.com/questions/5447675/how-to-convert-a-string-to-a-class-name/5447776
//
// 以下のようにクラスの型タイプを書かずに初期化できるので★if else文をいちいちコピーして増やさずに済む
// 例. いちいち書く例
// if (gid == 0) return Factory::make_shared<Zako0>(x + 32, y + 32);
// else if(gid == 1) return Factory::make_shared<Zako1>(x + 32, y + 32);
// .......(敵の種類が増えるたびにいちいちif elseコピペで書かないと..)
// ↓
// ↓★"Zako1"の文字列で初期化できれば1行で書けるようになる余地が出てくる!(CSVのクラス名の文字から初期化も目指せる)
// ↓
// 例. 初期化に<Zako1>を使わずに済むようになる "Zako1"で書ける
// auto enemy = g_ClassFactory.Construct("Zako1", x + 32, y + 32);//←"Zako1"で初期化 <Zako1>だとコードに書くしかない
// クラス名 classTypeName 例. Zako0 のコンストラクタを s_umConstructors 辞書に登録するための↓#文字取得マクロ
#define REGISTER_CONSTRUCTOR(classTypeName) RegisterConstructor<classTypeName>( #classTypeName )
// Factoryクラスの「依存性の逆転」を引き起こすには.hでは★#includeしたくない https://blog.ecbeing.tech/entry/2021/01/20/114000
// だが、テンプレート関数は普通.hに実装までする必要があるが、それを回避する方法もある https://qiita.com/i153/items/38f9688a9c80b2cb7da7
// .cppでtemplate関数の実体を.hと分けて定義するには「★最小限下記の定義をオブジェクトごとにする必要」がある
template std::shared_ptr<Object> Factory::ConstructByClassName<Object>(const std::string&, std::shared_ptr<InitParams>, World*, Vector3, Quaternion);
template std::shared_ptr<Enemy> Factory::ConstructByClassName<Enemy>(const std::string&, std::shared_ptr<InitParams>, World*, Vector3, Quaternion);
namespace // 無名名前空間(他ファイルから参照できない変数や関数を宣言)
{
bool isConstructorRegistered{ false }; // 初回だけクラスのコンストラクタ登録を1回だけするためのフラグ
// 初回だけ Zako0 などのクラスのコンストラクタを辞書に登録を1回だけする
inline void InitConstructors()
{
if (!isConstructorRegistered) // 初回だけクラスのコンストラクタ登録を1回だけする
{ //クラス登録されていないなら初回登録して "文字列"→コンストラクタ を呼び出せるようにしておく
REGISTER_CONSTRUCTOR(Zako0); // Zako0のコンストラクタを辞書に登録しておく
//REGISTER_CONSTRUCTOR(Zako1);
//REGISTER_CONSTRUCTOR(Zako2);
//REGISTER_CONSTRUCTOR(Zako3);
//REGISTER_CONSTRUCTOR(Boss);
//REGISTER_CONSTRUCTOR(ItemBullet);
//REGISTER_CONSTRUCTOR(ItemBlock);
//REGISTER_CONSTRUCTOR(ItemMoney);
//REGISTER_CONSTRUCTOR(Bullet);
isConstructorRegistered = true;
}
}
}
void Factory::Init(const std::string& class_name)//int id)
{
// 初回だけ Zako0 などのクラスのコンストラクタを辞書に登録を1回だけする
InitConstructors();
// 辞書に class_name が登録されているかチェック
assert(g_Constructors.count(class_name) != 0 && "まだ登録してない クラス名 が Factory.cpp にありました。");
}
// class_name に応じて敵などのオブジェクトを生成してreturnで返す
template <typename TypeT> //[templateを.hと.cppに分けて書くのが難しい] https://qiita.com/i153/items/38f9688a9c80b2cb7da7
std::shared_ptr<TypeT> Factory::ConstructByClassName(const std::string& class_name, std::shared_ptr<InitParams> initParams, World* world, Vector3 position, Quaternion rotation)
{
// class_nameに応じて敵などのオブジェクトを生成してreturnで返す
// ★Zako1*でもBoss*でもOK、ベース基底のObject型であいまいな状態で受け取る
auto pObject = Construct(class_name, initParams, world, position, rotation);
return std::dynamic_pointer_cast<TypeT>(pObject); // タイプをキャストでTypeTで指定した型に変換して返す
}
GameObject.hを変更
して、OnDamageなどダメージ系の処理のベースを準備しましょう。
#ifndef GAMEOBJECT_H_
#define GAMEOBJECT_H_
#include <memory> // 共有ポインタの定義のため
#include "Vector3.h"
#include "Quaternion.h"
#include "Tile.h" // OnHitTileのコールバックに必要
#include "Object.h" // インスタンスIDや検索タグを持ち、shared_from_this_as<継承先クラス名>などができる
#include "World.h"
class Map; // 前方宣言
class Component; // 前方宣言
class Collision; // 前方宣言
class CollisionParams; // 前方宣言
// ゲーム上に表示される物体の基底クラス。
// プレイヤーや敵、アイテムなどはこのクラスを継承して作る。
class GameObject : public Object
{
protected:
// コンストラクタはprotected:にしてあるのでCreate関数経由で初期生成してください
GameObject(World* world = nullptr, Vector3 position = { 0,0,0 }, Quaternion rotation = { 0,0,0,1 }, Vector3 velocity = { 0,0,0 }, Vector3 force = { 0,0,0 })
: Object(), m_world{ world }, position{ position }, rotation{ rotation }, velocity{ velocity }, force{ force }
{
}
public:
// 仮想デストラクタ
virtual ~GameObject() {}
union { // ★共用体unionテクニック https://inemaru.hatenablog.com/entry/2016/03/02/005408
//★下記3つはメモリ上で共用状態になる(position、x,y,z、xyzどの名前から数値を変えたり読出してもメモリ上は同じ)
// 同じデータに3種類の名前を付けたイメージ、しかもVector3の機能や配列としてのアクセスの仕方もできて便利
struct { float x, y, z; }; // XYZ座標 [匿名共用体とは] https://code.i-harness.com/ja-jp/q/4d437c
Vector3 position; // XYZ座標
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で回せる
Quaternion rotation; // 回転クオータニオン
union {
struct { float vx, vy, vz; }; // XYZ方向の速度
Vector3 velocity; // XYZ方向の速度
std::array<float, 3> vxyz;
};
union {
struct { float vxForce, vyForce, vzForce; }; // XYZ方向にかかる力(Unityでいうと AddForce関数 )
Vector3 force; // XYZ方向の力(物理的には加速度:1フレームごとにvelocityの増える=加速する量)
std::array<float, 3> vxyzForce;
};
// 重力を返す、重力を変えたいときはoverrideして処理を変えてください、重力を変えてない場合は 0.0f
virtual inline float gravity() { return 0.0f; }
protected:
World* m_world; // GameObjectが配置されたワールドへのリンク
public:
// 現在いるワールドへのポインタを得る
virtual World* world() { return m_world; }
// 現在いるワールドのポインタを変更する
virtual void world(World* changeWorld) { m_world = changeWorld; }
// GameObjectが現在いるマップへのポインタを返す
Map* map() { return m_world->map.get(); }
bool isDead{ false }; // 死んだ(削除対象)フラグ
bool isStatic{ false }; // 動かない(静的オブジェクトか?)
bool isGround{ false }; // 地面についているか
bool isJumping{ false }; // ジャンプ中か
bool isFlying{ false }; // 空をとんでいるか
float pushPower{ 5.0f }; // コライダで接触したときに押し返す力(相手側が強いと押される)
float mass{ 1.0f }; // 質量(重さ)コライダで接触したときに重さでforceを割り算すると動きにくくなる
std::shared_ptr<CollisionParams> collisionParams; // 衝突関係の物理パラメータ
hash32 typeTag{ "" }; // 小カテゴリ Zako0など個別のタイプ
hash32 baseTag{ "" }; // 大カテゴリ Enemyなど大まかなベースジャンル
protected:
// コンポーネントの<ハッシュID, 共有所有ポインタ> の辞書でコンポーネントの共有カウンタ+1を保持する
std::unordered_map<size_t, std::shared_ptr<Component>> components;
public:
// コンポーネントを得る 例えば GetComponent<BoxCollider>でBoxColliderという名前のコンポーネントをcomponents辞書から得る
template<class ComponentT>
std::vector<std::weak_ptr<ComponentT>> GetComponents()
{ //★[ComponentTのハッシュコードを得るテク] https://cpprefjp.github.io/reference/typeindex/type_index/hash_code.html
size_t hashID = typeid(ComponentT).hash_code(); //[まずハッシュでID化]
// equal_range 関数を使って、指定したキーの最初と終端の位置を示すイテレータを std::pair<> で取得します。
auto range = components.equal_range(hashID); //https://ez-net.jp/article/9E/DFLX8hNG/G5J8DPmsltz4/
std::vector<std::weak_ptr<ComponentT>> returnList; // "ComponentT" 例."CircleCollider"など をキーとするコンポーネント一覧
for (auto ite = range.first; ite != range.second; ++ite)
returnList.emplace_back(ite->second); // 辞書で見つかったコンポーネントを返すリストに追加
return returnList;
}
// コンポーネントを追加する .AddComponent<BoxCollider>(...)で追加できる
template<class ComponentT, typename ...ArgsT>
std::weak_ptr<ComponentT> AddComponent(ArgsT&&...args)
{ //★[ComponentTのハッシュコードを得るテク] https://cpprefjp.github.io/reference/typeindex/type_index/hash_code.html
size_t hashID = typeid(ComponentT).hash_code(); //[まずハッシュでID化]
// shared_from_this()を必ず渡してセットで初期化して、オーナーがリンク切れのコンポーネントができないように気をつける
std::shared_ptr<ComponentT> newComponent = std::make_shared<ComponentT>(shared_from_this_as<GameObject>(), std::forward<ArgsT>(args)...);
// https://ez-net.jp/article/9E/DFLX8hNG/G5J8DPmsltz4/
// std::unrodered_multimap<> と同型の値を取る std::pair<> を作って insert 関数でcomponents辞書に追加
components.insert(std::pair<size_t, std::shared_ptr<ComponentT>>(hashID, newComponent));
return newComponent;
}
// コンポーネントを削除する
template<typename ComponentT>
bool RemoveComponent(std::shared_ptr<ComponentT> removeComponent)
{ //★[ComponentTのハッシュコードを得るテク] https://cpprefjp.github.io/reference/typeindex/type_index/hash_code.html
size_t hashID = typeid(ComponentT).hash_code(); //[まずハッシュでID化]
// equal_range 関数を使って、指定したキーの最初と終端の位置を示すイテレータを std::pair<> で取得します。
auto range = components.equal_range(hashID); //https://ez-net.jp/article/9E/DFLX8hNG/G5J8DPmsltz4/
//[イテレート中に探しながら消す] https://cpprefjp.github.io/reference/unordered_map/unordered_multimap/erase.html
for (auto ite = range.first; ite != range.second;)
if (ite->second == removeComponent)
{
components.erase(ite); // 削除された要素の次を指すイテレータが返される
return true; // 見つかって消せた場合は true
}
else
++ite; // 次の要素へイテレータを進める
return false; // 見つからずに消せなかった場合は false
}
// 更新処理[純粋仮想関数 = 0 なのでoverride必須の関数]
virtual void Update() = 0; // 純粋仮想関数 = 0 にすると【絶対 Update()関数はoverrideしなきゃいけない義務を付与できる 】
// 描画処理[純粋仮想関数 = 0 なのでoverride必須の関数]
virtual void Draw() = 0; // 純粋仮想関数 = 0 にすると【絶対 Draw()関数はoverrideしなきゃいけない義務を継承先クラスに付与 】
// 衝突開始したときの関数 overrideして衝突したときの処理などをオブジェクトの種類ごとに実装する
virtual void OnCollisionEnter(std::shared_ptr<Collision> pCollision)
{
// overrideして衝突開始したときの処理などをオブジェクトの種類ごとに実装する
};
// 衝突したときの関数 overrideして衝突したときの処理などをオブジェクトの種類ごとに実装する
virtual void OnCollision(std::shared_ptr<Collision> pCollision)
{
// overrideして衝突したときの処理などをオブジェクトの種類ごとに実装する
};
// 衝突終了したときの関数 overrideして衝突したときの処理などをオブジェクトの種類ごとに実装する
virtual void OnCollisionExit(std::shared_ptr<Collision> pCollision)
{
// overrideして衝突終了したときの処理などをオブジェクトの種類ごとに実装する
};
// ジャンプ開始した直後に呼ばれる関数
virtual void OnStartJump()
{
// overrideしてジャンプした瞬間に音を鳴らしたりする処理などをオブジェクトの種類ごとに実装する
};
// 空中でジャンプの上昇中に呼ばれる関数
virtual void OnJumping()
{
// overrideして空中でジャンプの上昇中にエフェクトを出したりする処理などをオブジェクトの種類ごとに実装する
};
// 空中で落ちている最中に呼ばれる関数
virtual void OnFalling()
{
// overrideして空中で落ちている最中に音を鳴らしたり、エフェクトを出したりする処理などをオブジェクトの種類ごとに実装する
};
// タイルに当たったときのコールバック
virtual void OnHitTile(HitPart hitPart, HitBit hitPartBit, TileBit checkBit, float xLeft, float xMiddle, float xRight,
float yBottom, float yMiddle, float yTop, float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
// 継承 overrideしてタイルに当たったときの処理を実装する
}
// 継承 overrideしてGameObjectのもつ個別のダメージの値を返す( otherの種類などでダメージ計算式をカスタムできる )
virtual float Damage(std::shared_ptr<GameObject> other) { return 0.0f; }
// 継承 overrideして個別のダメージの値を返す
virtual float Damage() { return 0.0f; }
virtual void OnDamage(std::shared_ptr<GameObject> other)
{
// overrideしてダメージを受けた時の処理を個別に実装
}
};
#endif
Enemy.hを新規作成
して、Zako0などの敵を表現するベース基底クラスを準備しましょう。
#ifndef ENEMY_H_
#define ENEMY_H_
#include "GameObject.h"
#include "Collider.h"
#include "CollisionParams.h"
class World; //前方宣言
// アイテムの情報のベース基底クラス 継承override してアイテム情報をjsonなどから受け取る
struct EnemyInfo : public InitParams
{
float life{ 1.0f }; // HP lifeが0になったらisDeadになる
float damage{ 0.0f }; // プレイヤが当たったときに受けるダメージ
float speedMax{ 3.0f }; // 敵の移動スピードのMAX値
float colliderRadius{ 8.0f }; // アイテムとしてのコライダの半径
float bulletRadius{ 16.0f }; // 敵が弾を撃つ際の弾としてのコライダの半径
int bulletCount{ -1 }; // 弾を撃てる数のカウント値( -1のときは無限に撃てるタイプの敵 )
float bulletDamage{ 1 }; // 弾の与えるダメージ
float bulletSpeed{ 10.0f }; // 弾のスピード
std::shared_ptr<CollisionParams> collisionParams; // 衝突関係の物理パラメータ
std::weak_ptr<GameObject> shooter; // 弾を撃った後に弾の撃ち手の弱共有ポインタのリンクをBulletに渡す
float shooterAngleY{ 0.0f }; // 撃ち手の撃った際の角度
int imageIndex{ -1 }; // 敵の画像が分割画像だったらどの位置のIDの画像か
int bulletImageIndex{ -1 }; // 弾の画像が分割画像だったらどの位置のIDの画像か
std::string imagePath{ "" }; // 敵の画像のパス
std::string bulletImagePath{ "" }; // 敵の撃つ弾の画像のパス
inline EnemyInfo(std::shared_ptr<InitParams> initParams = nullptr) : InitParams{}
{
auto tmxParams = std::static_pointer_cast<InitTMXProperty>(initParams);
if (tmxParams->properties == nullptr) return;
auto tmxProperties = tmxParams->properties;
// tmxのjsonの追加プロパティからデータを読み取る
if (tmxProperties->count("pushPower") > 0 || tmxProperties->count("collision_e") > 0)
collisionParams = std::make_shared<CollisionParams>(initParams); // 物理パラメータを読み取り
auto itr = tmxProperties->find("life");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Float && "json の imagePath のタイプが Floatでない!jsonのプロパティを見直してください");
life = itr->second.floatValue;
}
itr = tmxProperties->find("damage");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Float && "json の damage のタイプが Floatでない!jsonのプロパティを見直してください");
damage = itr->second.floatValue;
}
itr = tmxProperties->find("imageIndex");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Int && "json の imageIndex のタイプが Intでない!jsonのプロパティを見直してください");
imageIndex = itr->second.intValue;
}
itr = tmxProperties->find("imagePath");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::String && "json の imagePath のタイプが Stringでない!jsonのプロパティを見直してください");
imagePath = itr->second.stringValue;
}
itr = tmxProperties->find("bulletImageIndex");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Int && "json の bulletImageIndex のタイプが Intでない!jsonのプロパティを見直してください");
bulletImageIndex = itr->second.intValue;
}
itr = tmxProperties->find("bulletImagePath");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::String && "json の bulletImagePath のタイプが Stringでない!jsonのプロパティを見直してください");
bulletImagePath = itr->second.stringValue;
}
itr = tmxProperties->find("colliderRadius");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Float && "json の colliderRadius のタイプが Floatでない!jsonのプロパティを見直してください");
colliderRadius = itr->second.floatValue;
}
itr = tmxProperties->find("bulletRadius");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Float && "json の bulletRadius のタイプが Floatでない!jsonのプロパティを見直してください");
bulletRadius = itr->second.floatValue;
}
itr = tmxProperties->find("bulletSpeed");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Float && "json の bulletSpeed のタイプが Floatでない!jsonのプロパティを見直してください");
bulletSpeed = itr->second.floatValue;
}
itr = tmxProperties->find("bulletCount");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Int && "json の bulletCount のタイプが Intでない!jsonのプロパティを見直してください");
bulletCount = itr->second.intValue;
}
itr = tmxProperties->find("bulletDamage");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Float && "json の bulletDamage のタイプが Floatでない!jsonのプロパティを見直してください");
bulletDamage = itr->second.floatValue;
}
}
virtual ~EnemyInfo() {}
};
// 敵の基底クラス。
// 全ての敵は、このクラスを継承して作る。
// ★【勉強】abstract型はC++にない!!【純粋仮想関数】=0を使って作る
class Enemy : public GameObject
{
public:
// コンストラクタ
Enemy(World* world, Vector3 position, Quaternion rotation = { 0,0,0,1 })
// Enemy(const Object::ID& stopper, World* world, Vector3 position)
: GameObject(world, position, rotation)
{
this->baseTag = "Enemy";
}
public:
float speedMax{ 1.0f };
float life{ 1.0f }; // 耐久力
float m_Damage{ 0 }; // 敵に当たった時に与えるダメージ
float Damage() override { return m_Damage; }
std::shared_ptr<Resource> m_pImage; // 画像や3Dモデルなどのパス
std::shared_ptr<EnemyInfo> m_enemyInfo; // 敵のjsonから読み取った情報
virtual void Init(std::shared_ptr<InitParams> initParams = nullptr) override
{
if (initParams == nullptr) return;
m_enemyInfo = std::make_shared<EnemyInfo>(initParams); // アイテム情報を受け取る
collisionParams = m_enemyInfo->collisionParams; // 物理衝突パラメータを受け取る
//if (Resource::TypeByExt(params->imagePath) == Resource::Type::Texture)
if (m_enemyInfo->imagePath != "")
m_pImage = Resource::MakeShared<Texture>(m_enemyInfo->imagePath);
m_Damage = m_enemyInfo->damage;
life = m_enemyInfo->life;
speedMax = m_enemyInfo->speedMax;
}
virtual ~Enemy() {}
// 更新処理。派生クラスでオーバーライドして使う
virtual void Update() = 0;//virtual ~ = 0;で【純粋仮想関数に】これで【C#のabstract型に】
// 描画処理。派生クラスでオーバーライドして使う
virtual void Draw() = 0; //virtual ~ = 0;で【純粋仮想関数に】【関数 = 0で未定義状態を表現】
virtual void OnCollision(std::shared_ptr<Collision> pCollision) override
{
if (isDead) return;
auto otherCollider = pCollision->contacts[0].otherCollider.lock();
if (otherCollider == nullptr) return;
auto other = otherCollider->owner();
if (other->isDead) return; // other側がisDeadだったら処理をスルー
if (other->typeTag == "PlayerBullet" || other->baseTag == "Enemy" || other->baseTag == "Player")
{ // 相手方がPlayerBulletの場合は相手側の物理パラメータを使用する
auto colObj = (other->typeTag == "PlayerBullet") ? other : shared_from_this_as<GameObject>();
float objPushPower = (colObj->collisionParams != nullptr) ? colObj->collisionParams->pushPower : other->pushPower;
if (colObj->collisionParams != nullptr && colObj->collisionParams->isDEMcollision)
{ //[離散要素法] https://qiita.com/konbraphat51/items/157e5803c514c60264d2
float k = colObj->collisionParams->k; // バネ係数(めり込みに対するバネ反発の掛け率)
float e = colObj->collisionParams->e; // 跳ね返り係数(直前と比べどれくらいの速さで離れるか 0)
float log_e = colObj->collisionParams->log_e();
float eta = colObj->collisionParams->eta(mass); // 粘性係数 の mass は自分側を使う?
auto norm = pCollision->contacts[0].normal;// *other->pushPower; // pushPowerの力でotherに押し返される
auto v_norm = dot(other->velocity - velocity, norm) * norm; // 法線norm方向のv = (v1 - v0)・norm [ドット積]
auto sep = pCollision->contacts[0].separation; // 衝突点どうしのはなれ具合
auto F = -k * sep * norm - eta * v_norm;
force += F / mass; // 衝突法線方向の力を加える
}
else
{
auto norm = pCollision->contacts[0].normal * objPushPower;
force += -norm;
}
}
}
// 継承overrideしてダメージ計算式で値を返す
virtual float Damage(std::shared_ptr<GameObject> other) override
{
return other->Damage(); // 弾の側のダメージ値をそのまま採用(防御力とかで式をカスタムもできる)
}
virtual void OnDamage(std::shared_ptr<GameObject> other)
{
if (other->baseTag == "Bullet")
{
life -= Damage(other); // ライフを減らす
}
// ライフが無くなったら、死亡
if (life <= 0)
{
isDead = true;
}
}
};
#endif
Zako0.hを新規作成
して、Enemyを継承overrideして敵の実体を定義しましょう。
#ifndef ZAKO_0_H_
#define ZAKO_0_H_
#include "Enemy.h"
#include "MyRandom.h"
#include "Resource.h"
#include "MapHitBoxComponent.h"
#include "Collider.h"
#include "GridCollidersComponent.h"
#include "GridSteps.h"
#include "PhysicsComponent.h" // 重力落下などの物理処理コンポーネントを使う
class World; // 前方宣言
// ザコ0クラス
class Zako0 : public Enemy
{
public:
std::shared_ptr<Texture> Image()
{
return std::static_pointer_cast<Texture>(m_pImage);
}
float MoveSpeed = 0;//移動速度
float moveAngle = 0; // X軸→方向から何度か
float deltaAngle = 0; // 動く角度の変化率、ハンドルを切ったらだんだんもとに戻る
int animCount = 0;
protected:
std::weak_ptr<GravityComponent> gravityPhysics; // 落下の重力の物理処理コンポーネント
// マップ地形に対するヒットボックスの当たり判定と押し戻しをするコンポーネント
std::weak_ptr<MapHitBoxComponent> mapHitBox;
std::weak_ptr<GridCollidersComponent> gridCollision;
std::weak_ptr<SphereCollider> sphereCollider;
public:
virtual void Init(std::shared_ptr<InitParams> initParams = nullptr) override
{
Enemy::Init(initParams); // 基底ベースで共通パラメータを読み取り
if (m_enemyInfo->imagePath == "" && m_pImage == nullptr)
m_pImage = Resource::MakeShared<Texture>("Image/zako0.png");
std::vector<float> vyGravity{ 0.8f,0.8f,0.8f,0.8f }; // 重力の設定値
std::vector<float> vyDownSpeedMax{ 16, 16, 16, 8 }; // 降下スピードのリミット
// AddComponent内部でshared_from_this()を必ず渡してセットで初期化して、オーナーがリンク切れのコンポーネントができないように気をつける
gravityPhysics = AddComponent<GravityComponent>(vyGravity, vyDownSpeedMax);
// 半径16(直径32)の球体コライダ
sphereCollider = AddComponent<SphereCollider>(Vector3(0, 0, 0), 16);
gridCollision = AddComponent<GridCollidersComponent>("Enemy");
gridCollision.lock()->LinkCollider(sphereCollider); // コライダをリンクする
float mapHitBoxSize = sphereCollider.lock()->m_radius * 2.0f; // 球体コライダの直径に合わす
Vector3 boxSize{ mapHitBoxSize,mapHitBoxSize,mapHitBoxSize };
Vector3 offsetLeftBottomBack{ 3, 0, 3 }; // ボックスの端を内側にどれだけ削るか(キャラの画像に合わせて小さくできる)
Vector3 offsetRightTopForward{ 3, 4, 3 }; // ボックスの端を内側にどれだけ削るか(頭を4削る)
mapHitBox = AddComponent<MapHitBoxComponent>(boxSize, offsetLeftBottomBack, offsetRightTopForward);
}
// overrideして物理落下コンポーネントからの重力値を返す
virtual float gravity() override { auto pGravityPhysics = gravityPhysics.lock(); return (pGravityPhysics != nullptr) ? pGravityPhysics->gravity() : 0.0f; }
// コンストラクタ
Zako0(World* world, Vector3 position, Quaternion rotation = { 0,0,0,1 }) : Enemy(world, position, rotation)
{
this->typeTag = "Zako0";
this->pushPower = 5.0f;
}
// 更新処理
void Update() override
{
vxForce *= 0.9f; // かかっている力の減衰率
vyForce *= 0.9f; // かかっている力の減衰率
vzForce *= 0.9f; // かかっている力の減衰率
deltaAngle *= 0.5F; // 車のハンドルの方向をだんだんニュートラルに戻す
//MoveSpeed *= 0.9F; // 移動速度も減速する
MoveSpeed += MyRandom::Range(-0.07f, 0.1f); //アクセルを踏む
if (MoveSpeed > speedMax) MoveSpeed = speedMax;//Maxスピードで止める
if (MoveSpeed < -0.7f) MoveSpeed = -0.7f;//ちょっとだけバックできるように
// ランダムAI動作処理
deltaAngle += MyRandom::PlusMinus(0.3f); // プレイヤがみつからないときはランダム(同じフロアにいないとき)
moveAngle += deltaAngle; // ハンドルを回したぶんプレイヤの進行方向を変える
if (moveAngle < -180) moveAngle = 360 + moveAngle; // -180以下の角度にならないようにマイナスになったら+180度にループさせる
else if (moveAngle > +180) moveAngle = -360 + moveAngle; // +180以下の角度にならないようにマイナスになったら-180度にループさせる
auto pGravityPhysics = gravityPhysics.lock();
if (pGravityPhysics != nullptr)
pGravityPhysics->Update(); // 重力落下のコンポーネントを更新する
// ↓進行方向角度をX方向とZ方向の速度に変える
vx = (float)std::cos(moveAngle) * MoveSpeed;
vz = (float)std::sin(moveAngle) * MoveSpeed;
float vyForceGravity = (pGravityPhysics != nullptr) ? pGravityPhysics->vyForce() : 0.0f;
vx += vxForce;
vy += vyForce + vyForceGravity; // 勢い(力・フォースを加算
vz += vzForce;
vy += vyForce; // 勢い(力・フォースを加算
auto pMapHitBox = mapHitBox.lock();
float cellHalf = (map() != nullptr) ? (float)map()->CellSize() / 2.0f : 16.0f; // マップのセルのサイズの半分
// 速すぎて当たり判定がすり抜けないように細切れのステップのグリッドで移動させつつ当たり判定する
Vector3 gridStepSize = (pMapHitBox == nullptr) ? Vector3{ cellHalf, cellHalf, cellHalf } : pMapHitBox->m_BoxSize / 2;
GridSteps vSteps{ velocity, gridStepSize }; // 細切れのステップの数値を計算
bool isCollisionX{ false }, isCollisionY{ false }, isCollisionZ{ false }, isCollisionCollider{ false };
for (int i = 1, iSize = vSteps.div + 1; i <= iSize; ++i)
{
auto& vStep = (i < iSize) ? vSteps.step : vSteps.mod;
// まず横に移動する
if (pMapHitBox == nullptr)
x += vx;
else if (isCollisionX == false) // まだ前のステップでX方向の衝突がないなら
{
x += vStep.x;
isCollisionX = pMapHitBox->hitCheckX((TileBit)TileType::Wall);
}
// 次に縦に動かす
if (pMapHitBox == nullptr)
y += vy;
else if (isCollisionY == false) // まだ前のステップでY方向の衝突がないなら
{
y += vStep.y;
isCollisionY = pMapHitBox->hitCheckY((TileBit)TileType::Wall | (TileBit)TileType::Floor);
}
// 次にZ奥行き方向に動かす
if (pMapHitBox == nullptr)
z += vz;
else if (isCollisionZ == false) // まだ前のステップでZ方向の衝突がないなら
{
z += vStep.z;
isCollisionZ = pMapHitBox->hitCheckZ((TileBit)TileType::Wall);
}
if (auto pGridCollision = gridCollision.lock())
{
pGridCollision->Update(); // 所属するマス目セルを更新する
for (auto& collider : *pGridCollision->colliders())
isCollisionCollider |= collider.lock()->IsCollision();
}
// 衝突があったら、速すぎてすり抜けちゃう前に今のステップでbreakで抜ける
if (isCollisionCollider)
break; // ぶつかっているのにさらに次のステップの速度も足すとすり抜けちゃう
}
}
// 描画処理
void Draw() override
{
if (world() == nullptr) return;
auto image = Image();
int animIndex = animCount / 20;
if (image->m_handles.size() > 8)
{
float abs_vx = (vx > 0) ? vx : -vx; // x方向絶対値
float abs_vz = (vz > 0) ? vz : -vz; // y方向絶対値
if (abs_vx > 0.01f || abs_vz > 0.01f)
animCount += 6;
if (animCount >= 60) animCount = 0; // アニメ時間を0へループ
}
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
for (auto&& keyValue : world()->cameras) // GameObjectのいるワールドにあるすべてのカメラぶんループ
{
auto& camera = keyValue.second;
camera->Draw([&]()
{
// 常にカメラ方向を向くビルボード回転の角度を得る
VECTOR_D angle = MyDraw::GetAngle(DxLib::GetCameraBillboardMatrixD());
angle.x = 0; angle.z = 0; // Y軸周りのビルボード回転だけ有効にする X軸,Z軸まわりの回転は 0 にする
int imgIndex = 0; // キャラチップ画像の起点番号
if (image->m_handles.size() > 8)
{ // ビルボードの角度 と キャラの向いている向き の 差 によって、別のプレイヤのカメラから見たキャラチップの向きの画像を切り替える
// -180 ~ +180 : angle.y moveAngle : -180 ~ +180 ↓
float difAngleY = angle.y * 180.0f / DX_PI_F + moveAngle;
imgIndex = (difAngleY < -135 || 135 < difAngleY) ? 0 // 奥向きの画僧
: (difAngleY < -45) ? 6 // 右 向きの画像
: (difAngleY > 45) ? 3 // 左 向きの画像
: 9; // 前向きの画像
}
if (0 <= imgIndex && imgIndex < image->m_handles.size())
{
int handleWidth, handleHeight; // 描画するサイズを得る
DxLib::GetGraphSize(image->m_handles[imgIndex], &handleWidth, &handleHeight);
float ExRate = 32.0f / handleWidth;
MyDraw::DrawDivRotaGraphF3D(MyDraw::Plane::Z, x, y, z, ExRate, angle, *image, imgIndex + animIndex, TRUE);
}
else
imgIndex = 0;
#ifdef _DEBUG
if (auto pSphereCollider = sphereCollider.lock()) pSphereCollider->Draw(); // コライダを描く
if (auto pMapHitBox = mapHitBox.lock()) pMapHitBox->Draw(); // ヒットボックスを描く
#endif
});
}
}
};
#endif
GameScene.cppを変更
して、敵を更新する処理と描く処理を実装しましょう。
#include "GameScene.h"
#include "Player.h"
#include "Enemy.h"
#include "Map.h"
GameSceneSetting GameScene::settings{ "Map/GameSceneSetting.csv" }; // ゲームシーンの設定情報
// 指定したゲームシーンのタグに関連づいたファイル名に対応したワールドを初期化する
bool GameScene::LoadWorlds(const std::string& gameSceneTag)
{
if (settings.info.count(gameSceneTag) == 0)
return false;
auto& info = settings[gameSceneTag];
for (auto&& tag_mapPath : info)
{
CreateWorld(tag_mapPath.second.tag); // ワールドをタグをつけて生成
// ワールドのマップをロード
worlds[tag_mapPath.second.tag]->LoadMap(tag_mapPath.second.mapFilePath);
}
return true;
}
void GameScene::ChangeWorld(const std::string& worldTag, std::shared_ptr<Player> pPlayer)
{
if (pPlayer == nullptr) return;
if (worlds.count(worldTag) == 0) return; // 存在しないタグだった
pPlayer->world(worlds[worldTag].get()); // ワールドへのリンクをすげ替える
}
void GameScene::Initialize()
{// Init処理
// Zバッファを有効にする [Zの深度]
//[参考]https://docs.google.com/presentation/d/1Z23t1yAS7uzPDVakgW_M02p20qt9DeTs4ku4IiMDLco/edit?usp=sharing
//[参考] https://dxlib.xsrv.jp/function/dxfunc_3d_draw.html#R14N12
DxLib::SetUseZBuffer3D(TRUE);
// Zバッファへの書き込みを有効にする
DxLib::SetWriteZBuffer3D(TRUE);
// 光の明暗計算を無効に
DxLib::SetUseLighting(FALSE);
// SetDrawScreen に引数として渡せる( 描画対象として使用できる )グラフィックハンドルを作成するかどうかを設定する
// ( TRUE:描画可能グラフィックハンドルを作成する FLASE:通常のグラフィックハンドルを作成する( デフォルト ) )
DxLib::SetDrawValidGraphCreateFlag(TRUE);
// サブのスクリーンを2つ生成 画面の全体の縦方向サイズの Height / 2 にして、上下分割で配置できるようにする
screenHandle0 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
screenHandle1 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
// スクリーンを作成したらデフォルト設定に戻しておかないと
DxLib::SetDrawValidGraphCreateFlag(FALSE);
Resource::MakeShared<Texture>(playerImgPath1, 3, 4, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
Resource::MakeShared<Texture>(playerImgPath2, 3, 4, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
Resource::MakeShared<Texture>("Image/zako0.png")->Load(); // 敵画像を読込
Resource::MakeShared<Texture>("Image/skullman.png", 3, 4, 32, 32)->LoadDivGraph(); // 敵画像を読込
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
// プレイヤの生成
if (pWorld != nullptr)
{
//pWorld->AddCamera("カメラ1", Camera::defaultCamera); // カメラ1というタグをデフォルトカメラにつけてワールドにデフォルトカメラを配置
pWorld->AddCamera("カメラ1", std::make_shared<Camera>(pWorld, Vector3{ 0,0,0 }));
auto pCamera1 = pWorld->camera("カメラ1"); // カメラ1 というタグのついたカメラへの共有ポインタを得る
pCamera1->SetScreenHandle(screenHandle0); // カメラから見える3Dを描く スクリーンのハンドル を関連付ける
auto pPlayer1 = Object::Create<Player>(nullptr, pWorld, Pad::Key, Vector3(90 + 256, 32, 95 + 256));
pWorld->AddPlayer("プレイヤ1", pPlayer1); // シーンにプレイヤを追加する
pPlayer1->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath1]);
pPlayer1->camera = pCamera1.get(); // カメラをプレイヤにリンクする
pPlayer1->mass = 100.0f; // 体重を100にして衝突されてもびくともしないようにする
pWorld->AddCamera("カメラ2", std::make_shared<Camera>(pWorld, Vector3{ 0,0,0 }));
auto pCamera2 = pWorld->camera("カメラ2"); // プレイヤ2 というタグのついたカメラへの共有ポインタを得る
pCamera2->SetScreenHandle(screenHandle1); // カメラから見える3Dを描く スクリーンのハンドル を関連付ける
auto pPlayer2 = Object::Create<Player>(nullptr, pWorld, Pad::Two, Vector3(190, 32, 195));
pWorld->AddPlayer("プレイヤ2", pPlayer2); // シーンにプレイヤを追加する
pPlayer2->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath2]);
pPlayer2->camera = pCamera2.get(); // カメラをプレイヤにリンクする
pPlayer2->mass = 1.0f; // 体重を1にして普通にふっとぶようにする
}
};
void GameScene::Update()
{// 更新処理
Screen::ClearDrawScreen(screenHandle0); // 一旦キャンバスをきれいにまっさらに
Screen::ClearDrawScreen(screenHandle1); // 一旦キャンバスをきれいにまっさらに
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
for (const auto& player : players)
player.second->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
// 敵の更新処理
for (const auto& world : worlds)
for (const auto& e : world.second->enemies)
e->Update();
// グリッドを更新して隣接するグリッドに含まれる敵や弾の当たり判定を行う
for (const auto& world : worlds)
if (world.second->grid() != nullptr)
for (const auto& cell : world.second->grid()->cellData)
{
auto pGridObject = cell.second;
while (pGridObject != nullptr)
{ // while文で数珠つなぎのm_pNextを次々とたどって最後にたどり着くと次がなくてnullptrになる
pGridObject->checkCell();
pGridObject = pGridObject->m_pNext; // リンクリスト構造を次々とたどる
}
}
}
void GameScene::Draw()
{// 描画処理
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
std::unordered_set<World*> drawEndSet; // すでに描いたWorldをポインタのsetでかぶらないように判定する
for (const auto& player : players)
{
player.second->Draw(); // プレイヤの描画【忘れるとプレイヤ表示されない】
auto world = player.second->world(); // プレイヤのいるワールド
if (drawEndSet.count(world) > 0) continue; // すでに描いたWorldは描かずスキップする
drawEndSet.emplace(world); // 描いたWorldのポインタ住所をsetに登録する
world->map->DrawTerrain(); // マップの描画
// 敵の描画
for (const auto& enemy : world->enemies)
{
enemy->Draw();
#ifdef _DEBUG
//enemy->DrawHitBox(); // 当たり判定の描画
#endif
}
#ifdef _DEBUG // デバッグのときだけ_DEBUGが定義され描画される
if (world->grid() != nullptr)
for (const auto& cell : world->grid()->cellData)
cell.second->Draw(); // グリッドの描画
#endif
}
// スクリーン0の内容を画面に描く
DxLib::DrawGraph(0, 0, screenHandle0, FALSE);
DxLib::DrawLineBox(0, 0, Screen::Width, Screen::Height / 2, GetColor(255, 0, 0));
// スクリーン1の内容を画面に描く
DxLib::DrawGraph(0, Screen::Height / 2, screenHandle1, FALSE);
DxLib::DrawLineBox(0, Screen::Height / 2, Screen::Width, Screen::Height, GetColor(0, 255, 0));
}
Map.hを変更
して、Factoryから敵を生成する処理を追加します。
#ifndef MAP_H_
#define MAP_H_
#include <vector> // C#におけるListリストの機能
#include <unordered_map> // 辞書型連想配列に必要
#include <memory> // メモリを扱う【共有ポインタに必要】
#include <limits> //intやfloatなどの無限大∞に使う https://cpprefjp.github.io/reference/limits/numeric_limits/max.html
#include <fstream> // ファイル読み出しifstreamに必要
#include <string> //文字列に必要
#include <sstream> // 文字列ストリームに必要
#include <assert.h> // マップ読込み失敗表示用
#include "Screen.h"
#include "MyMath.h"
#include "MyHash.h"
#include "MyDraw.h"
#include "DataCsv.h"
#include "TMXJson.h"
#include "Singleton.h"
#include "GameObject.h"
#include "World.h"
#include "Camera.h"
#include "Factory.h"
#include "MapRange.h" // マス目のレンジ表現するクラス
#include "Tile.h"
#include "Vector3.h"
class AI_Map; // 前方宣言
// csvから読み取るマップの基本情報
struct MapInfo
{
MapInfo() = default;
inline MapInfo(unsigned int CellSize, int SpawnRangeX, int SpawnRangeY, MapRange::Type type = MapRange::Type::Rect)
: CellSize{ CellSize }, spawnRange{ MapRange(SpawnRangeX,SpawnRangeY,type) } {};
~MapInfo() {};
unsigned int CellSize{ 1 }; // マップの1マスのピクセル数
MapRange spawnRange; // 敵の新規出現レンジ
};
struct MapSetting // マップ情報をMapSetting.csvなどから読みだす
{
MapSetting(const std::string& csvFileName = "")
{
if (csvFileName != "")
Load(csvFileName);
}
~MapSetting() {}; //デストラクタ
std::string FilePath{ "" }; // csvのファイルパス
std::unordered_map<hash32, std::shared_ptr<MapInfo>> info; //[辞書]stageファイル名→マップ情報
MapInfo& operator[](const std::string& stageFilePath) {
assert(info.count(stageFilePath) != 0 && "マップのステージ情報が読み取れていません Map/MapSettings.csv にステージ情報があるか確認!");
return *info[stageFilePath]; // 読み書き
}
void Load(const std::string& csvFileName = "")
{
if (csvFileName != "") this->FilePath = csvFileName;
if (this->FilePath == "") return;
// マップ情報のデータ CSVファイルからデータをロード
DataCsv Data{ this->FilePath, DataCsv::CsvType::CsvValue };
std::string stageFile; //ステージファイル名
for (int j = 0; j < Data.size(); ++j)
{
if ((std::string)Data[j][0] == "//")
continue; // 一番左の列が//なら読み飛ばしのコメント行
else if ((std::string)Data[j][0] == "File")
{
stageFile = Data[j][1]; //ステージファイル名
info.emplace(stageFile, std::make_shared<MapInfo>());
continue;
}
if (stageFile == "") continue; //ステージファイル名が未確定
else if ((std::string)Data[j][0] == "CellSize")
info[stageFile]->CellSize = (int)Data[j][1]; //マス目サイズ
else if ((std::string)Data[j][0] == "SpawnRange")
{ // 敵出現レンジ
MapRange::Type rangeType = ((std::string)Data[j][3] == "Ellipse") ? MapRange::Type::Ellipse : MapRange::Type::Rect; //敵出現のタイプ
info[stageFile]->spawnRange.InitRange(rangeType, Data[j][1].intFromString(), Data[j][2].intFromString()); //敵出現のタイプを初期化設定
}
}
}
};
// ステージのタイルマップのクラス
class Map
{
public:
// コンストラクタ
Map(World* world, const std::string& filePath = "")
: m_world{ world }
{
if (filePath == "") return;
Load(filePath);
}
// デストラクタ
~Map()
{ // マップデータのお掃除
enemyData.clear();
//terrains.clear(); //★地形のお掃除
}
static MapSetting settings; // マップに関する設定 Map.cpp で初期読込み デフォルトは"Map/MapSetting.csv" を読み込む
static TileSetting tileSettings; // タイルに関する設定 Map.cpp で初期読込み デフォルトは"Map/MapSetting.csv" を読み込む
// タイルのチップ画像のパスとidからTileの情報とタイルのgid情報を得る
static Tile GetTile(const std::string& mapChipPath, int id, unsigned int& gid)
{
auto pos = mapChipPath.find(".", 0);
std::string tsxPath{ "" };
if (pos != std::string::npos) tsxPath = mapChipPath.substr(0, pos) + ".tsx";
auto found = Tilesets::umTsxInfo.find(tsxPath);
if (found != end(Tilesets::umTsxInfo) && found->second.gid.size() > id)
gid = found->second.gid[id]; // 辞書で id から gidに変換しておく
else return Tile(); // id が-1のときは見つからなかったとき
pos = tsxPath.find("/", 0);
if (pos != std::string::npos) tsxPath = tsxPath.substr(pos + 1, tsxPath.size() - 1);
return Map::tileSettings.info[tsxPath]->tiles[id];
}
protected:
World* m_world{ nullptr }; // マップに配置する敵などのポインタを保持するワールド
public:
// 現在いるワールドへのポインタを得る
World* world() { return m_world; }
// 現在いるワールドのポインタを変更する
void world(World* changeWorld) { m_world = changeWorld; }
std::shared_ptr<AI_Map> aiMap{ nullptr }; // AIの経路探索に使うAIマップ
// 読込んだデータファイルの情報
std::string FilePath{ "" };
protected:
unsigned int m_CellSize{ 1 }; // マップの1マスのピクセル数 1のときはtmxに設定されたサイズが採用される
unsigned int m_HalfCellSize{ 1 }; // マス目サイズの 1/2
public:
inline unsigned int CellSize() { return m_CellSize; }
inline unsigned int HalfCellSize() { return m_HalfCellSize; }
inline void SetCellSize(unsigned int cellSize) { m_CellSize = cellSize; m_HalfCellSize = m_CellSize / 2; }
MapRange spawnRange; // 敵の新規出現レンジをプレイヤから半径X,Yの楕円にする
inline float centerX(int cellX) const { return (cellX >= 0) ? (float)((cellX * m_CellSize) + m_HalfCellSize) : (float)((cellX + 1) * m_CellSize) - m_HalfCellSize; } //マス目サイズ/2ずらし
inline float centerY(int cellY) const { return (cellY >= 0) ? (float)((cellY * m_CellSize) + m_HalfCellSize) : (float)((cellY + 1) * m_CellSize) - m_HalfCellSize; } //マス目サイズ/2ずらし
inline float centerZ(int cellZ) const { return (cellZ >= 0) ? (float)((cellZ * m_CellSize) + m_HalfCellSize) : (float)((cellZ + 1) * m_CellSize) - m_HalfCellSize; } //マス目サイズ/2ずらし
inline int cellX(float worldX) const { return (worldX >= 0) ? (int)worldX / m_CellSize : (int)worldX / m_CellSize - 1; } // マイナス方向にも対応
inline int cellY(float worldY) const { return (worldY >= 0) ? (int)worldY / m_CellSize : (int)worldY / m_CellSize - 1; } // マイナス方向にも対応
inline int cellZ(float worldZ) const { return (worldZ >= 0) ? (int)worldZ / m_CellSize : (int)worldZ / m_CellSize - 1; } // マイナス方向にも対応
TMXCsv enemyData;// 敵配置データ
TMXJson tmxData; // タイルマップエディタのjsonデータ
protected:
// <gid→idに対応したタイプのbit> へ変換するvector型の辞書
std::vector<TileBit> gidTypeBits{ 0 };
public:
inline TileBit& gidTypeBit(unsigned int gid)
{
if (gid >= (int)gidTypeBits.size()) gidTypeBits.resize(gid + 1, 0);
return (gid < 0) ? gidTypeBits[0] : gidTypeBits[gid];
}
// id→ビットのタイプ の辞書に |= でbitTypeをビット論理和して、Wall:壁 や Bounce:はずむ などの特性を追加する
TileBit SetTypeBit(unsigned int gid, TileBit bitType) { return gidTypeBit(gid) |= bitType; }
// id→ビットのタイプ の辞書で & ビット論理積で 指定したidが Wall:壁 や Bounce:はずむ などの特性を持つかを判定する
inline bool isType(unsigned int gid, TileType bitType) { return gidTypeBit(gid) & bitType; }
// id→ビットのタイプ の辞書で & ビット論理積で 指定したidが Wall:壁 や Bounce:はずむ などの特性を持つかを判定し一致したBitを返す(複数が1のビットもありうる)
inline TileBit matchTypes(unsigned int gid, TileBit bitType) { return gidTypeBit(gid) & bitType; }
// tilesetsの.tsxのfirstgidをたどって、SetTypeBitでビット辞書に
// csvのtileSettingsから読み取っておいたビット設定を登録
bool InitTypeBit(const Tilesets& tilesets)
{
assert(tilesets.size() > 0 && "ステージのjsonファイルにタイルセット設定が一つもありません");
if (tilesets.size() == 0) return false;
for (auto& pair : tilesets)
{
auto firstgid = pair.first;
auto& tsxFilePath = pair.second;
auto& tsxInfo = tilesets.umTsxInfo.at(tsxFilePath);
// [親フォルダを抽出] https://qiita.com/takano_tak/items/acf34b4a30cb974bab65
size_t path_i = tsxFilePath.find_last_of("\\/");
if (path_i + 1 >= tsxFilePath.size()) continue;
auto tsxFile = tsxFilePath.substr(path_i + 1);
auto csvInfo = tileSettings.info.find(tsxFile);
if (csvInfo == end(tileSettings.info)) continue; // csv側にタイルの情報がなかった
// gidが0 idが-1 のデフォルトのタイルのビット特性を設定
SetTypeBit(0, csvInfo->second->idTypeBit(-1));
int id = 0; // gidじゃなく0から始まるローカルなid
for (auto gid = firstgid, iEnd = firstgid + tsxInfo.tilecount; gid < iEnd; ++gid)
{ // このMapのgidのビット辞書にcsvのtileSettingsから読み取っておいたビット設定を登録
SetTypeBit(gid, csvInfo->second->idTypeBit(id));
++id; // ローカルなidも次に進める
}
}
}
// マップファイルのロード
void Load(const std::string& filePath)
{
this->FilePath = filePath;
// ★jsonファイルの読み込み
tmxData.Load(filePath);
// tilesetsの.tsxのfirstgidをたどって、SetTypeBitでビット辞書に
// csvのtileSettingsから読み取っておいたビット設定を登録
InitTypeBit(tmxData.tilesets);
auto info = settings[filePath];
if (info.spawnRange.umRange.size() != 0)
spawnRange = info.spawnRange;
else
spawnRange.InitRange(MapRange::Type::Ellipse, 10, 10);// 敵の新規出現レンジをプレイヤから半径X,Yの楕円にする
if (info.CellSize != 1)
{
SetCellSize(info.CellSize); // csvに書かれているCellSizeを設定
tmxData.SetGridSize({(float)info.CellSize ,(float)info.CellSize ,(float)info.CellSize });
}
else // if (info.CellSize == 1)
SetCellSize(tmxData.tmxInfo.tilewidth); // tmxのタイルの幅を取得
}
// オブジェクトを Factoryクラスで className に応じて見きわめて 生成・配置する
template<typename TypeT>
inline std::shared_ptr<TypeT> SpawnTmxObject(const std::string& className, std::shared_ptr<InitParams> initParams, Vector3 spawnPos, Quaternion rotation = { 0,0,0,1 })
{
//IDに応じてFactory工場で作り分ける
return Factory::MakeSharedObject<TypeT>(className, initParams, m_world, spawnPos);
}
// 画面スクロール(位置の更新)
void Scroll3D(Vector3 prevPos, Vector3 delta, MapRange* pSpawnRange = nullptr)
{
std::vector<TMXobjects*> tmxObjectsList{ &tmxData.enemies,&tmxData.lights,&tmxData.items,&tmxData.objects };
for (auto& tmxObjects : tmxObjectsList)
{
if (tmxObjects->data.size() == 0) continue; // リストにデータが一つもなければスクロールせず次へ
tmxObjects->Scroll(prevPos, delta, (pSpawnRange != nullptr) ? *pSpawnRange : (tmxObjects->pSpawnRange != nullptr) ? *(tmxObjects->pSpawnRange) : spawnRange,
[&](int gridX, int gridY, int gridZ)
{
auto pObject = tmxObjects->top(gridX, gridY, gridZ); // グリッドからオブジェクトを取り出す
while (pObject != nullptr)
{
auto pObj = (TMXobject*)pObject;
//int spawnID = pObj->spawnID; // 生成する敵のID情報をTMXの jsonファイルのデータから読み取る
float ratioX = (float)tmxObjects->cellWidth / tmxData.tmxInfo.tilewidth;
float ratioY = (float)tmxObjects->cellHeight / tmxData.tmxInfo.tileheight; // タイルマップエディタとのスケールの違いの比率を計算
float ratioZ = (float)tmxObjects->cellDepth / tmxData.tmxInfo.tilewidth;
//クラス名とベースクラス名に応じて工場で作り分ける
if (pObj->baseType == "Enemy")
m_world->enemies.emplace_back(SpawnTmxObject<Enemy>(pObj->type, pObj->GetInitProperty(), Vector3(pObj->x * ratioX, pObj->z * ratioZ, pObj->y * ratioY)));
//if (pObj->baseType == "Light")
// m_world->lights.emplace_back(SpawnTmxObject<Light>(pObj->type, Vector3(pObj->x * ratioX, pObj->z * ratioZ, pObj->y * ratioY)));
//if (pObj->baseType == "Item")
// m_world->items.emplace_back(SpawnTmxObject<Item>(pObj->type, pObj->GetInitProperty(), Vector3(pObj->x * ratioX, pObj->z * ratioZ, pObj->y * ratioY)));
//if (pObj->baseType == "Object")
// m_world->objects.emplace_back(SpawnTmxObject<Object>(pObj->type, Vector3(pObj->x * ratioX, pObj->z * ratioZ, pObj->y * ratioY)));
//m_world->enemies.emplace_back(Factory::MakeSharedTmxObject(spawnID,m_world, Vector3(pEnemy->x * ratioX, pEnemy->z * ratioZ, pEnemy->y * ratioY)));
pObject = pObject->m_pNext; // 同じグリッドの次のオブジェクトへ
tmxObjects->Erase(pObj); // tmxObjectsのdataとグリッドからデータを消して再びスポーンしないようにする
}
// このグリッドには何もいなくなるのでマス目自体eraseして高速化
tmxObjects->cellData.erase(CellXYZ(gridX, gridY, gridZ));
});
}
}
// 指定された座標(ワールド座標)の地形データを取得する
unsigned int GetTerrain(float worldX, float worldY, float worldZ = (std::numeric_limits<float>::min)())
{
if (worldZ != (std::numeric_limits<float>::min)())
{
float tmpY = worldY;
worldY = worldZ; //【YとZを変換】Zの入力があるときはZをYとして扱う
worldZ = tmpY; //【YとZを変換】
}
// 負の座標が指定された場合は、何も無いものとして扱う
if (worldX < 0 || worldY < 0)
return Tile::None;
// マップ座標系(二次元配列の行と列)に変換する
int mapX = (int)(worldX / m_CellSize);
int mapY = (int)(worldY / m_CellSize);
int mapZ = (int)(worldZ / m_CellSize);
if (tmxData.terrains.count(mapZ) == 0) // zの階層がまだなかったとき
return Tile::None; // 配列の範囲外は、何も無いものとして扱う
auto& terrainZ = tmxData.terrains[mapZ]; // 辞書からZに対応するデータへの参照を得る
if (mapY >= terrainZ.size() || mapX >= terrainZ[mapY].size())
return Tile::None; // 配列の範囲外は、何も無いものとして扱う
return terrainZ[mapY][mapX]; // 二次元配列から地形IDを取り出して返却する
}
// 指定された座標(ワールド座標)の地形データを書き換える return には書き換える前のgidが返る
bool SetTerrain(unsigned int gid, float worldX, float worldY, float worldZ = (std::numeric_limits<float>::min)())
{
if (worldZ != (std::numeric_limits<float>::min)())
{
float tmpY = worldY;
worldY = worldZ; //【YとZを変換】Zの入力があるときはZをYとして扱う
worldZ = tmpY; //【YとZを変換】
}
// 負の座標が指定された場合は、書き換えられない
if (worldX < 0 || worldY < 0)
return false;
// マップ座標系(二次元配列の行と列)に変換する
int mapX = (int)(worldX / m_CellSize);
int mapY = (int)(worldY / m_CellSize);
int mapZ = (int)(worldZ / m_CellSize);
// 二次元配列の範囲外は、書き換えられない
//if (mapY >= terrain.size() || mapX >= terrain[mapY].size())
// return;
bool isNewZ = false;
if (tmxData.terrains.count(mapZ) == 0) isNewZ = true; // 新しい z の階層の場合
auto& terrainZ = tmxData.terrains[mapZ]; // 辞書からZに対応するデータへの参照を得る
if (isNewZ) // 新しい z の階層の場合
{
terrainZ.z = mapZ;
terrainZ.tilesets = &(tmxData.tilesets); // 新しい z の階層のtilesetsを設定する
terrainZ.Width = tmxData.tmxInfo.width; terrainZ.Height = tmxData.tmxInfo.height;
auto& terrinZYX = terrainZ[mapY][mapX];
terrinZYX.tilesets = &(tmxData.tilesets);
terrinZYX = gid; // 地形データ書き換え
}
else
terrainZ[mapY][mapX] = gid; // 地形データ書き換え
return true;
}
// gidに応じてタイルを描き分ける
void DrawTile(unsigned int gid, float worldX, float worldY, float worldZ, float ExRate)
{
auto texture = TsxInfo::id_images[gid].second.lock();
int id = TsxInfo::id_images[gid].first;
if (isType(gid, TileType::Floor)) // 板ポリゴンで床を描く
MyDraw::DrawDivRotaFloorF3D(MyDraw::Plane::Y, worldX, worldY, worldZ, ExRate, 0, *texture, id);
else if (isType(gid, TileType::Wall)) //壁のときはボックスを描く
MyDraw::DrawDivBoxF3D(worldX, worldY, worldZ, ExRate, *texture, id);
}
// 地形をタイルで描く
void DrawTerrain()
{
for (auto&& terrainN : tmxData.terrains)
{
auto& terrainZ = *terrainN.second;
int cellZ = terrainN.first;
for (int cellY = 0, ySize = terrainZ.size(); cellY < ySize; cellY++)
{
for (int cellX = 0, xSize = terrainZ[cellY].size(); cellX < ySize; cellX++)
{
float x = centerX(cellX); //マス目サイズ/2ずらし
float y = centerY(cellY); //マス目サイズ/2ずらし
float z = centerZ(cellZ); //マス目サイズ/2ずらし
int id = -1;
unsigned int gid = 0;
if (cellY < terrainZ.size() && cellX < terrainZ[cellY].size())
{
gid = terrainZ[cellY][cellX];
}
if (0 <= gid && gid < TsxInfo::id_images.size())
{
auto pTileImage = TsxInfo::id_images[gid].second.lock();
if (pTileImage == nullptr) continue;
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
for (auto&& keyValue : world()->cameras) // ワールドにあるすべてのカメラぶんループ
{
auto& camera = keyValue.second;
//auto stepXYZs = MyMath::CrossGridVec(camera->pos - camera->m_targetXYZ, Vector3{ CellSize, CellSize, CellSize });
camera->Draw([&]()
{
float ExRate = (float)m_CellSize / (float)pTileImage->m_XSize;
DrawTile(gid, x, z, y, ExRate); // idに応じてタイルを描き分ける
});
}
}
}
}
}
}
// 指定された座標(ワールド座標)の地形が壁か調べる
inline bool IsWall(float worldX, float worldY, float worldZ = (std::numeric_limits<float>::min)())
{
auto terrainID = GetTerrain(worldX, worldY, worldZ); // 指定された座標の地形のIDを取得
return IsWall(terrainID);
}
//あるIDが壁かどうかだけ調べる
inline bool IsWall(unsigned int gid)
{ // 地形が壁ならtrue、違うならfalseを返却する
return isType(gid, TileType::Wall);
}
// 指定された座標worldPosの地形が 壁 や ダメージブロックかなどをチェックする関数
bool checkID(TileBit checkBit, float worldX, float worldY, float worldZ,
int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, unsigned int* pTerrainID = nullptr, TileBit* pMatchBits = nullptr)
{
unsigned int terrainID; // 指定された座標の地形のID
if (worldZ != (std::numeric_limits<float>::min)())
{ //【YとZを変換】Zの入力があるときはZをYとして扱う タイルマップの X,Yは3DではYがZ:奥行に対応するから
float tmpY = worldY;
worldY = worldZ;
worldZ = tmpY; //【YとZを変換】
}
if (worldX < 0 || worldY < 0) return false; // 負の座標が指定された場合
// マップ座標系(二次元配列の行と列)に変換する
int mapX = cellX(worldX), mapY = cellY(worldY), mapZ = cellZ(worldZ);
if (tmxData.terrains.count(mapZ) == 0) // zの階層がまだなかったときは 0
terrainID = 0; // 範囲外のときは 0
else // zの階層がすでにあったとき
{ // 二次元配列の範囲内か判定
auto& terrainZ = tmxData.terrains[mapZ];
if (mapY >= terrainZ.size() || mapX >= terrainZ[mapY].size())
terrainID = 0; // 範囲外のときは 0
else
terrainID = terrainZ[mapY][mapX]; // 配列から地形IDを取り出す
}
if (pTerrainID != nullptr)
*pTerrainID = terrainID;
TileBit matchBits = matchTypes(terrainID, checkBit); // ビット論理積 & で地形のchekckBitを確かめる
bool isCheckBit = matchBits != (TileBit)TileType::None;
if (isCheckBit && pMapX != nullptr && pMapY != nullptr && pMapZ != nullptr)
*pMapX = mapX, * pMapY = mapY, * pMapZ = mapZ; // ブロックのマス目のセル番号をポインタに記録(nullptrの場合は記録しない)
if (pMatchBits != nullptr)
*pMatchBits = matchBits; // マッチしたビットをポインタ先に代入して返す
return isCheckBit;
}
};
#endif
Player.cppを変更
して、マップをスクロールして敵を出現させましょう。
#include "Player.h"
#include <cmath> // 浮動小数点の余り%演算子がC++にないのでfmodを使うため
#include "MyMath.h"
#include "MyDraw.h"
#include "Camera.h"
#include "World.h"
#include "GridSteps.h"
#include <vector>
void Player::HandleInput()
{
if (Input::GetButton(pad, PAD_INPUT_LEFT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_A)) )
{
//vx = - MoveSpeed; // 左
deltaAngle += 1; //ハンドルを左に回す
}
if (Input::GetButton(pad, PAD_INPUT_RIGHT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_D)))
{
//vx = MoveSpeed; // 右
deltaAngle -= 1; //ハンドルを右に回す
}
if (Input::GetButton(pad, PAD_INPUT_UP)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_W)))
{
//vy = - MoveSpeed; // 上
MoveSpeed += 0.6f; //アクセルを踏む
if (MoveSpeed > MoveSpeedMax)
MoveSpeed = MoveSpeedMax;//Maxスピードで止める
}
else if (Input::GetButton(pad, PAD_INPUT_DOWN)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_S)))
{
//vy = MoveSpeed; // 下
MoveSpeed -= 0.6f; //ブレーキを踏む
if (MoveSpeed < -0.9f)
MoveSpeed = -0.9f;//ちょっとだけバックできるように
}
if (pad == Pad::Key || pad == Pad::One) // コントローラ1またはキーボード操作のプレイヤのときだけ
{
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGUP)) // キーボードのPageUp ボタンを押したときは
{
vy = 2.0f; // テスト用に空中の上方向に移動
}
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGDN)) // キーボードのPageDown ボタンを押したときは
{
vy = -2.0f; // テスト用に空中の下方向に移動
}
}
}
// ジャンプ開始した直後に呼ばれる関数
void Player::OnStartJump()
{
this->isGround = false; // ジャンプ中はisGroundフラグがfalse
}
// 更新処理
void Player::Update()
{
vxForce *= 0.9f; // かかっている力の減衰率
vyForce *= 0.9f; // かかっている力の減衰率
vzForce *= 0.9f; // かかっている力の減衰率
deltaAngle *= 0.5F; // 車のハンドルの方向をだんだんニュートラルに戻す
MoveSpeed *= 0.9F; // 移動速度も減速する
// 入力を受けての処理
HandleInput();
auto pJumpPhysics = jumpPhysics.lock();
if (pJumpPhysics != nullptr)
{
if (isGround)
{
if (Input::GetButtonDown(pad, PAD_INPUT_A) // Zキーを押した瞬間
|| (pad == Pad::Two && Input::GetButtonDown(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを押した瞬間
{
pJumpPhysics->JumpStart(); // ジャンプのスタート処理を発動
}
}
else if (vy > 0 && Input::GetButtonUp(pad, PAD_INPUT_A) // Zキーを離した瞬間
|| (pad == Pad::Two && Input::GetButtonUp(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを離した瞬間
{
pJumpPhysics->JumpCancel(); // ジャンプのキャンセル処理を発動
}
pJumpPhysics->Update(); // ジャンプのコンポーネントを更新する
}
auto pGravityPhysics = gravityPhysics.lock();
if (pGravityPhysics != nullptr)
pGravityPhysics->Update(); // 重力落下のコンポーネントを更新する
moveAngle += deltaAngle; // ハンドルを回したぶんプレイヤの進行方向を変える
if (moveAngle < -180) moveAngle = 360 + moveAngle; // -180以下の角度にならないようにマイナスになったら+180度にループさせる
else if (moveAngle > +180) moveAngle = -360 + moveAngle; // +180以下の角度にならないようにマイナスになったら-180度にループさせる
rotation.SetRotation(VGet(0, (180 - moveAngle) * MyMath::Deg2Rad, 0)); // 前向きの3Dモデルなら180度回転させて奥方向の向きに設定
// ↓進行方向角度をX方向とZ方向の速度に変える
vx = (float)std::cos((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
vz = (float)std::sin((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
float vyForceJump = (pJumpPhysics != nullptr) ? pJumpPhysics->vyForce() : 0.0f;
float vyForceGravity = (pGravityPhysics != nullptr) ? pGravityPhysics->vyForce() : 0.0f;
vx += vxForce; // 力をかけて速度に加算する
vy += vyForce + vyForceJump + vyForceGravity; // 勢い(力・フォースを加算
vz += vzForce; // 力をかけて速度に加算する
// 実際に位置を動かす
auto pMapHitBox = mapHitBox.lock();
float cellHalf = (map() != nullptr) ? (float)map()->CellSize() / 2.0f : 16.0f; // マップのセルのサイズの半分
// 速すぎて当たり判定がすり抜けないように細切れのステップのグリッドで移動させつつ当たり判定する
Vector3 gridStepSize = (pMapHitBox == nullptr) ? Vector3{ cellHalf, cellHalf, cellHalf } : pMapHitBox->m_BoxSize / 2;
GridSteps vSteps{ velocity, gridStepSize }; // 細切れのステップの数値を計算
#ifdef _DEBUG
if (vSteps.div >= 1000) // 分割が1000を超える場合は物理式かどこかで∞=inf や 計算不可能数=nanが発生してそう
{ // vx,vy,vzに原因がありそうなのでデバッグ表示
if (!isfinite(vx)) printfDx(("vx:∞かnan x:" + std::to_string(x) + "\n").c_str());
if (!isfinite(vy)) printfDx(("vy:∞かnan y:" + std::to_string(y) + "\n").c_str());
if (!isfinite(vz)) printfDx(("vz:∞かnan z:" + std::to_string(z) + "\n").c_str());
}
#endif
bool isCollisionX{ false }, isCollisionY{ false }, isCollisionZ{ false }, isCollisionCollider{ false };
for (int i = 1, iSize = vSteps.div + 1; i <= iSize; ++i)
{
auto& vStep = (i < iSize) ? vSteps.step : vSteps.mod;
// まず横に移動する
if (pMapHitBox == nullptr)
x += vx;
else if (isCollisionX == false) // まだ前のステップでX方向の衝突がないなら
{
x += vStep.x;
isCollisionX = pMapHitBox->hitCheckX((TileBit)TileType::Wall);
}
// 次に縦に動かす
if (pMapHitBox == nullptr)
y += vy;
else if (isCollisionY == false) // まだ前のステップでY方向の衝突がないなら
{
y += vStep.y;
isCollisionY = pMapHitBox->hitCheckY((TileBit)TileType::Wall | (TileBit)TileType::Floor);
}
// 次にZ奥行き方向に動かす
if (pMapHitBox == nullptr)
z += vz;
else if (isCollisionZ == false) // まだ前のステップでZ方向の衝突がないなら
{
z += vStep.z;
isCollisionZ = pMapHitBox->hitCheckZ((TileBit)TileType::Wall);
}
if (auto pGridCollision = gridCollision.lock())
{
pGridCollision->Update(); // 所属するマス目セルを更新する
for (auto& collider : *pGridCollision->colliders())
isCollisionCollider |= collider.lock()->IsCollision();
}
// 衝突があったら、速すぎてすり抜けちゃう前に今のステップでbreakで抜ける
if (isCollisionCollider)
break; // ぶつかっているのにさらに次のステップの速度も足すとすり抜けちゃう
}
map()->Scroll3D(position, Vector3(vx, vy, vz)); // マップ(敵)データをスクロール
// カメラのプレイヤ追尾
float camDistance = 150; // プレイヤからカメラまでのY平面上の距離
float camHeight = 100; // プレイヤからカメラまでのY方向上の高さ
// moveAngleは2D画像のX方向右を0度とするから、-90度回すと画像を正面に捉える位置にカメラを置ける
float camAngleX = (float)std::cos((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
float camAngleZ = (float)std::sin((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
if (camera != nullptr)
{ // カメラの位置をプレイヤの進行方向と真逆に設定
camera->SetPosition(position + Vector3(camAngleX, camHeight, camAngleZ));
camera->LookAt(position); //カメラはプレイヤの方を見る
}
}
void Player::OnCollision(std::shared_ptr<Collision> pCollision)
{
if (isDead) return; // すでにisDeadなら return
auto otherCollider = pCollision->contacts[0].otherCollider.lock(); // コライダの弱共有ポインタをロック
if (otherCollider == nullptr) return; // すでにコライダが消去済だった
auto other = otherCollider->owner();
if (other->isDead) return; // other側がisDeadだったら処理をスルー
if (other->baseTag == "Enemy" || other->baseTag == "Player")
{
//[離散要素法:DEM] https://qiita.com/konbraphat51/items/157e5803c514c60264d2
constexpr float k = 1.0f; // バネ係数(めり込みに対するバネ反発の掛け率)
constexpr float e = 0.1f; // はね返り係数(直前と比べどれくらいの速さで離れるか0.5だと半分の速さではね返る)
float log_e = std::log(e); // はね返り係数の自然対数log
constexpr float pi_2 = DX_PI_F * DX_PI_F; // πの2乗
float eta = -2.0f * log_e * std::sqrt(k * mass / (log_e * log_e + pi_2)); // 粘性係数η(イータ)
auto norm = pCollision->contacts[0].normal; // 衝突の法線 * other->pushPower; // pushPowerの力でotherに押し返される
auto v_norm = dot(other->velocity - velocity, norm) * norm; // [相対速度v] 法線norm方向のv = (v1 - v0)・norm [ドット積]
auto sep = pCollision->contacts[0].separation; // 衝突点どうしの距離(めりこみ距離)
auto F = -k * sep * norm - eta * v_norm; // F (法線方向の力) = -k × sep - η × v (相対速度:自分から見た相手otherの速度)
force += F / mass; // 衝突法線方向の力を加える
}
}
// 描画処理
void Player::Draw()
{
int animIndex = animCount / 20; // キャラのパラパラアニメの画像番号(最大3)
float abs_vx = (vx > 0) ? vx : -vx; // x方向絶対値
float abs_vz = (vz > 0) ? vz : -vz; // y方向絶対値
if (abs_vx > 1.00f || abs_vz > 1.00f)
animCount += 6; // キャラが地面のXとZ方向に少しでもスピードがあったらアニメを走らせる
if (animCount >= 60) animCount = 0; // アニメ時間を0へループ
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
for (auto&& keyValue : world()->cameras) // プレイヤのいるワールドにあるすべてのカメラぶんループ
{
auto& camera = keyValue.second;
camera->Draw([&]()
{
// 常にカメラ方向を向くビルボード回転の角度を得る
VECTOR_D angle = MyDraw::GetAngle(DxLib::GetCameraBillboardMatrixD());
angle.x = 0; angle.z = 0; // Y軸周りのビルボード回転だけ有効にする X軸,Z軸まわりの回転は 0 にする
int imgIndex = 9; // キャラチップ画像の起点番号
// ビルボードの角度 と キャラの向いている向き の 差 によって、別のプレイヤのカメラから見たキャラチップの向きの画像を切り替える
// -180 ~ +180 : angle.y moveAngle : -180 ~ +180 ↓
float difAngleY = angle.y * 180.0f / DX_PI_F + moveAngle;
imgIndex = (difAngleY < -135 || 135 < difAngleY) ? 0 // カメラ向きの画像
: (difAngleY < -45) ? 6 // 右 向きの画像
: (difAngleY > 45) ? 3 // 左 向きの画像
: 9; // 奥向きの画像
MyDraw::DrawDivRotaGraphF3D(MyDraw::Plane::Z, x, y, z, 1.0f, angle, *image, imgIndex + animIndex, TRUE);
#ifdef _DEBUG // デバッグのときだけ_DEBUGが定義され描かれる
if (auto pSphereCollider = sphereCollider.lock()) pSphereCollider->Draw(); // コライダを描く
if (auto pMapHitBox = mapHitBox.lock()) pMapHitBox->Draw(); // ヒットボックスを描く
#endif
});
}
}
Item.hを新規作成
して、アイテムのベース基底クラスを定義しましょう。
#ifndef ITEM_H_
#define ITEM_H_
#include "GameObject.h"
#include "Collider.h"
class World; //前方宣言
// アイテムの情報のベース基底クラス 継承override してアイテム情報をjsonなどから受け取る
struct ItemInfo : public InitParams
{
//std::shared_ptr<InitParams> initParams{ nullptr };
hash32 itemTag{ "" }; // アイテムを区別して、アイコンを区別して辞書に分けていれるためのタグ
inline ItemInfo(std::shared_ptr<InitParams> initParams = nullptr) : InitParams()
{}
virtual ~ItemInfo() {}
};
// アイテムの基底クラス。
// 全てのアイテムは、このクラスを継承して作る。
class Item : public GameObject
{
public:
std::shared_ptr<ItemInfo> m_ItemInfo{ nullptr }; // アイテムに関する情報
protected:
// コンストラクタ
Item(World* world, Vector3 position, Quaternion rotation = { 0,0,0,1 })
: GameObject(world, position, rotation)
{
this->baseTag = "Item";
}
public:
virtual ~Item() {}
// 継承overrideしてアイテムを取った時にアイテム一覧で選択できるアクションを持つかをtrueかfalseで返す
virtual bool hasAction() { return false; }
// Initをoverrideしてアイテムの情報をjsonなどから受け取っておく
virtual void Init(std::shared_ptr<InitParams> initParams = nullptr) override
{
m_ItemInfo = std::make_shared<ItemInfo>(initParams); // アイテム情報を持っておく
}
virtual void OnCollision(std::shared_ptr<Collision> pCollision) override
{
if (isDead) return;
auto otherCollider = pCollision->contacts[0].otherCollider.lock();
if (otherCollider == nullptr) return;
auto thisCollider = pCollision->contacts[0].thisCollider.lock();
if (thisCollider == nullptr) return;
auto other = otherCollider->owner();
if (other->isDead) return; // other側がisDeadだったら処理をスルー
if (other->baseTag == "Player")
{
if (!thisCollider->isTrigger)
{ // 跳ね返したいアイテムの場合は forceをかける
auto norm = pCollision->contacts[0].normal * other->pushPower;
force += -norm;
}
other->OnItem(shared_from_this_as<Item>()); // プレイヤのときはアイテムの効果を発動
OnItem(shared_from_this_as<Item>()); // 自身のOnItemも呼ぶ(アイテム自身が爆発したりできる)
}
}
// アイテムを取ったときの共通処理を overrideして実装
virtual void OnItem(std::shared_ptr<Item> pItem) override
{
if (pItem == shared_from_this_as<Item>())
{
// 自分自身からOnItemが呼ばれた場合はこちら
}
else
{
//if (pItem->baseTag == "Player")
//{
// //life += 1; // ライフを増やすなど
//}
}
}
};
#endif
ItemAction.hを新規作成
して、アイテムを使ったときに起こるアクションのベース基底クラスを定義しましょう。
#ifndef ITEM_ACTION_H_
#define ITEM_ACTION_H_
#include "Component.h"
#include "Vector3.h"
#include "Resource.h"
#include "Item.h"
class ItemListComponent; // 前方宣言
// アイテムを使ったときに起きるアクション
class ItemAction : public Component
{
//protected:
public:
inline ItemAction(ItemListComponent* pItemList, std::shared_ptr<GameObject> pOwner)
: m_items{ pItemList }, Component(pOwner)//, m_itemTag{ itemTag }
{
this->baseTag = "ItemAction";
}
virtual ~ItemAction() {}
std::shared_ptr<ItemInfo> m_ItemInfo{ nullptr }; // アイテムに関する情報
// アイテムを区別して、アイコンを区別して辞書に分けていれるためのタグ
virtual hash32 ItemTag() { return (m_ItemInfo == nullptr) ? "" : m_ItemInfo->itemTag; };
ItemListComponent* m_items; // プレイヤの持っているアイテム一覧へのリンク
std::weak_ptr<ItemAction> m_prev; // リンクリスト構造の前へのポインタ
std::weak_ptr<ItemAction> m_next; // リンクリスト構造の次へのポインタ
//hash32 m_itemTag{ "" }; // アイテムのタグ
//int order{ 0 }; // アイテムの並び変え順番の
public:
std::shared_ptr<ItemAction> Prev() { auto pPrev = m_prev.lock(); return pPrev; }
void Prev(std::shared_ptr<ItemAction> pPrev) { m_prev = pPrev; }
std::shared_ptr<ItemAction> Next() { auto pNext = m_next.lock(); return pNext; }
void Next(std::shared_ptr<ItemAction> pNext) { m_next = pNext; }
// 一番最初を返す
std::shared_ptr<ItemAction> Top()
{
auto pPrev = Prev();
if (pPrev == nullptr)
return shared_from_this_as<ItemAction>(); // 前がないならすでに1番最初
for (;;)
{ // 前の前がnullptrなら一番最初
auto pPrevPrev = pPrev->Prev();
if (pPrevPrev == nullptr) return pPrev; // 1番最初
pPrev = pPrevPrev;
}
}
// 次がnullptrのときにも一番最初にループして次を返す
std::shared_ptr<ItemAction> LoopNext()
{
auto pNext = Next();
if (pNext != nullptr) return pNext;
auto pTop = Top();
auto pSelf = shared_from_this_as<ItemAction>(); // 自分自身
if (pTop == pSelf) return pSelf; // 1番最初が自身になるのは1つしかない場合
return pTop; // 一番最初へループする
}
// 例えば、すでに取得したプレイヤの弾と同じ種類の弾のアイテムをゲットしたら、
// アイテムとしてダブらせるのではなく使用回数を増やす処理をこの関数をoverrideしてやる
virtual void AddSameItem(std::shared_ptr<InitParams> initParam)
{
// 継承 overrideして、アイテムとしてダブらせるのではなく例えば、弾の使用回数を増やす処理などを実装
}
// なにかアイテムのアクション系のコンポーネントで共通して更新したいことがあればここに実装
virtual void Update(GameObject* obj = nullptr) override
{
}
// なにかアイテムのアクション系のコンポーネントで共通して描画したいことがあればここに実装
virtual inline void Draw(GameObject* obj = nullptr) override;
// アイコンを描く
virtual Vector3 DrawIcon(const Vector3& drawPos, const Vector3& limitPos, int nextIntervalX = 30)
{
// overrideしてアイコンを描く処理を個別実装する
return drawPos;
}
// 次のアイテムのUIを数珠つなぎで描く nextPos に次はどのくらいずらすか指定
// limitPosを超える位置には描かない、超えない範囲でループしてselectingに戻るまでつぎつぎと描く
virtual void DrawNextUI(const Vector3& nextPos, const Vector3& limitPos, std::shared_ptr<ItemAction> pSelecting);
};
class Item; // 前方宣言
// 複数のアイテムをリストとして管理するコンポーネント(UIも描画する)
class ItemListComponent : public Component
{
//protected:
public:
inline ItemListComponent(std::shared_ptr<GameObject> pOwner) : Component(pOwner)
{
this->baseTag = "ItemListComponent";
}
virtual ~ItemListComponent() {}
public:
int heightUI{ 32 };
int frameWidth{ 2 }; // アイテムの枠の幅
unsigned int frameColor = GetColor(255, 255, 255); // アイテムの枠の色
Vector3 drawPos{ (float)frameWidth, (float)frameWidth + 20, 0 }; // アイテムを描くUI上の2D位置の起点
protected:
//std::map<PlayerItemAction, hash32> order; // アイテムの並び順番
//hash32 m_selectingTag{ "" }; // 取得したアイテムの選択中のアクションのタグ
std::weak_ptr<ItemAction> m_selecting;
std::weak_ptr<ItemAction> m_topAction;
std::unordered_map<hash32, std::shared_ptr<ItemAction>> m_actions; // 取得したアイテムのアクションのリスト
public:
// アイテムのアクションを得る 例えば Get<ItemBulletAction>でItemBulletActionという名前のアクションをm_actions辞書から得る
template<class ActionT>
std::shared_ptr<ActionT> Get(const std::string& itemTag)
{
auto itr = m_actions.find(itemTag);
if (itr == end(m_actions)) return nullptr;
return itr->second;
}
// アイテムを削除する
bool RemoveAction(std::shared_ptr<ItemAction> removeAction)
{
if (removeAction == nullptr || removeAction->ItemTag() == "")
return false; // 見つからずに消せなかった場合は false
m_topAction = removeAction->LoopNext(); // 消すの前に次をトップにしておく
m_actions.erase(removeAction->ItemTag());
return true;
}
// アイテムを追加する .Add<ItemBulletAction>(...)で追加できる
template<class ActionT, typename ...ArgsT>
std::shared_ptr<ActionT> Add(std::shared_ptr<ItemInfo> initParam, ArgsT&&...args)
{
//★[ActionTのハッシュコードを得るテク] https://cpprefjp.github.io/reference/typeindex/type_index/hash_code.html
//size_t hashID = typeid(ActionT).hash_code(); //[まずハッシュでID化]
auto found = m_actions.find(initParam->itemTag);
if (found != end(m_actions))
{ // すでに取得済の種類のアイテムだったらAddSameItemで取得済のアイテムの追加処理を呼ぶ
found->second->AddSameItem(initParam);
return found->second->shared_from_this_as<ActionT>();
}
// shared_from_this()を必ず渡してセットで初期化して、オーナーがリンク切れのコンポーネントができないように気をつける
std::shared_ptr<ActionT> newAction = Create<ActionT>(initParam, std::forward<ArgsT>(args)...);
m_actions.emplace(initParam->itemTag, newAction);
auto pSelecting = Selecting(); // 現在選択中のアイテムのリンクを取得
if (pSelecting == nullptr) // 現在ひとつもアイテムがない場合は先頭Topとして追加する
{
m_topAction = newAction; // Topを newAction に設定する
return newAction;
}
//↓リンクリスト構造のリストを使ってリンクを相互に張る
auto selectingNext = pSelecting->Next(); // [今選択中のアイテムの★次]
pSelecting->Next(newAction); // 「今選択中のアイテム」の → 次を newActionにする
newAction->Prev(pSelecting); // 「今選択中のアイテム」を ← newAction の前 にする
newAction->Next(selectingNext); // selecting と selectingのNext の間にはさむ
if (selectingNext)
selectingNext->Prev(newAction); // newAction ←[今選択中のアイテムの★次] のリンク
//↑ここまでの処理で数珠つなぎのリンクリスト構造になる
return newAction;
}
std::shared_ptr<ItemAction> Add(std::shared_ptr<Item> pItem);
virtual std::shared_ptr<ItemAction> Top()
{
auto pTopAction = m_topAction.lock(); // 弱共有ポインタをlock()する
if (m_actions.size() == 0 || pTopAction == nullptr)
return nullptr; // リストにひとつもアクションがない場合は nullptr を返す
return pTopAction;
}
virtual std::shared_ptr<ItemAction> Selecting()
{
auto pSelecting = m_selecting.lock(); // 弱共有ポインタをlock()する
if (pSelecting == nullptr) // なにも選択中のアイテムがない場合は
return Top(); // リンクリスト構造の一番最初のトップを返す
return pSelecting;
}
template <typename ActionT>
std::shared_ptr<ItemAction> Select()
{
size_t hashID = typeid(ActionT).hash_code(); //[まずハッシュでID化]
auto found = m_actions.find(hashID);
if (found == end(m_actions)) return nullptr; // 見つからなかったら
return m_selecting = found->second; // 見つかったアクションをm_selectingに設定してそのアクションを返す
}
// 選択中のアイテムの次のアイテムを返す
virtual std::shared_ptr<ItemAction> SelectNext()
{
auto pSelecting = Selecting();
if (pSelecting == nullptr) return pSelecting;
auto pNext = pSelecting->Next();
if (pNext == nullptr) pNext = Top(); // リンクリスト構造の次がないときはループして一番最初のTopを返す
m_selecting = pNext; // 現在選択中のアイテムを更新する
return pNext;
}
virtual void Update(GameObject* obj = nullptr) override
{
auto pTop = Top();
if (pTop == nullptr) return;
pTop->Update();
auto pNext = pTop->Next();
while (pNext != nullptr)
{
pNext->Update();
pNext = pNext->Next();
}
}
virtual void Draw(GameObject* obj = nullptr) override
{
auto pSelecting = Selecting();
if (pSelecting == nullptr) return;
pSelecting->Draw();
//auto pNext = pSelecting->Next();
//pNext->Draw();
}
};
// なにかアイテムのアクション系のコンポーネントで共通して描画したいことがあればここに実装
inline void ItemAction::Draw(GameObject* obj)
{
Vector3 limitPos{ 400, -1, -1 };
Vector3 nextIconPos = DrawIcon(m_items->drawPos, limitPos); // アイコンを描く
if (nextIconPos == m_items->drawPos) return; // nextの位置とdrawPosが同じならアイコンを描けなかった
auto pNext = Next(); // 次のアイテムへのリンクを得る
auto pSelf = shared_from_this_as<ItemAction>(); // 自分自身
if (pNext != nullptr)
pNext->DrawNextUI(nextIconPos, limitPos, pSelf); // 数珠つなぎで次のアイコンを描く
else
{
auto pLoopNext = LoopNext(); // 次がnullptrでも最初のアイテムへループして描く
if (pLoopNext == pSelf) return; // ループしても自分自身ならアイテムは一つしかないので終了
pLoopNext->DrawNextUI(nextIconPos, limitPos, pSelf); // 数珠つなぎで次のアイコンを描く
}
}
// 次のアイテムのUIを数珠つなぎで描く nextPos に次はどのくらいずらすか指定
// limitPosを超える位置には描かない、超えない範囲でループしてselectingに戻るまでつぎつぎと描く
inline void ItemAction::DrawNextUI(const Vector3& nextPos, const Vector3& limitPos, std::shared_ptr<ItemAction> pSelecting)
{
Vector3 nextIconPos = DrawIcon(nextPos, limitPos); // アイコンを描く
if (nextIconPos == m_items->drawPos) return; // nextの位置とdrawPosが同じならアイコンを描けなかった
auto pLoopNext = LoopNext(); // 次のアイテムへのリンクを得る
if (pLoopNext == pSelecting) return; // 次が現在選択中アイテムならすべてループしたのでここで終了
pLoopNext->DrawNextUI(nextIconPos, limitPos, pSelecting); // 数珠つなぎで次のアイコンを描く
}
#endif
ItemAction.cppを新規作成
して.cpp側でItemBlock.h(ブロック)やItemBullet.h(弾)などをインクルードすることで、
アイテムの種類に応じてアイテムを使ったときに起こるアクションをAddでItemListComponent(アイテムのリスト)に追加しましょう。
#include "ItemAction.h"
#include "Item.h"
#include "ItemBullet.h"
#include "ItemBlock.h"
// .cppで各種Itemを継承したクラスとItemActionを継承したクラスを#includeして
// アイテムの種類に応じてItemからItemActionを初期化する
std::shared_ptr<ItemAction> ItemListComponent::Add(std::shared_ptr<Item> pItem)
{
if (pItem->typeTag == "") return nullptr;
if (pItem->typeTag == "ItemBullet")
return this->Add<ItemBulletAction>(pItem->m_ItemInfo, this, owner());
else if (pItem->typeTag == "ItemBlock")
return this->Add<ItemBlockAction>(pItem->m_ItemInfo, this, owner());
return nullptr;
}
GameObject.hを変更
して、OnItemなどアイテムを取ったときのコールバック関数を準備しましょう。
#ifndef GAMEOBJECT_H_
#define GAMEOBJECT_H_
#include <memory> // 共有ポインタの定義のため
#include "Vector3.h"
#include "Quaternion.h"
#include "Tile.h" // OnHitTileのコールバックに必要
#include "Object.h" // インスタンスIDや検索タグを持ち、shared_from_this_as<継承先クラス名>などができる
#include "World.h"
class Map; // 前方宣言
class Component; // 前方宣言
class Collision; // 前方宣言
class CollisionParams; // 前方宣言
class Item; // 前方宣言
// ゲーム上に表示される物体の基底クラス。
// プレイヤーや敵、アイテムなどはこのクラスを継承して作る。
class GameObject : public Object
{
protected:
// コンストラクタはprotected:にしてあるのでCreate関数経由で初期生成してください
GameObject(World* world = nullptr, Vector3 position = { 0,0,0 }, Quaternion rotation = { 0,0,0,1 }, Vector3 velocity = { 0,0,0 }, Vector3 force = { 0,0,0 })
: Object(), m_world{ world }, position{ position }, rotation{ rotation }, velocity{ velocity }, force{ force }
{
}
public:
// 仮想デストラクタ
virtual ~GameObject() {}
union { // ★共用体unionテクニック https://inemaru.hatenablog.com/entry/2016/03/02/005408
//★下記3つはメモリ上で共用状態になる(position、x,y,z、xyzどの名前から数値を変えたり読出してもメモリ上は同じ)
// 同じデータに3種類の名前を付けたイメージ、しかもVector3の機能や配列としてのアクセスの仕方もできて便利
struct { float x, y, z; }; // XYZ座標 [匿名共用体とは] https://code.i-harness.com/ja-jp/q/4d437c
Vector3 position; // XYZ座標
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で回せる
Quaternion rotation; // 回転クオータニオン
union {
struct { float vx, vy, vz; }; // XYZ方向の速度
Vector3 velocity; // XYZ方向の速度
std::array<float, 3> vxyz;
};
union {
struct { float vxForce, vyForce, vzForce; }; // XYZ方向にかかる力(Unityでいうと AddForce関数 )
Vector3 force; // XYZ方向の力(物理的には加速度:1フレームごとにvelocityの増える=加速する量)
std::array<float, 3> vxyzForce;
};
// 重力を返す、重力を変えたいときはoverrideして処理を変えてください、重力を変えてない場合は 0.0f
virtual inline float gravity() { return 0.0f; }
protected:
World* m_world; // GameObjectが配置されたワールドへのリンク
public:
// 現在いるワールドへのポインタを得る
virtual World* world() { return m_world; }
// 現在いるワールドのポインタを変更する
virtual void world(World* changeWorld) { m_world = changeWorld; }
// GameObjectが現在いるマップへのポインタを返す
Map* map() { return m_world->map.get(); }
bool isDead{ false }; // 死んだ(削除対象)フラグ
bool isStatic{ false }; // 動かない(静的オブジェクトか?)
bool isGround{ false }; // 地面についているか
bool isJumping{ false }; // ジャンプ中か
bool isFlying{ false }; // 空をとんでいるか
float pushPower{ 5.0f }; // コライダで接触したときに押し返す力(相手側が強いと押される)
float mass{ 1.0f }; // 質量(重さ)コライダで接触したときに重さでforceを割り算すると動きにくくなる
std::shared_ptr<CollisionParams> collisionParams; // 衝突関係の物理パラメータ
hash32 typeTag{ "" }; // 小カテゴリ Zako0など個別のタイプ
hash32 baseTag{ "" }; // 大カテゴリ Enemyなど大まかなベースジャンル
protected:
// コンポーネントの<ハッシュID, 共有所有ポインタ> の辞書でコンポーネントの共有カウンタ+1を保持する
std::unordered_map<size_t, std::shared_ptr<Component>> components;
public:
// コンポーネントを得る 例えば GetComponent<BoxCollider>でBoxColliderという名前のコンポーネントをcomponents辞書から得る
template<class ComponentT>
std::vector<std::weak_ptr<ComponentT>> GetComponents()
{ //★[ComponentTのハッシュコードを得るテク] https://cpprefjp.github.io/reference/typeindex/type_index/hash_code.html
size_t hashID = typeid(ComponentT).hash_code(); //[まずハッシュでID化]
// equal_range 関数を使って、指定したキーの最初と終端の位置を示すイテレータを std::pair<> で取得します。
auto range = components.equal_range(hashID); //https://ez-net.jp/article/9E/DFLX8hNG/G5J8DPmsltz4/
std::vector<std::weak_ptr<ComponentT>> returnList; // "ComponentT" 例."CircleCollider"など をキーとするコンポーネント一覧
for (auto ite = range.first; ite != range.second; ++ite)
returnList.emplace_back(ite->second); // 辞書で見つかったコンポーネントを返すリストに追加
return returnList;
}
// コンポーネントを追加する .AddComponent<BoxCollider>(...)で追加できる
template<class ComponentT, typename ...ArgsT>
std::weak_ptr<ComponentT> AddComponent(ArgsT&&...args)
{ //★[ComponentTのハッシュコードを得るテク] https://cpprefjp.github.io/reference/typeindex/type_index/hash_code.html
size_t hashID = typeid(ComponentT).hash_code(); //[まずハッシュでID化]
// shared_from_this()を必ず渡してセットで初期化して、オーナーがリンク切れのコンポーネントができないように気をつける
std::shared_ptr<ComponentT> newComponent = std::make_shared<ComponentT>(shared_from_this_as<GameObject>(), std::forward<ArgsT>(args)...);
// https://ez-net.jp/article/9E/DFLX8hNG/G5J8DPmsltz4/
// std::unrodered_multimap<> と同型の値を取る std::pair<> を作って insert 関数でcomponents辞書に追加
components.insert(std::pair<size_t, std::shared_ptr<ComponentT>>(hashID, newComponent));
return newComponent;
}
// コンポーネントを削除する
template<typename ComponentT>
bool RemoveComponent(std::shared_ptr<ComponentT> removeComponent)
{ //★[ComponentTのハッシュコードを得るテク] https://cpprefjp.github.io/reference/typeindex/type_index/hash_code.html
size_t hashID = typeid(ComponentT).hash_code(); //[まずハッシュでID化]
// equal_range 関数を使って、指定したキーの最初と終端の位置を示すイテレータを std::pair<> で取得します。
auto range = components.equal_range(hashID); //https://ez-net.jp/article/9E/DFLX8hNG/G5J8DPmsltz4/
//[イテレート中に探しながら消す] https://cpprefjp.github.io/reference/unordered_map/unordered_multimap/erase.html
for (auto ite = range.first; ite != range.second;)
if (ite->second == removeComponent)
{
components.erase(ite); // 削除された要素の次を指すイテレータが返される
return true; // 見つかって消せた場合は true
}
else
++ite; // 次の要素へイテレータを進める
return false; // 見つからずに消せなかった場合は false
}
// 更新処理[純粋仮想関数 = 0 なのでoverride必須の関数]
virtual void Update() = 0; // 純粋仮想関数 = 0 にすると【絶対 Update()関数はoverrideしなきゃいけない義務を付与できる 】
// 描画処理[純粋仮想関数 = 0 なのでoverride必須の関数]
virtual void Draw() = 0; // 純粋仮想関数 = 0 にすると【絶対 Draw()関数はoverrideしなきゃいけない義務を継承先クラスに付与 】
// 衝突開始したときの関数 overrideして衝突したときの処理などをオブジェクトの種類ごとに実装する
virtual void OnCollisionEnter(std::shared_ptr<Collision> pCollision)
{
// overrideして衝突開始したときの処理などをオブジェクトの種類ごとに実装する
};
// 衝突したときの関数 overrideして衝突したときの処理などをオブジェクトの種類ごとに実装する
virtual void OnCollision(std::shared_ptr<Collision> pCollision)
{
// overrideして衝突したときの処理などをオブジェクトの種類ごとに実装する
};
// 衝突終了したときの関数 overrideして衝突したときの処理などをオブジェクトの種類ごとに実装する
virtual void OnCollisionExit(std::shared_ptr<Collision> pCollision)
{
// overrideして衝突終了したときの処理などをオブジェクトの種類ごとに実装する
};
// ジャンプ開始した直後に呼ばれる関数
virtual void OnStartJump()
{
// overrideしてジャンプした瞬間に音を鳴らしたりする処理などをオブジェクトの種類ごとに実装する
};
// 空中でジャンプの上昇中に呼ばれる関数
virtual void OnJumping()
{
// overrideして空中でジャンプの上昇中にエフェクトを出したりする処理などをオブジェクトの種類ごとに実装する
};
// 空中で落ちている最中に呼ばれる関数
virtual void OnFalling()
{
// overrideして空中で落ちている最中に音を鳴らしたり、エフェクトを出したりする処理などをオブジェクトの種類ごとに実装する
};
// タイルに当たったときのコールバック
virtual void OnHitTile(HitPart hitPart, HitBit hitPartBit, TileBit checkBit, float xLeft, float xMiddle, float xRight,
float yBottom, float yMiddle, float yTop, float zBack, float zMiddle, float zForward,
unsigned int* pTerrainID = nullptr, int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, TileBit* pMatchBits = nullptr)
{
// 継承 overrideしてタイルに当たったときの処理を実装する
}
// 継承 overrideしてGameObjectのもつ個別のダメージの値を返す( otherの種類などでダメージ計算式をカスタムできる )
virtual float Damage(std::shared_ptr<GameObject> other) { return 0.0f; }
// 継承 overrideして個別のダメージの値を返す
virtual float Damage() { return 0.0f; }
virtual void OnDamage(std::shared_ptr<GameObject> other)
{
// overrideしてダメージを受けた時の処理を個別に実装
}
virtual void OnItem(std::shared_ptr<Item> pItem)
{
// overrideしてアイテムに触れたときのコールバック処理を個別に実装
}
virtual void OnMoney(int getMoney)
{
// overrideしてアイテムに触れたときのコールバック処理を個別に実装
}
};
#endif
ItemBullet.hを新規作成
して、ステージ上のエネルギー弾などのアイテムとそれを使ったときのアクションを準備しましょう。
#ifndef ITEM_BULLET_H_
#define ITEM_BULLET_H_
#include "Item.h"
#include "ItemAction.h"
#include "Resource.h"
#include "DataJson.h"
#include "GridCollidersComponent.h"
#include "Factory.h"
#include "Player.h"
#include "CollisionParams.h"
// アイテムの情報のクラス 継承override して情報をjsonなどから受け取る
struct ItemBulletInfo : public ItemInfo
{
//int bulletId{ 0 }; // 弾の種類
int count{ -1 }; // 弾を撃てる数のカウントアップ値( -1のときは一度取得したら無限に撃てるタイプのアイテム )
float colliderRadius{ 8.0f }; // アイテムとしてのコライダの半径
float bulletRadius{ 16.0f }; // 撃つ際の弾としてのコライダの半径
float bulletDamage{ 1 }; // 弾の与えるダメージ
float bulletSpeed{ 10.0f }; // 弾のスピード
std::shared_ptr<CollisionParams> collisionParams; // 衝突関係の物理パラメータ
std::weak_ptr<GameObject> shooter; // 弾を撃った後に弾の撃ち手の弱共有ポインタのリンクをBulletに渡す
float shooterAngleY{ 0.0f }; // 撃ち手の撃った際の角度
int imageIndex{ -1 }; // 弾の画像が分割画像だったらどの位置のIDの画像か
int iconImageIndex{ -1 }; // 弾のアイコン画像が分割画像だったらどの位置のIDの画像か
std::string imagePath{ "" }; // 弾の画像のパス
std::string iconImagePath{ "" }; // 弾のアイテムのアイコン画像のパス
inline ItemBulletInfo(std::shared_ptr<InitParams> initParams = nullptr)
: ItemInfo{ initParams }
{
auto tmxParams = std::static_pointer_cast<InitTMXProperty>(initParams);
if (tmxParams->properties == nullptr) return;
auto tmxProperties = tmxParams->properties;
// tmxのjsonの追加プロパティからデータを読み取る
if (tmxProperties->count("pushPower") > 0 || tmxProperties->count("collision_e") > 0)
collisionParams = std::make_shared<CollisionParams>(initParams); // 物理パラメータを読み取り
auto itr = tmxProperties->find("itemTag");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::String && "json の itemTag のタイプが Stringでない!jsonのプロパティを見直してください");
itemTag = itr->second.stringValue;
}
itr = tmxProperties->find("count");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Int && "json の count のタイプが Intでない!jsonのプロパティを見直してください");
count = itr->second.intValue;
}
itr = tmxProperties->find("imagePath");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::String && "json の imagePath のタイプが Stringでない!jsonのプロパティを見直してください");
imagePath = itr->second.stringValue;
}
itr = tmxProperties->find("imageIndex");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Int && "json の imageIndex のタイプが Intでない!jsonのプロパティを見直してください");
imageIndex = itr->second.intValue;
}
itr = tmxProperties->find("iconImagePath");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::String && "json の iconImagePath のタイプが Stringでない!jsonのプロパティを見直してください");
iconImagePath = itr->second.stringValue;
}
itr = tmxProperties->find("iconImageIndex");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Int && "json の iconImageIndex のタイプが Intでない!jsonのプロパティを見直してください");
iconImageIndex = itr->second.intValue;
}
itr = tmxProperties->find("colliderRadius");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Float && "json の colliderRadius のタイプが Floatでない!jsonのプロパティを見直してください");
colliderRadius = itr->second.floatValue;
}
itr = tmxProperties->find("bulletRadius");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Float && "json の bulletRadius のタイプが Floatでない!jsonのプロパティを見直してください");
bulletRadius = itr->second.floatValue;
}
itr = tmxProperties->find("bulletSpeed");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Float && "json の bulletSpeed のタイプが Floatでない!jsonのプロパティを見直してください");
bulletSpeed = itr->second.floatValue;
}
itr = tmxProperties->find("bulletDamage");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Float && "json の bulletDamage のタイプが Floatでない!jsonのプロパティを見直してください");
bulletDamage = itr->second.floatValue;
}
}
virtual ~ItemBulletInfo() {}
};
class ItemBulletAction : public ItemAction
{
protected:
inline ItemBulletAction(ItemListComponent* pItemList, std::shared_ptr<GameObject> pOwner)
: ItemAction(pItemList, pOwner)
{
this->baseTag = "ItemBulletAction";
}
public:
virtual ~ItemBulletAction() {}
inline virtual void Init(std::shared_ptr<InitParams> initParams = nullptr) override
{
auto bulletInfo = std::static_pointer_cast<ItemBulletInfo>(initParams);
m_ItemInfo = bulletInfo;
if (bulletInfo->iconImagePath != "")
{
m_pIconImage = Resource::at<Texture>(bulletInfo->iconImagePath);
m_iconImageIndex = bulletInfo->iconImageIndex;
}
else if (bulletInfo->imagePath != "")
{
m_pIconImage = Resource::at<Texture>(bulletInfo->imagePath);
m_iconImageIndex = bulletInfo->imageIndex;
}
// 弾の数を増やす(-1のときは無限撃ち可能、マイナス指定のときは弾の数を減らす)
AddBullet(bulletInfo->count);
}
// 弾の数を増やす(-1のときは無限撃ち可能、マイナス指定のときは弾の数を減らす)
void AddBullet(int newBulletCount)
{
if (m_count != -1) // -1のときは無限に撃てる状態だから無理に変えない
if (newBulletCount == -1)
m_count = -1; // -1にすると無限に撃てる状態
else
{
m_count += newBulletCount;
if (m_count < 0) m_count = 0;
}
}
int m_count{ 0 }; // 弾の残数
std::shared_ptr<Texture> m_pIconImage; // アイコン画像への共有ポインタ
int m_iconImageIndex{ -1 }; // アイコン画像が分割画像だったときの画像位置ID
// 例えば、すでに取得したプレイヤの弾と同じ種類の弾のアイテムをゲットしたら、
// アイテムとしてダブらせるのではなく使用回数を増やす処理をこの関数をoverrideしてやる
virtual void AddSameItem(std::shared_ptr<InitParams> initParam)
{
auto params = std::static_pointer_cast<ItemBulletInfo>(initParam);
// 弾の数を増やす(-1のときは無限撃ち可能、マイナス指定のときは弾の数を減らす)
AddBullet(params->count);
}
virtual void Update(GameObject* obj = nullptr) override
{
if (m_count == 0) return;
auto pOwner = owner();
if (pOwner == nullptr) return;
auto selecting = m_items->Selecting();
auto pWorld = pOwner->world();
if (pOwner->baseTag == "Player" && selecting != nullptr && selecting == shared_from_this_as<ItemAction>())
{
auto pPlayer = std::static_pointer_cast<Player>(pOwner);
// ボタン押下で自機弾を発射
if (Input::GetButtonDown(pPlayer->pad, PAD_INPUT_C) // コントローラのCボタン(キーボードのCキー)を押したら
|| (pPlayer->pad == Pad::Two && Input::GetButtonDown(Pad::Keyboard, KEY_INPUT_T))) // プレイヤ2はキーボードのTを押したら
{
auto bulletInfo = std::static_pointer_cast<ItemBulletInfo>(m_ItemInfo);
bulletInfo->shooter = pPlayer; // 撃ち手の弱共有ポインタをInfoに設定してBulletに渡す
bulletInfo->shooterAngleY = pPlayer->moveAngle + 90; // 撃ち手の角度をInfoに渡す
pWorld->bullets.emplace_back(Factory::MakeSharedObject<Bullet>("Bullet", bulletInfo, pWorld, pPlayer->position));
if (m_count != -1) // -1のときは∞なので減らさない
--m_count; // 弾の数を減らす
}
if (m_count == 0) // 弾の数が0になったらアイテムのアクションのリストからも削除
m_items->RemoveAction(shared_from_this_as<ItemAction>());
}
}
// アイコンを描く
virtual Vector3 DrawIcon(const Vector3& drawPos, const Vector3& limitPos, int nextIntervalX = 30) override
{
Vector3 nextPos{ drawPos };
if (m_pIconImage != nullptr)
{
int iconWidth = m_pIconImage->m_XSize, iconHeight = m_pIconImage->m_YSize;
int widthUI = m_items->heightUI * iconWidth / iconHeight;
int heightUI = m_items->heightUI;
int frameWidth = m_items->frameWidth;
unsigned int frameColor = m_items->frameColor;
int iconIndex = (m_iconImageIndex != -1 && m_iconImageIndex < m_pIconImage->m_handles.size()) ? m_iconImageIndex : 0;
int iconHandle = (m_pIconImage->m_handles.size() > 0) ? m_pIconImage->m_handles[iconIndex] : -1;
if (limitPos.x < drawPos.x + widthUI + nextIntervalX)
return nextPos; // 描いたら limitPosの位置を超えちゃうときは描かない
// アイテムのアイコンのフレームの枠を描く
DxLib::DrawBox(drawPos.x - frameWidth, drawPos.y - frameWidth, drawPos.x + widthUI + frameWidth, drawPos.y + heightUI + frameWidth, frameColor, TRUE);
DxLib::DrawBox(drawPos.x, drawPos.y, drawPos.x + widthUI, drawPos.y + heightUI, GetColor(0, 0, 0), TRUE);
// アイテムのアイコン画像を描く
DxLib::DrawRotaGraph(drawPos.x + widthUI / 2, drawPos.y + heightUI / 2, 1.0, 0.0, iconHandle, TRUE);
// アイテムの残りの数を表示する
std::string str{ (m_count == -1) ? "∞" : std::to_string(m_count) };
int fontHeight = DxLib::GetFontSize();
DxLib::DrawString(drawPos.x + widthUI + 2, drawPos.y + heightUI - fontHeight, str.c_str(), GetColor(255, 255, 255));
nextPos = Vector3{ drawPos.x + widthUI + nextIntervalX, drawPos.y, 0 }; // 次のアイコンのずらし位置を計算
}
return nextPos;
}
};
// プレイヤの撃つ弾のアイテムのクラス
class ItemBullet : public Item
{
protected:
// コンストラクタ
ItemBullet(World* world, Vector3 position, Quaternion rotation = { 0,0,0,1 })
: Item(world, position, rotation)
{
this->typeTag = "ItemBullet";
}
public:
virtual ~ItemBullet() {}
// 継承overrideしてアイテムを取った時にアイテム一覧で選択できるアクションを持つかをtrueかfalseで返す
virtual bool hasAction() override { return true; }
std::shared_ptr<Texture> m_pImage{ nullptr }; // 画像テクスチャへの共有リンク +1
int m_imageIndex{ -1 }; // 分割画像だったら表示させたい画像番号
std::shared_ptr<GridCollidersComponent> gridCollision; // 当たり判定用のグリッドのコンポーネント
std::shared_ptr<SphereCollider> sphereCollider; // 球体コライダ
// Initをoverrideしてアイテムの情報をjsonなどから受け取っておく
virtual void Init(std::shared_ptr<InitParams> initParams = nullptr) override
{
auto itemBulletInfo = std::make_shared<ItemBulletInfo>(initParams); // アイテム情報を持っておく
m_ItemInfo = itemBulletInfo;
m_pImage = Resource::MakeShared<Texture>(itemBulletInfo->imagePath);
// 球体コライダ
sphereCollider = std::make_shared<SphereCollider>(shared_from_this_as<ItemBullet>(), Vector3(0, 0, 0), itemBulletInfo->colliderRadius);
gridCollision = std::make_shared<GridCollidersComponent>(shared_from_this_as<ItemBullet>(), "ItemBullet");
gridCollision->LinkCollider(sphereCollider); // コライダをリンクする
}
// プレイヤの弾の画像のハンドル を 事前にItemInfo から読み取ったtextureから返す(画像番号があるときはindexで返される)
std::shared_ptr<Texture> image(int& index) { if (m_imageIndex >= 0) index = m_imageIndex; return m_pImage; }
// 更新処理を override
virtual void Update() override
{
}
// 描画処理を override
virtual void Draw() override
{
if (world() == nullptr) return;
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
world()->Draw([&]()
{
// 常にカメラ方向を向くビルボード回転の角度を得る
VECTOR_D angle = MyDraw::GetAngle(DxLib::GetCameraBillboardMatrixD());
int index = 0; // 画像番号
auto pImage = image(index); // 画像の共有リンク(indexで画像番号を受け取る)
float radius = (sphereCollider != nullptr) ? sphereCollider->m_radius : 8; // コライダの半径(なかったら8)
float ExRate = radius / pImage->m_XSize; // 画像の拡大率
MyDraw::DrawDivRotaGraphF3D(MyDraw::Plane::Z, x, y, z, ExRate, angle, *pImage, (index == -1) ? 0 : index, TRUE);
#ifdef _DEBUG
if (sphereCollider != nullptr) sphereCollider->Draw();
#endif
});
}
virtual void OnCollision(std::shared_ptr<Collision> pCollision) override
{
Item::OnCollision(pCollision); // ベース基底の共通処理のOnCollisionを呼ぶ
}
// アイテムを取ったときの処理を overrideして実装
virtual void OnItem(std::shared_ptr<Item> pItem) override
{
if (pItem == shared_from_this_as<Item>())
{ // 自分自身からOnItemが呼ばれた場合はこちら
isDead = true; // 取られたら消える
}
}
};
#endif
ItemBlock.hを新規作成
して、ステージ上のレンガなどブロックのアイテムとそれを使ったときのアクションを準備しましょう。
#ifndef ITEM_BLOCK_H_
#define ITEM_BLOCK_H_
#include "Item.h"
#include "ItemAction.h"
#include "Resource.h"
#include "TMXJson.h"
#include "DataTsx.h"
#include "GridCollidersComponent.h"
#include "Factory.h"
#include "Player.h"
#include "Map.h"
// アイテムの情報のクラス 継承override して情報をjsonなどから受け取る
struct ItemBlockInfo : public ItemInfo
{
unsigned int gid{ 0 }; // タイルのgid
int count{ -1 }; // アイテム数のカウントアップ値( -1のときは一度取得したら無限に撃てるタイプのアイテム )
int cost{ 1 }; // 経路探索のコスト
float colliderRadius{ 8.0f }; // アイテムとしてのコライダの半径
std::weak_ptr<GameObject> shooter; // 撃ち手の弱共有ポインタのリンクをBulletに渡す
float shooterAngleY{ 0.0f }; // 撃ち手の撃った際の角度
int imageIndex{ -1 }; // 画像が分割画像だったらどの位置のIDの画像か
//int iconImageIndex{ -1 }; // アイコン画像が分割画像だったらどの位置のIDの画像か
std::string imagePath{ "" }; // 画像のパス
//std::string iconImagePath{ "" }; // アイテムのアイコン画像のパス
inline ItemBlockInfo(std::shared_ptr<InitParams> initParams = nullptr)
: ItemInfo{ initParams }
{
auto tmxParams = std::static_pointer_cast<InitTMXProperty>(initParams);
if (tmxParams->properties == nullptr) return;
auto tmxProperties = tmxParams->properties;
// tmxのjsonの追加プロパティからデータを読み取る
auto itr = tmxProperties->find("itemTag");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::String && "json の itemTag のタイプが Stringでない!jsonのプロパティを見直してください");
itemTag = itr->second.stringValue;
}
itr = tmxProperties->find("count");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Int && "json の count のタイプが Intでない!jsonのプロパティを見直してください");
count = itr->second.intValue;
}
itr = tmxProperties->find("imagePath");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::String && "json の imagePath のタイプが Stringでない!jsonのプロパティを見直してください");
imagePath = itr->second.stringValue;
}
itr = tmxProperties->find("imageIndex");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Int && "json の imageIndex のタイプが Intでない!jsonのプロパティを見直してください");
imageIndex = itr->second.intValue;
}
if (imageIndex >= 0) // id が 0以上なら
{
Tile tile = Map::GetTile(imagePath, imageIndex, gid);
cost = tile.cost;
}
itr = tmxProperties->find("colliderRadius");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Float && "json の colliderRadius のタイプが Floatでない!jsonのプロパティを見直してください");
colliderRadius = itr->second.floatValue;
}
}
virtual ~ItemBlockInfo() {}
};
// アイテムでブロックを選択中のアクション
class ItemBlockAction : public ItemAction
{
protected:
inline ItemBlockAction(ItemListComponent* pItemList, std::shared_ptr<GameObject> pOwner)
: ItemAction(pItemList, pOwner)
{
this->baseTag = "ItemBulletAction";
}
public:
virtual ~ItemBlockAction() {}
inline virtual void Init(std::shared_ptr<InitParams> initParams = nullptr) override
{
auto blockInfo = std::static_pointer_cast<ItemBlockInfo>(initParams);
m_ItemInfo = blockInfo;
if (blockInfo->imagePath != "")
{
m_pIconImage = Resource::at<Texture>(blockInfo->imagePath);
m_iconImageIndex = blockInfo->imageIndex;
}
// ブロックの数を増やす(-1のときは無限撃ち可能、マイナス指定のときは弾の数を減らす)
AddBlock(blockInfo->count);
}
// ブロックの数を増やす(-1のときは無限置き可能、マイナス指定のときはブロックの数を減らす)
void AddBlock(int newBlockCount)
{
if (m_count != -1) // -1のときは無限に置ける状態だから無理に変えない
if (newBlockCount == -1)
m_count = -1; // -1にすると無限に置ける状態
else
{
m_count += newBlockCount;
if (m_count < 0) m_count = 0;
}
}
int m_count{ 0 }; // ブロックの残数
std::shared_ptr<Texture> m_pIconImage; // アイコン画像への共有ポインタ
int m_iconImageIndex{ -1 }; // アイコン画像が分割画像だったときの画像位置ID
// 例えば、すでに取得したプレイヤのブロックと同じ種類のブロックのアイテムをゲットしたら、
// アイテムとしてダブらせるのではなく使用回数を増やす処理をこの関数をoverrideしてやる
virtual void AddSameItem(std::shared_ptr<InitParams> initParam)
{
auto params = std::static_pointer_cast<ItemBlockInfo>(initParam);
// ブロックの数を増やす(-1のときは無限置き可能、マイナス指定のときはブロックの数を減らす)
AddBlock(params->count);
}
// ボタンを押したときにブロックを置く処理
virtual void Update(GameObject* obj = nullptr) override
{
if (m_count == 0) return;
auto pOwner = owner();
if (pOwner == nullptr) return;
auto selecting = m_items->Selecting();
auto pWorld = pOwner->world();
if (pOwner->baseTag == "Player" && selecting != nullptr && selecting == shared_from_this_as<ItemAction>())
{
auto pPlayer = std::static_pointer_cast<Player>(pOwner);
// ボタン押下でステージにタイルを配置する
if (Input::GetButtonDown(pPlayer->pad, PAD_INPUT_C) // コントローラのCボタン(キーボードのCキー)を押したら
|| (pPlayer->pad == Pad::Two && Input::GetButtonDown(Pad::Keyboard, KEY_INPUT_T))) // プレイヤ2はキーボードのTを押したら
{
auto blockInfo = std::static_pointer_cast<ItemBlockInfo>(m_ItemInfo);
blockInfo->shooter = pPlayer; // 置き手の弱共有ポインタをInfoに設定
blockInfo->shooterAngleY = pPlayer->moveAngle + 90; // 置き手の角度をInfoに渡す
float cellSize = pWorld->map->CellSize();
float putX = (float)std::cos((pPlayer->moveAngle + 90) * MyMath::Deg2Rad) * cellSize;
float putZ = (float)std::sin((pPlayer->moveAngle + 90) * MyMath::Deg2Rad) * cellSize;
float writeX = pPlayer->position.x + putX; // 地形を書き換えるワールド位置
float writeY = pPlayer->position.y;
float writeZ = pPlayer->position.z + putZ;
unsigned int gid = blockInfo->gid; // 書き換える gid
bool isWall = pWorld->map->IsWall(writeX, writeY, writeZ); // 書き換え位置が壁か
int mapX, mapY, mapZ; unsigned int gid_now = 0; // checkID関数で読み出す数値
TileBit matchBit;
bool isMatch = pWorld->map->checkID(TileType::Wall | TileType::Floor, writeX, writeY, writeZ, &mapX, &mapY, &mapZ, &gid_now, &matchBit);
if (matchBit & TileType::Wall) return; // マッチしたbitが壁か
if (gid_now != 0 && gid_now == gid) return; // 書き換え先の現在のgidがこれから書き換えるgidと同じならアイテム数は減らさず return
//if ((matchBit & TileType::Floor) && !(pWorld->map->matchTypes(gid, (TileBit)TileType::Floor) & TileType::Floor))
// return;
// 地形を書き換え
pWorld->map->SetTerrain(gid, writeX, writeY, writeZ);
if (m_count != -1) // -1のときは∞なので減らさない
--m_count; // ブロックの数を減らす
}
if (m_count == 0) // ブロックの数が0になったらアイテムのアクションのリストからも削除
m_items->RemoveAction(shared_from_this_as<ItemAction>());
}
}
// アイコンを描く
virtual Vector3 DrawIcon(const Vector3& drawPos, const Vector3& limitPos, int nextIntervalX = 30) override
{
Vector3 nextPos{ drawPos };
if (m_pIconImage != nullptr)
{
int iconWidth = m_pIconImage->m_XSize, iconHeight = m_pIconImage->m_YSize;
int widthUI = m_items->heightUI * iconWidth / iconHeight; // アイコンの実際の画像サイズの縦横比率×heightUI
int heightUI = m_items->heightUI;
int frameWidth = m_items->frameWidth;
unsigned int frameColor = m_items->frameColor;
int iconIndex = (m_iconImageIndex != -1 && m_iconImageIndex < m_pIconImage->m_handles.size()) ? m_iconImageIndex : 0;
int iconHandle = (m_pIconImage->m_handles.size() > 0) ? m_pIconImage->m_handles[iconIndex] : -1;
if (limitPos.x < drawPos.x + widthUI + nextIntervalX)
return nextPos; // 描いたら limitPosの位置を超えちゃうときは描かない
// アイテムのアイコンのフレームの枠を描く
DxLib::DrawBox(drawPos.x - frameWidth, drawPos.y - frameWidth, drawPos.x + widthUI + frameWidth, drawPos.y + heightUI + frameWidth, frameColor, TRUE);
DxLib::DrawBox(drawPos.x, drawPos.y, drawPos.x + widthUI, drawPos.y + heightUI, GetColor(0, 0, 0), TRUE);
// アイテムのアイコン画像を描く
DxLib::DrawRotaGraph(drawPos.x + widthUI / 2, drawPos.y + heightUI / 2, 1.0, 0.0, iconHandle, TRUE);
// アイテムの残りの数を表示する
std::string str{ (m_count == -1) ? "∞" : std::to_string(m_count) };
int fontHeight = DxLib::GetFontSize();
DxLib::DrawString(drawPos.x + widthUI + 2, drawPos.y + heightUI - fontHeight, str.c_str(), GetColor(255, 255, 255));
nextPos = Vector3{ drawPos.x + widthUI + nextIntervalX, drawPos.y, 0 }; // 次のアイコンのずらし位置を計算
}
return nextPos;
}
};
// ブロックのアイテムのクラス
// 壊したブロックなどを回収してステージに再配置できる
class ItemBlock : public Item
{
protected:
// コンストラクタ
ItemBlock(World* world, Vector3 position, Quaternion rotation = { 0,0,0,1 })
: Item(world, position, rotation)
{
this->typeTag = "ItemBlock";
}
public:
virtual ~ItemBlock() {}
// 継承overrideしてアイテムを取った時にアイテム一覧で選択できるアクションを持つかをtrueかfalseで返す
virtual bool hasAction() override { return true; }
std::shared_ptr<Texture> m_pImage{ nullptr }; // 画像テクスチャへの共有リンク +1
int m_imageIndex{ -1 }; // 分割画像だったら表示させたい画像番号
std::shared_ptr<GridCollidersComponent> gridCollision; // 当たり判定用のグリッドのコンポーネント
std::shared_ptr<SphereCollider> sphereCollider; // 球体コライダ
// Initをoverrideしてアイテムの情報をjsonなどから受け取っておく
virtual void Init(std::shared_ptr<InitParams> initParams = nullptr) override
{
auto itemBlockInfo = std::make_shared<ItemBlockInfo>(initParams); // アイテム情報を持っておく
m_ItemInfo = itemBlockInfo;
m_pImage = Resource::MakeShared<Texture>(itemBlockInfo->imagePath);
m_imageIndex = itemBlockInfo->imageIndex;
// 球体コライダ
sphereCollider = std::make_shared<SphereCollider>(shared_from_this_as<ItemBlock>(), Vector3(0, 0, 0), itemBlockInfo->colliderRadius);
gridCollision = std::make_shared<GridCollidersComponent>(shared_from_this_as<ItemBlock>(), "ItemBlock");
gridCollision->LinkCollider(sphereCollider); // コライダをリンクする
}
// プレイヤの弾の画像のハンドル を 事前にItemInfo から読み取ったtextureから返す(画像番号があるときはindexで返される)
std::shared_ptr<Texture> image(int& id) { if (m_imageIndex >= 0) id = m_imageIndex; return m_pImage; }
// ブロックの gid を ItemInfo から読み取って返す
unsigned int gid()
{
if (m_ItemInfo == nullptr) return 0;
return std::static_pointer_cast<ItemBlockInfo>(m_ItemInfo)->gid;
}
// 更新処理を override
virtual void Update() override
{
}
// 描画処理を override
virtual void Draw() override
{
if (world() == nullptr) return;
int id = 0; // 画像番号
auto pImage = image(id); // 画像の共有リンク(indexで画像番号を受け取る)
if (pImage == nullptr || id < 0) return;
float radius = (sphereCollider != nullptr) ? sphereCollider->m_radius : 8; // コライダの半径(なかったら8)
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
world()->Draw([&]()
{
// 常にカメラ方向を向くビルボード回転の角度を得る
VECTOR_D angle = MyDraw::GetAngle(DxLib::GetCameraBillboardMatrixD());
float ExRate = radius / pImage->m_XSize; // 画像の拡大率
MyDraw::DrawDivRotaGraphF3D(MyDraw::Plane::Z, x, y, z, ExRate, angle, *pImage, (id == -1) ? 0 : id, TRUE);
#ifdef _DEBUG
if (sphereCollider != nullptr) sphereCollider->Draw();
#endif
});
}
// [アイテムとして]フィールドに落ちているブロックと衝突したときの処理
virtual void OnCollision(std::shared_ptr<Collision> pCollision) override
{
Item::OnCollision(pCollision); // ベース基底の共通処理のOnCollisionを呼ぶ
}
// アイテムを取ったときの処理を overrideして実装
virtual void OnItem(std::shared_ptr<Item> pItem) override
{
if (pItem == shared_from_this_as<Item>())
{ // 自分自身からOnItemが呼ばれた場合はこちら
isDead = true; // 取られたら消える
}
}
};
#endif
ItemMoney.hを新規作成
して、フィールド上のお金のクラスを準備しましょう。
#ifndef ITEM_MONEY_H_
#define ITEM_MONEY_H_
#include "Item.h"
#include "ItemAction.h"
#include "Resource.h"
#include "DataJson.h"
#include "GridCollidersComponent.h"
#include "Factory.h"
#include "Player.h"
// アイテムの情報のクラス 継承override して情報をjsonなどから受け取る
struct ItemMoneyInfo : public ItemInfo
{
int money{ 0 }; // 取得する金額
float colliderRadius{ 8.0f }; // アイテムとしてのコライダの半径
int imageIndex{ -1 }; // 画像が分割画像だったらどの位置のIDの画像か
std::string imagePath{ "" }; // 画像のパス
inline ItemMoneyInfo(std::shared_ptr<InitParams> initParams = nullptr)
: ItemInfo{ initParams }
{
auto tmxParams = std::static_pointer_cast<InitTMXProperty>(initParams);
if (tmxParams->properties == nullptr) return;
auto tmxProperties = tmxParams->properties;
// tmxのjsonの追加プロパティからデータを読み取る
auto itr = tmxProperties->find("itemTag");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::String && "json の itemTag のタイプが Stringでない!jsonのプロパティを見直してください");
itemTag = itr->second.stringValue;
}
itr = tmxProperties->find("imagePath");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::String && "json の imagePath のタイプが Stringでない!jsonのプロパティを見直してください");
imagePath = itr->second.stringValue;
}
itr = tmxProperties->find("imageIndex");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Int && "json の imageIndex のタイプが Intでない!jsonのプロパティを見直してください");
imageIndex = itr->second.intValue;
}
itr = tmxProperties->find("colliderRadius");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Float && "json の colliderRadius のタイプが Floatでない!jsonのプロパティを見直してください");
colliderRadius = itr->second.floatValue;
}
itr = tmxProperties->find("money");
if (itr != end(*tmxProperties))
{
assert(itr->second == TMXProperty::Type::Int && "json の colliderRadius のタイプが Intでない!jsonのプロパティを見直してください");
money = itr->second.intValue;
}
}
virtual ~ItemMoneyInfo() {}
};
class ItemMoneyAction : public ItemAction
{
protected:
inline ItemMoneyAction(ItemListComponent* pItemList, std::shared_ptr<GameObject> pOwner)
: ItemAction(pItemList, pOwner)
{
this->baseTag = "ItemMoneyAction";
}
public:
virtual ~ItemMoneyAction() {}
std::shared_ptr<ItemMoneyInfo> m_moneyInfo; // アイテムの情報
inline virtual void Init(std::shared_ptr<InitParams> initParams = nullptr) override
{
m_moneyInfo = std::static_pointer_cast<ItemMoneyInfo>(initParams);
if (m_moneyInfo->money != 0)
{
}
}
virtual void Update(GameObject* obj = nullptr) override
{
}
virtual void Draw(GameObject* obj = nullptr) override
{
}
};
// お金のアイテムのクラス
class ItemMoney : public Item
{
protected:
// コンストラクタ
ItemMoney(World* world, Vector3 position, Quaternion rotation = { 0,0,0,1 })
: Item(world, position, rotation)
{
this->typeTag = "ItemMoney";
}
public:
virtual ~ItemMoney() {}
// 継承overrideしてアイテムを取った時にアイテム一覧で選択できるアクションを持つかをtrueかfalseで返す
virtual bool hasAction() override { return false; }
int m_money{ 0 };
std::shared_ptr<Texture> m_pImage{ nullptr }; // 画像テクスチャへの共有リンク +1
int m_imageIndex{ -1 }; // 分割画像だったら表示させたい画像番号
std::shared_ptr<GridCollidersComponent> gridCollision; // 当たり判定用のグリッドのコンポーネント
std::shared_ptr<SphereCollider> sphereCollider; // 球体コライダ
// Initをoverrideしてアイテムの情報をjsonなどから受け取っておく
virtual void Init(std::shared_ptr<InitParams> initParams = nullptr) override
{
auto itemMoneyInfo = std::make_shared<ItemMoneyInfo>(initParams); // アイテム情報を持っておく
m_ItemInfo = itemMoneyInfo;
m_pImage = Resource::MakeShared<Texture>(itemMoneyInfo->imagePath);
m_money = itemMoneyInfo->money; // お金の額の情報を得る
// 球体コライダ
sphereCollider = std::make_shared<SphereCollider>(shared_from_this_as<ItemMoney>(), Vector3(0, 0, 0), itemMoneyInfo->colliderRadius);
gridCollision = std::make_shared<GridCollidersComponent>(shared_from_this_as<ItemMoney>(), "ItemMoney");
gridCollision->LinkCollider(sphereCollider); // コライダをリンクする
}
// プレイヤの弾の画像のハンドル を 事前にItemInfo から読み取ったtextureから返す(画像番号があるときはindexで返される)
std::shared_ptr<Texture> image(int& index) { if (m_imageIndex >= 0) index = m_imageIndex; return m_pImage; }
// 更新処理を override
virtual void Update() override
{
}
// 描画処理を override
virtual void Draw() override
{
if (world() == nullptr) return;
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
world()->Draw([&]()
{
// 常にカメラ方向を向くビルボード回転の角度を得る
VECTOR_D angle = MyDraw::GetAngle(DxLib::GetCameraBillboardMatrixD());
int index = 0; // 画像番号
auto pImage = image(index); // 画像の共有リンク(indexで画像番号を受け取る)
float radius = (sphereCollider != nullptr) ? sphereCollider->m_radius : 8; // コライダの半径(なかったら8)
float ExRate = radius / pImage->m_XSize; // 画像の拡大率
MyDraw::DrawDivRotaGraphF3D(MyDraw::Plane::Z, x, y, z, ExRate, angle, *pImage, (index == -1) ? 0 : index, TRUE);
#ifdef _DEBUG
if (sphereCollider != nullptr) sphereCollider->Draw();
#endif
});
}
virtual void OnCollision(std::shared_ptr<Collision> pCollision) override
{
Item::OnCollision(pCollision); // ベース基底の共通処理のOnCollisionを呼ぶ
auto otherCollider = pCollision->contacts[0].otherCollider.lock();
if (otherCollider == nullptr) return;
auto other = otherCollider->owner();
other->OnMoney(m_money); // お金をゲットした時の処理を呼ぶ
}
// アイテムを取ったときの処理を overrideして実装
virtual void OnItem(std::shared_ptr<Item> pItem) override
{
if (pItem == shared_from_this_as<Item>())
{ // 自分自身からOnItemが呼ばれた場合はこちら
isDead = true; // 取られたら消える
}
}
};
#endif
Bullet.hを新規作成
して、エネルギー弾などの撃つ弾のクラスを準備しましょう。
#ifndef PLAYERBULLET_H_
#define PLAYERBULLET_H_
#include <cmath>
#include "DxLib.h"
#include "Screen.h"
#include "MyDraw.h"
#include "GameObject.h"
#include "Collider.h"
#include "GridCollidersComponent.h"
#include "ItemBullet.h"
class World; // 前方宣言
// 弾クラス
class Bullet : public GameObject
{
public:
// プレイヤ弾と敵の当たり判定を空間分割Gridで高速化
std::shared_ptr<GridCollidersComponent> gridCollision;
std::shared_ptr<SphereCollider> sphereCollider;
virtual void Init(std::shared_ptr<InitParams> initParams = nullptr) override
{
if (initParams == nullptr) return;
auto params = std::static_pointer_cast<ItemBulletInfo>(initParams); // アイテム情報を受け取る
collisionParams = params->collisionParams; // 物理衝突パラメータを受け取る
m_pImage = Resource::MakeShared<Texture>(params->imagePath);
sphereCollider = std::make_shared<SphereCollider>(shared_from_this_as<Bullet>(), Vector3(0, 0, 0), params->bulletRadius);
gridCollision = std::make_shared<GridCollidersComponent>(shared_from_this_as<Bullet>(), "Bullet");
gridCollision->LinkCollider(sphereCollider); // コライダをリンクする
m_shooter = params->shooter; // 弾の撃ち手の弱共有リンクを得ておく
auto pShooter = m_shooter.lock();
if (pShooter != nullptr)
{ // 撃ち手のタグに応じて弾のタイプをPlayerBulletとEnemyBulletに切り分ける
if (pShooter->baseTag == "Player")
this->typeTag = "PlayerBullet";
else if (pShooter->baseTag == "Enemy")
this->typeTag = "EnemyBullet";
}
Speed = params->bulletSpeed;
m_Damage = params->bulletDamage;
vx = (float)std::cos(params->shooterAngleY * MyMath::Deg2Rad) * Speed;
vz = (float)std::sin(params->shooterAngleY * MyMath::Deg2Rad) * Speed;
}
int deadCounter = 60 * 5; // 5秒後にisDead
float Speed{ 10.0 }; // 移動速度
float m_Damage{ 0 }; // 弾が当たった時に与えるダメージ
float Damage() override { return m_Damage; }
protected:
std::weak_ptr<GameObject> m_shooter; // 弾の撃ち手への弱共有ポインタ
public:
// 弾の撃ち手への共有ポインタ(撃ち手がすでに死んでいた場合は nullptr)
std::shared_ptr<GameObject> Shooter() { auto pShooter = m_shooter.lock(); return pShooter; }
std::shared_ptr<Texture> m_pImage; // 画像のパス
// コンストラクタ
Bullet(World* world, Vector3 position, Quaternion rotation)
: GameObject(world, position, rotation)
{
this->baseTag = "Bullet";
//this->pushPower = 15.0f;
#if 0
double angle = rotation.getPitch();
if (auto a = angle / MyMath::Deg2Rad)
{
printfDx("%f\n", (float)a);
}
vx = (float)std::cos(angle) * Speed;
vz = (float)std::sin(angle) * Speed;
#endif
}
// デストラクタ
virtual ~Bullet()
{
}
// 更新処理
void Update()
{
// 移動
x += vx;
z += vz;
deadCounter--;
if (deadCounter < 0) isDead = true; // 一定の秒数がたったら消す
if (gridCollision != nullptr)
gridCollision->Update(); // 所属するマス目セルを更新する
}
// 描画処理
void Draw()
{
if (world() == nullptr) return;
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
world()->Draw([&]()
{
// 常にカメラ方向を向くビルボード回転の角度を得る
VECTOR_D angle = MyDraw::GetAngle(DxLib::GetCameraBillboardMatrixD());
MyDraw::DrawDivRotaGraphF3D(MyDraw::Plane::Z, x, y, z, 1.0f, angle, *m_pImage, 0, TRUE);
#ifdef _DEBUG
if (sphereCollider != nullptr) sphereCollider->Draw();
#endif
});
}
virtual void OnCollision(std::shared_ptr<Collision> pCollision) override
{
if (isDead) return; // すでにisDeadだったらreturn
auto otherCollider = pCollision->contacts[0].otherCollider.lock();
if (otherCollider == nullptr) return;
auto other = otherCollider->owner();
if (other->isDead) return; // other側がisDeadだったら処理をスルー
if (typeTag == "PlayerBullet" && other->baseTag != "Enemy") return; // Enemyタイプ以外はスルー
if (typeTag == "EnemyBullet" && other->baseTag != "Player") return; // Playerタイプ以外はスルー
if (other->typeTag == "Zako1")
{ //名前タグでぶつかった相手方を判別できる
}
if (m_Damage != 0)
{
other->OnDamage(shared_from_this_as<Bullet>());
}
//isDead = true;
}
};
#endif
Factory.cppを変更
して、Item系やBulletなどの初期化コンストラクタ発動して生成できるようにしましょう。
#include "Factory.h"
#include <unordered_map> //★ #文字列 マクロで<クラス名→コンストラクタ>をビルドする瞬間に 辞書に登録
// new や std::make_shared1 や Createで初期生成するには
// #includeしてコンストラクタを呼び出せる必要が出てくる
#include "Enemy.h"
#include "Zako0.h"
//#include "Zako1.h"
//#include "Zako2.h"
//#include "Zako3.h"
//#include "Boss.h"
#include "Item.h"
#include "ItemBlock.h"
#include "ItemBullet.h"
#include "ItemMoney.h"
#include "Bullet.h"
#include "World.h"
//【上級1】★以下サイトを参考に【文字列"Zako1"とかからクラスを初期化するテクニック】
// https://stackoverflow.com/questions/5447675/how-to-convert-a-string-to-a-class-name/5447776
//
// 以下のようにクラスの型タイプを書かずに初期化できるので★if else文をいちいちコピーして増やさずに済む
// 例. いちいち書く例
// if (gid == 0) return Factory::make_shared<Zako0>(x + 32, y + 32);
// else if(gid == 1) return Factory::make_shared<Zako1>(x + 32, y + 32);
// .......(敵の種類が増えるたびにいちいちif elseコピペで書かないと..)
// ↓
// ↓★"Zako1"の文字列で初期化できれば1行で書けるようになる余地が出てくる!(CSVのクラス名の文字から初期化も目指せる)
// ↓
// 例. 初期化に<Zako1>を使わずに済むようになる "Zako1"で書ける
// auto enemy = g_ClassFactory.Construct("Zako1", x + 32, y + 32);//←"Zako1"で初期化 <Zako1>だとコードに書くしかない
// クラス名 classTypeName 例. Zako0 のコンストラクタを s_umConstructors 辞書に登録するための↓#文字取得マクロ
#define REGISTER_CONSTRUCTOR(classTypeName) RegisterConstructor<classTypeName>( #classTypeName )
// Factoryクラスの「依存性の逆転」を引き起こすには.hでは★#includeしたくない https://blog.ecbeing.tech/entry/2021/01/20/114000
// だが、テンプレート関数は普通.hに実装までする必要があるが、それを回避する方法もある https://qiita.com/i153/items/38f9688a9c80b2cb7da7
// .cppでtemplate関数の実体を.hと分けて定義するには「★最小限下記の定義をオブジェクトごとにする必要」がある
template std::shared_ptr<Object> Factory::ConstructByClassName<Object>(const std::string&, std::shared_ptr<InitParams>, World*, Vector3, Quaternion);
template std::shared_ptr<Enemy> Factory::ConstructByClassName<Enemy>(const std::string&, std::shared_ptr<InitParams>, World*, Vector3, Quaternion);
template std::shared_ptr<Item> Factory::ConstructByClassName<Item>(const std::string&, std::shared_ptr<InitParams>, World*, Vector3, Quaternion);
template std::shared_ptr<Bullet> Factory::ConstructByClassName<Bullet>(const std::string&, std::shared_ptr<InitParams>, World*, Vector3, Quaternion);
namespace // 無名名前空間(他ファイルから参照できない変数や関数を宣言)
{
bool isConstructorRegistered{ false }; // 初回だけクラスのコンストラクタ登録を1回だけするためのフラグ
// 初回だけ Zako0 などのクラスのコンストラクタを辞書に登録を1回だけする
inline void InitConstructors()
{
if (!isConstructorRegistered) // 初回だけクラスのコンストラクタ登録を1回だけする
{ //クラス登録されていないなら初回登録して "文字列"→コンストラクタ を呼び出せるようにしておく
REGISTER_CONSTRUCTOR(Zako0); // Zako0のコンストラクタを辞書に登録しておく
//REGISTER_CONSTRUCTOR(Zako1);
//REGISTER_CONSTRUCTOR(Zako2);
//REGISTER_CONSTRUCTOR(Zako3);
//REGISTER_CONSTRUCTOR(Boss);
REGISTER_CONSTRUCTOR(ItemBullet);
REGISTER_CONSTRUCTOR(ItemBlock);
REGISTER_CONSTRUCTOR(ItemMoney);
REGISTER_CONSTRUCTOR(Bullet);
isConstructorRegistered = true;
}
}
}
void Factory::Init(const std::string& class_name)//int id)
{
// 初回だけ Zako0 などのクラスのコンストラクタを辞書に登録を1回だけする
InitConstructors();
// 辞書に class_name が登録されているかチェック
assert(g_Constructors.count(class_name) != 0 && "まだ登録してない クラス名 が Factory.cpp にありました。");
}
// class_name に応じて敵などのオブジェクトを生成してreturnで返す
template <typename TypeT> //[templateを.hと.cppに分けて書くのが難しい] https://qiita.com/i153/items/38f9688a9c80b2cb7da7
std::shared_ptr<TypeT> Factory::ConstructByClassName(const std::string& class_name, std::shared_ptr<InitParams> initParams, World* world, Vector3 position, Quaternion rotation)
{
// class_nameに応じて敵などのオブジェクトを生成してreturnで返す
// ★Zako1*でもBoss*でもOK、ベース基底のObject型であいまいな状態で受け取る
auto pObject = Construct(class_name, initParams, world, position, rotation);
return std::dynamic_pointer_cast<TypeT>(pObject); // タイプをキャストでTypeTで指定した型に変換して返す
}
Player.hを変更
して、所持しているアイテムを更新し描画する処理を追加しましょう。
#ifndef PLAYER_H_
#define PLAYER_H_
#include "DxLib.h"
#include "Input.h"
#include "Resource.h"
#include "GameObject.h"
#include "ItemAction.h" // 複数アイテムを所持しItemActionを継承して使うためのコンポーネント
#include "PhysicsComponent.h"
#include "MapHitBoxComponent.h"
#include "GridCollidersComponent.h"
#include "Collider.h"
class World; // 前方宣言
class Camera; // 前方宣言
class Player : public GameObject
{
protected:
// コンストラクタはprotected:にしてあるので、Create関数経由で初期生成してください
Player(World* world, Pad pad, Vector3 position, Quaternion rotation = { 0,0,0,1 }, Vector3 velocity = { 0,0,0 }, Vector3 force = { 0,0,0 })
: pad{ pad }, GameObject(world, position, rotation, velocity, force)
{
this->typeTag = "Player"; // タグは個別の"プレイヤ1"などに使い、タイプはtypeTagで判別する形にしてタイプのジャンルを区別
this->baseTag = "Player";
this->pushPower = 5.0f; // コライダで衝突したときに押し返す力
}
public:
// 仮想デストラクタ
virtual ~Player() {}
Pad pad; // 操作するコントローラー番号
hash32 worldTag{ "" }; // 現在いるワールドのタグ
int money{ 0 }; // ゲームのお金
float MoveSpeedMax = 6; // 移動速度Max値
float MoveSpeed = 0;//移動速度
float moveAngle = 0; // X軸→方向から何度か
float deltaAngle = 0; // プレイヤの動く角度の変化率、ハンドルを切ったらだんだんもとに戻る
int animCount = 0;
std::shared_ptr<Texture> image; // プレイヤの板ポリゴンで描くタイル分割画像への共有リンク
Camera* camera{ nullptr }; // プレイヤを追随するカメラ
void SetCamera(Camera* pCamera) { camera = pCamera; }
// overrideして物理落下コンポーネントからの重力値を返す
virtual float gravity() override { auto pGravityPhysics = gravityPhysics.lock(); return (pGravityPhysics != nullptr) ? pGravityPhysics->gravity() : 0.0f; }
protected:
std::weak_ptr<JumpComponent> jumpPhysics; // ジャンプの物理処理コンポーネント
std::weak_ptr<GravityComponent> gravityPhysics; // 落下の重力の物理処理コンポーネント
// マップ地形に対するヒットボックスの当たり判定と押し戻しをするコンポーネント
std::weak_ptr<MapHitBoxComponent> mapHitBox;
std::weak_ptr<GridCollidersComponent> gridCollision; // グリッドマス目分割コンポーネント
std::weak_ptr<SphereCollider> sphereCollider; // 球コライダのコンポーネント
std::weak_ptr<ItemListComponent> itemList; // アイテムの所持リスト
public:
virtual void Init(std::shared_ptr<InitParams> initParams = nullptr) override
{
std::vector<float> vyJumpSpeed{ 13, 14, 15, 4 }; // ジャンプ開始の初期速度
std::vector<float> vyForceJump{ 0.5f, 0.4f, 0.3f, 0.1f }; // ジャンプ上昇にかかる力の初期値
std::vector<float> vyGravity{ 0.8f,0.8f,0.8f,0.8f }; // 重力の設定値
std::vector<float> vyDownSpeedMax{ 16, 16, 16, 8 }; // 降下スピードのリミット
// AddComponent内部でshared_from_this()を必ず渡してセットで初期化して、オーナーがリンク切れのコンポーネントができないように気をつける
jumpPhysics = AddComponent<JumpComponent>(vyJumpSpeed, vyForceJump);
gravityPhysics = AddComponent<GravityComponent>(vyGravity, vyDownSpeedMax);
// 半径16(直径32)の球体コライダ
sphereCollider = AddComponent<SphereCollider>(Vector3(0, 0, 0), 16);
gridCollision = AddComponent<GridCollidersComponent>("Player"); //とりあえずわかりやすいように"Player"というタグをつけておく(別に何でもいい)
gridCollision.lock()->LinkCollider(sphereCollider); // コライダをリンクする
float mapHitBoxSize = sphereCollider.lock()->m_radius * 2.0f; // 球体コライダの直径に合わす
Vector3 boxSize{ mapHitBoxSize,mapHitBoxSize,mapHitBoxSize };
Vector3 offsetLeftBottomBack{ 3, 0, 3 }; // ボックスの端を内側にどれだけ削るか(キャラの画像に合わせて小さくできる)
Vector3 offsetRightTopForward{ 3, 8, 3 }; // ボックスの端を内側にどれだけ削るか(頭を8削る)
mapHitBox = AddComponent<MapHitBoxComponent>(boxSize, offsetLeftBottomBack, offsetRightTopForward);
itemList = AddComponent<ItemListComponent>();
}
// 入力を受けての処理
virtual void HandleInput();
// 弾を撃ったりブロックの配置などアイテムのインプット操作を受け取る
void HandleItemInput();
// 更新処理
virtual void Update() override;
// 描画処理
virtual void Draw() override;
// UIの描画処理はDrawとは別にすることで、一番最後にスクリーンに上書きで描ける
virtual void DrawUI();
// ジャンプ開始した直後に呼ばれる関数
virtual void OnStartJump() override;
// コライダの衝突した際に呼ばれるコールバック関数
virtual void OnCollision(std::shared_ptr<Collision> pCollision) override;
// アイテムを取得した時に呼ばれる処理を override して実装
virtual void OnItem(std::shared_ptr<Item> pItem) override;
// お金を取得した時に呼ばれる処理を override して実装
virtual void OnMoney(int getMoney) override;
};
#endif
World.hを変更
して、BulletやItemなどのリストを追加しましょう。
#ifndef WORLD_H_
#define WORLD_H_
#include <list>
#include <memory>
#include <unordered_map>
#include <functional> // std::functionのラムダ式 で 関数を Draw関数の引数にして、カメラすべてに対して描画処理を発動させる
#include "MyHash.h" // ワールドのタグをhash32型で管理する
// [前方宣言] 宣言だけして、#includeはしてないので、ポインタだけはこのファイル内でつかえる
// #includeはしてないので、hpなどの変数には#include "~.h"しないとアクセスできないので循環インクルード防止のため.cppでインクルードしてください
class GameScene;
class Player;
class Bullet;
class Item;
class Enemy;
class Camera;
class Grid;
class Map;
// プレイヤや敵や弾などの共有ポインタを管理し、EraseRemoveIfで消して参照カウンタが0になったらそれらを消滅させるクラス(C#のガベージコレクタの代わり)
class World
{
public:
inline World(GameScene* pScene, const std::string& worldTag = "") : m_pScene{ pScene }, tag{ worldTag } {}
inline ~World() {}
protected: // public:にすると他で外部で後からタグやシーンを書き換えられると辞書での管理などの前提がおかしくなるのでprotected:する
hash32 tag{ "" }; // ワールド名のタグ
GameScene* m_pScene{ nullptr }; // ワールドを所有するシーン
public:
hash32 Tag() const { return tag; }
GameScene* scene() { return m_pScene; }
//std::unordered_map<hash32, std::shared_ptr<Player>> players; // プレイヤのタグ辞書
// ワールドを所有するシーンにプレイヤを追加する
void AddPlayer(const std::string& playerTag, std::shared_ptr<Player> pPlayer);
// ワールドにいるプレイヤを得る
std::vector<std::weak_ptr<Player>> GetPlayers();
bool LoadMap(const std::string& mapFilePath); // ワールドのマップをロード
std::shared_ptr<Map> map{ nullptr }; // マップ(共有ポインタ) マップのメモリ解放は World と運命をともにして消える
protected:
std::shared_ptr<Grid> m_pGrid{ nullptr }; // 当たり判定を高速にするためグリッドで空間分割
public:
Grid* grid() { return m_pGrid.get(); } // グリッドを返す
Grid* makeGrid(int cellWidth = 128, int cellHeight = 128, int cellDepth = 128); // グリッドを生成する
std::unordered_map<hash32, std::shared_ptr<Player>> players; // プレイヤのタグ辞書
std::list<std::shared_ptr<Bullet>> bullets; // 弾のリスト
std::list<std::shared_ptr<Item>> items; // アイテムのリスト
std::list<std::shared_ptr<Enemy>> enemies; // 敵のリスト
// 削除処理を共通テンプレート関数にする
// [共通テンプレート関数]https://programming-place.net/ppp/contents/cpp/language/009.html#function_template
template <typename TypeT, class T_if>
void EraseRemoveIf(std::list<TypeT>& 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()の位置
};
std::unordered_map<hash32, std::shared_ptr<Camera>> cameras; // このワールドに存在するカメラの辞書<カメラにつけたタグ, カメラの共有ポインタ>
// 指定されたタグのカメラへの共有ポインタを得る カメラ辞書に存在しないタグでアクセスしたときにunordered_mapの特性で勝手に意図しないカメラができるのを防ぐ関数
std::shared_ptr<Camera> camera(const std::string& cameraTag) { auto itr = cameras.find(cameraTag); return (itr != end(cameras)) ? itr->second : nullptr; }
// このワールドに存在するカメラすべてに対して drawFuncで描きたい処理を渡して 全カメラのスクリーンに対して描画処理を走らせる
void Draw(std::function<void()> drawFunc) noexcept;
// カメラにタグをつけてワールドに追加 カメラにもワールドへのポインタリンクを張る
inline bool AddCamera(const std::string& cameraTag, std::shared_ptr<Camera> pCamera);
// 指定したタグのカメラをこのワールドから削除する カメラ側の worldへのリンクのポインタも nullptr にする
inline bool EraseCamera(const std::string& cameraTag);
// カメラを nowWorld から nextWorld に移動させる
static bool MoveCamera(const std::string& cameraTag, World* nowWorld, World* nextWorld);
};
#endif
Player.cppを変更
して、マップをスクロールして敵を出現させましょう。
#include "Player.h"
#include <cmath> // 浮動小数点の余り%演算子がC++にないのでfmodを使うため
#include "MyMath.h"
#include "MyDraw.h"
#include "Camera.h"
#include "World.h"
#include "GridSteps.h"
#include <vector>
void Player::HandleInput()
{
if (Input::GetButton(pad, PAD_INPUT_LEFT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_A)) )
{
//vx = - MoveSpeed; // 左
deltaAngle += 1; //ハンドルを左に回す
}
if (Input::GetButton(pad, PAD_INPUT_RIGHT)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_D)))
{
//vx = MoveSpeed; // 右
deltaAngle -= 1; //ハンドルを右に回す
}
if (Input::GetButton(pad, PAD_INPUT_UP)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_W)))
{
//vy = - MoveSpeed; // 上
MoveSpeed += 0.6f; //アクセルを踏む
if (MoveSpeed > MoveSpeedMax)
MoveSpeed = MoveSpeedMax;//Maxスピードで止める
}
else if (Input::GetButton(pad, PAD_INPUT_DOWN)
|| (pad == Pad::Two && Input::GetButton(Pad::Keyboard, KEY_INPUT_S)))
{
//vy = MoveSpeed; // 下
MoveSpeed -= 0.6f; //ブレーキを踏む
if (MoveSpeed < -0.9f)
MoveSpeed = -0.9f;//ちょっとだけバックできるように
}
if (pad == Pad::Key || pad == Pad::One) // コントローラ1またはキーボード操作のプレイヤのときだけ
{
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGUP)) // キーボードのPageUp ボタンを押したときは
{
vy = 2.0f; // テスト用に空中の上方向に移動
}
if (Input::GetButton(Pad::Keyboard, KEY_INPUT_PGDN)) // キーボードのPageDown ボタンを押したときは
{
vy = -2.0f; // テスト用に空中の下方向に移動
}
}
}
// ジャンプ開始した直後に呼ばれる関数
void Player::OnStartJump()
{
this->isGround = false; // ジャンプ中はisGroundフラグがfalse
}
// 更新処理
void Player::Update()
{
vxForce *= 0.9f; // かかっている力の減衰率
vyForce *= 0.9f; // かかっている力の減衰率
vzForce *= 0.9f; // かかっている力の減衰率
deltaAngle *= 0.5F; // 車のハンドルの方向をだんだんニュートラルに戻す
MoveSpeed *= 0.9F; // 移動速度も減速する
// 入力を受けての処理
HandleInput();
// 弾を撃ったりブロックの配置などアイテムのインプット操作を受け取る
HandleItemInput();
auto pJumpPhysics = jumpPhysics.lock();
if (pJumpPhysics != nullptr)
{
if (isGround)
{
if (Input::GetButtonDown(pad, PAD_INPUT_A) // Zキーを押した瞬間
|| (pad == Pad::Two && Input::GetButtonDown(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを押した瞬間
{
pJumpPhysics->JumpStart(); // ジャンプのスタート処理を発動
}
}
else if (vy > 0 && Input::GetButtonUp(pad, PAD_INPUT_A) // Zキーを離した瞬間
|| (pad == Pad::Two && Input::GetButtonUp(Pad::Keyboard, KEY_INPUT_Q))) // プレイヤ2がQキーを離した瞬間
{
pJumpPhysics->JumpCancel(); // ジャンプのキャンセル処理を発動
}
pJumpPhysics->Update(); // ジャンプのコンポーネントを更新する
}
auto pGravityPhysics = gravityPhysics.lock();
if (pGravityPhysics != nullptr)
pGravityPhysics->Update(); // 重力落下のコンポーネントを更新する
moveAngle += deltaAngle; // ハンドルを回したぶんプレイヤの進行方向を変える
if (moveAngle < -180) moveAngle = 360 + moveAngle; // -180以下の角度にならないようにマイナスになったら+180度にループさせる
else if (moveAngle > +180) moveAngle = -360 + moveAngle; // +180以下の角度にならないようにマイナスになったら-180度にループさせる
rotation.SetRotation(VGet(0, (180 - moveAngle) * MyMath::Deg2Rad, 0)); // 前向きの3Dモデルなら180度回転させて奥方向の向きに設定
// ↓進行方向角度をX方向とZ方向の速度に変える
vx = (float)std::cos((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
vz = (float)std::sin((moveAngle + 90) * MyMath::Deg2Rad) * MoveSpeed;
float vyForceJump = (pJumpPhysics != nullptr) ? pJumpPhysics->vyForce() : 0.0f;
float vyForceGravity = (pGravityPhysics != nullptr) ? pGravityPhysics->vyForce() : 0.0f;
vx += vxForce; // 力をかけて速度に加算する
vy += vyForce + vyForceJump + vyForceGravity; // 勢い(力・フォースを加算
vz += vzForce; // 力をかけて速度に加算する
// 実際に位置を動かす
auto pMapHitBox = mapHitBox.lock();
float cellHalf = (map() != nullptr) ? (float)map()->CellSize() / 2.0f : 16.0f; // マップのセルのサイズの半分
// 速すぎて当たり判定がすり抜けないように細切れのステップのグリッドで移動させつつ当たり判定する
Vector3 gridStepSize = (pMapHitBox == nullptr) ? Vector3{ cellHalf, cellHalf, cellHalf } : pMapHitBox->m_BoxSize / 2;
GridSteps vSteps{ velocity, gridStepSize }; // 細切れのステップの数値を計算
#ifdef _DEBUG
if (vSteps.div >= 1000) // 分割が1000を超える場合は物理式かどこかで∞=inf や 計算不可能数=nanが発生してそう
{ // vx,vy,vzに原因がありそうなのでデバッグ表示
if (!isfinite(vx)) printfDx(("vx:∞かnan x:" + std::to_string(x) + "\n").c_str());
if (!isfinite(vy)) printfDx(("vy:∞かnan y:" + std::to_string(y) + "\n").c_str());
if (!isfinite(vz)) printfDx(("vz:∞かnan z:" + std::to_string(z) + "\n").c_str());
}
#endif
bool isCollisionX{ false }, isCollisionY{ false }, isCollisionZ{ false }, isCollisionCollider{ false };
for (int i = 1, iSize = vSteps.div + 1; i <= iSize; ++i)
{
auto& vStep = (i < iSize) ? vSteps.step : vSteps.mod;
// まず横に移動する
if (pMapHitBox == nullptr)
x += vx;
else if (isCollisionX == false) // まだ前のステップでX方向の衝突がないなら
{
x += vStep.x;
isCollisionX = pMapHitBox->hitCheckX((TileBit)TileType::Wall);
}
// 次に縦に動かす
if (pMapHitBox == nullptr)
y += vy;
else if (isCollisionY == false) // まだ前のステップでY方向の衝突がないなら
{
y += vStep.y;
isCollisionY = pMapHitBox->hitCheckY((TileBit)TileType::Wall | (TileBit)TileType::Floor);
}
// 次にZ奥行き方向に動かす
if (pMapHitBox == nullptr)
z += vz;
else if (isCollisionZ == false) // まだ前のステップでZ方向の衝突がないなら
{
z += vStep.z;
isCollisionZ = pMapHitBox->hitCheckZ((TileBit)TileType::Wall);
}
if (auto pGridCollision = gridCollision.lock())
{
pGridCollision->Update(); // 所属するマス目セルを更新する
for (auto& collider : *pGridCollision->colliders())
isCollisionCollider |= collider.lock()->IsCollision();
}
// 衝突があったら、速すぎてすり抜けちゃう前に今のステップでbreakで抜ける
if (isCollisionCollider)
break; // ぶつかっているのにさらに次のステップの速度も足すとすり抜けちゃう
}
map()->Scroll3D(position, Vector3(vx, vy, vz)); // マップ(敵)データをスクロール
// カメラのプレイヤ追尾
float camDistance = 150; // プレイヤからカメラまでのY平面上の距離
float camHeight = 100; // プレイヤからカメラまでのY方向上の高さ
// moveAngleは2D画像のX方向右を0度とするから、-90度回すと画像を正面に捉える位置にカメラを置ける
float camAngleX = (float)std::cos((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
float camAngleZ = (float)std::sin((moveAngle - 90) * MyMath::Deg2Rad) * camDistance;
if (camera != nullptr)
{ // カメラの位置をプレイヤの進行方向と真逆に設定
camera->SetPosition(position + Vector3(camAngleX, camHeight, camAngleZ));
camera->LookAt(position); //カメラはプレイヤの方を見る
}
}
// 弾を撃ったりブロックの配置などアイテムのインプット操作を受け取る
void Player::HandleItemInput()
{
if (auto pItemList = itemList.lock())
{
pItemList->Update(); // アイテムのリストをUpdateすると弾を撃つボタンを押したら弾が出るようになる
if (Input::GetButtonDown(pad, PAD_INPUT_B)
|| (pad == Pad::Two && Input::GetButtonDown(Pad::Keyboard, KEY_INPUT_R)))
{
pItemList->SelectNext();
}
}
}
// アイテムを取得した時に呼ばれる処理を override して実装
void Player::OnItem(std::shared_ptr<Item> pItem)
{
#if 0
if (pItem->typeTag == "ItemBlock") // アイテムブロックの場合はOnItemBlockに引き渡す
{
OnItemBlock(std::static_pointer_cast<ItemBlock>(pItem));
//itemList->Add<>
}
//else if (pItem->typeTag == "ItemBullet")
#endif
if (pItem->hasAction())
itemList.lock()->Add(pItem);
}
// お金を取得した時に呼ばれる処理を override して実装
void Player::OnMoney(int getMoney)
{
money += getMoney; // getMoneyがマイナスのときはお金は減ることもある
//if (money < 0) money = 0; // 借金できない場合はコメントはずす
}
// UIの描画処理はDrawとは別にすることで、一番最後にスクリーンに上書きで描ける
void Player::DrawUI()
{
if (camera != nullptr)
{
camera->Draw([&]()
{
std::string strMoney{ std::to_string(money) + "円" };
DxLib::DrawString(0, 0, strMoney.c_str(), GetColor(255, 255, 255));
if (auto pItemList = itemList.lock())
pItemList->Draw();
});
}
}
void Player::OnCollision(std::shared_ptr<Collision> pCollision)
{
if (isDead) return; // すでにisDeadなら return
auto otherCollider = pCollision->contacts[0].otherCollider.lock(); // コライダの弱共有ポインタをロック
if (otherCollider == nullptr) return; // すでにコライダが消去済だった
auto other = otherCollider->owner();
if (other->isDead) return; // other側がisDeadだったら処理をスルー
if (other->baseTag == "Enemy" || other->baseTag == "Player")
{
//[離散要素法:DEM] https://qiita.com/konbraphat51/items/157e5803c514c60264d2
constexpr float k = 1.0f; // バネ係数(めり込みに対するバネ反発の掛け率)
constexpr float e = 0.1f; // はね返り係数(直前と比べどれくらいの速さで離れるか0.5だと半分の速さではね返る)
float log_e = std::log(e); // はね返り係数の自然対数log
constexpr float pi_2 = DX_PI_F * DX_PI_F; // πの2乗
float eta = -2.0f * log_e * std::sqrt(k * mass / (log_e * log_e + pi_2)); // 粘性係数η(イータ)
auto norm = pCollision->contacts[0].normal; // 衝突の法線 * other->pushPower; // pushPowerの力でotherに押し返される
auto v_norm = dot(other->velocity - velocity, norm) * norm; // [相対速度v] 法線norm方向のv = (v1 - v0)・norm [ドット積]
auto sep = pCollision->contacts[0].separation; // 衝突点どうしの距離(めりこみ距離)
auto F = -k * sep * norm - eta * v_norm; // F (法線方向の力) = -k × sep - η × v (相対速度:自分から見た相手otherの速度)
force += F / mass; // 衝突法線方向の力を加える
}
}
// 描画処理
void Player::Draw()
{
int animIndex = animCount / 20; // キャラのパラパラアニメの画像番号(最大3)
float abs_vx = (vx > 0) ? vx : -vx; // x方向絶対値
float abs_vz = (vz > 0) ? vz : -vz; // y方向絶対値
if (abs_vx > 1.00f || abs_vz > 1.00f)
animCount += 6; // キャラが地面のXとZ方向に少しでもスピードがあったらアニメを走らせる
if (animCount >= 60) animCount = 0; // アニメ時間を0へループ
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
for (auto&& keyValue : world()->cameras) // プレイヤのいるワールドにあるすべてのカメラぶんループ
{
auto& camera = keyValue.second;
camera->Draw([&]()
{
// 常にカメラ方向を向くビルボード回転の角度を得る
VECTOR_D angle = MyDraw::GetAngle(DxLib::GetCameraBillboardMatrixD());
angle.x = 0; angle.z = 0; // Y軸周りのビルボード回転だけ有効にする X軸,Z軸まわりの回転は 0 にする
int imgIndex = 9; // キャラチップ画像の起点番号
// ビルボードの角度 と キャラの向いている向き の 差 によって、別のプレイヤのカメラから見たキャラチップの向きの画像を切り替える
// -180 ~ +180 : angle.y moveAngle : -180 ~ +180 ↓
float difAngleY = angle.y * 180.0f / DX_PI_F + moveAngle;
imgIndex = (difAngleY < -135 || 135 < difAngleY) ? 0 // カメラ向きの画像
: (difAngleY < -45) ? 6 // 右 向きの画像
: (difAngleY > 45) ? 3 // 左 向きの画像
: 9; // 奥向きの画像
MyDraw::DrawDivRotaGraphF3D(MyDraw::Plane::Z, x, y, z, 1.0f, angle, *image, imgIndex + animIndex, TRUE);
#ifdef _DEBUG // デバッグのときだけ_DEBUGが定義され描かれる
if (auto pSphereCollider = sphereCollider.lock()) pSphereCollider->Draw(); // コライダを描く
if (auto pMapHitBox = mapHitBox.lock()) pMapHitBox->Draw(); // ヒットボックスを描く
#endif
});
}
}
Map.hを変更
して、アイテムをフィールドにFactoryで生成する処理を追加しましょう。
#ifndef MAP_H_
#define MAP_H_
#include <vector> // C#におけるListリストの機能
#include <unordered_map> // 辞書型連想配列に必要
#include <memory> // メモリを扱う【共有ポインタに必要】
#include <limits> //intやfloatなどの無限大∞に使う https://cpprefjp.github.io/reference/limits/numeric_limits/max.html
#include <fstream> // ファイル読み出しifstreamに必要
#include <string> //文字列に必要
#include <sstream> // 文字列ストリームに必要
#include <assert.h> // マップ読込み失敗表示用
#include "Screen.h"
#include "MyMath.h"
#include "MyHash.h"
#include "MyDraw.h"
#include "DataCsv.h"
#include "TMXJson.h"
#include "Singleton.h"
#include "GameObject.h"
#include "World.h"
#include "Camera.h"
#include "Factory.h"
#include "MapRange.h" // マス目のレンジ表現するクラス
#include "Tile.h"
#include "Vector3.h"
class AI_Map; // 前方宣言
// csvから読み取るマップの基本情報
struct MapInfo
{
MapInfo() = default;
inline MapInfo(unsigned int CellSize, int SpawnRangeX, int SpawnRangeY, MapRange::Type type = MapRange::Type::Rect)
: CellSize{ CellSize }, spawnRange{ MapRange(SpawnRangeX,SpawnRangeY,type) } {};
~MapInfo() {};
unsigned int CellSize{ 1 }; // マップの1マスのピクセル数
MapRange spawnRange; // 敵の新規出現レンジ
};
struct MapSetting // マップ情報をMapSetting.csvなどから読みだす
{
MapSetting(const std::string& csvFileName = "")
{
if (csvFileName != "")
Load(csvFileName);
}
~MapSetting() {}; //デストラクタ
std::string FilePath{ "" }; // csvのファイルパス
std::unordered_map<hash32, std::shared_ptr<MapInfo>> info; //[辞書]stageファイル名→マップ情報
MapInfo& operator[](const std::string& stageFilePath) {
assert(info.count(stageFilePath) != 0 && "マップのステージ情報が読み取れていません Map/MapSettings.csv にステージ情報があるか確認!");
return *info[stageFilePath]; // 読み書き
}
void Load(const std::string& csvFileName = "")
{
if (csvFileName != "") this->FilePath = csvFileName;
if (this->FilePath == "") return;
// マップ情報のデータ CSVファイルからデータをロード
DataCsv Data{ this->FilePath, DataCsv::CsvType::CsvValue };
std::string stageFile; //ステージファイル名
for (int j = 0; j < Data.size(); ++j)
{
if ((std::string)Data[j][0] == "//")
continue; // 一番左の列が//なら読み飛ばしのコメント行
else if ((std::string)Data[j][0] == "File")
{
stageFile = Data[j][1]; //ステージファイル名
info.emplace(stageFile, std::make_shared<MapInfo>());
continue;
}
if (stageFile == "") continue; //ステージファイル名が未確定
else if ((std::string)Data[j][0] == "CellSize")
info[stageFile]->CellSize = (int)Data[j][1]; //マス目サイズ
else if ((std::string)Data[j][0] == "SpawnRange")
{ // 敵出現レンジ
MapRange::Type rangeType = ((std::string)Data[j][3] == "Ellipse") ? MapRange::Type::Ellipse : MapRange::Type::Rect; //敵出現のタイプ
info[stageFile]->spawnRange.InitRange(rangeType, Data[j][1].intFromString(), Data[j][2].intFromString()); //敵出現のタイプを初期化設定
}
}
}
};
// ステージのタイルマップのクラス
class Map
{
public:
// コンストラクタ
Map(World* world, const std::string& filePath = "")
: m_world{ world }
{
if (filePath == "") return;
Load(filePath);
}
// デストラクタ
~Map()
{ // マップデータのお掃除
enemyData.clear();
//terrains.clear(); //★地形のお掃除
}
static MapSetting settings; // マップに関する設定 Map.cpp で初期読込み デフォルトは"Map/MapSetting.csv" を読み込む
static TileSetting tileSettings; // タイルに関する設定 Map.cpp で初期読込み デフォルトは"Map/MapSetting.csv" を読み込む
// タイルのチップ画像のパスとidからTileの情報とタイルのgid情報を得る
static Tile GetTile(const std::string& mapChipPath, int id, unsigned int& gid)
{
auto pos = mapChipPath.find(".", 0);
std::string tsxPath{ "" };
if (pos != std::string::npos) tsxPath = mapChipPath.substr(0, pos) + ".tsx";
auto found = Tilesets::umTsxInfo.find(tsxPath);
if (found != end(Tilesets::umTsxInfo) && found->second.gid.size() > id)
gid = found->second.gid[id]; // 辞書で id から gidに変換しておく
else return Tile(); // id が-1のときは見つからなかったとき
pos = tsxPath.find("/", 0);
if (pos != std::string::npos) tsxPath = tsxPath.substr(pos + 1, tsxPath.size() - 1);
return Map::tileSettings.info[tsxPath]->tiles[id];
}
protected:
World* m_world{ nullptr }; // マップに配置する敵などのポインタを保持するワールド
public:
// 現在いるワールドへのポインタを得る
World* world() { return m_world; }
// 現在いるワールドのポインタを変更する
void world(World* changeWorld) { m_world = changeWorld; }
std::shared_ptr<AI_Map> aiMap{ nullptr }; // AIの経路探索に使うAIマップ
// 読込んだデータファイルの情報
std::string FilePath{ "" };
protected:
unsigned int m_CellSize{ 1 }; // マップの1マスのピクセル数 1のときはtmxに設定されたサイズが採用される
unsigned int m_HalfCellSize{ 1 }; // マス目サイズの 1/2
public:
inline unsigned int CellSize() { return m_CellSize; }
inline unsigned int HalfCellSize() { return m_HalfCellSize; }
inline void SetCellSize(unsigned int cellSize) { m_CellSize = cellSize; m_HalfCellSize = m_CellSize / 2; }
MapRange spawnRange; // 敵の新規出現レンジをプレイヤから半径X,Yの楕円にする
inline float centerX(int cellX) const { return (cellX >= 0) ? (float)((cellX * m_CellSize) + m_HalfCellSize) : (float)((cellX + 1) * m_CellSize) - m_HalfCellSize; } //マス目サイズ/2ずらし
inline float centerY(int cellY) const { return (cellY >= 0) ? (float)((cellY * m_CellSize) + m_HalfCellSize) : (float)((cellY + 1) * m_CellSize) - m_HalfCellSize; } //マス目サイズ/2ずらし
inline float centerZ(int cellZ) const { return (cellZ >= 0) ? (float)((cellZ * m_CellSize) + m_HalfCellSize) : (float)((cellZ + 1) * m_CellSize) - m_HalfCellSize; } //マス目サイズ/2ずらし
inline int cellX(float worldX) const { return (worldX >= 0) ? (int)worldX / m_CellSize : (int)worldX / m_CellSize - 1; } // マイナス方向にも対応
inline int cellY(float worldY) const { return (worldY >= 0) ? (int)worldY / m_CellSize : (int)worldY / m_CellSize - 1; } // マイナス方向にも対応
inline int cellZ(float worldZ) const { return (worldZ >= 0) ? (int)worldZ / m_CellSize : (int)worldZ / m_CellSize - 1; } // マイナス方向にも対応
TMXCsv enemyData;// 敵配置データ
TMXJson tmxData; // タイルマップエディタのjsonデータ
protected:
// <gid→idに対応したタイプのbit> へ変換するvector型の辞書
std::vector<TileBit> gidTypeBits{ 0 };
public:
inline TileBit& gidTypeBit(unsigned int gid)
{
if (gid >= (int)gidTypeBits.size()) gidTypeBits.resize(gid + 1, 0);
return (gid < 0) ? gidTypeBits[0] : gidTypeBits[gid];
}
// id→ビットのタイプ の辞書に |= でbitTypeをビット論理和して、Wall:壁 や Bounce:はずむ などの特性を追加する
TileBit SetTypeBit(unsigned int gid, TileBit bitType) { return gidTypeBit(gid) |= bitType; }
// id→ビットのタイプ の辞書で & ビット論理積で 指定したidが Wall:壁 や Bounce:はずむ などの特性を持つかを判定する
inline bool isType(unsigned int gid, TileType bitType) { return gidTypeBit(gid) & bitType; }
// id→ビットのタイプ の辞書で & ビット論理積で 指定したidが Wall:壁 や Bounce:はずむ などの特性を持つかを判定し一致したBitを返す(複数が1のビットもありうる)
inline TileBit matchTypes(unsigned int gid, TileBit bitType) { return gidTypeBit(gid) & bitType; }
// tilesetsの.tsxのfirstgidをたどって、SetTypeBitでビット辞書に
// csvのtileSettingsから読み取っておいたビット設定を登録
bool InitTypeBit(const Tilesets& tilesets)
{
assert(tilesets.size() > 0 && "ステージのjsonファイルにタイルセット設定が一つもありません");
if (tilesets.size() == 0) return false;
for (auto& pair : tilesets)
{
auto firstgid = pair.first;
auto& tsxFilePath = pair.second;
auto& tsxInfo = tilesets.umTsxInfo.at(tsxFilePath);
// [親フォルダを抽出] https://qiita.com/takano_tak/items/acf34b4a30cb974bab65
size_t path_i = tsxFilePath.find_last_of("\\/");
if (path_i + 1 >= tsxFilePath.size()) continue;
auto tsxFile = tsxFilePath.substr(path_i + 1);
auto csvInfo = tileSettings.info.find(tsxFile);
if (csvInfo == end(tileSettings.info)) continue; // csv側にタイルの情報がなかった
// gidが0 idが-1 のデフォルトのタイルのビット特性を設定
SetTypeBit(0, csvInfo->second->idTypeBit(-1));
int id = 0; // gidじゃなく0から始まるローカルなid
for (auto gid = firstgid, iEnd = firstgid + tsxInfo.tilecount; gid < iEnd; ++gid)
{ // このMapのgidのビット辞書にcsvのtileSettingsから読み取っておいたビット設定を登録
SetTypeBit(gid, csvInfo->second->idTypeBit(id));
++id; // ローカルなidも次に進める
}
}
}
// マップファイルのロード
void Load(const std::string& filePath)
{
this->FilePath = filePath;
// ★jsonファイルの読み込み
tmxData.Load(filePath);
// tilesetsの.tsxのfirstgidをたどって、SetTypeBitでビット辞書に
// csvのtileSettingsから読み取っておいたビット設定を登録
InitTypeBit(tmxData.tilesets);
auto info = settings[filePath];
if (info.spawnRange.umRange.size() != 0)
spawnRange = info.spawnRange;
else
spawnRange.InitRange(MapRange::Type::Ellipse, 10, 10);// 敵の新規出現レンジをプレイヤから半径X,Yの楕円にする
if (info.CellSize != 1)
{
SetCellSize(info.CellSize); // csvに書かれているCellSizeを設定
tmxData.SetGridSize({(float)info.CellSize ,(float)info.CellSize ,(float)info.CellSize });
}
else // if (info.CellSize == 1)
SetCellSize(tmxData.tmxInfo.tilewidth); // tmxのタイルの幅を取得
}
// オブジェクトを Factoryクラスで className に応じて見きわめて 生成・配置する
template<typename TypeT>
inline std::shared_ptr<TypeT> SpawnTmxObject(const std::string& className, std::shared_ptr<InitParams> initParams, Vector3 spawnPos, Quaternion rotation = { 0,0,0,1 })
{
//IDに応じてFactory工場で作り分ける
return Factory::MakeSharedObject<TypeT>(className, initParams, m_world, spawnPos);
}
// 画面スクロール(位置の更新)
void Scroll3D(Vector3 prevPos, Vector3 delta, MapRange* pSpawnRange = nullptr)
{
std::vector<TMXobjects*> tmxObjectsList{ &tmxData.enemies,&tmxData.lights,&tmxData.items,&tmxData.objects };
for (auto& tmxObjects : tmxObjectsList)
{
if (tmxObjects->data.size() == 0) continue; // リストにデータが一つもなければスクロールせず次へ
tmxObjects->Scroll(prevPos, delta, (pSpawnRange != nullptr) ? *pSpawnRange : (tmxObjects->pSpawnRange != nullptr) ? *(tmxObjects->pSpawnRange) : spawnRange,
[&](int gridX, int gridY, int gridZ)
{
auto pObject = tmxObjects->top(gridX, gridY, gridZ); // グリッドからオブジェクトを取り出す
while (pObject != nullptr)
{
auto pObj = (TMXobject*)pObject;
//int spawnID = pObj->spawnID; // 生成する敵のID情報をTMXの jsonファイルのデータから読み取る
float ratioX = (float)tmxObjects->cellWidth / tmxData.tmxInfo.tilewidth;
float ratioY = (float)tmxObjects->cellHeight / tmxData.tmxInfo.tileheight; // タイルマップエディタとのスケールの違いの比率を計算
float ratioZ = (float)tmxObjects->cellDepth / tmxData.tmxInfo.tilewidth;
//クラス名とベースクラス名に応じて工場で作り分ける
if (pObj->baseType == "Enemy")
m_world->enemies.emplace_back(SpawnTmxObject<Enemy>(pObj->type, pObj->GetInitProperty(), Vector3(pObj->x * ratioX, pObj->z * ratioZ, pObj->y * ratioY)));
//if (pObj->baseType == "Light")
// m_world->lights.emplace_back(SpawnTmxObject<Light>(pObj->type, Vector3(pObj->x * ratioX, pObj->z * ratioZ, pObj->y * ratioY)));
if (pObj->baseType == "Item")
m_world->items.emplace_back(SpawnTmxObject<Item>(pObj->type, pObj->GetInitProperty(), Vector3(pObj->x * ratioX, pObj->z * ratioZ, pObj->y * ratioY)));
//if (pObj->baseType == "Object")
// m_world->objects.emplace_back(SpawnTmxObject<Object>(pObj->type, Vector3(pObj->x * ratioX, pObj->z * ratioZ, pObj->y * ratioY)));
//m_world->enemies.emplace_back(Factory::MakeSharedTmxObject(spawnID,m_world, Vector3(pEnemy->x * ratioX, pEnemy->z * ratioZ, pEnemy->y * ratioY)));
pObject = pObject->m_pNext; // 同じグリッドの次のオブジェクトへ
tmxObjects->Erase(pObj); // tmxObjectsのdataとグリッドからデータを消して再びスポーンしないようにする
}
// このグリッドには何もいなくなるのでマス目自体eraseして高速化
tmxObjects->cellData.erase(CellXYZ(gridX, gridY, gridZ));
});
}
}
// 指定された座標(ワールド座標)の地形データを取得する
unsigned int GetTerrain(float worldX, float worldY, float worldZ = (std::numeric_limits<float>::min)())
{
if (worldZ != (std::numeric_limits<float>::min)())
{
float tmpY = worldY;
worldY = worldZ; //【YとZを変換】Zの入力があるときはZをYとして扱う
worldZ = tmpY; //【YとZを変換】
}
// 負の座標が指定された場合は、何も無いものとして扱う
if (worldX < 0 || worldY < 0)
return Tile::None;
// マップ座標系(二次元配列の行と列)に変換する
int mapX = (int)(worldX / m_CellSize);
int mapY = (int)(worldY / m_CellSize);
int mapZ = (int)(worldZ / m_CellSize);
if (tmxData.terrains.count(mapZ) == 0) // zの階層がまだなかったとき
return Tile::None; // 配列の範囲外は、何も無いものとして扱う
auto& terrainZ = tmxData.terrains[mapZ]; // 辞書からZに対応するデータへの参照を得る
if (mapY >= terrainZ.size() || mapX >= terrainZ[mapY].size())
return Tile::None; // 配列の範囲外は、何も無いものとして扱う
return terrainZ[mapY][mapX]; // 二次元配列から地形IDを取り出して返却する
}
// 指定された座標(ワールド座標)の地形データを書き換える return には書き換える前のgidが返る
bool SetTerrain(unsigned int gid, float worldX, float worldY, float worldZ = (std::numeric_limits<float>::min)())
{
if (worldZ != (std::numeric_limits<float>::min)())
{
float tmpY = worldY;
worldY = worldZ; //【YとZを変換】Zの入力があるときはZをYとして扱う
worldZ = tmpY; //【YとZを変換】
}
// 負の座標が指定された場合は、書き換えられない
if (worldX < 0 || worldY < 0)
return false;
// マップ座標系(二次元配列の行と列)に変換する
int mapX = (int)(worldX / m_CellSize);
int mapY = (int)(worldY / m_CellSize);
int mapZ = (int)(worldZ / m_CellSize);
// 二次元配列の範囲外は、書き換えられない
//if (mapY >= terrain.size() || mapX >= terrain[mapY].size())
// return;
bool isNewZ = false;
if (tmxData.terrains.count(mapZ) == 0) isNewZ = true; // 新しい z の階層の場合
auto& terrainZ = tmxData.terrains[mapZ]; // 辞書からZに対応するデータへの参照を得る
if (isNewZ) // 新しい z の階層の場合
{
terrainZ.z = mapZ;
terrainZ.tilesets = &(tmxData.tilesets); // 新しい z の階層のtilesetsを設定する
terrainZ.Width = tmxData.tmxInfo.width; terrainZ.Height = tmxData.tmxInfo.height;
auto& terrinZYX = terrainZ[mapY][mapX];
terrinZYX.tilesets = &(tmxData.tilesets);
terrinZYX = gid; // 地形データ書き換え
}
else
terrainZ[mapY][mapX] = gid; // 地形データ書き換え
return true;
}
// gidに応じてタイルを描き分ける
void DrawTile(unsigned int gid, float worldX, float worldY, float worldZ, float ExRate)
{
auto texture = TsxInfo::id_images[gid].second.lock();
int id = TsxInfo::id_images[gid].first;
if (isType(gid, TileType::Floor)) // 板ポリゴンで床を描く
MyDraw::DrawDivRotaFloorF3D(MyDraw::Plane::Y, worldX, worldY, worldZ, ExRate, 0, *texture, id);
else if (isType(gid, TileType::Wall)) //壁のときはボックスを描く
MyDraw::DrawDivBoxF3D(worldX, worldY, worldZ, ExRate, *texture, id);
}
// 地形をタイルで描く
void DrawTerrain()
{
for (auto&& terrainN : tmxData.terrains)
{
auto& terrainZ = *terrainN.second;
int cellZ = terrainN.first;
for (int cellY = 0, ySize = terrainZ.size(); cellY < ySize; cellY++)
{
for (int cellX = 0, xSize = terrainZ[cellY].size(); cellX < ySize; cellX++)
{
float x = centerX(cellX); //マス目サイズ/2ずらし
float y = centerY(cellY); //マス目サイズ/2ずらし
float z = centerZ(cellZ); //マス目サイズ/2ずらし
int id = -1;
unsigned int gid = 0;
if (cellY < terrainZ.size() && cellX < terrainZ[cellY].size())
{
gid = terrainZ[cellY][cellX];
}
if (0 <= gid && gid < TsxInfo::id_images.size())
{
auto pTileImage = TsxInfo::id_images[gid].second.lock();
if (pTileImage == nullptr) continue;
// ワールドに存在するすべてのカメラに関連づいたスクリーンに対して描画する
for (auto&& keyValue : world()->cameras) // ワールドにあるすべてのカメラぶんループ
{
auto& camera = keyValue.second;
//auto stepXYZs = MyMath::CrossGridVec(camera->pos - camera->m_targetXYZ, Vector3{ CellSize, CellSize, CellSize });
camera->Draw([&]()
{
float ExRate = (float)m_CellSize / (float)pTileImage->m_XSize;
DrawTile(gid, x, z, y, ExRate); // idに応じてタイルを描き分ける
});
}
}
}
}
}
}
// 指定された座標(ワールド座標)の地形が壁か調べる
inline bool IsWall(float worldX, float worldY, float worldZ = (std::numeric_limits<float>::min)())
{
auto terrainID = GetTerrain(worldX, worldY, worldZ); // 指定された座標の地形のIDを取得
return IsWall(terrainID);
}
//あるIDが壁かどうかだけ調べる
inline bool IsWall(unsigned int gid)
{ // 地形が壁ならtrue、違うならfalseを返却する
return isType(gid, TileType::Wall);
}
// 指定された座標worldPosの地形が 壁 や ダメージブロックかなどをチェックする関数
bool checkID(TileBit checkBit, float worldX, float worldY, float worldZ,
int* pMapX = nullptr, int* pMapY = nullptr, int* pMapZ = nullptr, unsigned int* pTerrainID = nullptr, TileBit* pMatchBits = nullptr)
{
unsigned int terrainID; // 指定された座標の地形のID
if (worldZ != (std::numeric_limits<float>::min)())
{ //【YとZを変換】Zの入力があるときはZをYとして扱う タイルマップの X,Yは3DではYがZ:奥行に対応するから
float tmpY = worldY;
worldY = worldZ;
worldZ = tmpY; //【YとZを変換】
}
if (worldX < 0 || worldY < 0) return false; // 負の座標が指定された場合
// マップ座標系(二次元配列の行と列)に変換する
int mapX = cellX(worldX), mapY = cellY(worldY), mapZ = cellZ(worldZ);
if (tmxData.terrains.count(mapZ) == 0) // zの階層がまだなかったときは 0
terrainID = 0; // 範囲外のときは 0
else // zの階層がすでにあったとき
{ // 二次元配列の範囲内か判定
auto& terrainZ = tmxData.terrains[mapZ];
if (mapY >= terrainZ.size() || mapX >= terrainZ[mapY].size())
terrainID = 0; // 範囲外のときは 0
else
terrainID = terrainZ[mapY][mapX]; // 配列から地形IDを取り出す
}
if (pTerrainID != nullptr)
*pTerrainID = terrainID;
TileBit matchBits = matchTypes(terrainID, checkBit); // ビット論理積 & で地形のchekckBitを確かめる
bool isCheckBit = matchBits != (TileBit)TileType::None;
if (isCheckBit && pMapX != nullptr && pMapY != nullptr && pMapZ != nullptr)
*pMapX = mapX, * pMapY = mapY, * pMapZ = mapZ; // ブロックのマス目のセル番号をポインタに記録(nullptrの場合は記録しない)
if (pMatchBits != nullptr)
*pMatchBits = matchBits; // マッチしたビットをポインタ先に代入して返す
return isCheckBit;
}
};
#endif
GameScene.cppを変更
して、アイテムとUIのアイコンを更新して、描く処理を追加しましょう。
#include "GameScene.h"
#include "Player.h"
#include "Enemy.h"
#include "Bullet.h"
#include "Item.h"
#include "ItemBlock.h"
#include "ItemBullet.h"
#include "Map.h"
GameSceneSetting GameScene::settings{ "Map/GameSceneSetting.csv" }; // ゲームシーンの設定情報
// 指定したゲームシーンのタグに関連づいたファイル名に対応したワールドを初期化する
bool GameScene::LoadWorlds(const std::string& gameSceneTag)
{
if (settings.info.count(gameSceneTag) == 0)
return false;
auto& info = settings[gameSceneTag];
for (auto&& tag_mapPath : info)
{
CreateWorld(tag_mapPath.second.tag); // ワールドをタグをつけて生成
// ワールドのマップをロード
worlds[tag_mapPath.second.tag]->LoadMap(tag_mapPath.second.mapFilePath);
}
return true;
}
void GameScene::ChangeWorld(const std::string& worldTag, std::shared_ptr<Player> pPlayer)
{
if (pPlayer == nullptr) return;
if (worlds.count(worldTag) == 0) return; // 存在しないタグだった
pPlayer->world(worlds[worldTag].get()); // ワールドへのリンクをすげ替える
}
void GameScene::Initialize()
{// Init処理
// Zバッファを有効にする [Zの深度]
//[参考]https://docs.google.com/presentation/d/1Z23t1yAS7uzPDVakgW_M02p20qt9DeTs4ku4IiMDLco/edit?usp=sharing
//[参考] https://dxlib.xsrv.jp/function/dxfunc_3d_draw.html#R14N12
DxLib::SetUseZBuffer3D(TRUE);
// Zバッファへの書き込みを有効にする
DxLib::SetWriteZBuffer3D(TRUE);
// 光の明暗計算を無効に
DxLib::SetUseLighting(FALSE);
// SetDrawScreen に引数として渡せる( 描画対象として使用できる )グラフィックハンドルを作成するかどうかを設定する
// ( TRUE:描画可能グラフィックハンドルを作成する FLASE:通常のグラフィックハンドルを作成する( デフォルト ) )
DxLib::SetDrawValidGraphCreateFlag(TRUE);
// サブのスクリーンを2つ生成 画面の全体の縦方向サイズの Height / 2 にして、上下分割で配置できるようにする
screenHandle0 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
screenHandle1 = DxLib::MakeScreen(Screen::Width, Screen::Height / 2, FALSE);
// スクリーンを作成したらデフォルト設定に戻しておかないと
DxLib::SetDrawValidGraphCreateFlag(FALSE);
Resource::MakeShared<Texture>(playerImgPath1, 3, 4, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
Resource::MakeShared<Texture>(playerImgPath2, 3, 4, 32, 32)->Load(); // メソッドチェーン -> でMakeShared読込後にLoadもする
Resource::MakeShared<Texture>("Image/zako0.png")->Load(); // 敵画像を読込
Resource::MakeShared<Texture>("Image/skullman.png", 3, 4, 32, 32)->LoadDivGraph(); // 敵画像を読込
Resource::MakeShared<Texture>("Image/yen1.png")->Load(); // お金の画像を読込
Resource::MakeShared<Texture>("Image/yen10.png")->Load(); // お金の画像を読込
Resource::MakeShared<Texture>("Image/bullet.png")->Load(); // 弾の画像を読込
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
// プレイヤの生成
if (pWorld != nullptr)
{
//pWorld->AddCamera("カメラ1", Camera::defaultCamera); // カメラ1というタグをデフォルトカメラにつけてワールドにデフォルトカメラを配置
pWorld->AddCamera("カメラ1", std::make_shared<Camera>(pWorld, Vector3{ 0,0,0 }));
auto pCamera1 = pWorld->camera("カメラ1"); // カメラ1 というタグのついたカメラへの共有ポインタを得る
pCamera1->SetScreenHandle(screenHandle0); // カメラから見える3Dを描く スクリーンのハンドル を関連付ける
auto pPlayer1 = Object::Create<Player>(nullptr, pWorld, Pad::Key, Vector3(90 + 256, 32, 95 + 256));
pWorld->AddPlayer("プレイヤ1", pPlayer1); // シーンにプレイヤを追加する
pPlayer1->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath1]);
pPlayer1->camera = pCamera1.get(); // カメラをプレイヤにリンクする
pPlayer1->mass = 100.0f; // 体重を100にして衝突されてもびくともしないようにする
pWorld->AddCamera("カメラ2", std::make_shared<Camera>(pWorld, Vector3{ 0,0,0 }));
auto pCamera2 = pWorld->camera("カメラ2"); // プレイヤ2 というタグのついたカメラへの共有ポインタを得る
pCamera2->SetScreenHandle(screenHandle1); // カメラから見える3Dを描く スクリーンのハンドル を関連付ける
auto pPlayer2 = Object::Create<Player>(nullptr, pWorld, Pad::Two, Vector3(190, 32, 195));
pWorld->AddPlayer("プレイヤ2", pPlayer2); // シーンにプレイヤを追加する
pPlayer2->image = std::dynamic_pointer_cast<Texture>(Resource::files[playerImgPath2]);
pPlayer2->camera = pCamera2.get(); // カメラをプレイヤにリンクする
pPlayer2->mass = 1.0f; // 体重を1にして普通にふっとぶようにする
}
};
void GameScene::Update()
{// 更新処理
Screen::ClearDrawScreen(screenHandle0); // 一旦キャンバスをきれいにまっさらに
Screen::ClearDrawScreen(screenHandle1); // 一旦キャンバスをきれいにまっさらに
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
for (const auto& player : players)
player.second->Update(); // プレイヤの更新【忘れるとプレイヤが動かない】
// 弾の更新処理
for (const auto& world : worlds)
for (const auto& b : world.second->bullets)
b->Update();
// 敵の更新処理
for (const auto& world : worlds)
for (const auto& e : world.second->enemies)
e->Update();
// アイテムの更新処理
for (const auto& world : worlds)
for (const auto& item : world.second->items)
item->Update();
// グリッドを更新して隣接するグリッドに含まれる敵や弾の当たり判定を行う
for (const auto& world : worlds)
if (world.second->grid() != nullptr)
for (const auto& cell : world.second->grid()->cellData)
{
auto pGridObject = cell.second;
while (pGridObject != nullptr)
{ // while文で数珠つなぎのm_pNextを次々とたどって最後にたどり着くと次がなくてnullptrになる
pGridObject->checkCell();
pGridObject = pGridObject->m_pNext; // リンクリスト構造を次々とたどる
}
}
// 敵のリストから死んでるものを除去する
for (auto&& world : worlds)
world.second->EraseRemoveIf(world.second->enemies,
[](std::shared_ptr<Enemy>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// 自機弾の削除処理
for (auto&& world : worlds)
world.second->EraseRemoveIf(world.second->bullets,
[](std::shared_ptr<Bullet>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
// アイテムのリストから死んでるものを除去する
for (auto&& world : worlds)
world.second->EraseRemoveIf(world.second->items,
[](std::shared_ptr<Item>& ptr) { return ptr->isDead; });//isDead==trueの条件のものを削除
}
void GameScene::Draw()
{// 描画処理
auto pWorld = world("ステージ1"); // ワールドへのリンクを得る
std::unordered_set<World*> drawEndSet; // すでに描いたWorldをポインタのsetでかぶらないように判定する
for (const auto& player : players)
{
player.second->Draw(); // プレイヤの描画【忘れるとプレイヤ表示されない】
auto world = player.second->world(); // プレイヤのいるワールド
if (drawEndSet.count(world) > 0) continue; // すでに描いたWorldは描かずスキップする
drawEndSet.emplace(world); // 描いたWorldのポインタ住所をsetに登録する
world->map->DrawTerrain(); // マップの描画
// 敵の描画
for (const auto& enemy : world->enemies)
{
enemy->Draw();
#ifdef _DEBUG
//enemy->DrawHitBox(); // 当たり判定の描画
#endif
}
// 弾の描画処理
for (const auto& bullet : world->bullets)
bullet->Draw();
// アイテムの描画処理
for (const auto& item : world->items)
item->Draw();
#ifdef _DEBUG // デバッグのときだけ_DEBUGが定義され描画される
if (world->grid() != nullptr)
for (const auto& cell : world->grid()->cellData)
cell.second->Draw(); // グリッドの描画
#endif
}
// UIの描画処理はDrawとは別にすることで、他のマップなどに塗りつぶされずに一番上のレイヤでスクリーンに上書きで描ける
for (const auto& player : players)
player.second->DrawUI();
// スクリーン0の内容を画面に描く
DxLib::DrawGraph(0, 0, screenHandle0, FALSE);
DxLib::DrawLineBox(0, 0, Screen::Width, Screen::Height / 2, GetColor(255, 0, 0));
// スクリーン1の内容を画面に描く
DxLib::DrawGraph(0, Screen::Height / 2, screenHandle1, FALSE);
DxLib::DrawLineBox(0, Screen::Height / 2, Screen::Width, Screen::Height, GetColor(0, 255, 0));
}