【C++】第二回 C++を使ったゲーム開発 〜SDLを使った2Dゲーム開発編〜

スポンサーリンク
PC創作
マイケル
マイケル
みなさんこんにちは!
マイケルです!
エレキベア
エレキベア
こんにちクマ〜〜
マイケル
マイケル
今日は前回に引き続きC++を使ったゲーム開発を進めていくよ!
前回はC++の基礎をざっとと通ったから実際にゲーム開発に入っていきます!
エレキベア
エレキベア
待ちくたびれたクマ〜〜〜
マイケル
マイケル
ちなみに作成するゲームはこのような2Dシューティングゲーム
ランダムに出現する敵をミサイルで撃ち倒すというシンプルなものになっています!
01 shopoting clear
↑作成するシューティングゲーム
エレキベア
エレキベア
(またカニクマか・・・)
でもちゃんとゲームになってるクマね
マイケル
マイケル
最近は座学ばかりだったので、めっちゃ楽しかったです!!
エレキベア
エレキベア
まあそれはいいことクマ・・・
マイケル
マイケル
ちなみに今回もGitHubにソースコードをあげています!
触るも勉強するも、ご自由に使ってください!


GetHub(masarito617) – 2Dシューティングゲーム

マイケル
マイケル
それでは早速やっていこう!
スポンサーリンク

参考書籍

マイケル
マイケル
今回参考にした書籍は下記になります!

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

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

マイケル
マイケル
どちらもゲームプログラミングの基礎から丁寧に書かれているため、
読み応えがありますし、楽しいです!
マイケル
マイケル
特に「ゲームプログラミングC++」の方はSDLを使って一から開発していて、
今回作ったゲームもこちらの設計をベースにさせていただいています。
記事を見て気になった方はぜひ読んでみてください!
エレキベア
エレキベア
クマもこの2冊は持ってるクマ〜〜〜
スポンサーリンク

SDLとは

マイケル
マイケル
今回作るゲームはSDLというライブラリを使って
描画やウィンドウの作成を行なっています。

SDL2.0 日本語リファレンスマニュアル

エレキベア
エレキベア
SDLって何クマ?
マイケル
マイケル
SDLはマルチメディアライブラリの1つで、
描画やオーディオ、キー入力などの機能が揃っているんだ!
マイケル
マイケル
DirectXOpenGLはよく聞くかもしれないけど、それと同じような類だね!
簡単に比較すると下記のようになるよ。
種類 特徴
DirectX Windowsに標準搭載されているマルチメディアライブラリ。
グラフィックスだけでなく、オーディオ制御や入力制御など、ゲーム開発機能も搭載している。
OpenGL 世界標準の規格としてグラフィックス機能のみを提供するAPI。
マルチプラットフォームに対応するためDirectXよりは進化が遅い。
SDL マルチプラットフォームに対応したマルチメディアライブラリ。
3Dグラフィックスはサポートしていない。

↑マルチメディアライブラリ比較

マイケル
マイケル
SDLはマルチプラットフォームに対応しているからWindowsでもMacでも使えるし、
DirectXと比べたら使い方も簡単
です!
ちなみに今回のゲームはMacで開発しています。
エレキベア
エレキベア
C++でのゲーム開発はWindowsの印象が強かったクマが
Macでも開発できるのクマね
マイケル
マイケル
しかしSDLは3Dグラフィックスはサポートしていないため、
次回以降は SDL+OpenGL の組み合わせで開発していこうと思います!
エレキベア
エレキベア
OpenGLも楽しみクマ〜〜〜
マイケル
マイケル
それでは早速作っていこう!
スポンサーリンク

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

マイケル
マイケル
作ったゲームについて解説していきますが、
ソースコードについては量が多いため全ては載せていません。
要所要所で解説していくので、ソースコードはGitHubを参照するようにしてください!


GetHub(masarito617) – 2Dシューティングゲーム

エレキベア
エレキベア
全部載せられても困るクマね

ゲームの仕様

