DXライブラリでゲームの基礎を作ろう3 位置角度編

#大昔からの与太話

前回、いつも使ってるのに何であんなこと書いたんだろう…それはさておき与太話。

const参照、詳細を話すと長くなりますがとても便利です。

普通の値渡し(int aとか)だと無駄にコピーがとられることになります。
そこでC言語ジャンキーならコピーを回避するためにポインタを使うかもしれません。しかしC++はその一歩上を行きました。参照です。

参照はポインタのように&や*でポインタと実体との変換を行う必要はありませんし、ポインタのように実体の変更を参照元に反映できます。
変更が反映されないように参照したい…という場合でもconstを付ければOK。

関数の引数はconst参照をバンバン使っていきましょう！

#はじめに

今回は3Dの座標(VECTOR,MATRIX)について解説します。表現の幅を広げる大事な箇所ですね。

#3.3Dの位置回転を制御しよう

3Dで作成したオブジェクトの位置回転を制御する方法を追加します。

##VECTORとは
3D空間では、XYZの三つの軸をもとにその軸方向にどれだけ離れているかを示すことで位置を表すことができます。
DXライブラリで設定されているVECTORはXYZのfloatデータを持った構造体となります。

##VECTORの演算
さて、空間座標などの名目で３次元の演算を高校で習うかもしれませんので、基本的な内容をDXライブラリに沿っておさらいしましょう。

###VGet
VECTORの中身を指定してVECTORを返します。
###VAdd
VECTORどうしの足し算をします。イメージとしては元のベクトルの先に足したベクトルが付くような感じですね。
###VSub
VECTORどうしの引き算をします。
###VDot
VECTORどうしの内積をします。cosを取得するので、正負を見ることで元のベクトルに対し前方(９０度の範囲)を向いているかいないかがわかります。
###VCross
VECTORどうしの外積をします。sinを取得するので、(ちょっと工夫すると)正負を見ることで元のベクトルに対し左右どちらを向いているかの判別に使えます。
###VScale
VECTORのスケーリングをします。
###VSize
VECTORのサイズを取得します。スカラー。
###VNorm
VECTORのサイズを1にします。サイズがもともと0のときは…？

##…これめんどくさくない？
そうですね、足し算をするのに

```cpp
vect1 = VAdd(vect1,vect2);
```

みたいなことをするより

```cpp
vect1 += vect2;
```

とできる方がいいですよね。わかりやすいし数学っぽいです。

##オペレーター3分クッキング
そこで使うのがoperater!c++の便利な機能です。
こちらはclassの関数として追加することで、演算子を用いた処理を簡単に追加することができます。

例として

```cpp
class intel {
	int data = 0;//hansei
public:
	int operator+(const intel& tgt) {//+を使った際にする処理を指定
		return this->data + tgt.data;
	}
};
const intel add(const intel& a, const intel& b) {
	return a + b;
}
```

こんな感じで、例では+を用いた場合の演算を決めています。

ではこれを用いたVECTORのclassを作りましょう。**完成品はこちら！**

https://github.com/yabushita0312/TankFlanker

…実は前回使用したDXLib_vec.hppの中に**VECTOR_ref**としてあったんですね。
詳細はVECTOR_refクラスを実際に見てください。

###operator==
xyz値がすべて等しいかを返します。float同士で比較するため原点などでない限り意味はほぼないと思われます。
###operator!=
==の逆です。
###operator+、operator+=
足し算
###operator-、operator-=
引き算
###operator*、operator*=
n倍のスケーリングをします
###operator/、operator/=
1/nのスケーリングをします
###cross
外積をします
###dot
内積をします
###Norm
サイズを1に
###clear
中身を原点にします。
###size
サイズ取得
###get()
VECTORを返します。DXライブラリ本体に渡す際に使います。
###x()、y()、z()
xyzそれぞれの値を見ます
###vget
VGetのようにVECTOR_refを返します

##前回指定したエフェクトの位置を変更してみよう
前回のエフェクトは位置を固定していましたが、今回は見回せるようにしてみましょう。ついでにVECTORで示していた部分をVECTOR_refに直します。

