
マイケル
みなさんこんばんは!
マイケルです!
マイケルです!

エレキベア
クマ〜〜〜〜!!

マイケル
さあC++ゲーム開発シリーズもついに第五回!
いよいよ3Dゲームを作ります!!
いよいよ3Dゲームを作ります!!
↑前回の記事

エレキベア
長い道のりだったクマ・・・。

マイケル
まあとはいっても基本的なゲームエンジン部分は前回までで作ってあるから、
あとはそれを利用して開発していくだけ!
一番楽しい部分になるね!
あとはそれを利用して開発していくだけ!
一番楽しい部分になるね!

マイケル
そして、今回作ったのは下記のゲーム!


マイケル
敵を全て破壊したらクリアという、
シンプルな3Dシューティングゲームになります!
第二回で作った2Dシューティングの3D版、のようなイメージです!
シンプルな3Dシューティングゲームになります!
第二回で作った2Dシューティングの3D版、のようなイメージです!

エレキベア
ちゃんとゲームっぽくなってるクマ〜〜

マイケル
ソースはこれまでと同様GitHubにあげてありますので
是非こちらも参考に読んでみてください!
是非こちらも参考に読んでみてください!

エレキベア
楽しみクマ〜〜〜
参考書籍

マイケル
書籍について、今回はゲームの作り込みだったのであまり使っていませんが、
ところどころ下記書籍を読み返しながら開発しました!
ところどころ下記書籍を読み返しながら開発しました!

マイケル
前回まで使い続けた彼らは
もはやバイブルと化しています・・・。
もはやバイブルと化しています・・・。

エレキベア
愛着も湧いてくるクマね・・・
作るゲームの仕様

マイケル
作る前に、どんなゲームを作るのかざっと説明します!
プレイヤーは常に中心

マイケル
プレイヤーの位置は常に(0, 0)!
そこにカニが目掛けて襲いかかります!
そこにカニが目掛けて襲いかかります!

向きを変えてショット攻撃

マイケル
プレイヤーは向きを変えてショット攻撃ができます!
カニが全滅するかプレイヤーが破壊されるかで結末が変わる仕様です。
カニが全滅するかプレイヤーが破壊されるかで結末が変わる仕様です。


マイケル
つまり空間は3Dだけど、基本的には2Dゲームの考えで開発します!
それに加えて カニの移動や攻撃を撃てる方向に縦軸を追加 する
といった味付けを加えることで3D感を出していきます。
それに加えて カニの移動や攻撃を撃てる方向に縦軸を追加 する
といった味付けを加えることで3D感を出していきます。

エレキベア
それくらいなら作りやすそうクマね
シーン遷移

マイケル
そしてシーン遷移は下記の通り!
こちらも2Dゲーム開発の時と同じだね!
こちらも2Dゲーム開発の時と同じだね!


エレキベア
なんだか作れそうな気がしてきたクマ

マイケル
作る内容が決まったところで
早速制作にとりかかろう!
早速制作にとりかかろう!
素材の作成

マイケル
まずは必要な素材について!
必要最低限なものだけざっと作ってしまいましょう!
必要最低限なものだけざっと作ってしまいましょう!
3Dモデル

マイケル
3DモデルはBlenderを使用して作成しました。
・プレイヤー

・カニ

・ミサイル

・爆発エフェクト
※Planeに画像貼り付け

・SkyBox
※球体の裏側にテクスチャ貼り付け

その他画像

マイケル
そして画像については2Dゲーム開発時のを使い回しているのと、
追加でいくつか作成しています!
追加でいくつか作成しています!
・マーカー

・各種メッセージ






マイケル
使う素材はこれだけ!
あとはガンガン作っていきましょう!
あとはガンガン作っていきましょう!

エレキベア
やったるクマ〜〜〜!!
シューティングゲームの開発

マイケル
それでは準備ができたところでゲームの中身に触れていきます!

エレキベア
楽しみクマ〜〜〜〜〜

マイケル
基本的なゲームループやシーン遷移の考え方については、
【C++】第二回 C++を使ったゲーム開発 〜SDLを使った2Dゲーム開発編〜
3Dゲームエンジン部分の基礎については
【C++】第三回 C++を使ったゲーム開発 〜3Dゲーム開発基礎 OpenGLと座標変換編〜
【C++】第四回 C++を使ったゲーム開発 〜3Dゲーム開発基礎 fbx読込とシェーダ編〜
をそれぞれ参照してもらって、
今回はそれ以外の、3Dゲームを作る上での考えや工夫した点について解説していくよ!
【C++】第二回 C++を使ったゲーム開発 〜SDLを使った2Dゲーム開発編〜
3Dゲームエンジン部分の基礎については
【C++】第三回 C++を使ったゲーム開発 〜3Dゲーム開発基礎 OpenGLと座標変換編〜
【C++】第四回 C++を使ったゲーム開発 〜3Dゲーム開発基礎 fbx読込とシェーダ編〜
をそれぞれ参照してもらって、
今回はそれ以外の、3Dゲームを作る上での考えや工夫した点について解説していくよ!

エレキベア
これまでの知識を使ってどう作ったかというところクマね
フォルダ構成