マイケル
マイケル
それではまず作るゲームの仕様について!
開始シーン、ゲームシーン、終了シーンの3つに分かれていて、
ランダムな動きで一定数出てくる敵を全て倒せばクリア
敵を逃すかぶつかったらゲームオーバー
というシンプルな仕様になっています!
ScreenShot 2021 08 16 0 40 49
↑シーン遷移
01 shopoting clear
↑敵を全て消滅させたらクリア
02 shooting ng
↑敵にぶつかるか下まで逃したらゲームオーバー
エレキベア
エレキベア
いい感じの規模感クマね
マイケル
マイケル
ちなみに画像素材は下記を使っています!
イラレでざっと描いただけのものになります・・・
Ship
ship.png
Enemy
enemy.png
Missile
missile.png
Bomb
bomb.png
Msg start
msg_start.png
Msg clear
msg_clear.png
Msg over
msg_over.png
Bg front
bg_front.png
Bg back
bg_back.png

エレキベア
エレキベア
クマの使用料を取りたいくらいクマ

ソースコード構成

マイケル
マイケル
ソースコードの構成は下記のようになっています!
src
├── Actors
│   ├── Actor.cpp
│   ├── Actor.h
│   ├── BombEffect.cpp
│   ├── BombEffect.h
│   ├── Enemy.cpp
│   ├── Enemy.h
│   ├── Missile.cpp
│   ├── Missile.h
│   ├── Ship.cpp
│   └── Ship.h
├── Commons
│   ├── Math.h
│   ├── Vector2.cpp
│   └── Vector2.h
├── Components
│   ├── ColliderComponent.cpp
│   ├── ColliderComponent.h
│   ├── Component.cpp
│   ├── Component.h
│   ├── ScrollSpriteComponent.cpp
│   ├── ScrollSpriteComponent.h
│   ├── SpriteComponent.cpp
│   └── SpriteComponent.h
├── Scenes
│   ├── EndScene.cpp
│   ├── EndScene.h
│   ├── GameScene.cpp
│   ├── GameScene.h
│   ├── ReadyScene.cpp
│   ├── ReadyScene.h
│   ├── Scene.cpp
│   └── Scene.h
├── Game.cpp
├── Game.h
└── main.cpp
↑ソースコード構成
マイケル
マイケル
Actorsフォルダには宇宙船や敵といったゲームオブジェクト
Componentsフォルダには衝突や描画等のコンポーネント
Commonsフォルダには計算など共通的な処理
Scenesフォルダには各シーンのクラス
をそれぞれ格納しています。
エレキベア
エレキベア
フォルダごとに見ていくと分かりやすいクマね

ゲームオブジェクトとコンポーネント

マイケル
マイケル
フォルダ構成で気付いた方もいるかもしれませんが、
ゲームオブジェクトとコンポーネントという、ゲームエンジンでよく触るような単語になっていたと思います。
これらの設計や実装方法について軽く解説します!
エレキベア
エレキベア
Unityでいつもコンポーネントをアタッチしてるクマ〜〜
クラス設計
マイケル
マイケル
まず単純なクラス設計としては、
継承を使用したモノシリックな設計があります。
ScreenShot 2021 08 16 23 13 54
↑継承を使用した設計
エレキベア
エレキベア
前回の基礎編でも出てきた構造クマね
マイケル
マイケル
この構造は依存関係が分かりやすく共通の処理を持たせることができるけど、
規模が大きくなるほど複雑化してしまう というデメリットがあるんだ。
その点、下記のようなコンポーネントを使用した設計は必要な機能だけを持たせやすい!
ScreenShot 2021 08 16 23 14 20
↑コンポーネントを使用した設計
エレキベア
エレキベア
オブジェクトの中にも衝突を必要としないものがあったりするクマね
マイケル
マイケル
この2つを組み合わせて使うことで、
最小限の機能だけ持たせて依存関係も分かりやすい設計にすることができます!
ScreenShot 2021 08 16 23 13 20
↑組み合わせた設計
エレキベア
エレキベア
これは便利クマ〜〜〜
マイケル
マイケル
この設計手法を使って実装しています!
更に詳しく知りたい方は
ゲームプログラミングC++を読んでみてください!
依存性の注入(DI)
マイケル
マイケル
実装について補足ですが、
下記がアクタとコンポーネントの基底クラスになります!
#include "Actor.h"
#include <vector>
#include "../Game.h"
#include "../Components/Component.h"

Actor::Actor(Game* game)
:mState(EActive)
,mPosition(Vector2::Zero)
,mScale(1.0f)
,mRotation(0.0f)
,mGame(game)
{
    // アクタ追加
    mGame->AddActor(this);
}

Actor::~Actor()
{
    // アクタ削除
    mGame->RemoveActor(this);
    // コンポーネント削除
    while (!mComponents.empty())
    {
        delete mComponents.back();
    }
}

