Siv3Dを始めてできるようになったこと

  • 6
    いいね
  • 0
    コメント

これは「CCS Advent Calender 2016」18日目の記事です。

ごあいさつ

18日目の記事を担当させていただくだろすと申します。
CCSでは主にプログラミング、稀にDTPを担当しております。

最近はグラフィッカーの友人と一緒に「武龍突 -Blitz-」という2Dシューティングゲーム(2DSTG)を開発していました。
1〜2週間という開発期間の短さもありバグがあったクオリティアップのために公開延期となりましたが、何らかの形でいつか公開したいと考えております。

さて本題ですが、今回は上記STG製作時に使用したC++ライブラリ「Siv3D」を紹介させていただきます。

Siv3Dについて

Siv3DとはC++向けに作られたライブラリであり、主にゲームやメディアアートの開発に役立つ機能が用意されています。CCS会員にはDXライブラリのようなものと言えば伝わるでしょうか。
http://play-siv3d.hateblo.jp/

文字・図形・画像の描画や音の再生、マウス・キーボードの入力、乱数生成、ゲームパッド対応などゲームに欠かせない機能に加えて、異なる図形同士の当たり判定やINI・CSVファイルの入出力、HSVによる色の指定など複雑な処理も簡単に実装することが可能です。

GUIの表示や外部アプリケーション・Webブラウザの起動、Twitterクライアントなどの機能もあり、ゲーム以外の開発にも使えます(CCSの場合は2016年度大学祭展示向けのランチャーアプリにSiv3Dを採用しました)。

導入方法や実際の使い方については上記リンク先のリファレンスをご覧ください。

なぜSiv3Dを選んだのか

機能の多さやコードの簡潔さもありますが、最大の理由は(個人的に)挫折しがちな処理の実装が簡単にできることです。

具体的には当たり判定や動的配列、シーン遷移など、本格的にゲーム製作を始める際の「目に見えない」部分。

ゲーム製作の楽しみの1つが「絵が動いた」「音が鳴った」「弾幕ができた」など成果がひと目で分かる「作ってる感」だと思うのですが、当たり判定などは内側の処理のためゲーム画面には反映されず、どうしても地味で面倒に感じてしまいます。

加えてクラス・ポインタ・Vectorなど求められる知識が一気に増えるので、面倒なうえに難しいという二重苦。
「ミニゲームの開発を経ていざ本格的なゲームを作ろう!」となった時、ここで挫折する人もいるのではないでしょうか。

しかし、Siv3Dではそのような処理も簡単に実装できます。実際にSTGを作って特に便利だと思ったものは以下の通りです。

たった1行で当たり判定が実装できる

Circle、Rect、Triangleなどの図形クラスによって当たり判定が実装できます。

Main.cpp
# include <Siv3D.hpp>

void Main()
{
    Font font(30);

    String text;

    //四角を生成する(横200px。縦100pxの長方形を座標(300, 300)に)
    Rect rect(300, 300, 200, 100);

    //円を生成する(マウス座標を中心とした半径30の円)
    Circle circle(Mouse::Pos(), 30);

    while (System::Update())
    {
        //円の座標を更新する
        circle = Circle(Mouse::Pos(), 30);

        //円が四角に触れているか
        if (circle.intersects(rect))
        {
            text = L"あててんのよ";
        }
        else
        {
            text = L"あててないよ";
        }

        //四角を描画する
        rect.draw();

        //円を描画する
        circle.draw(Palette::Red);

        //文字を描画する
        font(text).draw(10, 10);
    }
}

20161218-135318-757.png

20161218-135322-373.png

上記のように図形の交差判定がcircle.intersects(rect)のみで行えるので非常に楽です。
円が完全に四角に包含されているかを確認する場合はcircle.contains(rect)で行えます。

マウスカーソルやクリックの判定も一行で済むので、マウス操作ゲーの開発も大幅に楽になりました。
以下は実際に製作したマウス操作ゲーのタイトル画面のコードの一部です。

SceneManager.h

//前略

class Title : public Iscene::Scene {
public:
    Title()
    {

    }

    ~Title()
    {
        m_data->count = 0;
    }

    void update() override;