マイケル
フォルダ構成は下記の通り!
基本的に前回と大きく変わってはいなくて、Scenesフォルダを追加したのと、
衝突判定用のコンポーネントを追加しただけの構成になっています!
基本的に前回と大きく変わってはいなくて、Scenesフォルダを追加したのと、
衝突判定用のコンポーネントを追加しただけの構成になっています!
.
└── src
├── Actors
│ ├── Actor.cpp
│ ├── Actor.h
│ ├── Bomb.cpp
│ ├── Bomb.h
│ ├── Camera.cpp
│ ├── Camera.h
│ ├── Enemy.cpp
│ ├── Enemy.h
│ ├── EnemyMarker.cpp
│ ├── EnemyMarker.h
│ ├── Missile.cpp
│ ├── Missile.h
│ ├── Ship.cpp
│ ├── Ship.h
│ ├── SkyBox.cpp
│ └── SkyBox.h
├── Commons
│ ├── Collider.cpp
│ ├── Collider.h
│ ├── Math.h
│ ├── Mesh.cpp
│ ├── Mesh.h
│ ├── Renderer.cpp
│ ├── Renderer.h
│ ├── Shader.cpp
│ ├── Shader.h
│ ├── Texture.cpp
│ ├── Texture.h
│ ├── VertexArray.cpp
│ └── VertexArray.h
├── Components
│ ├── BoxColliderComponent.cpp
│ ├── BoxColliderComponent.h
│ ├── Component.cpp
│ ├── Component.h
│ ├── MeshComponent.cpp
│ ├── MeshComponent.h
│ ├── SpriteComponent.cpp
│ └── SpriteComponent.h
├── Scenes
│ ├── EndScene.cpp
│ ├── EndScene.h
│ ├── GameScene.cpp
│ ├── GameScene.h
│ ├── ReadyScene.cpp
│ ├── ReadyScene.h
│ ├── Scene.cpp
│ └── Scene.h
├── Shaders
│ ├── BasicFrag.glsl
│ ├── BasicVert.glsl
│ ├── LambertFrag.glsl
│ ├── LambertVert.glsl
│ ├── PhongFrag.glsl
│ ├── PhongVert.glsl
│ ├── SpriteFrag.glsl
│ └── SpriteVert.glsl
├── Game.cpp
├── Game.h
└── main.cpp
↑フォルダ構成
マイケル
そして使用したライブラリは下記の通り!
こちらも前回と変わりはありません。
こちらも前回と変わりはありません。
[使用ライブラリ]
・SDL、SDL_Image
・GLEW
・fbxsdk

エレキベア
OpenGLとSDLの組み合わせクマね
SkyBoxの作成

マイケル
まずは背景、SkyBoxの作成について!
画面上で動いている星々になります。
画面上で動いている星々になります。


エレキベア
これはどうやって設定しているクマ?

マイケル
球体の裏面に星のテクスチャを貼って回転
させることでそれっぽく見せているよ!
させることでそれっぽく見せているよ!

マイケル
合わせて視差スクロールの考えも応用して、
2つの球体を異なるスピードで回転させることで奥行きを表現しています!
2つの球体を異なるスピードで回転させることで奥行きを表現しています!


エレキベア
なるほどクマ!
案外簡単な仕組みだったクマね
案外簡単な仕組みだったクマね

マイケル
コードは下記の部分!
回転するActorを異なる速度で2つ生成しています!
回転するActorを異なる速度で2つ生成しています!
// データロード処理
bool Game::LoadData()
{
// 描画データロード
if (!mRenderer->LoadData()) return false;
// 背景の作成
auto* bgBack = new SkyBox(this, "bg_back.fbx", 5.0f);
bgBack->SetScale(Vector3(1.2f, 1.2f, 1.2f));
auto* bgFront = new SkyBox(this, "bg_front.fbx", 7.0f);
bgFront->SetScale(Vector3(0.65f, 0.65f, 0.65f));
return true;
}
#include "SkyBox.h"
#include "../Game.h"
#include "../Commons/Mesh.h"
#include "../Components/MeshComponent.h"
SkyBox::SkyBox(class Game *game, std::string fbxName, float rotSpeed)
:Actor(game)
{
auto* bgBackMeshComp = new MeshComponent(this);
auto* bgBackMesh = game->GetRenderer()->GetMesh(game->GetAssetsPath() + fbxName);
bgBackMeshComp->SetMesh(bgBackMesh);
auto* bgBackShader = game->GetRenderer()->GetShader(Shader::ShaderType::SPRITE);
bgBackMeshComp->SetShader(bgBackShader);
// 回転速度を設定
mRotSpeed = rotSpeed;
}
void SkyBox::UpdateActor(float deltaTime)
{
Actor::UpdateActor(deltaTime);
// 回転させる
Vector3 rotation = GetRotation();
rotation.y += mRotSpeed * deltaTime;
SetRotation(rotation);
}
void SkyBox::ProcessInput(const uint8_t *state, float deltaTime)
{
Actor::ProcessInput(state, deltaTime);
}

エレキベア
これで背景は完成クマ〜〜〜
プレイヤーの操作

マイケル
そして次はプレイヤーの操作について!
位置は(0, 0)固定だから、入力に合わせて
・回転する処理
・攻撃する処理
を実装しています!
合わせて、回転中は機体の傾きも変えるようにしています。
位置は(0, 0)固定だから、入力に合わせて
・回転する処理
・攻撃する処理
を実装しています!
合わせて、回転中は機体の傾きも変えるようにしています。

入力に応じた処理