// 更新処理
void Actor::Update(float deltaTime)
{
    if (mState == EActive)
    {
        UpdateComponents(deltaTime);
        UpdateActor(deltaTime);
    }
}

// コンポーネント更新処理
void Actor::UpdateComponents(float deltaTime)
{
    for (auto component : mComponents)
    {
        component->Update(deltaTime);
    }
}

// 各アクタはoverrideして記述する
void Actor::UpdateActor(float deltaTime)
{
}

// コンポーネント追加
void Actor::AddComponent(Component* component)
{
    // 設定された更新順となるようソートする
    int myOrder = component->GetUpdateOrder();
    auto iter = mComponents.begin();
    for (; iter != mComponents.end(); ++iter)
    {
        if (myOrder < (*iter)->GetUpdateOrder())
        {
            break;
        }
    }
    mComponents.insert(iter, component);
}

// コンポーネント削除
void Actor::RemoveComponent(Component* component)
{
    auto iter = std::find(mComponents.begin(), mComponents.end(), component);
    if (iter != mComponents.end())
    {
        mComponents.erase(iter);
    }
}
#include "Component.h"
#include "../Actors/Actor.h"

Component::Component(Actor* actor, int updateOrder)
:mActor(actor)
,mUpdateOrder(updateOrder)
{
    // アクタにコンポーネントを追加
    mActor->AddComponent(this);
}

Component::~Component()
{
    // アクタからコンポーネントを削除
    mActor->RemoveComponent(this);
}

// 各コンポーネントはoverrideして記述する
void Component::Update(float deltaTime)
{
}
マイケル
マイケル
GameクラスでActorのリストを管理
ActorクラスでComponentクラスのリストを管理していますが、
それぞれオブジェクト自身で追加、削除するクラスを受け取って処理を呼び出していることが分かります。
エレキベア
エレキベア
受け取らなくてもシングルトン化して使えばいいのではないクマ?
マイケル
マイケル
Gameクラスはシングルトン化すれば使えるけど、
Actorクラスのように複数インスタンス必要になった場合に対応できなくなるから、このような設計になっているよ!
これは 依存性の注入(DI) と呼ばれます!
エレキベア
エレキベア
聞いたことはあったクマがこんな設計のことだったクマね
マイケル
マイケル
また、DIには モジュール化してテストしやすくなる といったメリットもあります。
Unityにも Zenject というDIのためのツールがあるので、興味のある方は調べてみてください!

ゲームループ

マイケル
マイケル
それではゲームの流れがどうなっているのか見ていこうと思います!
まずmain.cppから処理が始まります。
#include "Game.h"

// メインクラス
// *ここから処理が呼ばれる
int main(int argc, char* argv[]) {
    // 初期化->ループ->終了
    Game game;
    bool success = game.Initialize();
    if (success)
    {
        game.RunLoop();
    }
    game.Shutdown();
    return 0;
}
マイケル
マイケル
これはもう同じみのゲームループですね!
初期化→ループ→終了の順番でGameクラスの処理を呼び出しています。
エレキベア
エレキベア
ゲームプログラムはとりあえずここからクマね
#include "Game.h"
#include "SDL_image.h"
#include "Actors/Actor.h"
#include "Actors/Ship.h"
#include "Actors/Enemy.h"
#include "Components/SpriteComponent.h"
#include "Components/ScrollSpriteComponent.h"
#include "Commons/Math.h"

Game::Game()
:mWindow(nullptr)
,mRenderer(nullptr)
,mTicksCount(0)
,mIsRunning(true)
,mUpdatingActors(false)
,mGameClear(false)
{
}

// ゲーム初期化
bool Game::Initialize()
{
    // SDL関連初期化
    if (!InitSDL())
    {
        SDL_Log("%s", SDL_GetError());
        return false;
    }
    // ゲーム時間取得
    mTicksCount = SDL_GetTicks();

    // シーン初期化
    InitScene();

    return true;
}

・・・略・・・

// ゲームループ処理
void Game::RunLoop()
{
    // 開始シーンを設定
    mScene = new ReadyScene(this);
    mNextScene = mScene;
    StartScene();

    while (mIsRunning)
    {
        // シーン更新処理
        UpdateScene();
        // シーン開始処理
        if (mScene->GetSceneName().compare(mNextScene->GetSceneName()) != 0)
        {
            delete mScene;
            mScene = mNextScene;
            StartScene();
        }
    }
}

