Siv3DDay 17

ポーズの管理をしやすくする

More than 1 year has passed since last update.


はじめに

ゲームを作っていてポーズを実装したいときに例えば


Main.cpp

void Main()

{

bool isPause=false;

while (System::Update())
{
if (Input::MouseL.clicked)
{
isPause = !isPause;
}

if (!isPause)
{
//更新
//Update();
}

//描画
//Draw();

}
}


このようにしたり、

HamFrameworkのSceneManagerを使っている場合は


Scene.h

void update()override

{
if (Input::MouseL.clicked)
{
m_isPause = !m_isPause;
}
if (m_isPause)
return;

/*
処理
*/

}


このようにして更新の処理を呼ばない用にして対処することができます。

しかし、経過時間に依存する


  • Stopwatch

  • EasingController

  • Effect

などを使用して実装していた場合は、これら全てのpause関数も呼び出さないと意図していない動作になってしまう場合があります。

数が少ないときは問題ないかもしれませんが、ゲームの規模が大きくなったり細かい演出のためにEasingを多用したりした時に全てのpause関数を呼ぶのは大変だったりします。

自分もゲームを作りこんだ後にポーズを実装しようとした時に、この問題に苦労したことがありました。

そこで今回は、これら全てをたった1行の関数を呼ぶだけで止めたり再開できるようにしたいと思います。


方針

では、それを実装するためにどのようにするかの方針をざっくり説明します。


  1. ポーズが必要なクラスはIPauseクラスを継承する


    • ポーズを行うpause関数をオーバーライド

    • リジュームを行うresume関数をオーバーライド



  2. ポーズを管理するPauseManagerをつくる


    • メンバ変数にポーズが必要なインスタンスへのポインタのリストstd::list<IPause*>をもつ

    • 全てのポーズを行うメンバ関数pauseAllをもつ

    • 全てのリジュームを行うメンバ関数resumeAllをもつ



  3. IPauseを継承したクラスのインスタンスの生成時に、自身のポインタをPauseManagerのリストにわたす

  4. IPauseを継承したクラスのインスタンスの破棄時に、PauseManagerのリストから自身へのポインタを削除する


実装

実際に上のような方針で実装してみた一例

今回はPuaseManagerをシングルトンパターンにして実装していますが必ずしもそうである必要はありません。


PauseManager.h

#pragma once

#include<Siv3D.hpp>

class IPause;

//-------------------------------------------------------
// PauseManager
//-------------------------------------------------------

//シングルトンで実装
class PauseManager final : Uncopyable
{
friend IPause;
private:
PauseManager() = default;

//管理するリスト
std::list<IPause*> m_pauseList;

void pauseAll();
void resumeAll();

//リストへデータの追加
void add(IPause*const pause)
{
m_pauseList.emplace_back(pause);
}
//リストからデータの削除
void erase(const IPause*const pause)
{
s3d::Erase(m_pauseList, pause);
}

static PauseManager& Instance()
{
static PauseManager instance;
return instance;
}

static void Add(IPause*const pause)
{
Instance().add(pause);
}

static void Erase(const IPause*const pause)
{
Instance().erase(pause);
}
public:

//すべてポーズ
static void Pause()
{
Instance().pauseAll();
}

//すべてリジューム
static void Resume()
{
Instance().resumeAll();
}

};

//-------------------------------------------------------
// IPause
//-------------------------------------------------------

//ポーズさせたいクラスはこれを継承
class IPause
{
public:
IPause()
{
PauseManager::Add(this);
}
virtual ~IPause()
{
PauseManager::Erase(this);
}

virtual void pause() = 0;
virtual void resume() = 0;

};

//-------------------------------------------------------

//すべてポーズ
void PauseManager::pauseAll()
{
for (auto&& elm : m_pauseList)
{
elm->pause();
}
}

//すべてリジューム
void PauseManager::resumeAll()
{
for (auto&& elm : m_pauseList)
{
elm->resume();
}
}


今回の実装方法は2つのclassが絡み合うので宣言や定義順を考慮する必要があります。


Stopwatchなどに適応させるために

ここまで実装できたら、実際にStopwatchやEasingContlloerに適応してみましょう。


Main.cpp

class PauseableStopwatch:

public IPause,
public Stopwatch
{
public:
//コンストラクタ
PauseableStopwatch(bool startImmediately = false):
Stopwatch(startImmediately)
{}

void pause()override
{
this->Stopwatch::pause();
}

void resume()override
{
this->Stopwatch::resume();
}

};


これで、PauseableStopwatchのインスタンスはPauseManager::Pause関数を呼べばポーズされるようになりました。

同様にEasingControllerやEffectもやってもいいですが、このようにそれぞれ対応する新しいクラスを作っていては大変なのでtemplateを使用すると1つのクラスですみます。


Main.cpp

/*

Stopwatch Effect EasingControllerはメソッド名が共有なのでtemplateでやんちゃする
*/

template<class Type>
class Pauseable:
public IPause,
public Type
{
public:
//コンストラクタの引継ぎ
using Type::Type;

void pause()override
{
this->Type::pause();
}

void resume()override
{
this->Type::resume();
}

};


using 基底クラス名::基底クラス名;と書くことで基底クラスのコンストラタを引き継いで省略することできます。


サンプル

実際に使ってみる


Main.cpp

# include <Siv3D.hpp>

#include"PauseManager.h"

template<class Type>
class Pauseable:
public IPause,
public Type
{
public:
//コンストラクタの引継ぎ
using Type::Type;

void pause()override
{
this->Type::pause();
}

void resume()override
{
this->Type::resume();
}

};

void Main()
{

Pauseable<EasingController<int>> easing(0,1000,Easing::Quad,5000);

Pauseable<Stopwatch> stopwatch(true);

bool isPause=false;

while (System::Update())
{
if (!easing.isActive())
{
easing.start();
}

if (Input::MouseL.clicked)
{
if (isPause)
{
PauseManager::Resume();
}
else
{
PauseManager::Pause();
}
isPause = !isPause;
}

ClearPrint();
Println(easing.easeInOut());
Println(stopwatch.ms());

}
}



まとめ


  • 経過時間に依存するものはポーズしたいときに管理が大変になる

  • インスタンスへのポインタのリストを作っておくことでまとめてポーズできる