マイケル
入力に応じた挙動について、コードは下記の通り!
#include "Ship.h"
#include "Bomb.h"
#include "Camera.h"
#include "Enemy.h"
#include "Missile.h"
#include "../Game.h"
#include "../Commons/Mesh.h"
#include "../Components/BoxColliderComponent.h"
#include "../Components/MeshComponent.h"
#include "../Scenes/EndScene.h"
Ship::Ship(class Game *game)
: Actor(game)
{
auto* meshComp = new MeshComponent(this);
auto* mesh = game->GetRenderer()->GetMesh(game->GetAssetsPath() + "ship.fbx");
meshComp->SetMesh(mesh);
auto* shader = game->GetRenderer()->GetShader(Shader::ShaderType::PHONG);
meshComp->SetShader(shader);
mCollider = new BoxColliderComponent(this);
mCollider->SetObjectAABB(mesh->GetBox());
// gameクラスに保持
game->SetShip(this);
}
void Ship::UpdateActor(float deltaTime)
{
Actor::UpdateActor(deltaTime);
// GameScene以外は向き固定
if (GetGame()->GetScene()->GetSceneName() != "GAME")
{
Vector3 rotation = GetRotation();
rotation.z = 0.0f;
SetRotation(rotation);
return;
}
// 回転が変わった場合
bool isChangeRotLeft = mIsRotLeft != mIsRotLeftBefore;
bool isChangeRotRight = mIsRotRight != mIsRotRightBefore;
if (isChangeRotLeft || isChangeRotRight)
{
// 船の傾きを調整
// 同時押しされた場合も考慮
float rot = 0.0f;
if (mIsRotLeft) rot += mRotTilt;
if (mIsRotRight) rot += -mRotTilt;
Vector3 rotation = GetRotation();
rotation.z = rot;
SetRotation(rotation);
}
// 回転中フラグを保持
mIsRotLeftBefore = mIsRotLeft;
mIsRotRightBefore = mIsRotRight;
// ミサイルを撃つ間隔を開ける
if (!mIsCanShot)
{
mDeltaShotTime += deltaTime;
if (mDeltaShotTime > CanShotTime)
{
mIsCanShot = true;
mDeltaShotTime = 0.0f;
}
}
}
・・・略・・・
void Ship::ProcessInput(const uint8_t *state, float deltaTime)
{
Actor::ProcessInput(state, deltaTime);
// GameScene以外は操作不能にする
if (GetGame()->GetScene()->GetSceneName() != "GAME") return;
// 押下キー方向に回転する
mIsRotLeft = false;
mIsRotRight = false;
if (state[SDL_SCANCODE_A])
{
Vector3 rotation = GetRotation();
rotation.y += -mRotSpeed * deltaTime;
SetRotation(rotation);
mIsRotLeft = true;
}
if (state[SDL_SCANCODE_D])
{
Vector3 rotation = GetRotation();
rotation.y += mRotSpeed * deltaTime;
SetRotation(rotation);
mIsRotRight = true;
}
// ミサイル発射
if (state[SDL_SCANCODE_K])
{
if (mIsCanShot) {
// 撃つ間隔を開けるためフラグを変更
mIsCanShot = false;
mDeltaShotTime = 0.0f;
// 前方にミサイル生成
auto* missile = new Missile(GetGame());
missile->SetPosition(GetPosition() + 3.0f * GetForward());
// 縦方向の調整
Vector3 rotation = GetRotation();
if (state[SDL_SCANCODE_W])
{
rotation.x = -mShotRotVertical;
}
if (state[SDL_SCANCODE_S])
{
rotation.x = mShotRotVertical;
}
missile->SetRotation(rotation);
}
}
}

マイケル
これまでの記事を読んだ方は気づいた方はいるかもしれませんが、
実は第二回の2Dゲーム作成の時と内容はほぼ同じです!
3Dになって軸が増えても、基本的な考えは同じように実装できるということですね。
実は第二回の2Dゲーム作成の時と内容はほぼ同じです!
3Dになって軸が増えても、基本的な考えは同じように実装できるということですね。

マイケル
あとは回転が変わったプレイヤーの後ろを常にカメラがついていくように、
Cameraクラスの挙動も実装してあげましょう。
Cameraクラスの挙動も実装してあげましょう。
void Camera::UpdateActor(float deltaTime)
{
Actor::UpdateActor(deltaTime);
Vector3 position; // カメラ位置
Vector3 target; // ターゲット位置
Vector3 up = Math::VEC3_UNIT_Y; // 上方向ベクトル
・・・略・・・
// カメラ位置をターゲットの後方に設定
position = mTargetActor->GetPosition();
position -= mOffsetPosForward * mTargetActor->GetForward();
position += mOffsetPosUp * Math::VEC3_UNIT_Y;
SetPosition(position);
// 注視点をターゲットの前方に設定
target = mTargetActor->GetPosition();
target += mOffsetLookForward * mTargetActor->GetForward();
・・・略・・・
Matrix4 viewMatrix = Matrix4::CreateLookAt(position, target, up);
GetGame()->GetRenderer()->SetViewMatrix(viewMatrix);
}

エレキベア
3Dな分、カメラの挙動は少し考える必要がありそうクマね
オイラー角での指定とクォータニオン変換

