【C++】第五回 C++を使ったゲーム開発 〜OpenGLを使った3Dゲーム開発編〜

スポンサーリンク
PC創作
マイケル
マイケル
みなさんこんばんは!
マイケルです!
エレキベア
エレキベア
クマ〜〜〜〜!!
マイケル
マイケル
さあC++ゲーム開発シリーズもついに第五回!
いよいよ3Dゲームを作ります!!

↑前回の記事

エレキベア
エレキベア
長い道のりだったクマ・・・。
マイケル
マイケル
まあとはいっても基本的なゲームエンジン部分は前回までで作ってあるから、
あとはそれを利用して開発していくだけ!
一番楽しい部分になるね!
マイケル
マイケル
そして、今回作ったのは下記のゲーム!
01 3d shooting↑3Dシューティングゲーム
マイケル
マイケル
敵を全て破壊したらクリアという、
シンプルな3Dシューティングゲームになります!
第二回で作った2Dシューティングの3D版、のようなイメージです!
エレキベア
エレキベア
ちゃんとゲームっぽくなってるクマ〜〜
マイケル
マイケル
ソースはこれまでと同様GitHubにあげてありますので
是非こちらも参考に読んでみてください!
エレキベア
エレキベア
楽しみクマ〜〜〜
スポンサーリンク

参考書籍

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

ゲームプログラミングC++

ゲームプログラマになる前に覚えておきたい技術

ゲームを動かす数学・物理 R

マイケル
マイケル
前回まで使い続けた彼らは
もはやバイブルと化しています・・・。
エレキベア
エレキベア
愛着も湧いてくるクマね・・・
スポンサーリンク

作るゲームの仕様

マイケル
マイケル
作る前に、どんなゲームを作るのかざっと説明します!

プレイヤーは常に中心

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

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

マイケル
マイケル
プレイヤーは向きを変えてショット攻撃ができます!
カニが全滅するかプレイヤーが破壊されるかで結末が変わる仕様です。
3dshoot 02 1↑プレイヤーは向きを変えて攻撃
マイケル
マイケル
つまり空間は3Dだけど、基本的には2Dゲームの考えで開発します!
それに加えて カニの移動や攻撃を撃てる方向に縦軸を追加 する
といった味付けを加えることで3D感を出していきます。
エレキベア
エレキベア
それくらいなら作りやすそうクマね

シーン遷移

マイケル
マイケル
そしてシーン遷移は下記の通り!
こちらも2Dゲーム開発の時と同じだね!
3dshoot 03 1↑シーン遷移
エレキベア
エレキベア
なんだか作れそうな気がしてきたクマ
マイケル
マイケル
作る内容が決まったところで
早速制作にとりかかろう!
スポンサーリンク

素材の作成

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

3Dモデル

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

・プレイヤー

ScreenShot 2021 11 17 23 32 30

・カニ

ScreenShot 2021 11 17 23 33 00

・ミサイル

ScreenShot 2021 11 17 23 33 39

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

ScreenShot 2021 11 17 23 34 48

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

ScreenShot 2021 11 17 23 36 54

ScreenShot 2021 11 17 23 42 02

その他画像

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

・マーカー

Marker

・各種メッセージ

Msg start
Msg over
Msg clear
Msg title
Msg menu
マイケル
マイケル
使う素材はこれだけ!
あとはガンガン作っていきましょう!
エレキベア
エレキベア
やったるクマ〜〜〜!!
スポンサーリンク

シューティングゲームの開発

マイケル
マイケル
それでは準備ができたところでゲームの中身に触れていきます!
エレキベア
エレキベア
楽しみクマ〜〜〜〜〜
マイケル
マイケル
基本的なゲームループやシーン遷移の考え方については、
【C++】第二回 C++を使ったゲーム開発 〜SDLを使った2Dゲーム開発編〜
3Dゲームエンジン部分の基礎については
【C++】第三回 C++を使ったゲーム開発 〜3Dゲーム開発基礎 OpenGLと座標変換編〜
【C++】第四回 C++を使ったゲーム開発 〜3Dゲーム開発基礎 fbx読込とシェーダ編〜
をそれぞれ参照してもらって、
今回はそれ以外の、3Dゲームを作る上での考えや工夫した点について解説していくよ!
エレキベア
エレキベア
これまでの知識を使ってどう作ったかというところクマね

フォルダ構成

