夏休み勉強会 3日目

前回までは追いかけっこゲームを作成した。だんだんスパルタになっているこの勉強会だが、今回からスロットがまた1段上がるので覚悟してほしい。
それでは今回からはTPSを作っていこうと思う!


3Dゲーム制作ではまずカメラ!

カメラクラスの枠組み

まずはPlaySceneCamera.hに以下のコードを記述してくれ!

#pragma once

//	インクルード達
#include "Libraries/Development/CameraObject.h"

/// <summary>
/// プレイシーンカメラ
/// </summary>
class PlaySceneCamera : public Develop::CameraObject
{
public:

	//	プレイヤーとの距離
	static constexpr float PLAYER_DISTANCE = 3.0f;

	//	カメラの高さ位置
	static constexpr float HEIGHT_POSITION = 1.0f;

public:

	//	コンストラクタ
	PlaySceneCamera();
	//	デストラクタ
	~PlaySceneCamera() = default;

	//	開始関数
	void Initialize();
	//	更新関数
	void Update();
};

続いてはPlaySceneCamera.cppに以下のコードを記述してくれ

#include "pch.h"
#include "PlaySceneCamera.h"

#include "../Screen.h"

//	名前空間の使用
using namespace DirectX;

/// <summary>
/// コンストラクタ
/// </summary>
PlaySceneCamera::PlaySceneCamera()
{
}

/// <summary>
/// 開始関数
/// </summary>
void PlaySceneCamera::Initialize()
{
	//	射影行列の作成
	CreateProjMatrix(Screen::SIZE_RECT);
}

/// <summary>
/// 更新関数
/// </summary>
void PlaySceneCamera::Update()
{
	SimpleMath::Vector3 eye		(  0,  5, 10);	//	レンズ座標
	SimpleMath::Vector3 target	(  0,  0,  0);	//	ターゲット座標
	SimpleMath::Vector3 up		(  0,  1,  0);	//	姿勢ベクトル

	//	ビュー行列の作成
	CreateViewMatrix(eye, target, up);
}

今回は前回とは違いそのままクラスを作成したが、実際には前回のプロジェクトとやってることは同じでテスト用の固定カメラをまず作るところから始めていく。

実際にカメラを設置する

やることはいつも一緒、クラスを書いたらインスタンス化する。
この流れだ。ついでに描画するものとして、グリッド床も生成しよう。
それでは、PlayScene.hに以下のコードを記述してくれ

private:

	//	共通リソース
	CommonResources* m_commonResources;

	//	カメラ
	PlaySceneCamera m_camera;

	//	グリッド床
	Grid m_grid;

生成したら今度は開始関数を呼んでいこう。PlayScene.cppのInitialize関数に以下のコードを記述してくれ

	//	共通リソースの取得
	m_commonResources = common;

	//	カメラの初期化
	m_camera.Initialize();

	//	グリッド床の初期化
	m_grid.Initialize(common);

それでは次にカメラの更新関数を呼んでいこう。
Update関数に以下のコードを記述してくれ。(deltaTimeはそのうち使うのであらかじめ書いておく。)

	//	フレーム間秒数の取得
	float deltaTime = static_cast<float>(
		m_commonResources->GetStepTimer()->GetElapsedSeconds()
	);

	//	カメラの更新
	m_camera.Update();

そして最後に、グリッド床の描画を行おう。
Render関数に以下のコードを記述してくれ。

	//	グリッド床の描画
	m_grid.Render(m_camera);

それではこの状態で実行してみよう。以下のような画面になればOK!

グリッド床を映すカメラ

プレイヤーの移動を作成

プレイヤークラスの枠組みを作る

それでは、まず枠組みを作っていこう。
Player.hに以下のコードを記述してくれ。

#pragma once

//	インクルード達
#include "Libraries/Development/Model3D.h"
#include "Libraries/Development/BoxCollider.h"

//	前方宣言
class CommonResources;

/// <summary>
/// プレイヤー
/// </summary>
class Player
{
public:

	//	モデルのディレクトリパスと名前
	static constexpr wchar_t MODEL_DIRECTORY[]	= L"Resources/Models/";
	static constexpr wchar_t MODEL_NAME[]		= L"Player";

	//	移動速度
	static constexpr float MOVE_SPEED = 5.0f;

	//	旋回速度
	static constexpr float TURN_SPEED = 10.0f;

	//	モデルのスケール
	static constexpr DirectX::SimpleMath::Vector3 MODEL_SCALE{ 0.25f, 0.25f, 0.25f };

	//	衝突判定の大きさ
	static constexpr DirectX::SimpleMath::Vector3 COLLIDER_SIZE{ 0.5f, 0.5f, 0.5f };

	//	衝突判定位置のオフセット
	static constexpr DirectX::SimpleMath::Vector3 COLLIDER_OFFSET{ 0, 0.25f, 0 };

	//	開始地点
	static constexpr DirectX::SimpleMath::Vector3 START_POSITION{ 0.0f, 0.0f, 5.0f };

	//	銃口オフセット
	static constexpr DirectX::SimpleMath::Vector3 MUZZLE_OFFSET{ 0.0f, 0.25f, -0.25f };

public:

	//	座標を取得
	const DirectX::SimpleMath::Vector3& GetPosition() const { return m_position; }

	//	衝突判定を取得
	const Develop::BoxCollider& GetCollider() const { return m_collider; }

	//	アクティブフラグを取得
	bool GetIsActive() const { return m_isActive; }

public:

	//	座標を設定
	void SetPosition(const DirectX::SimpleMath::Vector3& position);

public:

	//	コンストラクタ
	Player();
	//	デストラクタ
	~Player() = default;

	//	開始関数
	void Initialize(CommonResources* common);
	//	更新関数
	void Update(float deltaTime);
	//	描画関数
	void Render(const Develop::CameraObject& camera);
	//	終了関数
	void Finalize();

	//	死亡関数
	void Dead();

private:

	//	移動関数
	void Move(const DirectX::SimpleMath::Vector3& inputDirection, float deltaTime);

	//	クランプ関数
	float Clamp(float num, float min, float max) const;

private:

	//	共通リソース
	CommonResources* m_commonResources;

	//	モデル
	Develop::Model3D m_model;

	//	衝突判定
	Develop::BoxCollider m_collider;

	//	座標
	DirectX::SimpleMath::Vector3 m_position;

	//	方向
	DirectX::SimpleMath::Vector3 m_direction;

	//	アクティブフラグ
	bool m_isActive;
};

Player.cppに以下のコードを記述してくれ。

#include "pch.h"
#include "Player.h"

#include "../CommonResources.h"
#include "Libraries/MyLib/InputManager.h"

//	名前空間の使用
using namespace DirectX;

/// <summary>
/// コンストラクタ
/// </summary>
Player::Player()
	:
	m_commonResources	(nullptr)	,
	m_isActive			(true)
{
}

/// <summary>
/// 開始関数
/// </summary>
/// <param name="common">共通リソース</param>
void Player::Initialize(CommonResources* common)
{
	//	共通ステートの取得
	m_commonResources = common;

	//	コンテキストの取得
	ID3D11DeviceContext* context = common->GetDeviceResources()->GetD3DDeviceContext();
	//	共通ステートの取得
	CommonStates* states = common->GetCommonStates();

	//	リソース管理クラスの取得
	Develop::ResourceManager* resourceManager = common->GetResourceManager();

	//	モデルハンドルを取得
	Develop::ResourceManager::ModelHandle modelHandle;
	modelHandle = resourceManager->LoadModelCMO(MODEL_DIRECTORY, MODEL_NAME);

	//	モデルの初期化
	m_model.Initialize(context, states, modelHandle);

	//	モデルのスケール
	m_model.SetScale(MODEL_SCALE);

	//	衝突判定のサイズ
	m_collider.SetSize(COLLIDER_SIZE);

	//	方向ベクトルを初期化
	m_direction = SimpleMath::Vector3::Forward;

	//	アクティブフラグの初期化
	m_isActive = true;

	//	開始地点に設定
	SetPosition(START_POSITION);
}