・・・略・・・
// シーン開始処理
void Game::StartScene()
{
・・・略・・・
}

// シーン更新処理
void Game::UpdateScene()
{
    // 入力検知
    ProcessInput();

・・・略・・・

    // 出力処理
    GenerateOutput();
}

// ゲームループ 入力検知
void Game::ProcessInput()
{
・・・略・・・
}

// ゲームループ 出力処理
void Game::GenerateOutput()
{
・・・略・・・
}

// シャットダウン処理
void Game::Shutdown()
{
・・・略・・・
}
マイケル
マイケル
Gameクラスにつひてだいぶ省略していますが、簡単に説明すると

初期化処理ではSDL関連やシーンの初期化、
ゲームループではシーンの更新と切替

を行なっています。
マイケル
マイケル
そしてシーンの更新では
入力検知、各アクタの更新、画面出力処理
を呼び出しています。
エレキベア
エレキベア
ざっくりした流れは分かってきたクマね
マイケル
マイケル
次はそれぞれ細かく見ていこう!
初期化処理
マイケル
マイケル
まずは初期化処理!
InitSDL関数では、SDL関連の初期化やウィンドウの作成を行なっています。
// ゲーム初期化
bool Game::Initialize()
{
    // SDL関連初期化
    if (!InitSDL())
    {
        SDL_Log("%s", SDL_GetError());
        return false;
    }
    // ゲーム時間取得
    mTicksCount = SDL_GetTicks();

    // シーン初期化
    InitScene();

    return true;
}

// SDL関連初期化
bool Game::InitSDL()
{
    // 初期化に失敗したらfalseを返す
    bool success = SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO) == 0;
    if (!success) return false;

    mWindow = SDL_CreateWindow("ShootingGame", 100, 100, ScreenWidth, ScreenHeight, 0);
    if (!mWindow) return false;

    mRenderer = SDL_CreateRenderer(mWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
    if (!mRenderer) return false;

    success = IMG_Init(IMG_INIT_PNG) != 0;
    if (!success) return false;

    return true;
}
マイケル
マイケル
そしてシーンの初期化では背景を作成しています!
なお、読み込んだテクスチャはキャッシュして使いまわすよう LoadTexture関数 内でキャッシュしています。
// シーン初期化処理(一度だけ呼ばれる)
void Game::InitScene()
{
    // 背景の作成
    // 2枚のスクロール速度を変えて視差スクロールにする
    auto* bgBack = new Actor(this);
    bgBack->SetPosition(Vector2(ScreenWidth / 2, ScreenHeight / 2));
    auto* bgBackSprite = new ScrollSpriteComponent(bgBack, 10);
    bgBackSprite->SetTexture(LoadTexture(GetAssetsPath() + "bg_back.png"));
    bgBackSprite->SetScrollSpeedY(100.0f); // 速度:100

    auto* bgFront = new Actor(this);
    bgFront->SetPosition(Vector2(ScreenWidth / 2, ScreenHeight / 2));
    auto* bgFrontSprite = new ScrollSpriteComponent(bgFront, 20);
    bgFrontSprite->SetTexture(LoadTexture(GetAssetsPath() + "bg_front.png"));
    bgFrontSprite->SetScrollSpeedY(200.0f); // 速度:200
}