    void draw() const override
    {
        LoadingData::get().texture[L"title"].draw(0, 0);

        Rect(0, 0, 800, 300).draw({ 0, 0, 0, 122 });
        LoadingData::get().texture[L"titlelogo"].drawAt(400, 100);

        m_data->font2(L"最高記録:" + Pad(RankingManager::get().GetScore(), { 6, L'0' })).drawAt(400, 250);

        //ボタン
        sbutton.draw({ 0, 0, 0, 122 });
        m_data->font(L"はじめる").drawAt(400, 375);

        ebutton.draw({ 0, 0, 0, 122 });
        m_data->font(L"おわる").drawAt(400, 475);

        //画面下のTips表示部分
        tipsrect.draw({ 0, 0, 0, 122 });
        m_data->font(tips).draw(20, 560);
    }

private:
    //ボタン
    Rect sbutton = Rect(0, 350, 800, 50);
    Rect ebutton = Rect(0, 450, 800, 50);

    //tips表示
    Rect tipsrect = Rect(0, 550, 800, 50);
    String tips = L"モードを選んでクリックしてください。";
};

//以下略

SceneManager.cpp

//前略

void Title::update()
{
    m_data->count++;

    if (sbutton.mouseOver)
    {
        tips = L"ゲームを開始します。";
    }
    else if (ebutton.mouseOver)
    {
        tips = L"ゲームを終了します。";
    }
    else
    {
        tips = L"モードを選んでクリックしてください。";
    }

    if (sbutton.leftClicked)
    {
        changeScene(L"maingame");
    }
    else if (ebutton.leftClicked)
    {
        System::Exit();
    }
}

//以下略

20161218-132820-740.png

sbuttonという四角オブジェクトを作ることで、描画はsbutton.draw({ 0, 0, 0, 122 })、マウスカーソルが重なったかはsbutton.mouseOver、クリックされたかはsbutton.leftClickedだけで確認できていることが分かります。

図形は描画せずとも当たり判定として利用できるため、画像に当たり判定を持たせる場合は見えない図形を重ねればOKです。
あえて画像の上に図形を描画することで、デバッグ時に当たり判定を確認するという使い方もあります。

CSV読み込みによるステージ作成が簡単

STGのステージ作成といえばCSV読み込みが多いですが、Siv3Dなら簡単に実装できます。

sample.csv
120,100,100,TRUE
140,100,150,FALSE
160,100,200,TRUE
180,100,250,FALSE

左から順に出現までのカウント、X座標、Y座標、描画色フラグです。これを読み込んで敵の生成を行うのが以下のプログラムです。

Main.cpp
# include <Siv3D.hpp>

//敵キャラ
class enemy{
public:
    enemy(Vec2 v, bool c) : coordinate(v), isRed(c), isDelete(false)
    {

    }

    void Move()
    {
        //移動する
        coordinate.x += 5;

        //画面に出たら削除フラグを立てる
        if (coordinate.x > 820.0)
        {
            isDelete = true;
        }
    }

    void Draw()
    {
        //描画する
        RectF(40, 40).setCenter(coordinate).draw(isRed ? Palette::Red : Palette::Navajowhite);
    }

    //削除フラグ
    bool isDelete;

private:
    //座標
    Vec2 coordinate;

    //trueなら赤で描画
    bool isRed;
};

void Main()
{
    Window::SetTitle(L"CCS Advent Calender 2016");
    Window::Resize(800, 600);

    //ファイル名を指定してCSVを読み込む
    CSVReader stageCSV = CSVReader(L"sample.csv");

    //時間経過
    int count = 0;

    //敵
    Array<enemy> enemies;

    while (System::Update())
    {
        count++;

        //敵生成
        for (int i = 0; i <= stageCSV.rows; i++)
        {
            //現在のカウントがCSVで指定したカウントになったら生成
            if (count == stageCSV.get<int>(i, 0))
            {
                enemies.push_back( enemy(Vec2(stageCSV.get<int>(i, 1), stageCSV.get<int>(i, 2)), stageCSV.get<bool>(i, 3)) );
            }
        }

        //敵動作&描画
        for (auto& e : enemies)
        {
            e.Move();
            e.Draw();
        }

        //削除フラグの立った敵を削除する
        Erase_if(enemies, [](const enemy e) { return e.isDelete; });
    }
}

stageCSV.rowsでCSVファイルの行数を取得することで、for文ですべての行をチェックできます。

