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

エレキベア
こんにちクマ〜〜

マイケル
今日は前回に引き続きC++を使ったゲーム開発を進めていくよ!
前回はC++の基礎をざっとと通ったから実際にゲーム開発に入っていきます!
前回はC++の基礎をざっとと通ったから実際にゲーム開発に入っていきます!

エレキベア
待ちくたびれたクマ〜〜〜

マイケル
ちなみに作成するゲームはこのような2Dシューティングゲーム!
ランダムに出現する敵をミサイルで撃ち倒すというシンプルなものになっています!
ランダムに出現する敵をミサイルで撃ち倒すというシンプルなものになっています!

↑作成するシューティングゲーム

エレキベア
(またカニクマか・・・)
でもちゃんとゲームになってるクマね
でもちゃんとゲームになってるクマね

マイケル
最近は座学ばかりだったので、めっちゃ楽しかったです!!

エレキベア
まあそれはいいことクマ・・・

マイケル
ちなみに今回もGitHubにソースコードをあげています!
触るも勉強するも、ご自由に使ってください!
触るも勉強するも、ご自由に使ってください!
GetHub(masarito617) – 2Dシューティングゲーム

マイケル
それでは早速やっていこう!
参考書籍

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

マイケル
どちらもゲームプログラミングの基礎から丁寧に書かれているため、
読み応えがありますし、楽しいです!
読み応えがありますし、楽しいです!

マイケル
特に「ゲームプログラミングC++」の方はSDLを使って一から開発していて、
今回作ったゲームもこちらの設計をベースにさせていただいています。
記事を見て気になった方はぜひ読んでみてください!
今回作ったゲームもこちらの設計をベースにさせていただいています。
記事を見て気になった方はぜひ読んでみてください!

エレキベア
クマもこの2冊は持ってるクマ〜〜〜
SDLとは

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

エレキベア
SDLって何クマ?

マイケル
SDLはマルチメディアライブラリの1つで、
描画やオーディオ、キー入力などの機能が揃っているんだ!
描画やオーディオ、キー入力などの機能が揃っているんだ!

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

マイケル
SDLはマルチプラットフォームに対応しているからWindowsでもMacでも使えるし、
DirectXと比べたら使い方も簡単です!
ちなみに今回のゲームはMacで開発しています。
DirectXと比べたら使い方も簡単です!
ちなみに今回のゲームはMacで開発しています。

エレキベア
C++でのゲーム開発はWindowsの印象が強かったクマが
Macでも開発できるのクマね
Macでも開発できるのクマね

マイケル
しかしSDLは3Dグラフィックスはサポートしていないため、
次回以降は SDL+OpenGL の組み合わせで開発していこうと思います!
次回以降は SDL+OpenGL の組み合わせで開発していこうと思います!

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

マイケル
それでは早速作っていこう!
2Dシューティングゲームの開発

マイケル
作ったゲームについて解説していきますが、
ソースコードについては量が多いため全ては載せていません。
要所要所で解説していくので、ソースコードはGitHubを参照するようにしてください!
ソースコードについては量が多いため全ては載せていません。
要所要所で解説していくので、ソースコードはGitHubを参照するようにしてください!
GetHub(masarito617) – 2Dシューティングゲーム

エレキベア
全部載せられても困るクマね
ゲームの仕様

マイケル
それではまず作るゲームの仕様について!
開始シーン、ゲームシーン、終了シーンの3つに分かれていて、
ランダムな動きで一定数出てくる敵を全て倒せばクリア、
敵を逃すかぶつかったらゲームオーバー
というシンプルな仕様になっています!
開始シーン、ゲームシーン、終了シーンの3つに分かれていて、
ランダムな動きで一定数出てくる敵を全て倒せばクリア、
敵を逃すかぶつかったらゲームオーバー
というシンプルな仕様になっています!

↑シーン遷移

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

↑敵にぶつかるか下まで逃したらゲームオーバー

エレキベア
いい感じの規模感クマね

マイケル
ちなみに画像素材は下記を使っています!
イラレでざっと描いただけのものになります・・・
イラレでざっと描いただけのものになります・・・

ship.png

enemy.png

missile.png

bomb.png

msg_start.png

msg_clear.png

msg_over.png

bg_front.png

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フォルダには各シーンのクラス
をそれぞれ格納しています。
Componentsフォルダには衝突や描画等のコンポーネント、
Commonsフォルダには計算など共通的な処理、
Scenesフォルダには各シーンのクラス
をそれぞれ格納しています。

エレキベア
フォルダごとに見ていくと分かりやすいクマね
ゲームオブジェクトとコンポーネント