// ファイル名からテクスチャをロードする
SDL_Texture* Game::LoadTexture(const std::string& fileName)
{
    SDL_Texture* tex = nullptr;
    auto iter = mCachedTextures.find(fileName);
    if (iter != mCachedTextures.end())
    {
        // キャッシュ済なら変数から取得
        tex = iter->second;
    }
    else
    {
        // テクスチャをロードする
        SDL_Surface* surf = IMG_Load(fileName.c_str());
        if (!surf)
        {
            SDL_Log("Error load texture file %s", fileName.c_str());
            return nullptr;
        }
        tex = SDL_CreateTextureFromSurface(mRenderer, surf);
        SDL_FreeSurface(surf);
        if (!tex)
        {
            SDL_Log("Error convert surface to texture %s", fileName.c_str());
            return nullptr;
        }
        // 変数にキャッシュする
        mCachedTextures.emplace(fileName.c_str(), tex);
    }
    return tex;
}
マイケル
マイケル
ここで、背景のスクロールはScrollSpriteComponent
というコンポーネントを作成して実現しています。
03 scroll
マイケル
マイケル
このように複数の画像を異なる速度でスクロールさせる手法を、
視差スクロールといいます。
手軽に遠近感が出せるのでおすすめです!!
エレキベア
エレキベア
綺麗クマ〜〜〜
// シャットダウン処理
void Game::Shutdown()
{
    // インスタンスを破棄
    while (!mActors.empty())
    {
        delete mActors.back();
    }
    for (auto i : mCachedTextures)
    {
        SDL_DestroyTexture(i.second);
    }
    mCachedTextures.clear();
    // SDL関連の変数を破棄
    SDL_DestroyRenderer(mRenderer);
    SDL_DestroyWindow(mWindow);
    SDL_Quit();
    IMG_Quit();
}
マイケル
マイケル
作成したSDL関連の変数については、
シャットダウン時に破棄するようにしています。
更新処理
マイケル
マイケル
次は更新処理!
シーン遷移を簡単なシーケンス処理で更新しています!
// ゲームループ処理
void Game::RunLoop()
{
    // 開始シーンを設定
    mScene = new ReadyScene(this);
    mNextScene = mScene;
    StartScene();

    while (mIsRunning)
    {
        // シーン更新処理
        UpdateScene();
        // シーン開始処理
        if (mScene->GetSceneName().compare(mNextScene->GetSceneName()) != 0)
        {
            delete mScene;
            mScene = mNextScene;
            StartScene();
        }
    }
}
マイケル
マイケル
シーンは3種類用意し、下記のように基底クラスを
継承して実装しています。
ScreenShot 2021 08 31 23 14 56
↑シーン構成
#pragma once
#include "SDL_image.h"
#include "string"

// シーン基底クラス
// *各シーンはこのクラスを継承する
class Scene
{
public:
    Scene(class Game* game);
    virtual ~Scene();
    virtual void Start();
    virtual void Update(float deltaTime);
    virtual void ProcessInput(const Uint8* state);
    virtual std::string const GetSceneName();

protected:
    class Game* mGame;

};
↑Scene基底クラス
#pragma once
#include "Scene.h"

// ゲームシーンクラス
class GameScene : public Scene {
public:
    GameScene(class Game* game);
    ~GameScene();
    void Start() override;
    void Update(float deltaTime) override;
    void ProcessInput(const Uint8* state) override;
    std::string const GetSceneName() override;

};
#include "GameScene.h"
#include "EndScene.h"
#include "../Game.h"
#include "../Actors/Enemy.h"
#include "../Actors/Ship.h"
#include "../Commons/Math.h"

GameScene::GameScene(class Game *game)
:Scene(game)
{
}

GameScene::~GameScene()
{
}

void GameScene::Start()
{
    // エネミーをランダム作成
    for (int i = 0; i < 30; i++)
    {
        auto* enemy = new Enemy(mGame);
        enemy->SetPosition(Vector2(Math::GetRand(100.0f, mGame->ScreenWidth - 100.0f), -100.0f));
        enemy->SetEnemySpeed(Math::GetRand(300.0f, 550.0f));
        enemy->SetScale(Math::GetRand(0.5f, 1.5f));
        // 数匹ごとに揺らす
        if (i % 2 == 0)
        {
            enemy->SetEnemyMoveType(Enemy::SHAKE);
            enemy->SetEnemyShakeWidth(Math::GetRand(5.0f, 15.0f));
        }
        // 数匹ずつ動かす
        enemy->SetWaitTime(i / 3 * Math::GetRand(80.0f, 100.0f));
    }
}

void GameScene::Update(float deltaTime)
{
    // エネミーを全部撃破したらゲームクリア
    if (mGame->GetEnemies().empty())
    {
        mGame->SetGameClear(true);
        mGame->SetNextScene(new EndScene(mGame));
    }
}

void GameScene::ProcessInput(const Uint8 *state)
{
    // 宇宙船のキー入力操作
    mGame->GetShip()->ProcessKeyboard(state);
}

std::string const GameScene::GetSceneName()
{
    return "GAME";
}
↑GameSceneクラス
エレキベア
エレキベア
基底クラスを用意することで呼び出し側は簡単に書けるクマね
マイケル
マイケル
その通り!
例えばシーン開始処理は下記のように、
設定してあるシーンの開始処理を呼び出すだけでよくなるよ!
// シーン開始処理
void Game::StartScene()
{
    mScene->Start();
}
エレキベア
エレキベア
これはなかなか簡潔クマね
マイケル
マイケル
そしてシーン更新処理は下記の通り!
各シーンの更新処理の他、
・フレームごとの経過時間(deltaTime)の取得
・各アクタの更新処理呼び出し
・削除状態になったアクタの破棄