マイケル
マイケル
フォルダ構成は下記の通り!
基本的に前回と大きく変わってはいなくて、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の作成について!
画面上で動いている星々になります。
02 skybox↑SkyBoxの動き
エレキベア
エレキベア
これはどうやって設定しているクマ?
マイケル
マイケル
球体の裏面に星のテクスチャを貼って回転
させることでそれっぽく見せているよ!
マイケル
マイケル
合わせて視差スクロールの考えも応用して、
2つの球体を異なるスピードで回転させることで奥行きを表現しています!
3dshoot 04↑視差スクロールを応用して奥行きを出す
エレキベア
エレキベア
なるほどクマ!
案外簡単な仕組みだったクマね
マイケル
マイケル
コードは下記の部分!
回転する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)固定だから、入力に合わせて
・回転する処理
・攻撃する処理
を実装しています!
合わせて、回転中は機体の傾きも変えるようにしています。
03 player↑プレイヤーの操作
入力に応じた処理
マイケル
マイケル
入力に応じた挙動について、コードは下記の通り!
#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になって軸が増えても、基本的な考えは同じように実装できるということですね。
マイケル
マイケル
あとは回転が変わったプレイヤーの後ろを常にカメラがついていくように、
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の角度情報は基本的にオイラー角で管理し、
座標変換する際にクォータニオンに変換して計算しています。
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)付近に到着したら移動をやめてくるくる回る
といったものになっています。
合わせて、縦・横方向の揺れも指定 できるようにしました。
04 enemy↑カニの動き
角度指定での生成
マイケル
マイケル
カニの生成については、上から見た時の角度指定で生成
できるようにしています。
下記のように一定距離離れたところに生成するイメージですね。
3dshoot 06↑角度指定での生成
マイケル
マイケル
この設定処理の部分は下記になります。
生成時の正面ベクトルに、指定した角度のクォータニオンをかけ合わせる
ことで位置を求めています。
マイケル
マイケル
合わせて、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つの判定が必要になります。
06 collider↑衝突判定処理
マイケル
マイケル
衝突判定には様々な種類がありますが、
今回は物体の形状と処理負荷が重くないことから、
AABBでの衝突判定を実装しました。
マイケル
マイケル
AABBは軸平行バウンディングボックスと呼ばれるもので、
下記のようにオブジェクトが回転しても、コライダ自体は回転せずに大きさが変わる
ものになります。
3dshoot 05↑AABBの考え方
マイケル
マイケル
コライダごと回転するOBBといった考えもありますが、
こちらは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;
        }
    }
}
エレキベア
エレキベア
これは扱いやすそうクマ〜〜〜

マーカーの付与

マイケル
マイケル
これで一通りゲームはできる状態ですが、
+αで遊びやすさやクオリティをあげてみましょう!
マイケル
マイケル
まずは敵のマーカー表示!
ゲームの特性上、画面外の敵の位置が分からないと遊びづらいので、
判定して表示するようにします。
05 marker↑画面外の敵のマーカーを表示
マイケル
マイケル
これは各エネミーのクリップ座標を求めて画面の範囲外の値だった場合に表示する
といった実装にしています。
試行錯誤の結果、
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なので奥行き感を見せたい!
かわいいモデルも作ったので見せたい!
という私の欲求になります・・・。
07 anim↑登場演出の作成
エレキベア
エレキベア
これは何か難しそうクマね
マイケル
マイケル
それが意外とそうでもなくて、ゲーム開始時のみカメラ位置を移動させているだけなんだ。
カメラの注視点だけしっかり設定してあげれば、簡単に実装することができるよ!
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形式等のゲームで扱いやすい形に変換しておく方がよいでしょう・・・。
3dshoot 07
エレキベア
エレキベア
規模が大きくなったらとても毎回読み込むのはしたく無いクマね

レベルデザイン

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

計算処理の効率化

マイケル
マイケル
クォータニオン変換や座標読み込み等、自分があげたコードには
効率がよくない箇所がまだまだあります。
オリジナルのゲームエンジンを使用するとなると、そういった部分をどんどん改善していく必要があると思います。
マイケル
マイケル
それとゲームを量産する場合には、
こういった機能はライブラリ化しておいた方がよさそうですね。
エレキベア
エレキベア
なんという長い道のりクマ・・・

エフェクト作成、透過処理

マイケル
マイケル
今回、エフェクトについて透過画像を使用しましたが、
アルファブレンドとZバッファを同時に使用するのは思わぬレンダリングミスが起こりえます
そのため3Dモデルではアルファブレンドは切って、モデルとして描画する必要があります。
マイケル
マイケル
今回のゲームだと、爆発エフェクトがうまく透過処理されない場合があるのは、
これのせいになりますね・・・。
エレキベア
エレキベア
透過画像を貼り付けるだけでは危険があるクマね
スポンサーリンク

おわりに

マイケル
マイケル
というわけで今回は3Dゲーム開発編でした!!
どうだったかな??
エレキベア
エレキベア
やっと3Dでゲームが作れて感動だったクマ〜〜〜〜
長い道のりだったクマ・・・。
マイケル
マイケル
これでC++ゲーム開発シリーズは一旦終わり!
・・・にしようと思っていたけど、最後に
このゲームをUnityで作ったらどうなるか?
についてだけ試して紹介しようと思うよ!
エレキベア
エレキベア
確かにどのくらい感覚が違うのかも、
クオリティがどこまでアップできるかも気になるところクマね
マイケル
マイケル
つまり次回こそが最終回だ!!
こちらもお楽しみに〜〜〜〜!!!
エレキベア
エレキベア
クマ〜〜〜〜〜〜

【C++】第五回 C++を使ったゲーム開発 〜OpenGLを使った3Dゲーム開発編〜 〜完〜

コメント