マイケル
フォルダ構成で気付いた方もいるかもしれませんが、
ゲームオブジェクトとコンポーネントという、ゲームエンジンでよく触るような単語になっていたと思います。
これらの設計や実装方法について軽く解説します!
ゲームオブジェクトとコンポーネントという、ゲームエンジンでよく触るような単語になっていたと思います。
これらの設計や実装方法について軽く解説します!

エレキベア
Unityでいつもコンポーネントをアタッチしてるクマ〜〜
クラス設計

マイケル
まず単純なクラス設計としては、
継承を使用したモノシリックな設計があります。
継承を使用したモノシリックな設計があります。

↑継承を使用した設計

エレキベア
前回の基礎編でも出てきた構造クマね

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

↑コンポーネントを使用した設計

エレキベア
オブジェクトの中にも衝突を必要としないものがあったりするクマね

マイケル
この2つを組み合わせて使うことで、
最小限の機能だけ持たせて依存関係も分かりやすい設計にすることができます!
最小限の機能だけ持たせて依存関係も分かりやすい設計にすることができます!

↑組み合わせた設計

エレキベア
これは便利クマ〜〜〜

マイケル
この設計手法を使って実装しています!
更に詳しく知りたい方は
ゲームプログラミングC++を読んでみてください!
更に詳しく知りたい方は
ゲームプログラミング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クラスのリストを管理していますが、
それぞれオブジェクト自身で追加、削除するクラスを受け取って処理を呼び出していることが分かります。
ActorクラスでComponentクラスのリストを管理していますが、
それぞれオブジェクト自身で追加、削除するクラスを受け取って処理を呼び出していることが分かります。

エレキベア
受け取らなくてもシングルトン化して使えばいいのではないクマ?

マイケル
Gameクラスはシングルトン化すれば使えるけど、
Actorクラスのように複数インスタンス必要になった場合に対応できなくなるから、このような設計になっているよ!
これは 依存性の注入(DI) と呼ばれます!
Actorクラスのように複数インスタンス必要になった場合に対応できなくなるから、このような設計になっているよ!
これは 依存性の注入(DI) と呼ばれます!

エレキベア
聞いたことはあったクマがこんな設計のことだったクマね

マイケル
また、DIには モジュール化してテストしやすくなる といったメリットもあります。
Unityにも ZenjectというDIのためのツールがあるので、興味のある方は調べてみてください!
Unityにも ZenjectというDIのためのツールがあるので、興味のある方は調べてみてください!
ゲームループ

マイケル
それではゲームの流れがどうなっているのか見ていこうと思います!
まずmain.cppから処理が始まります。
まず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クラスの処理を呼び出しています。
初期化→ループ→終了の順番で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関連やシーンの初期化、
ゲームループではシーンの更新と切替
を行なっています。
初期化処理ではSDL関連やシーンの初期化、
ゲームループではシーンの更新と切替
を行なっています。

マイケル
そしてシーンの更新では
入力検知、各アクタの更新、画面出力処理
を呼び出しています。
入力検知、各アクタの更新、画面出力処理
を呼び出しています。

エレキベア
ざっくりした流れは分かってきたクマね

マイケル
次はそれぞれ細かく見ていこう!
初期化処理

マイケル
まずは初期化処理!
InitSDL関数では、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関数 内でキャッシュしています。
なお、読み込んだテクスチャはキャッシュして使いまわすよう 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)の取得
・各アクタの更新処理呼び出し
・削除状態になったアクタの破棄
各シーンの更新処理の他、
・フレームごとの経過時間(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を掛け合わせることで
性能差が違っても処理の時間差が出ないようにできるんだ!
例えば移動処理だとフレームごとにそのまま距離を足してしまうと性能がいいマシンだと高速で移動してしまう動きになってしまうよ。
時間に関わる処理を行う時にdeltaTimeを掛け合わせることで
性能差が違っても処理の時間差が出ないようにできるんだ!
例えば移動処理だとフレームごとにそのまま距離を足してしまうと性能がいいマシンだと高速で移動してしまう動きになってしまうよ。

エレキベア
1フレームにどれだけ動かすかじゃなくて、
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では入力キー
をそれぞれ取得できます!
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関数を呼び出して描画します!
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関数内では下記のように大きさと位置を指定して描画しています!
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 に半径を持たせており、オブジェクト同士が衝突しているかどうかを返す関数を用意しています。
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関数を利用 して再現しています。
ゆらゆらしながら落ちる動きも追加しています。
こちらの動きは 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で追加された乱数ライブラリみたいですね。
これは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ゲーム開発に挑戦していこう!
次回からは3Dゲーム開発に挑戦していこう!

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

マイケル
それでは今日はこの辺で!
アデュー!!
アデュー!!

エレキベア
クマ〜〜〜〜
【C++】第二回 C++を使ったゲーム開発 〜SDLを使った2Dゲーム開発編〜 〜完〜
※続きはこちら!
コメント