をそれぞれ行なっています!
// シーン更新処理
void Game::UpdateScene()
{
    // 入力検知
    ProcessInput();

    // 最低16msは待機
    while (!SDL_TICKS_PASSED(SDL_GetTicks(), mTicksCount + 16));
    // フレームの経過時間を取得(最大50ms)
    float deltaTime = (SDL_GetTicks() - mTicksCount) / 1000.0f;
    if (deltaTime > 0.05f)
    {
        deltaTime = 0.05f;
    }
    mTicksCount = SDL_GetTicks();

    // アクタ更新処理
    mUpdatingActors = true;
    for (auto actor : mActors)
    {
        actor->Update(deltaTime);
    }
    mUpdatingActors = false;

    // 待機中のアクタを追加
    for (auto pending : mPendingActors)
    {
        mActors.emplace_back(pending);
    }
    mPendingActors.clear();

    // 各シーンの更新処理
    mScene->Update(deltaTime);

    // 死亡したアクタを破棄
    std::vector<Actor*> deadActors;
    for (auto actor : mActors)
    {
        if (actor->GetState() == Actor::EDead)
        {
            deadActors.emplace_back(actor);
        }
    }
    for (auto actor : deadActors)
    {
        delete actor;
    }

    // 出力処理
    GenerateOutput();
}
エレキベア
エレキベア
deltaTimeは何のために取得するクマ?
マイケル
マイケル
これはUnity何かでも使ってると思うけど、
時間に関わる処理を行う時にdeltaTimeを掛け合わせることで
性能差が違っても処理の時間差が出ない
ようにできるんだ!
例えば移動処理だとフレームごとにそのまま距離を足してしまうと性能がいいマシンだと高速で移動してしまう動きになってしまうよ。
エレキベア
エレキベア
1フレームにどれだけ動かすかじゃなくて、
1秒にどれだけ動かすか、という風に考えるクマね
マイケル
マイケル
各アクタ内ではdeltaTimeを用いて
下記のように移動処理等をおこなっています!
void Enemy::UpdateActor(float deltaTime)
{
    // 親のメソッド呼び出し
    Actor::UpdateActor(deltaTime);

    // 待機時間分は待つ
    mTimeCount++;
    if (mTimeCount < mWaitTime)
    {
        return;
    }

    // 移動処理
    Vector2 pos = GetPosition();
    switch (mEnemyMoveType)
    {
        case STRAIGHT:
            pos.y += mEnemySpeed * deltaTime;
            break;
        case SHAKE:
            pos.x = mInitPosition->x + (sinf(mTimeCount / 10.0f) * mEnemyShakeWidth);
            pos.y += mEnemySpeed * deltaTime;
            break;
        default:
            break;
    }
    // 画面外に出たらゲームオーバー
    if (pos.y >= Game::ScreenHeight)
    {
        SetState(EDead);
        GetGame()->SetNextScene(Game::END_SCENE);
    }
    SetPosition(pos);
}
エレキベア
エレキベア
なるほどクマ〜〜〜
入力検知
マイケル
マイケル
入力検知は下記の通り!
SDL_PollEventではウィンドウを閉じた時のイベント
SDL_GetKeyboardStateでは入力キー
をそれぞれ取得できます!
// ゲームループ 入力検知
void Game::ProcessInput()
{
    // SDLイベント
    SDL_Event event;
    while (SDL_PollEvent(&event))
    {
        switch (event.type)
        {
            case SDL_QUIT: // ウィンドウが閉じられた時
            mIsRunning = false;
            break;
        }
    }
    // キー入力イベント
    const Uint8* state = SDL_GetKeyboardState(NULL);
    if (state[SDL_SCANCODE_ESCAPE])
    {
        mIsRunning = false;
    }

    // 各シーンの入力検知
    mScene->ProcessInput(state);
}
マイケル
マイケル
また、シーンごとの入力検知の他、
アクタ内にも入力処理を持たせて呼び出すようにしています!
下記は宇宙船の移動処理の例です!
// キーボード入力
void Ship::ProcessKeyboard(const uint8_t* state)
{
    mRightMove = 0.0f;
    mDownMove = 0.0f;
    // キー入力で上下左右に移動させる
    if (state[SDL_SCANCODE_D])
    {
        mRightMove += ShipSpeed;
    }
    if (state[SDL_SCANCODE_A])
    {
        mRightMove -= ShipSpeed;
    }
    if (state[SDL_SCANCODE_S])
    {
        mDownMove += ShipSpeed;
    }
    if (state[SDL_SCANCODE_W])
    {
        mDownMove -= ShipSpeed;
    }
    // ミサイルを撃つ
    if (state[SDL_SCANCODE_K])
    {
        if (mIsCanShot)
        {
            // 撃つ間隔を開けるためフラグを変更
            mIsCanShot = false;
            mDeltaShotTime = 0.0f;
            // ミサイル生成
            auto* missile = new Missile(GetGame());
            Vector2 pos = GetPosition();
            missile->SetPosition(Vector2(pos.x, pos.y - 30.0f));
        }
    }
}
エレキベア
エレキベア
この辺は分かりやすいクマね
出力処理
マイケル
マイケル
そして最後に出力処理!
SpriteComponentを持つアクタの数だけDraw関数を呼び出して描画します!
// ゲームループ 出力処理
void Game::GenerateOutput()
{
    // 背景色をクリア
    SDL_SetRenderDrawColor(mRenderer,19,56,111,255); // 紺色
    SDL_RenderClear(mRenderer);

    // スプライトを描画
    for (auto sprite : mSprites)
    {
        sprite->Draw(mRenderer);
    }

    // バックバッファとスワップ(ダブルバッファ)
    SDL_RenderPresent(mRenderer);
}
マイケル
マイケル
ダブルバッファというのは、表示用と生成用で2枚の画面を持たせて、描画の際に入れ替える方法のことです。
Draw関数内では下記のように大きさと位置を指定して描画しています!
void SpriteComponent::Draw(SDL_Renderer* renderer)
{
    if (mTexture)
    {
        SDL_Rect r;
        // 大きさ、位置(左上座標)を設定
        r.w = static_cast<int>(mTexWidth * mActor->GetScale());
        r.h = static_cast<int>(mTexHeight * mActor->GetScale());
        r.x = static_cast<int>(mActor->GetPosition().x - r.w / 2);
        r.y = static_cast<int>(mActor->GetPosition().y - r.h / 2);
        // 描画
        SDL_RenderCopyEx(renderer,
                mTexture,
                nullptr,
                &r,
                -Math::ToDegrees(mActor->GetRotation()),
                nullptr,
                SDL_FLIP_NONE);
    }
}
エレキベア
エレキベア
これで一通りの流れが分かったクマ〜〜〜