stageCSV.get<int>(i, 0)のように、データの型とセルを指定することでデータが読み込まれます。
上記のプログラムではCSVのカウントを確認し、現在のカウントと一致するものがあれば敵を生成します。

20161218-142239-122.png

CSVに登録した4体の敵が生成されました。

実際に使用したものでは敵の種類に加えて、他にも攻撃するか、左右反転するかのフラグをCSVに登録しています。ステージ編集の度にいちいちソースコードを書き直さずに済むので開発効率が大きく上がります。

タスクシステムによるオブジェクトの一括管理

Siv3Dにはタスクシステムを実装するためのフレームワークが搭載されており、自機・敵・敵弾など複数のクラスのオブジェクト群をまとめて管理できます。自分が感じたメリットは以下の通りです。

各クラス型オブジェクトの作成・更新・削除が1つの関数で行える。

これまでは複数のオブジェクトを管理する際に、自機・敵・敵弾・・・というように、クラスごとに管理クラスと動的配列を用意し、それぞれに作成・更新・削除用の関数を作っていたため複雑でした。

一方、タスクシステムだと各オブジェクトはタスクとして扱われ、共通のCreate<クラス名>()で作成できるようになります。クラスごとにコンストラクタの引数が異なる場合でも、指定したクラスに合わせて自動で対応してくれます。
更新はTaskCall::All::Update()、削除はオブジェクト内でthis->destroy()を呼び出せばできるので、クラスごとに作成・更新・削除用の関数を自作する必要がありません。

更新・描画順の設定が簡単に実装できる。

各クラスに更新・描画順を設定できるので敵弾が後から追加された敵の下に隠れることがありません。
空中敵と地上敵の描画分けなども楽にできます。

時間経過によるタスクの削除機能が用意されている。

エフェクトやメッセージなど時間経過で削除されるオブジェクトの生成に役立ちます。
「何秒経過したら削除フラグを立てる」と書いていたものが、用意された引数に時間を書くだけで済むようになりました。

まとめ

その他の機能や使い方はフレームワークを製作したRinifisuさんによる解説記事でまとめられています。
http://qiita.com/Rinifisu/items/945b1a6972760a55c2fe

タスクシステムの導入によりVectorやポインタ、イテレータなどであれこれ書いていた処理が簡潔になりました。
タスク内でのタスク作成やタスク同士の当たり判定もサポートしているため、タスクの関係がゴチャゴチャしにくいのも利点でしょう。
数・種類ともに多くのオブジェクトを取り扱うため処理が複雑になりやすいSTG(特に弾幕系)では特にお世話になる機能です。

SceneManagerによるシーン遷移

タイトル、本編、オプションなどのシーン遷移が簡単にできます。遷移時のフェードイン・アウトに対応しているのも魅力です。

ゲームの規模が大きくなるほど各シーンの変数の管理やゲーム開始・終了時の初期化といった手間が増え、ifやswitchによるシーン遷移では不便な場面が多くなります。
その解決策の1つがクラスと継承を利用した手法なのですが、Siv3Dではそれをサポートするクラスが用意されており1から自作する必要はありません。

こちらの機能や使い方はSceneManagerを製作したhamukun8686さんによる解説記事でまとめられています。
http://qiita.com/hamukun8686/items/4620d630b538c78a6e02

結論

個人的には当たり判定、ファイル入出力、タスクシステム、シーン遷移のためだけに導入しても良いと言っても過言ではなく、これらを理由に挫折したという方には胸を張ってオススメできます。
かく言う自分もファイル入出力やオブジェクトの管理に苦労し、Siv3Dに助けられたプログラマーの1人です。

もちろん紹介したもの以外にも多くの機能を備えており、簡潔なコードゆえの可読性の高さも魅力的です。
以前はリファレンスの少なさが指摘されることがありましたが、有志の方々によって日々新しいリファレンスが追加されており、現在ではリファレンスに手を加えるだけで様々なものが作れます。

既に高い完成度にも関わらず、さらなる進化への期待にワクワクもんのSiv3D。
バリバリのプログラマーからミニゲーム職人、自分のキャラを動かしたいグラフィッカーまで幅広い層にオススメします。

19日目の記事を担当するのはてるさん。よろしくお願いします。