ゲーム開発
Unity
UnrealEngine
C++
Blender
ゲーム数学
ゲームAI
グラフィックス
サウンド
アニメーション
GBDK
制作日記
IT関連
ツール開発
フロントエンド関連
サーバサイド関連
WordPress関連
ソフトウェア設計
おすすめ技術書
音楽
DTM
楽器・機材
ピアノ
ラーメン日記
四コマ漫画
その他
おすすめアイテム
おもしろコラム
  • ゲーム開発
    • Unity
    • UnrealEngine
    • C++
    • Blender
    • ゲーム数学
    • ゲームAI
    • グラフィックス
    • サウンド
    • アニメーション
    • GBDK
    • 制作日記
  • IT関連
    • ツール開発
    • フロントエンド関連
    • サーバサイド関連
    • WordPress関連
    • ソフトウェア設計
    • おすすめ技術書
  • 音楽
    • DTM
    • 楽器・機材
    • ピアノ
  • ラーメン日記
    • 四コマ漫画
      • その他
        • おすすめアイテム
        • おもしろコラム
      1. ホーム
      2. 20210815_01

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

      C++ゲーム数学SDL
      2021-08-17

      マイケル
      マイケル
      みなさんこんにちは!
      マイケルです!
      エレキベア
      エレキベア
      こんにちクマ〜〜
      マイケル
      マイケル
      今日は前回に引き続きC++を使ったゲーム開発を進めていくよ!
      前回はC++の基礎をざっとと通ったから実際にゲーム開発に入っていきます!
      エレキベア
      エレキベア
      待ちくたびれたクマ〜〜〜
      マイケル
      マイケル
      ちなみに作成するゲームはこのような2Dシューティングゲーム
      ランダムに出現する敵をミサイルで撃ち倒すというシンプルなものになっています!

      ↑作成するシューティングゲーム
      エレキベア
      エレキベア
      (またカニクマか・・・)
      でもちゃんとゲームになってるクマね
      マイケル
      マイケル
      最近は座学ばかりだったので、めっちゃ楽しかったです!!
      エレキベア
      エレキベア
      まあそれはいいことクマ・・・
      マイケル
      マイケル
      ちなみに今回も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つに分かれていて、
      ランダムな動きで一定数出てくる敵を全て倒せばクリア
      敵を逃すかぶつかったらゲームオーバー
      というシンプルな仕様になっています!

      ↑シーン遷移

      ↑敵を全て消滅させたらクリア

      ↑敵にぶつかるか下まで逃したらゲームオーバー
      エレキベア
      エレキベア
      いい感じの規模感クマね
      マイケル
      マイケル
      ちなみに画像素材は下記を使っています!
      イラレでざっと描いただけのものになります・・・
      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でいつもコンポーネントをアタッチしてるクマ〜〜
      クラス設計
      マイケル
      マイケル
      まず単純なクラス設計としては、
      継承を使用したモノシリックな設計があります。

      ↑継承を使用した設計
      エレキベア
      エレキベア
      前回の基礎編でも出てきた構造クマね
      マイケル
      マイケル
      この構造は依存関係が分かりやすく共通の処理を持たせることができるけど、
      規模が大きくなるほど複雑化してしまう というデメリットがあるんだ。
      その点、下記のようなコンポーネントを使用した設計は必要な機能だけを持たせやすい!

      ↑コンポーネントを使用した設計
      エレキベア
      エレキベア
      オブジェクトの中にも衝突を必要としないものがあったりするクマね
      マイケル
      マイケル
      この2つを組み合わせて使うことで、
      最小限の機能だけ持たせて依存関係も分かりやすい設計にすることができます!

      ↑組み合わせた設計
      エレキベア
      エレキベア
      これは便利クマ〜〜〜
      マイケル
      マイケル
      この設計手法を使って実装しています!
      更に詳しく知りたい方は
      ゲームプログラミング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
      というコンポーネントを作成して実現しています。
      マイケル
      マイケル
      このように複数の画像を異なる速度でスクロールさせる手法を、
      視差スクロールといいます。
      手軽に遠近感が出せるのでおすすめです!!
      エレキベア
      エレキベア
      綺麗クマ〜〜〜
      // シャットダウン処理
      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種類用意し、下記のように基底クラスを
      継承して実装しています。

      ↑シーン構成
      #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;
      
      };
      
      マイケル
      マイケル
      その通り!
      例えばシーン開始処理は下記のように、
      設定してあるシーンの開始処理を呼び出すだけでよくなるよ!
      // シーン開始処理
      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ゲーム開発編〜 〜完〜

       ※続きはこちら!


      C++ゲーム数学SDL
      2021-08-17

      関連記事
      【UE5】第三回 ミニゲーム制作で学ぶUnrealC++ 〜UI・仕上げ実装 編〜
      2024-05-18
      【UE5】第二回 ミニゲーム制作で学ぶUnrealC++ 〜キャラクター・ゲーム実装 編〜
      2024-05-18
      【UE5】第一回 ミニゲーム制作で学ぶUnrealC++ 〜UnrealC++の概要 編〜
      2024-05-18
      【ゲーム数学】第九回 p5.jsで学ぶゲーム数学「フーリエ解析」
      2024-05-12
      【JUCE】DTMプラグインを作ってみる 〜ディストーション編〜【VST/AU】
      2024-03-22
      【ゲーム数学】第八回 p5.jsで学ぶゲーム数学「テイラー展開と近似式の計算」
      2022-12-24
      【ゲーム数学】第七回 p5.jsで学ぶゲーム数学「積分の実装とクロソイド曲線」
      2022-12-23
      【ゲーム数学】第六回 p5.jsで学ぶゲーム数学「微分の実装と波動方程式」
      2022-12-19