衝突判定

マイケル
マイケル
オブジェクト同士が衝突したかどうかの判定については、下記のように行なっています!
ColliderComponent に半径を持たせており、オブジェクト同士が衝突しているかどうかを返す関数を用意しています。
// 衝突処理
bool Intersect(const ColliderComponent& a, const ColliderComponent& b)
{
    // 計算負荷軽減のためsqrtせずに比較
    // 衝突した物体との距離
    Vector2 diff = a.GetCenter() - b.GetCenter();
    float distSq = diff.LengthSq();
    // 衝突円の半径の合計以下なら衝突
    float radDiff = a.GetRadius() + b.GetRadius();
    return distSq <= radDiff * radDiff;
}
マイケル
マイケル
判定方法は円の衝突を用いていて、オブジェクト同士の距離が半径の合計より小さければ衝突としています。
ベクトルの長さについては、本来平方根を使用して求めますが、
計算負荷短縮のため二乗した状態で比較を行なっています。
// vec length(not sqrt)
float Vector2::LengthSq() const
{
    return (x*x + y*y);
}
↑平方根は求めずに返す
マイケル
マイケル
以上の判定処理は、アクタの更新処理内で呼び出しています。
// アクタ更新
void Ship::UpdateActor(float deltaTime)
{

・・・略・・・

    // エネミーと衝突したら死亡
    for (auto enemy : GetGame()->GetEnemies())
    {
        if (Intersect(*mCollider, *(enemy->GetCollider())))
        {
            // ゲーム終了
            GetGame()->SetNextScene(Game::END_SCENE);
            SetState(EDead);
            // 宇宙船の位置で爆発エフェクト
            auto* bomb = new BombEffect(GetGame());
            bomb->SetPosition(Vector2(GetPosition()));
            return;
        }
    }

・・・略・・・

}
エレキベア
エレキベア
爆発エフェクトって何クマ?
マイケル
マイケル
爆発エフェクトはアクタクラスとして個別に作ったんだ!
下記のようにスケールを変更して簡単なアニメーションを付けているよ!
#include "BombEffect.h"
#include "../Game.h"
#include "../Components/SpriteComponent.h"