/// <summary>
/// 更新関数
/// </summary>
/// <param name="deltaTime">フレーム間秒数</param>
void Player::Update(float deltaTime)
{
	//	アクティブ状態かどうか
	if (m_isActive == false) { return; }

    //	キーボードステートとトラッカーを取得
	mylib::InputManager* inputManager = m_commonResources->GetInputManager();
	const Keyboard::State& kbState = inputManager->GetKeyboardState();
	const std::unique_ptr<Keyboard::KeyboardStateTracker>& kbTracker = inputManager->GetKeyboardTracker();
}

/// <summary>
/// 描画関数
/// </summary>
/// <param name="camera">カメラ</param>
void Player::Render(const Develop::CameraObject& camera)
{
	//	アクティブ状態かどうか
	if (m_isActive == false) { return; }

	//	モデルの描画
	m_model.Render(camera);
}

/// <summary>
/// 終了関数
/// </summary>
void Player::Finalize()
{
}

/// <summary>
/// 死亡関数
/// </summary>
void Player::Dead()
{
	//	アクティブフラグをオフに
	m_isActive = false;
}

/// <summary>
/// 移動関数
/// </summary>
/// <param name="inputDirection">入力方向</param>
/// <param name="deltaTime">フレーム間秒数</param>
void Player::Move(const DirectX::SimpleMath::Vector3& inputDirection, float deltaTime)
{

}

/// <summary>
/// クランプ関数
/// </summary>
/// <param name="num">丸め込む値</param>
/// <param name="min">最小値</param>
/// <param name="max">最大値</param>
/// <returns>丸め込んだ値</returns>
float Player::Clamp(float num, float min, float max) const
{
	return std::min(std::max(num, min), max);
}

/// <summary>
/// 座標を設定
/// </summary>
/// <param name="position">座標</param>
void Player::SetPosition(const DirectX::SimpleMath::Vector3& position)
{
	m_position = position;

	//	モデルの座標も設定
	m_model.SetPosition(position);
	//	衝突判定の座標も設定
	m_collider.SetPosition(position + COLLIDER_OFFSET);
}

今回は枠組みといいつつ結構な量を書いてもらった。
それでは、細かい部分の解説をしていく。

m_isActive : このオブジェクトがアクティブ状態つまり、生きているかどうかのフラグを保持するための変数。今回、プレイヤーはDead関数を呼ばれるとこのm_isActiveがfalseになり、モデルなども見えなくなって死亡したという扱いにされる。
m_direction : このオブジェクトが向いてる方向を示すベクトルを格納する変数。のちにこの変数を使いプログラムを書いていく。
m_collider : 四角の衝突判定。UnityのBoxColliderを想像してもらうとわかりやすい。このクラスは原点が中心にあるため、座標を設定する際にCOLLIDER_OFFSETという定数を設定座標に足すことでずれないようにしている。
Clamp(int num, int min, int max) : クランプ関数と呼ばれるものだ。numの値を見てmin以下だったらminの値を返し、max以上であればmaxの値を返す。そして、そのどちらでもなければnumをそのまま返すという関数だ。作っておくと何かと便利なので汎用関数としてグローバル化するのもありだと思う。

プレイヤーを生成する

それではPlayScene.hにて、以下のインスタンスを追加しよう。

	//	プレイヤー
	Player m_player;

続いてPlayScene.cppに行き、Initialize関数に以下の関数を呼び出そう。

	//	プレイヤーの初期化
	m_player.Initialize(common);

次にUpdate関数で更新関数を呼ぼう。

	//	プレイヤーの更新
	m_player.Update(deltaTime);

次にRender関数で描画関数を呼ぼう。

	//	プレイヤーの描画
	m_player.Render(m_camera);

最後にFinalize関数で終了関数を呼んで終了だ。

	//	プレイヤーの終了
	m_player.Finalize();

こうするとプレイヤーが描画されるはずだ、実際に描画されてればOK。

プレイヤーのたくましい背中

プレイヤーの移動処理を書いていく

