C++オリジナルタスクシステムの紹介

  • 15
    いいね
  • 0
    コメント

はじめに

C++で使用可能なオリジナルタスクシステムについて紹介します。

Webによくある記事は「タスクシステムの内部構造を知り、作ってみる」と言った内容ですが、
この記事は「改良を加えて完成した新タスクシステムを使い、処理を作ってみる」です。
もちろん、新タスクシステムは配布しています。
限定的ではありますが、サンプルも載せています。

タスクシステムを使うとどうなるか

今回はC++ライブラリSiv3Dを使って紹介していきます。
最新のC++コーディングで簡単にアプリやゲームが作れるのでお勧めです。

まずは、以下の処理をMain.cpp「タスクシステムを一切使わずに」書いてみます。
3.gif

Main.cpp
#include <Siv3D.hpp>

class Spark
{
private:
    Vec2        m_Pos;      //座標
    Vec2        m_Vec;      //移動法線
    double      m_Speed;    //移動速度
    Color       m_Color;    //描画色

    bool        m_Destroy;  //消去フラグ

public:
    Spark(const Vec2 & pos, const Color & color)
        : m_Pos(pos), m_Vec(RandomVec2())
        , m_Speed(Random(5.0, 20.0)), m_Color(color)
        , m_Destroy(false)
    { }

public:
    void Update()
    {
        //移動速度が0以下で消去フラグを立てる
        if (--m_Speed <= 0.0) m_Destroy = true;

        //移動
        m_Pos += m_Vec * m_Speed;
    }

    void Draw()
    {
        Circle(m_Pos, 2.0).draw(m_Color);
    }

    bool isDestroy() const
    {
        return m_Destroy;
    }
};

class Fireworks
{
private:
    int         m_Time;     //生存時間
    Vec2        m_Pos;      //座標
    Color       m_Color;    //描画色

    bool        m_Destroy;  //消去フラグ

public:
    Fireworks()
        : m_Time(Random(30, 90)), m_Pos(Mouse::Pos().x, 480.0)
        , m_Color(RandomColor())
        , m_Destroy(false)
    { }

public:
    void Update()
    {
        //生存時間が0以下で消去フラグを立てる
        if (--m_Time <= 0) m_Destroy = true;

        //移動
        m_Pos.y -= 5;
    }

    void Draw()
    {
        Circle(m_Pos, 5.0).draw(m_Color);
    }

    bool isDestroy() const
    {
        return m_Destroy;
    }

    Vec2 getPos() const
    {
        return m_Pos;
    }

    Color getColor() const
    {
        return m_Color;
    }
};

void Main()
{
    //Fireworks配列
    Array<Fireworks> fireworks;
    //Spark配列
    Array<Spark> spark;

    //打ち上げ時間
    int nextTime = 0;

    //システムの更新を行いながら無限ループ
    while (System::Update())
    {
        //花火を15フレームごとに生成
        if (++nextTime % 15 == 0) fireworks.emplace_back();

        //Fireworksの更新
        for (auto & i : fireworks) i.Update();
        //Sparkの更新
        for (auto & i : spark) i.Update();

        //Fireworksの消去フラグを確認して消す
        {
            Array<Fireworks>::iterator it = fireworks.begin();
            Array<Fireworks>::iterator it_End = fireworks.end();

            while (it != it_End)
            {
                if (it->isDestroy())
                {
                    //火花を100個生成(引数で座標と色を引き継ぐ)
                    for (int i = 0; i < 100; ++i) spark.emplace_back(it->getPos(), it->getColor());

                    it = fireworks.erase(it);
                    it_End = fireworks.end();
                }
                else ++it;
            }
        }

        //Sparkの消去フラグを確認して消す
        {
            Array<Spark>::iterator it = spark.begin();
            Array<Spark>::iterator it_End = spark.end();

            while (it != it_End)
            {
                if (it->isDestroy())
                {
                    it = spark.erase(it);
                    it_End = spark.end();
                }
                else ++it;
            }
        }

        //Fireworksの描画
        for (auto & i : fireworks) i.Draw();
        //Sparkの描画
        for (auto & i : spark) i.Draw();
    }
}

花火を作ってみました。
マウスカーソルの位置からFireworksを生成し、消滅する間際に座標と色情報を引き継いでSparkを100個生成します。

動的配列を使い、オブジェクトを追加・削除を行う仕組みはよくありますが、
1.更新や描画を、個別にiterator等で呼び出す処理がMain関数内に必要
2.呼び出す優先順位によっては乱雑になる
3.余計な取得関数が増える
4.別の関数への持ち出しが大変(互換性が無い)
5.一括生成が面倒
などといった問題が、制作を進めるにつれ、発生しやすくなると思います。
Manager系クラスを作って整理を行えば問題はありませんが、タスクシステムを使うとそれらが不要になります。

先ほどの動画と全く同じ挙動をする処理を「タスクシステムを使う」とこのようになります。

Main.cpp
#include <Siv3D.hpp>
#include "TaskSystem/rnfs.h"

class Spark : public Task
{
private:
    Vec2        m_Pos;      //座標
    Vec2        m_Vec;      //移動法線
    double      m_Speed;    //移動速度
    Color       m_Color;    //描画色