マイケル
一点、エンジン部分で前回と挙動を変えている部分については、
オイラー角で角度を指定できるようにしたということです。
Actorの角度情報は基本的にオイラー角で管理し、
座標変換する際にクォータニオンに変換して計算しています。
オイラー角で角度を指定できるようにしたということです。
Actorの角度情報は基本的にオイラー角で管理し、
座標変換する際にクォータニオンに変換して計算しています。
void Actor::SetRotation(const Vector3& rotation)
{
mRotation = rotation;
// 0〜360の間に収める
mRotation.x = fmod(mRotation.x, 360.0f);
mRotation.y = fmod(mRotation.y, 360.0f);
mRotation.z = fmod(mRotation.z, 360.0f);
// -の場合は+360する
if (mRotation.x < 0.0f) mRotation.x += 360.0f;
if (mRotation.y < 0.0f) mRotation.y += 360.0f;
if (mRotation.z < 0.0f) mRotation.z += 360.0f;
mRecalculateWorldTransform = true;
}
const Quaternion Actor::GetRotationQuaternion() const
{
Quaternion q = Quaternion();
// rotate Y Y軸そのまま回転
q = Quaternion::Concatenate(q, Quaternion(Math::VEC3_UNIT_Y, Math::ToRadians(mRotation.y)));
// rotate X Y軸適用後に回転
Vector3 right = Quaternion::RotateVec(Math::VEC3_UNIT_X, q);
q = Quaternion::Concatenate(q, Quaternion(right, Math::ToRadians(mRotation.x)));
// rotate Z XY軸適用後に回転
Vector3 forward = Quaternion::RotateVec(Math::VEC3_UNIT_Z, q);
q = Quaternion::Concatenate(q, Quaternion(forward, Math::ToRadians(mRotation.z)));
return q;
}
↑オイラー角での設定とクォータニオンへの変換
エレキベア
計算負荷は増えそうクマがとりあえずは仕方なさそうクマね・・・
ミサイルの距離判定

マイケル
発射したミサイルについては、基本的に真っ直ぐ進むようにし、
中心から一定距離離れたら破棄するようにしています。
中心から一定距離離れたら破棄するようにしています。

マイケル
今回の場合は中心が(0, 0)のため、
単純に位置をベクトルした時の長さを求めることで判定することができます。
単純に位置をベクトルした時の長さを求めることで判定することができます。
void Missile::UpdateActor(float deltaTime)
{
Actor::UpdateActor(deltaTime);
// 前方へ進む
Vector3 pos = GetPosition();
pos += mSpeed * deltaTime * GetForward();
SetPosition(pos);
// 一定距離離れたら破棄
if (pos.Length() > mDistance)
{
SetState(EDead);
}
}
↑位置ベクトルの長さで距離を判定
エレキベア
ここで数学が出てくるクマね

マイケル
ベクトル計算については下記記事でも解説しているので
こちらも是非ご参考ください!!
こちらも是非ご参考ください!!
カニの動き

マイケル
次は敵(カニ)の動きについて!
基本的な挙動としては
・中心(0, 0)に向かって進む
・中心(0, 0)付近に到着したら移動をやめてくるくる回る
といったものになっています。
合わせて、縦・横方向の揺れも指定 できるようにしました。
基本的な挙動としては
・中心(0, 0)に向かって進む
・中心(0, 0)付近に到着したら移動をやめてくるくる回る
といったものになっています。
合わせて、縦・横方向の揺れも指定 できるようにしました。

角度指定での生成

マイケル
カニの生成については、上から見た時の角度指定で生成
できるようにしています。
下記のように一定距離離れたところに生成するイメージですね。
できるようにしています。
下記のように一定距離離れたところに生成するイメージですね。


マイケル
この設定処理の部分は下記になります。
生成時の正面ベクトルに、指定した角度のクォータニオンをかけ合わせる
ことで位置を求めています。
生成時の正面ベクトルに、指定した角度のクォータニオンをかけ合わせる
ことで位置を求めています。

マイケル
合わせて、atan2を使用することで位置から向きも求めて設定しています。
アークタンジェントの考えについては下記記事をご参照ください!
アークタンジェントの考えについては下記記事をご参照ください!

エレキベア
atan懐かしいクマ〜〜〜〜
// 上から見た時の角度から位置を設定する
// *正面:0度 右斜め前:30度
void Enemy::SetInitPositionByDegree(float degree)
{
// Y軸の回転クォータニオン
Quaternion q(Math::VEC3_UNIT_Y, Math::ToRadians(degree));
// 正面のベクトルを回転した位置を求める
Vector3 pos = mAppearDistance * Math::VEC3_UNIT_Z;
pos = Quaternion::RotateVec(pos, q);
Actor::SetPosition(pos);
// 初期位置を保持する
mInitPosition = Vector3(pos.x, pos.y, pos.z);
// 中央を向かせる(270-θ)
float radian = atan2(pos.z, pos.x);
Vector3 rotation = GetRotation();
rotation.y += Math::ToDegrees(1.5f*Math::Pi - radian);
SetRotation(rotation);
}