それでは、Player.cppに戻ってプレイヤーの移動処理を書いていこう。
ではまずUpdate関数にプレイヤーの移動処理を記述してくれ。
仕様は以下の通りだ。

  • プレイヤーは矢印キーもしくはWASDで操作できる。

  • 左キー(Aキー)でX座標がマイナスされていく。

  • 右キー(Dキー)でX座標がプラスされていく。

  • 上キー(Wキー)でZ座標がマイナスされていく。

  • 下キー(Sキー)でZ座標がプラスされていく。

  • 移動速度は常に一定である。

  • fpsが変わっても速度は一定にしてほしい。

それでは実際にうまくいくとこのようになる。

今回はもうこの足並みをそろえるという意味を込めて、下記に正解のコードを記述しておいた。別の書き方だったかもしれないが、今回はこの書き方に統一させてもらう。
という事でPlayer.cppのUpdate関数に以下のコードを記述しなおしてくれ。

	//	キーの入力方向
	SimpleMath::Vector3 inputDirection = SimpleMath::Vector3::Zero;

	//	キーに応じて入力方向を計算
	if (kbState.A || kbState.Left)	{ inputDirection.x--; }
	if (kbState.D || kbState.Right) { inputDirection.x++; }
	if (kbState.W || kbState.Up)	{ inputDirection.z--; }
	if (kbState.S || kbState.Down)	{ inputDirection.z++; }

	//	方向の正規化
	inputDirection.Normalize();

	//	移動
	Move(inputDirection, deltaTime);

続いてMove関数の方に以下のコードを記述しなおしてくれ。

	//	入力方向が0なら何もしない
	if (inputDirection == SimpleMath::Vector3::Zero) { return; }

	//	移動方向を取得
	SimpleMath::Vector3 moveDirection = inputDirection;

	//	移動ベクトルを作成
	SimpleMath::Vector3 moveVelocity = moveDirection;
	moveVelocity *= MOVE_SPEED * deltaTime;

	//	移動ベクトルだけ移動
	SetPosition(m_position + moveVelocity);

さぁ、これで足並みがそろったという事で次に行こう。
inputDirectionをわざわざmoveDirectionに入れている理由はやってけばわかるのでそこまで深く考えなくてもいい。

動いている方向に体を向ける処理

と、その前に今回は内積と外積とクォータニオンを用いてこのプログラムを書いていくため。その事前知識を知っておく必要がある。
ここで書くとこの記事がえらく長くなるため以下のURLに飛んで理解してほしい。

理解してもらったところで実際にプログラムを組んでいく。
今回は今向いている方向移動したい方向この2つのベクトルを使って処理を書いていこう。
今回は計算処理がかなり多いため、順を追って説明していこう。

  1. 外積を用いて、移動したい方向へと旋回する回転の軸を求める

  2. 内積とアークコサイン用いて、移動したい方向へと旋回するのに必要な回転の残りの角度を求める。

  3. 内積とアークコサインで弾き出した残りの角度と、旋回角度を比較して小さい値をこのフレームの旋回角度とする。

  4. 旋回軸と旋回角度を用いてクォータニオンを生成する。

  5. モデルからクォータニオン(回転)を取り出して、生成したクォータニオンと掛け合わせる。

  6. 今向いている方向が変わったので再計算を行い、向ている方向を更新する。

1.外積を用いて、移動したい方向へと旋回する回転の軸を求める

それでは本格的にMove関数を改造していこう!
以下のコードをMove関数に記述してくれ。

	//	旋回軸を外積で算出
	SimpleMath::Vector3 turnAxis = m_direction.Cross(moveDirection);
	if (turnAxis.y > 0.0f)	{ turnAxis = SimpleMath::Vector3::Up;	}
	else					{ turnAxis = SimpleMath::Vector3::Down; }

ちなみにif文で上下の軸を代入している理由だが、エラー対処の意味合いが強い。
計算すればわかるが、向いてる方向と移動したい方向が真反対のベクトルだと軸がゼロベクトルになってしまうので、それをカバーするために上下の二択にしている。