    TaskCall    m_Update;   //更新設定
    TaskCall    m_Draw;     //描画設定

public:
    Spark(const Vec2 & pos, const Color & color) : Task()
        , m_Pos(pos), m_Vec(RandomVec2())
        , m_Speed(Random(5.0, 20.0)), m_Color(color)
        , m_Update(this, &Spark::Update, 0), m_Draw(this, &Spark::Draw, 1)
    { }

private:
    void Update()
    {
        //移動速度が0以下で消去
        if (--m_Speed <= 0.0) this->Destroy();

        //移動
        m_Pos += m_Vec * m_Speed;
    }

    void Draw()
    {
        Circle(m_Pos, 2.0).draw(m_Color);
    }
};

class Fireworks : public Task
{
private:
    Vec2        m_Pos;      //座標
    Color       m_Color;    //描画色

    TaskCall    m_Update;   //更新設定
    TaskCall    m_Draw;     //描画設定

public:
    Fireworks() : Task(Random(30, 90))
        , m_Pos(Mouse::Pos().x, 480.0)
        , m_Color(RandomColor())
        , m_Update(this, &Fireworks::Update, 0), m_Draw(this, &Fireworks::Draw, 1)
    { }

    ~Fireworks()
    {
        //火花を100個生成(引数で座標と色を引き継ぐ)
        creates<Spark>(100, m_Pos, m_Color);
    }

private:
    void Update()
    {
        //移動
        m_Pos.y -= 5;
    }

    void Draw()
    {
        Circle(m_Pos, 5.0).draw(m_Color);
    }
};

void Main()
{
    //打ち上げ時間
    int nextTime = 0;

    //システムの更新を行いながら無限ループ
    while (System::Update())
    {
        //花火を15フレームごとに生成
        if (++nextTime % 15 == 0) create<Fireworks>();

        //更新と描画
        TaskCall::All::Update(0);
        TaskCall::All::Update(1);
        Task::All::Update();
    }

    //後片付け
    Task::All::Clear();
}

Main関数に注目してください。
コメントや改行を除いても、以前より明らかにコード量が減っています。
更にFireworksSparkも関数やコード量が減っています。

タスクシステムを使う事でFireworksの生成はCreate<Fireworks>();だけで行えます。

タスクシステムで作ると
1.以下の処理が省略可能
    動的配列
    iterator等で更新や描画関数を呼び出す処理
    オブジェクトの消去処理
    オブジェクトの生存時間
    優先順位の設定
2.場所を問わずCreate関数でオブジェクトを生成できる
    #includeをするだけでCreate生成可能
    foremplace_backを使わずにCreatesで複数オブジェクトを生成可能
    クラス内関数に生成処理が書ける為、取得関数が不要
3.外部に記述していた処理がクラス内部に書けるので、オブジェクト指向らしさが生まれてくる

今回の場合はこれだけのメリットがありました。

タスクシステムをフル活用して作った無双ゲーム「CutterSword」
https://youtu.be/-DAwVHJxftM
グラフィックはほぼ自作なのでお察しクオリティ(OpenGL使用)

「Siv3D で TaskSystem を使ったエフェクト制作」
http://qiita.com/Rinifisu/items/f9841eb7c398e51ada5e

Rinifisu製タスクシステムの環境を準備する

導入方法を紹介します。
最新C++言語機能やMicrosoft独自機能等を使用している為、Visual Studio 2015 以外の動作保証はありません。
2017 は調査中・・・

Siv3D でタスクシステムを使用する

こちらの記事に詳しく載せています。
「Siv3D でタスクシステムを使った制作を行う」
http://qiita.com/Rinifisu/items/945b1a6972760a55c2fe

Siv3D やその他のライブラリでタスクシステムを使用する

今回は上記と同じゲームライブラリSiv3Dを使いますが、他のライブラリの互換性もあります。
DxLibAltseedで動作を確認しましたが、サポートは行っていない為、サンプルソースコードは載せません。

最初にSiv3Dのインストールを行います。リンク先の方法を参考に導入してください。
Siv3Dのダウンロードとインストール

最初のSiv3Dプログラムが動く段階まで完了できましたら、
こちらのURLからタスクシステム配布ページに移動します。
https://github.com/Rinifisu/TaskSystem

「Clone or download」をクリックし、次に「Download ZIP」をクリックしてダウンロードします。
1.png

次に「TaskSystem」フォルダを「Main.cpp」のあるディレクトリに解凍します。
2.png

完了したら、プロジェクトを開き「TaskSystem」フォルダをインポートします。
3.png

次に「Sample」フォルダの「Chapter_1.cpp」のソースコードを「Main.cpp」にコピーペーストします。
そして#include <rnfs.h>#include "TaskSystem/rnfs.h"に置き換えます。

これで準備は整いました。

実行すると、以下のようになります。
1.gif

最後に

サンプルの構造を理解しながら、最後の「Chapter_9.cpp」にたどり着くころには、このタスクシステムを扱えるようになっていると思います。
タスクシステムの内部には、細かな解説を含んだコメントがあります。参考にしてください。

不具合修正や機能追加は頻繁に行っているので、更新確認を行って頂けると助かります。

このタスクシステムのソースコードはMIT licenseです。
その為、作品に組み込む事はもちろん、商用利用や改変も問題ありませんし、報告も不要です。
しかし、自分が作ったように振舞ったり、ライセンス記述を消すような事は行わないようにお願いします。
タスクシステムで発生したトラブル等の責任は負えません。