マイケル
生成ができたら、指定した向きの方向へ進行させて
中心に来た時に動きを止めるだけ!
ここでは移動量から判定するよう実装しています!
中心に来た時に動きを止めるだけ!
ここでは移動量から判定するよう実装しています!
void Enemy::UpdateActor(float deltaTime)
{
Actor::UpdateActor(deltaTime);
// 待機時間分待つ
mTimeCount += deltaTime;
if (mTimeCount < mWaitTime) return;
// プレイヤー位置(0, 0)まで来た場合
Vector3 pos = GetPosition();
if (fabs(pos.x) <= 1.0f && fabs(pos.y) <= 1.0f && fabs(pos.z) <= 1.0f)
{
// 喜びの舞い
Vector3 rotation = GetRotation();
rotation.y += mHappyRotSpeed * deltaTime;
SetRotation(rotation);
return;
}
// 真ん中に来るまで進む
if (mTotalMove < mAppearDistance - 0.0f) {
mTotalMove += (mSpeed * deltaTime * GetForward()).Length();
}
// 幅が設定されている場合は揺らす
Vector3 rightOffset = Math::VEC3_ZERO + cosf(mTimeCount / 0.2f) * mShakeWidth.x * GetRight(); // 横揺れ
Vector3 upOffset = Vector3(0.0f, cosf(mTimeCount / 0.2f) * mShakeWidth.y, 0.0f); // 縦揺れ
// 位置更新
Vector3 newPos = mInitPosition + (mTotalMove * GetForward() + rightOffset + upOffset);
SetPosition(newPos);
}
カニのランダム生成

マイケル
あとはこれらを指定して生成するだけ!
下記のパラメータを乱数で指定して生成しています!
・出現位置
・速度
・大きさ
・揺れ幅
下記のパラメータを乱数で指定して生成しています!
・出現位置
・速度
・大きさ
・揺れ幅
void GameScene::Start()
{
Scene::Start();
// エネミー生成
float tmpDegree = 0.0f;
for (int i = 0; i < 15; i++)
{
auto* enemy = new Enemy(mGame);
// 前回の出現位置+-60度
tmpDegree += Math::GetRand(-60.0f, 60.0f);
enemy->SetInitPositionByDegree(tmpDegree);
// 速度徐々に速く
enemy->SetSpeed(Math::GetRand(25.0f, 45.0f) * (1.0f + i*0.02f));
// 大きさ
enemy->SetScale(Math::GetRand(1.0f, 3.0f) * Math::VEC3_UNIT);
// 揺れ幅 基本は横のみ
enemy->SetShakeWidth(Vector2(Math::GetRand(-7.0f, 7.0f), 0.0f));
// 数匹ごとに縦揺れも追加
if (i%3 == 2)
{
enemy->SetShakeWidth(Vector2(Math::GetRand(-10.0f, 10.0f), Math::GetRand(-8.5f, 8.5f)));
}
// 3匹ずつ出現させる
enemy->SetWaitTime(i / 3 * Math::GetRand(4.0f, 5.5f));
}
}
↑乱数を使用した生成
マイケル
ただ、調整はしているとはいえ、完全に乱数任せにしてしまう方法はよくありません。
というのも、面白い時と面白くない時のムラが出来てしまうためです。
極力、最大限楽しめるようにゲームデザインを作り込むべきところなので、
リリースする際には気をつけるようにしましょう!
というのも、面白い時と面白くない時のムラが出来てしまうためです。
極力、最大限楽しめるようにゲームデザインを作り込むべきところなので、
リリースする際には気をつけるようにしましょう!

マイケル
また、こういったパラメータは外出しして調整しやすくした方が望ましいでしょう・・・

エレキベア
(結局今回は手抜きなのかクマ・・・)
AABBでの衝突判定

マイケル
次は衝突判定について!
今回だと
・カニがプレイヤーに当たった時
・ミサイルがカニに当たった時
の2つの判定が必要になります。
今回だと
・カニがプレイヤーに当たった時
・ミサイルがカニに当たった時
の2つの判定が必要になります。


マイケル
衝突判定には様々な種類がありますが、
今回は物体の形状と処理負荷が重くないことから、
AABBでの衝突判定を実装しました。
今回は物体の形状と処理負荷が重くないことから、
AABBでの衝突判定を実装しました。

マイケル
AABBは軸平行バウンディングボックスと呼ばれるもので、
下記のようにオブジェクトが回転しても、コライダ自体は回転せずに大きさが変わる
ものになります。
下記のようにオブジェクトが回転しても、コライダ自体は回転せずに大きさが変わる
ものになります。


マイケル
コライダごと回転するOBBといった考えもありますが、
こちらはAABBと比べて計算負荷が高くなってしまいます。
またその他の衝突判定の例として過去記事で実装しているので、
興味があればご参照ください!
こちらはAABBと比べて計算負荷が高くなってしまいます。
またその他の衝突判定の例として過去記事で実装しているので、
興味があればご参照ください!

エレキベア
ゲームエンジン使いたくなるクマ・・・・・・

マイケル
AABBの実装は簡単で、下記のように各軸ごとに最小値、最大値をもたせて判定するものになります。
これらの値は、モデルを読み込む際に座標を渡してあげるだけで設定することができます。
これらの値は、モデルを読み込む際に座標を渡してあげるだけで設定することができます。
// AABBのMin、Max頂点を更新する
void AABB::UpdatePointMinMax(const Vector3 &point)
{
mMin.x = std::min(mMin.x, point.x);
mMin.y = std::min(mMin.y, point.y);
mMin.z = std::min(mMin.z, point.z);
mMax.x = std::max(mMax.x, point.x);
mMax.y = std::max(mMax.y, point.y);
mMax.z = std::max(mMax.z, point.z);
}
// Actorの更新に合わせてワールド座標変換する
void BoxColliderComponent::OnUpdateWorldTransform()
{
// オブジェクト座標でリセット
mWorldAABB = mObjectAABB;
// scale
Vector3 scale = mActor->GetScale();
mWorldAABB.mMin = Vector3(mWorldAABB.mMin.x*scale.x, mWorldAABB.mMin.y*scale.y, mWorldAABB.mMin.z*scale.z);
mWorldAABB.mMax = Vector3(mWorldAABB.mMax.x*scale.x, mWorldAABB.mMax.y*scale.y, mWorldAABB.mMax.z*scale.z);
// rotation
mWorldAABB.Rotate(mActor->GetRotationQuaternion());
// position
mWorldAABB.mMin += mActor->GetPosition();
mWorldAABB.mMax += mActor->GetPosition();
}