2.内積とアークコサイン用いて、移動したい方向へと旋回するのに必要な回転の残りの角度を求める。

それでは続いて以下のコードをMove関数に引き続き記述してくれ。

	//	旋回角度を内積で算出
	float turnAngle = m_direction.Dot(moveDirection);
	turnAngle = Clamp(turnAngle, -1.0f, 1.0f);
	turnAngle = acosf(turnAngle);

旋回角度を-1.0fから1.0fの間でクランプしている理由だが、これもまたエラー対処の意味合いが強い。
完成した後にクランプの処理を外して何回かデバッグするとわかるが、クランプを書けないと計算誤差の都合でこの範囲を超えることがたまにあるのだ。範囲を超えると値が無限大になってしまいバグってモデルが消えるのでこの書き方をわざわざしている。

3.内積とアークコサインで弾き出した残りの角度と、旋回角度を比較して小さい値をこのフレームの旋回角度とする。

次に以下のコードをMove関数に引き続き記述してくれ。

	//	このフレームでの旋回角度を求める
	turnAngle = std::min(turnAngle, TURN_SPEED * deltaTime);

この処理を加えることで、移動したい方向へ向いたらそのまま向き続けるようになる。このように内積を用いて比較していかないとモデルがぶるぶる震えるようになってしまうのだ。

4.旋回軸と旋回角度を用いてクォータニオンを生成する。

それでは次にクォータニオンの生成を行っていこうということで、
このコードをMove関数に記述してくれ。

	//	旋回軸と旋回角度をもとにクォータニオンを作成
	SimpleMath::Quaternion turnQuaternion;
	turnQuaternion = SimpleMath::Quaternion::CreateFromAxisAngle(turnAxis, turnAngle);

今までに作成した旋回軸と旋回角度を使って回転を表現するのだ!

5.モデルからクォータニオン(回転)を取り出して、生成したクォータニオンと掛け合わせる。

そろそろ接続詞に悩んできた所で次に行こう。このコードをMove関数に記述してくれ(n回目)。

	//	モデルをそのぶんだけ回転させる
	SimpleMath::Quaternion modelRotate = m_model.GetRotate();
	modelRotate *= turnQuaternion;
	m_model.SetRotate(modelRotate);

クォータニオンは掛け算をすることで回転同士を組み合わせることができる。その性質を利用して書いたコードである。

6.今向いている方向が変わったので再計算を行い、向ている方向を更新する。

それでは最後にこのコードをMove関数に記述してくれ。

	//	今向いてる方向もモデルの向いている向きに合わせて更新
	m_direction = SimpleMath::Vector3::Transform(SimpleMath::Vector3::Forward, modelRotate);
	m_direction.Normalize();

本来モデルが向いている方向(Forward(0, 0, -1))に対して回転を加えることで現在モデルが向いている方向に変換している。
ちなみにモデルを作成する段階からきちんForwardの向きが真正面になるように作成しているのでこのようにうまくいっているのである。

できたかどうかの確認

実際にできるとこのようになる。

しっかりと移動方向に回転しているのがわかる。ちょっとした動作ではあるが案外面倒くさい処理が多いことがわかってくれると嬉しい。このようにすこしのことを凝るプログラマーは伸びやすい傾向にあるので、是非こういうことを凝ってみて欲しい。

プレイヤーがカメラに沿って移動するようにする

それでは3日目最後の関門に移ろうと思う。
プレイヤーがカメラに沿って移動するプログラムを組んでいこう。

カメラに方向の概念を追加する

まずはカメラの方向ベクトルを作成していこう。
以下の変数をPlaySceneCamera.hに記述してくれ。


private:

	//	方向ベクトル
	DirectX::SimpleMath::Vector3 m_direction;

続いてその方向を外部からでも取得できるようにアクセサを記述していこう。ということで引き続きPlaySceneCamera.hに以下の関数を記述してくれ。

public:

	//	方向の取得
	const DirectX::SimpleMath::Vector3& GetDirection() const { return m_direction; }