```cpp
//エフェクト
class EffectS {
public:
	bool flug = false;					/*エフェクトの表示フラグ*/
	size_t id = 0;						/*エフェクトのID*/
	Effekseer3DPlayingHandle handle;	/*エフェクトのハンドル*/
	VECTOR_ref pos;							/*エフェクトの場所*/
	VECTOR_ref nor;							/*エフェクトのY軸の向き*/
	float scale = 1.f;					/*エフェクトのスケール*/
	/*エフェクトの場所を指定し開始フラグを立てる*/
	void set(const VECTOR_ref& pos_, const VECTOR_ref& nor_, float scale_ = 1.f) {
		this->flug = true;
		this->pos = pos_;
		this->nor = nor_;
		this->scale = scale_;
	}
	/*エフェクトの開始フラグが立っていればエフェクトを再生*/
	void put(const EffekseerEffectHandle& handle_) {
		if (this->flug) {
			//再生チェック
			if (this->handle.IsPlaying()) {
				this->handle.Stop();
			}
			//再生
			this->handle = handle_.Play3D();
			//場所を指定
			this->handle.SetPos(this->pos);
			//向きを指定
			this->handle.SetRotation(atan2(this->nor.y(), std::hypotf(this->nor.x(), this->nor.z())), atan2(-this->nor.x(), -this->nor.z()), 0);
			//大きさを指定
			this->handle.SetScale(this->scale);
			//開始フラグを切る
			this->flug = false;
		}
	}
};
//いろいろまとめるクラス
class DXDraw {

public:
	//コンストラクタ
	DXDraw() {
		SetOutApplicationLogValidFlag(FALSE);				/*log*/
		SetMainWindowText("game title");					/*タイトル*/
		ChangeWindowMode(TRUE);								/*窓表示*/
		SetUseDirect3DVersion(DX_DIRECT3D_11);				/*directX ver*/
		SetGraphMode(640, 480, 32);							/*解像度*/
		SetUseDirectInputFlag(TRUE);						/*DirectInput使用*/
		SetDirectInputMouseMode(TRUE);						/*DirectInputマウス使用*/
		SetWindowSizeChangeEnableFlag(FALSE, FALSE);		/*ウインドウサイズを手動変更不可、ウインドウサイズに合わせて拡大もしないようにする*/
		SetUsePixelLighting(TRUE);							/*ピクセルライティングの使用*/
		SetFullSceneAntiAliasingMode(4, 2);				    /*アンチエイリアス*/
		SetEnableXAudioFlag(TRUE);							/*XAudioを用いるか*/
		Set3DSoundOneMetre(1.0f);						    /*3Dオーディオの基準距離指定*/
		SetWaitVSyncFlag(TRUE);							    /*垂直同期*/
		DxLib_Init();									    /*ＤＸライブラリ初期化処理*/
		Effekseer_Init(8000);								/*Effekseerの初期化*/
		SetSysCommandOffFlag(TRUE);							/*タスクスイッチを有効にするかどうかを設定する*/
		SetChangeScreenModeGraphicsSystemResetFlag(FALSE);	/*Effekseer用*/
		Effekseer_SetGraphicsDeviceLostCallbackFunctions();	/*Effekseer用*/
		SetAlwaysRunFlag(TRUE);								/*background*/
		SetUseZBuffer3D(TRUE);								/*zbufuse*/
		SetWriteZBuffer3D(TRUE);						    /*zbufwrite*/
		MV1SetLoadModelPhysicsWorldGravity(-9.8f);		    /*重力*/
		SetWindowSize(640, 480);							/*表示するウィンドウサイズ*/
	}
	//デストラクタ
	~DXDraw() {
		Effkseer_End();	/*effkseer終了*/
		DxLib_End();    /*ＤＸライブラリ使用の終了処理*/
	}
	//裏画面のセッティング
	void SetDraw_Screen(const bool& clear = true) {
		SetDrawScreen(DX_SCREEN_BACK);
		if (clear) {
			ClearDrawScreen();
		}
	}
	//裏画面のカメラ情報付きセッティング
	void SetDraw_Screen(const VECTOR_ref& campos, const VECTOR_ref& camvec, const VECTOR_ref& camup, const float& fov, const float& near_, const float& far_) {
		SetDraw_Screen(true);
		SetCameraNearFar(near_, far_);
		SetupCamera_Perspective(fov);
		SetCameraPositionAndTargetAndUpVec(campos.get(), camvec.get(), camup.get());
	}
	//
};

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	//コピーしてはいけない場合unique_ptrを使おう
	auto draw = std::make_unique<DXDraw>();
	//
	std::vector<EffekseerEffectHandle> effHndle; /*エフェクトリソース*/
	std::vector<EffectS> effcs;//メインループで使うエフェクト
	//事前用意
	//エフェクトファイルを探す
	effHndle.resize(effHndle.size() + 1);
	effHndle.back() = EffekseerEffectHandle::load("data/effect/0.efk");

	effcs.resize(effHndle.size());//エフェクトのセットアップ
	//エフェクトの位置
	VECTOR_ref effect_pos = VECTOR_ref::vget(0.f, 0.f, 0.f);
	//カメラの注視点
	VECTOR_ref camera_target_pos = effect_pos;
	//カメラの位置
	VECTOR_ref camera_pos = effect_pos;
	//演算用の値
	float x = 0.f;
	float z = 0.f;
	float rad = 0.f;
	float size = 10.f;
	//メインループ
	while (ProcessMessage() == 0) {
		/*演算*/
		{
			//スペースを押しているときエフェクトを設定する
			if (CheckHitKey(KEY_INPUT_SPACE) != 0) {
				effcs[0].set(effect_pos, VGet(0.f, 1.f, 0.f), 0.1f);
			}
			if (CheckHitKey(KEY_INPUT_A) != 0) {
				rad += DX_PI_F / 180 * 6 * 60 / GetFPS();
			}
			if (CheckHitKey(KEY_INPUT_D) != 0) {
				rad -= DX_PI_F / 180 * 6 * 60 / GetFPS();
			}
		}
		//エフェクトの更新
		{
			for (size_t index = 0;index < effHndle.size();++index) {
				effcs[index].put(effHndle[index]);
			}
		}
		/*描画*/
		{
			//3D(主描画)
			{
				x = cos(rad) * size;
				z = sin(rad) * size;
				camera_target_pos = effect_pos;//注視点はエフェクトの位置
				camera_pos = camera_target_pos + VECTOR_ref::vget(x, 0.f, z);
			}
			draw->SetDraw_Screen(camera_pos, camera_target_pos, VGet(0, 1, 0), DX_PI_F / 4, 0.5f, 20.f);
			Effekseer_Sync3DSetting();
			{
				UpdateEffekseer3D();
				//draw3D
				DrawEffekseer3D();
			}
			//2D(UI:ユーザーインターフェース)
			//draw->SetDraw_Screen(false);
			{
				DrawPixel(320, 240, GetColor(255, 255, 255));   // 点を打つ
			}
		}
		ScreenFlip();
		//
		if (CheckHitKey(KEY_INPUT_ESCAPE) != 0) {
			break;
		}
	}
	return 0;// ソフトの終了 
}
```