マイケル
そしてActorが回転した際には各値も回転させることで
形状を変更させることができます!
形状を変更させることができます!
// 頂点の回転
void AABB::Rotate(const Quaternion& q)
{
// AABBの8点を設定
std::array<Vector3, 8> points;
points[0] = mMin;
points[1] = Vector3(mMax.x, mMin.y, mMin.z);
points[2] = Vector3(mMin.x, mMax.y, mMin.z);
points[3] = Vector3(mMin.x, mMin.y, mMax.z);
points[4] = Vector3(mMin.x, mMax.y, mMax.z);
points[5] = Vector3(mMax.x, mMin.y, mMax.z);
points[6] = Vector3(mMax.x, mMax.y, mMin.z);
points[7] = mMax;
// Min、Max頂点を更新
Vector3 p = Quaternion::RotateVec(points[0], q);
mMin = p;
mMax = p;
for (int i = 1; i < points.size(); i++)
{
p = Quaternion::RotateVec(points[i], q);
UpdatePointMinMax(p);
}
}

マイケル
あとはこれらを使って判定を行うだけ!
シンプルかつ計算負荷も軽い便利な判定方法ですね!
シンプルかつ計算負荷も軽い便利な判定方法ですね!
// 衝突判定処理
// AABB * AABB
bool Intersect(const AABB &a, const AABB &b)
{
// どれかの辺が重なっているか?
bool noCollision =
a.mMax.x < b.mMin.x ||
a.mMax.y < b.mMin.y ||
a.mMax.z < b.mMin.z ||
b.mMax.x < a.mMin.x ||
b.mMax.y < a.mMin.y ||
b.mMax.z < a.mMin.z;
return !noCollision;
}
void Ship::LateUpdateActor(float deltaTime)
{
// エネミーと衝突したら破壊
for (auto enemy : GetGame()->GetEnemies())
{
if (Intersect(mCollider->GetWorldBox(), enemy->GetCollider()->GetWorldBox()))
{
// プレイヤーが消えるため、ダミーのアクタをカメラターゲットに設定す
auto* dummy = new Actor(GetGame());
dummy->SetPosition(GetPosition());
dummy->SetRotation(GetRotation());
GetGame()->GetRenderer()->GetCamera()->SetTargetActor(dummy);
// プレイヤー破棄
SetState(EDead);
// 爆発エフェクトを生成
auto* bomb = new Bomb(GetGame());
Vector3 position = GetPosition();
position -= 2.0f * GetForward();
bomb->SetPosition(position);
// 画面の方を向かせる
Vector3 rotation = Vector3(-15.0f, GetRotation().y + 180.0f, 0.0f);
bomb->SetRotation(rotation);
// ゲーム終了
GetGame()->SetNextScene(new EndScene(GetGame()));
break;
}
}
}

エレキベア
これは扱いやすそうクマ〜〜〜
マーカーの付与

マイケル
これで一通りゲームはできる状態ですが、
+αで遊びやすさやクオリティをあげてみましょう!
+αで遊びやすさやクオリティをあげてみましょう!

マイケル
まずは敵のマーカー表示!
ゲームの特性上、画面外の敵の位置が分からないと遊びづらいので、
判定して表示するようにします。
ゲームの特性上、画面外の敵の位置が分からないと遊びづらいので、
判定して表示するようにします。