それでは続いてカメラ方向を初期化をしていく。
PlaySceneCamera.cppのInitialize関数に以下のコードを記述してくれ。

	//	方向ベクトルの初期化
	m_direction = SimpleMath::Vector3::Forward;

それでは続いてカメラ方向を計算していこう。
PlaySceneCamera.cppのUpdate関数に以下のコードを記述してくれ。

	//	方向ベクトルを更新
	m_direction = target - eye;
	m_direction.Normalize();

これでカメラに方向の概念を追加することができた。プレイヤー側でこの方向ベクトルを使ってプログラムを書いていこう。

プレイヤーにカメラのポインタを渡す

それでは、まず連携を取るためにプレイヤー側にカメラのポインタを渡そう。ではここはコードなしで実際に考えて記述してみてくれ。
手順は前回のプレイヤーのポインタと似ているが、以下の通りだ。

  1. 前方宣言でPlaySceneCameraというクラスがあることを知らせる。

  2. PlaySceneCameraのポインタを格納する変数を作る。

  3. PlaySceneCameraのポインタを外部から設定できるようにアクセサ(Set)を作る。

  4. Player.cppでPlaySceneCameraをインクルードする。

  5. 最後にPlaySceneで実際にm_playerにPlaySceneCameraのポインタを設定する。

プレイヤーの入力方向をカメラ方向に沿って回転させる

それでは本題のプログラムを書いていこう。というところで、またみんなに考えて実装してみてほしい。だいぶ難しいと思うが頑張ってみてくれ。
これに関しては難しいので最後に答えを記述しておく。ギブアップなら見てくれ。
それではヒントとしてまず手順を説明していこう。

  1. カメラの方向を取得してy成分を0にし、正規化をかける。

  2. Forwardベクトルとさっきのカメラ方向で外積をとり、移動軸を作成する。

  3. Forwardベクトルとさっきのカメラ方向で内積をとり、その値をアークコサインにいれて本来の移動ベクトルとどれだけずれているかの角度を算出。

  4. 移動軸と移動角を用いてクォータニオンを生成する。

  5. 入力方向を先ほどのクォータニオンで回転させ、移動方向として矯正する。

それでは頑張ってみてね!!!!!!
成功すると以下のようにカメラの角度を変えたとしても正常に動くようになる。

複雑な角度で見たとしても大乗だと思う、ただし真上もしくは真下から見るとバグるので注意!

ギブアップor答え合わせしたい人向け

…ということでギブアップした人向けにプログラムを置いておく。解読は各自で行ってほしい。基本的には手順通りなのでゆっくり読んでいけば大丈夫だろう。
Move関数に以下のコードを記述してくれ。(moveDirectionが少し書き換わっているので注意が必要。)

	//	カメラ方向を取得
	SimpleMath::Vector3 cameraDirection = m_pPlaySceneCamera->GetDirection();
	cameraDirection.y = 0.0f;
	cameraDirection.Normalize();

	//	移動軸を外積で算出
	SimpleMath::Vector3 moveAxis = SimpleMath::Vector3::Forward.Cross(cameraDirection);
	if (moveAxis.y > 0.0f)	{ moveAxis = SimpleMath::Vector3::Up;	}
	else					{ moveAxis = SimpleMath::Vector3::Down; }

	//	移動角度を内積で算出
	float moveAngle = SimpleMath::Vector3::Forward.Dot(cameraDirection);
	moveAngle = Clamp(moveAngle, -1.0f, 1.0f);
	moveAngle = acosf(moveAngle);

	//	移動軸と移動角度をもとにクォータニオンを作成
	SimpleMath::Quaternion moveQuaternion;
	moveQuaternion = SimpleMath::Quaternion::CreateFromAxisAngle(moveAxis, moveAngle);

	//	入力方向を矯正させ、移動方向に変える
	SimpleMath::Vector3 moveDirection;
	moveDirection = SimpleMath::Vector3::Transform(inputDirection, moveQuaternion);
	moveDirection.Normalize();

ということで以上で3日目終了です。お疲れ様でした!!!!!

この記事が気に入ったらサポートをしてみませんか?