ADキーを押すことでエフェクトの周りをまわるようにカメラが動きます。
まだ恩恵は大きくないですが、反射などを実装するときに涙が出るほど楽になると思います。
では続いてMATRIXの説明に移りましょう。

##MATRIXとは

MATRIXとは、4x4の行列を取り扱う構造体です。4元数などとも言われますね。
行列については「数や記号や式などを縦と横に矩形状に配列したものである。」(Wikipediaママ)ということで各自で調べてもらうとして、
MATRIXを使うことで角度や位置をすべて確保し、制限(ジンバルロックなど)にとらわれず自在に回転させることすらできるようになります。
自分は位置は別途VECTOR_refで持ち、回転の演算ではMATRIXを使っています。

##こいつもオペレーターを使おう
はい、DXLib_vecの中に**MATRIX_ref**があります。そちらが完成品です。

###operator+、operator+=
行列どうしの足し算です。
###operator*、operator*=
行列どうしの掛け算です。行列の場合掛ける順序で結果が変化する点に注意しましょう
###Scale
スケーリングをします
###Inverse
逆行列を出します。元の行列 * 元の行列.Inverse()=単位行列となります。
###clear
単位行列にリセットします
###get
MATRIXを返します。
###Axis1、Axis2
指定したxyzベクトルを持ったMATRIX_refを返します。位置を指定することもできます
###Axis1_YZ
AXis1はYZだけあれば後は外積でX軸が作れるため、Xの入力を省いたものです。
###RotX、RotY、RotZ
それぞれの軸で回転するMATRIX_refを出します
###RotAxis
任意の軸で回転するMATRIX_refを出します
###RotVec2
あるベクトルからあるベクトルへ向くためのMATRIX_refを出します
###GetScale
指定したVECTOR_refぶんの拡大成分を持つMATRIX_refを出します
###Mtrans
指定したVECTOR_refぶんの移動成分を持つMATRIX_refを出します
###Vtrans
VECTOR_refをMATRIX_refで動かしたものを出力します
###pos
MATRIX_refの持つ位置成分のVECTOR_refを出します
###xvec、yvec、zvec
位置成分を無視したそれぞれの軸のVECTOR_refを出します。


##演算してみよう

さて、先ほどはこんなことをしてカメラの位置を求めていましたね。