マイケル
これは各エネミーのクリップ座標を求めて画面の範囲外の値だった場合に表示する
といった実装にしています。
試行錯誤の結果、
X軸で外れている場合には画面端に調整、
Z軸で外れている場合にはXY値の符号を反転
することで表示することにしました!
といった実装にしています。
試行錯誤の結果、
X軸で外れている場合には画面端に調整、
Z軸で外れている場合にはXY値の符号を反転
することで表示することにしました!
void EnemyMarker::LateUpdateActor(float deltaTime)
{
Actor::LateUpdateActor(deltaTime);
// ターゲットが設定されていない場合
if (!mTarget) return;
// 一旦非表示
SetScale(Math::VEC3_ZERO);
// viewProjectionMatrix取得
Matrix4 viewProjection = GetGame()->GetRenderer()->GetProjectionMatrix() * GetGame()->GetRenderer()->GetViewMatrix();
// エネミーのクリップ座標を求める
Matrix4 enemyWorld = mTarget->GetWorldTransform();
Vector3 enemyViewPos = viewProjection * enemyWorld * Math::VEC3_UNIT;
// プレイヤー取得
auto* player = GetGame()->GetRenderer()->GetCamera()->GetTargetActor();
if (!player) return;
// プレイヤーのクリップ座標を求める
Matrix4 playerWorld = player->GetWorldTransform();
Vector3 playerViewPos = viewProjection * playerWorld * Math::VEC3_UNIT;
// 画面に映っていないエネミーのみマーカーを付ける
if (enemyViewPos.x < -1.0f || enemyViewPos.x > 1.0f || enemyViewPos.z > 1.0f)
{
// 横に範囲がずれているエネミー
// 見える範囲に調整
if (enemyViewPos.x < -1.0f) enemyViewPos.x = -0.95f;
if (enemyViewPos.x > 1.0f) enemyViewPos.x = 0.95f;
enemyViewPos.x *= GetGame()->ScreenWidth*0.5f;
enemyViewPos.y *= GetGame()->ScreenHeight*0.5f;
// 後ろ側のエネミー
// XY値を反転してZを0にする
if (enemyViewPos.z > 1.0f)
{
enemyViewPos.x *= -1.0f;
enemyViewPos.y *= -1.0f;
enemyViewPos.z = 0.0f;
}
SetPosition(enemyViewPos);
// 向き調整
Vector3 distance = enemyViewPos - playerViewPos;
Vector3 rotation = GetRotation();
rotation.z = Math::ToDegrees(atan2(distance.y, distance.x)-Math::Pi/2.0f); // atan2-90度
SetRotation(rotation);
// マーカー表示
// 距離によってサイズを変える
float markerScale = 0.0f;
float enemyDistance = mTarget->GetPosition().Length();
if (enemyDistance < 75.0f)
{
markerScale = 1.0f;
}
else if (enemyDistance < 150.0f)
{
markerScale = 2.0f - enemyDistance / 75.0f;
}
SetScale(Vector3(markerScale, markerScale, markerScale));
}
}

マイケル
距離に合わせてマーカーの大きさも変えることで、
どのくらいの位置に敵がいるのかを分かるようにもしています。
どのくらいの位置に敵がいるのかを分かるようにもしています。

エレキベア
マーカー無かったらマジでクソゲークマね
登場演出の作成

マイケル
最後に登場演出の作成について!
せっかく3Dなので奥行き感を見せたい!
かわいいモデルも作ったので見せたい!
という私の欲求になります・・・。
せっかく3Dなので奥行き感を見せたい!
かわいいモデルも作ったので見せたい!
という私の欲求になります・・・。


エレキベア
これは何か難しそうクマね

マイケル
それが意外とそうでもなくて、ゲーム開始時のみカメラ位置を移動させているだけなんだ。
カメラの注視点だけしっかり設定してあげれば、簡単に実装することができるよ!
カメラの注視点だけしっかり設定してあげれば、簡単に実装することができるよ!
void Camera::UpdateActor(float deltaTime)
{
Actor::UpdateActor(deltaTime);
Vector3 position; // カメラ位置
Vector3 target; // ターゲット位置
Vector3 up = Math::VEC3_UNIT_Y; // 上方向ベクトル
if (!mTargetActor)
{
// ターゲットが設定されていない場合
position = GetPosition();
target = GetPosition() + mOffsetLookForward * GetForward();
}
else
{
// 登場アニメーションが完了していない場合
if (!mIsAnimFinish)
{
// カメラ位置を少しずつずらす
position = GetPosition();
position += deltaTime * mAnimLookVec;
SetPosition(position);
// 注視点をターゲットの前方に設定
target = mTargetActor->GetPosition();
target += mAnimOffsetLookForward * mTargetActor->GetForward();
// アニメーション時間更新
mTotalAnimTime += deltaTime;
if (mTotalAnimTime >= mAnimTime)
{
mIsAnimFinish = true;
}
}
else
{
// カメラ位置をターゲットの後方に設定
position = mTargetActor->GetPosition();
position -= mOffsetPosForward * mTargetActor->GetForward();
position += mOffsetPosUp * Math::VEC3_UNIT_Y;
SetPosition(position);
// 注視点をターゲットの前方に設定
target = mTargetActor->GetPosition();
target += mOffsetLookForward * mTargetActor->GetForward();
}
}
Matrix4 viewMatrix = Matrix4::CreateLookAt(position, target, up);
GetGame()->GetRenderer()->SetViewMatrix(viewMatrix);
}
// アニメーション開始
void Camera::AnimStart()
{
// アニメーションフラグ初期化
mTotalAnimTime = 0.0f;
mIsAnimFinish = false;
// アニメーション初期位置に設定
SetPosition(mAnimOffsetInitDistance * mAnimLookVec);
}