BombEffect::BombEffect(class Game *game)
:Actor(game)
,mTimeCount(0.0f)
{
    // スプライト設定
    auto* sprite = new SpriteComponent(this);
    sprite->SetTexture(GetGame()->LoadTexture(GetGame()->GetAssetsPath() + "bomb.png"));
    // 最初は大きさ0にする
    SetScale(0.0f);
}

void BombEffect::UpdateActor(float deltaTime)
{
    // 親のメソッド呼び出し
    Actor::UpdateActor(deltaTime);
    // 徐々に大きくする
    float changeScale = mTimeCount / DisplayTime * 3.0f;
    if (changeScale > 1.0f)
    {
        changeScale = 1.0f;
    }
    SetScale(changeScale);
    // 表示時間を過ぎたら破棄
    mTimeCount += deltaTime;
    if (mTimeCount >= DisplayTime)
    {
        SetState(EDead);
    }
}
エレキベア
エレキベア
楽しそうで何よりクマ・・・

エネミーのランダム生成

マイケル
マイケル
長くなりましたが最後にエネミーのランダム生成について!
今回は適当な数ループして、ランダム値で動きや位置を変更 するようにしています。
// エネミーをランダム作成
for (int i = 0; i < 30; i++)
{
    auto* enemy = new Enemy(this);
    enemy->SetPosition(Vector2(Math::GetRand(100.0f, ScreenWidth - 100.0f), -100.0f));
    enemy->SetEnemySpeed(Math::GetRand(300.0f, 550.0f));
    enemy->SetScale(Math::GetRand(0.5f, 1.5f));
    // 数匹ごとに揺らす
    if (i % 2 == 0)
    {
        enemy->SetEnemyMoveType(Enemy::SHAKE);
        enemy->SetEnemyShakeWidth(Math::GetRand(5.0f, 15.0f));
    }
    // 数匹ずつ動かす
    enemy->SetWaitTime(i / 3 * Math::GetRand(80.0f, 100.0f));
}
マイケル
マイケル
動きの種類は、単純に落ちてくる動きの他、
ゆらゆらしながら落ちる動きも追加しています。
こちらの動きは sinf関数を利用 して再現しています。
    // 移動処理
    Vector2 pos = GetPosition();
    switch (mEnemyMoveType)
    {
        case STRAIGHT:
            pos.y += mEnemySpeed * deltaTime;
            break;
        case SHAKE:
            pos.x = mInitPosition->x + (sinf(mTimeCount / 10.0f) * mEnemyShakeWidth);
            pos.y += mEnemySpeed * deltaTime;
            break;
        default:
            break;
    }
エレキベア
エレキベア
sin値はグラフで書くと波のような形クマからね
マイケル
マイケル
そして乱数の生成については、今回は下記のようにして取得しました!
これはC++11で追加された乱数ライブラリみたいですね。
    // random
    static std::random_device seed_gen;
    static std::default_random_engine randomEngine(seed_gen());
    inline float GetRand(float min, float max)
    {
        std::uniform_real_distribution<> dist(min, max);
        return dist(Math::randomEngine);
    }
エレキベア
エレキベア
手軽に乱数も作れるクマね
マイケル
マイケル
精度のいい乱数生成方法は他にもあると思うので、
気になる方は調べてみてください!
スポンサーリンク

おわりに

マイケル
マイケル
というわけで今回はC++での2Dゲーム開発でした!
どうだったかな??
エレキベア
エレキベア
C++って聞くと難しそうだったクマが
やってみたら意外と簡単に作れたクマね
マイケル
マイケル
それとやっぱり一から作ってる感があって楽しいね!
次回からは3Dゲーム開発に挑戦していこう!
エレキベア
エレキベア
OpenGL楽しみクマ〜〜〜
マイケル
マイケル
それでは今日はこの辺で!
アデュー!!
エレキベア
エレキベア
クマ〜〜〜〜

【C++】第二回 C++を使ったゲーム開発 〜SDLを使った2Dゲーム開発編〜 〜完〜

コメント