```cpp
x = cos(rad) * size;
z = sin(rad) * size;
camera_target_pos = effect_pos;//注視点はエフェクトの位置
camera_pos = camera_target_pos + VECTOR_ref::vget(x, 0.f, z);
```
わざわざcos、sinで演算するのもいいのですが、MATRIX(_ref)でこの通り

```cpp
camera_target_pos = effect_pos;//注視点はエフェクトの位置
camera_pos = camera_target_pos + (MATRIX_ref::RotY(rad).zvec() * size);
```

RotYはY軸回転を行う行列演算です。これでradぶんの回転を取得ます。
zvec()を用いると、z軸向きのサイズ1のベクトルにMATRIXの持つ回転を付与したベクトルを取得できます。
それにVECTOR_refのスケーリング(operator*に付与しています)を用いると完成です。


##今回の全体像
今回書いたコードの全体です。

```cpp
#include "DxLib.h"
#include"EffekseerForDXLib.h"
#include<memory>
#include <array>
#include <vector>
#include <cmath>

#include"DXLib_ref/DXLib_vec.hpp"
#include"DXLib_ref/EffekseerEffectHandle.hpp"

//エフェクト
class EffectS {
public:
	bool flug = false;					/*エフェクトの表示フラグ*/
	size_t id = 0;						/*エフェクトのID*/
	Effekseer3DPlayingHandle handle;	/*エフェクトのハンドル*/
	VECTOR_ref pos;							/*エフェクトの場所*/
	VECTOR_ref nor;							/*エフェクトのY軸の向き*/
	float scale = 1.f;					/*エフェクトのスケール*/
	/*エフェクトの場所を指定し開始フラグを立てる*/
	void set(const VECTOR_ref& pos_, const VECTOR_ref& nor_, float scale_ = 1.f) {
		this->flug = true;
		this->pos = pos_;
		this->nor = nor_;
		this->scale = scale_;
	}
	/*エフェクトの開始フラグが立っていればエフェクトを再生*/
	void put(const EffekseerEffectHandle& handle_) {
		if (this->flug) {
			//再生チェック
			if (this->handle.IsPlaying()) {
				this->handle.Stop();
			}
			//再生
			this->handle = handle_.Play3D();
			//場所を指定
			this->handle.SetPos(this->pos);
			//向きを指定
			this->handle.SetRotation(atan2(this->nor.y(), std::hypotf(this->nor.x(), this->nor.z())), atan2(-this->nor.x(), -this->nor.z()), 0);
			//大きさを指定
			this->handle.SetScale(this->scale);
			//開始フラグを切る
			this->flug = false;
		}
	}
};
//いろいろまとめるクラス
class DXDraw {

public:
	//コンストラクタ
	DXDraw() {
		SetOutApplicationLogValidFlag(FALSE);				/*log*/
		SetMainWindowText("game title");					/*タイトル*/
		ChangeWindowMode(TRUE);								/*窓表示*/
		SetUseDirect3DVersion(DX_DIRECT3D_11);				/*directX ver*/
		SetGraphMode(640, 480, 32);							/*解像度*/
		SetUseDirectInputFlag(TRUE);						/*DirectInput使用*/
		SetDirectInputMouseMode(TRUE);						/*DirectInputマウス使用*/
		SetWindowSizeChangeEnableFlag(FALSE, FALSE);		/*ウインドウサイズを手動変更不可、ウインドウサイズに合わせて拡大もしないようにする*/
		SetUsePixelLighting(TRUE);							/*ピクセルライティングの使用*/
		SetFullSceneAntiAliasingMode(4, 2);				    /*アンチエイリアス*/
		SetEnableXAudioFlag(TRUE);							/*XAudioを用いるか*/
		Set3DSoundOneMetre(1.0f);						    /*3Dオーディオの基準距離指定*/
		SetWaitVSyncFlag(TRUE);							    /*垂直同期*/
		DxLib_Init();									    /*ＤＸライブラリ初期化処理*/
		Effekseer_Init(8000);								/*Effekseerの初期化*/
		SetSysCommandOffFlag(TRUE);							/*タスクスイッチを有効にするかどうかを設定する*/
		SetChangeScreenModeGraphicsSystemResetFlag(FALSE);	/*Effekseer用*/
		Effekseer_SetGraphicsDeviceLostCallbackFunctions();	/*Effekseer用*/
		SetAlwaysRunFlag(TRUE);								/*background*/
		SetUseZBuffer3D(TRUE);								/*zbufuse*/
		SetWriteZBuffer3D(TRUE);						    /*zbufwrite*/
		MV1SetLoadModelPhysicsWorldGravity(-9.8f);		    /*重力*/
		SetWindowSize(640, 480);							/*表示するウィンドウサイズ*/
	}
	//デストラクタ
	~DXDraw() {
		Effkseer_End();	/*effkseer終了*/
		DxLib_End();    /*ＤＸライブラリ使用の終了処理*/
	}
	//裏画面のセッティング
	void SetDraw_Screen(const bool& clear = true) {
		SetDrawScreen(DX_SCREEN_BACK);
		if (clear) {
			ClearDrawScreen();
		}
	}
	//裏画面のカメラ情報付きセッティング
	void SetDraw_Screen(const VECTOR_ref& campos, const VECTOR_ref& camvec, const VECTOR_ref& camup, const float& fov, const float& near_, const float& far_) {
		SetDraw_Screen(true);
		SetCameraNearFar(near_, far_);
		SetupCamera_Perspective(fov);
		SetCameraPositionAndTargetAndUpVec(campos.get(), camvec.get(), camup.get());
	}
	//
};

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	//コピーしてはいけない場合unique_ptrを使おう
	auto draw = std::make_unique<DXDraw>();
	//
	std::vector<EffekseerEffectHandle> effHndle; /*エフェクトリソース*/
	std::vector<EffectS> effcs;//メインループで使うエフェクト
	//事前用意
	//エフェクトファイルを探す
	effHndle.resize(effHndle.size() + 1);
	effHndle.back() = EffekseerEffectHandle::load("data/effect/0.efk");

	effcs.resize(effHndle.size());//エフェクトのセットアップ
	//エフェクトの位置
	VECTOR_ref effect_pos = VECTOR_ref::vget(0.f, 0.f, 0.f);
	//カメラの注視点
	VECTOR_ref camera_target_pos = effect_pos;
	//カメラの位置
	VECTOR_ref camera_pos = effect_pos;
	//演算用の値
	float rad = 0.f;
	float size = 10.f;
	//メインループ
	while (ProcessMessage() == 0) {
		/*演算*/
		{
			//スペースを押しているときエフェクトを設定する
			if (CheckHitKey(KEY_INPUT_SPACE) != 0) {
				effcs[0].set(effect_pos, VGet(0.f, 1.f, 0.f), 0.1f);
			}
			if (CheckHitKey(KEY_INPUT_A) != 0) {
				rad += DX_PI_F / 180 * 6 * 60 / GetFPS();
			}
			if (CheckHitKey(KEY_INPUT_D) != 0) {
				rad -= DX_PI_F / 180 * 6 * 60 / GetFPS();
			}
		}
		//エフェクトの更新
		{
			for (size_t index = 0;index < effHndle.size();++index) {
				effcs[index].put(effHndle[index]);
			}
		}
		/*描画*/
		{
			//3D(主描画)
			{
				camera_target_pos = effect_pos;//注視点はエフェクトの位置
				camera_pos = camera_target_pos + (MATRIX_ref::RotY(rad).zvec() * size);
			}
			draw->SetDraw_Screen(camera_pos, camera_target_pos, VGet(0, 1, 0), DX_PI_F / 4, 0.5f, 20.f);
			Effekseer_Sync3DSetting();
			{
				UpdateEffekseer3D();
				//draw3D
				DrawEffekseer3D();
			}
			//2D(UI:ユーザーインターフェース)
			{
				DrawPixel(320, 240, GetColor(255, 255, 255));   // 点を打つ
			}
		}
		ScreenFlip();
		//
		if (CheckHitKey(KEY_INPUT_ESCAPE) != 0) {
			break;
		}
	}
	return 0;// ソフトの終了 
}
```

##まとめ

3Dでの位置角度の制御ができるようになりました。カメラの角度を簡単に指定できるようになりましたね。
次回はモデルの表示とTPS、FPSな動作をするカメラを実装してみましょう。

##おまけ
後々解説します。

###解説をすっ飛ばしたリスト
・3Dのカメラ設定
	SetCameraPositionAndTargetAndUpVecなどを用いてカメラ座標、注視点、頭の向き、視野角、ニアファーを指定します。
	GraphHandleの解説の際に正式に実装します。
・MV1
	3Dモデルを管理するクラスです。アニメーションも紐づけできるように設計しています。

###できたら解説したいリスト
・forの速度比較
	イテレーターを移動していくのがかなり遅かったので比較してみようと思います
・逆運動学
	モデルの腕制御などで活用しました。難産でしたが意外と仕組みは簡単です。