マイケル
あとはこれをSceneから呼んであげるだけ!
カメラ視点が変わるので、ライティングも一時的に変更しています。
カメラ視点が変わるので、ライティングも一時的に変更しています。
void ReadyScene::Start()
{
Scene::Start();
// クリアフラグを初期化
mGame->SetGameClear(false);
// 宇宙船の作成
auto* ship = new Ship(mGame);
ship->SetPosition(Vector3(0.0f, 0.0f, 0.0f));
// カメラターゲットに設定
mGame->GetRenderer()->GetCamera()->SetTargetActor(ship);
// タイトルメッセージを表示
mTitleMsg = new Actor(mGame);
mTitleMsg->SetPosition(Vector3(-220.0f, 200.0f, 0.0f));
auto* titleMsgSprite = new SpriteComponent(mTitleMsg, 200);
titleMsgSprite->SetTexture(mGame->GetRenderer()->GetTexture(mGame->GetAssetsPath() + "msg_title.png"));
// 見えやすいようにライティング調整
mGame->GetRenderer()->SetDirLightDirection(Vector3(0.3f, 0.3f, 0.5f));
// 登場アニメーション開始
mGame->GetRenderer()->GetCamera()->AnimStart();
}
void ReadyScene::Update(float deltaTime)
{
Scene::Update(deltaTime);
// アニメーションが終わった場合
if (!mStartMsg && mGame->GetRenderer()->GetCamera()->GetIsAnimFinish())
{
// タイトルメッセージを破棄
mTitleMsg->SetState(Actor::EDead);
// 開始メッセージを表示
mStartMsg = new Actor(mGame);
mStartMsg->SetPosition(Math::VEC3_ZERO);
auto* startMsgSprite = new SpriteComponent(mStartMsg, 200);
startMsgSprite->SetTexture(mGame->GetRenderer()->GetTexture(mGame->GetAssetsPath() + "msg_start.png"));
// 操作説明を表示
mMenuMsg = new Actor(mGame);
mMenuMsg->SetPosition(Vector3(0.0f, 210.0f, 0.0f));
auto* menuMsgSprite = new SpriteComponent(mMenuMsg, 200);
menuMsgSprite->SetTexture(mGame->GetRenderer()->GetTexture(mGame->GetAssetsPath() + "msg_menu.png"));
// ライティング元に戻す
mGame->GetRenderer()->SetDirLightDirection(Vector3(0.5f, -0.35f, 0.35f));
}
}

エレキベア
ちょっとしたことでクオリティが上がるのはいいクマね〜〜〜
反省点と課題

マイケル
とここまでで一通りの解説は終了です!
最後に今後作る人に向けて、今回の妥協点や課題についても共有しておきます!
最後に今後作る人に向けて、今回の妥協点や課題についても共有しておきます!
サウンドが無い

マイケル
サウンドについては別ライブラリの導入が必要であろうこともあり
今回は妥協しました・・・
今回は妥協しました・・・

エレキベア
あまり複雑にもしたくないクマね

マイケル
まあどうせブログに載せるだけなので無くても分からないしね!

エレキベア
・・・・・。
fbx読込の効率化

マイケル
それから3Dモデルの読み込みについて、ゲーム中にfbx読み込みを行なっていますが、
これは非常に効率が悪いです!
これは非常に効率が悪いです!

マイケル
そのため下記のように、事前にJSON形式等のゲームで扱いやすい形に変換しておく方がよいでしょう・・・。


エレキベア
規模が大きくなったらとても毎回読み込むのはしたく無いクマね
レベルデザイン

マイケル
これは上の方でも説明した通り、乱数決め打ちでエネミーを生成している点です!
ステージ作成、難易度調整といったレベルデザインは行った方がよいです。
ステージ作成、難易度調整といったレベルデザインは行った方がよいです。

エレキベア
手抜きクマね
計算処理の効率化

マイケル
クォータニオン変換や座標読み込み等、自分があげたコードには
効率がよくない箇所がまだまだあります。
オリジナルのゲームエンジンを使用するとなると、そういった部分をどんどん改善していく必要があると思います。
効率がよくない箇所がまだまだあります。
オリジナルのゲームエンジンを使用するとなると、そういった部分をどんどん改善していく必要があると思います。

マイケル
それとゲームを量産する場合には、
こういった機能はライブラリ化しておいた方がよさそうですね。
こういった機能はライブラリ化しておいた方がよさそうですね。

エレキベア
なんという長い道のりクマ・・・
エフェクト作成、透過処理

マイケル
今回、エフェクトについて透過画像を使用しましたが、
アルファブレンドとZバッファを同時に使用するのは思わぬレンダリングミスが起こりえます。
そのため3Dモデルではアルファブレンドは切って、モデルとして描画する必要があります。
アルファブレンドとZバッファを同時に使用するのは思わぬレンダリングミスが起こりえます。
そのため3Dモデルではアルファブレンドは切って、モデルとして描画する必要があります。

マイケル
今回のゲームだと、爆発エフェクトがうまく透過処理されない場合があるのは、
これのせいになりますね・・・。
これのせいになりますね・・・。

エレキベア
透過画像を貼り付けるだけでは危険があるクマね
おわりに

マイケル
というわけで今回は3Dゲーム開発編でした!!
どうだったかな??
どうだったかな??

エレキベア
やっと3Dでゲームが作れて感動だったクマ〜〜〜〜
長い道のりだったクマ・・・。
長い道のりだったクマ・・・。

マイケル
これでC++ゲーム開発シリーズは一旦終わり!
・・・にしようと思っていたけど、最後に
このゲームをUnityで作ったらどうなるか?
についてだけ試して紹介しようと思うよ!
・・・にしようと思っていたけど、最後に
このゲームをUnityで作ったらどうなるか?
についてだけ試して紹介しようと思うよ!

エレキベア
確かにどのくらい感覚が違うのかも、
クオリティがどこまでアップできるかも気になるところクマね
クオリティがどこまでアップできるかも気になるところクマね

マイケル
つまり次回こそが最終回だ!!
こちらもお楽しみに〜〜〜〜!!!
こちらもお楽しみに〜〜〜〜!!!

エレキベア
クマ〜〜〜〜〜〜
【C++】第五回 C++を使ったゲーム開発 〜OpenGLを使った3Dゲーム開発編〜 〜完〜
コメント