見出し画像

[Siv3D] メッセージを表示するためのスナックバーを作成する


はじめに

ぼくは現在、「ポイント&クリックアドベンチャー」ゲームを作成しております。

開発中の画面

ステージ上の定められた領域をクリックされた際、ヒントや物語のフレーバーとなるテキストを、メッセージとして表示させる必要があります。そこで、スナックバーを作成しました。

スナックバー?

Snackbar is usually placed at the bottom of the screen and provides temporary feedback on user actions.
The entire container acts as a link or an action can be provided as well. Snackbar is used as feedback requiring less attention compared to pop-ups.

LINE Design System

スナックバーは通常、画面の下部に配置され、ユーザーのアクションに対する一時的なフィードバックを提供します。
コンテナ全体がリンクとして機能したり、アクションを提供することもできます。スナックバーは、ポップアップと比較して、より少ない注意を必要とするフィードバックとして使用されます。

DeepL より翻訳
LINE Design Systemより

スナックバーを作成する

Siv3D公式サンプル

Siv3Dの公式サンプル集に、『ゲームのメッセージボックス』というサンプルがありましたので、これをベースに作成しました。

Figmaで見た目を考える

Figmaの画面

まずは、Figmaで見た目を考えます。黒を基調とした、落ち着いた雰囲気にしたいので、黒地に白い文字としました。ただ、ステージや操作キャラクターが見えなくなるのは避けたいため、半透明の黒としました。文字は大きめにし、万人の読みやすさを心がけました。

MessageSnackクラス

Siv3Dのサンプルを参考に、MessageSnackクラスを作成しました。

機能

  • メッセージの表示

  • メッセージの切り替え

  • メッセージ表示の状態管理

メソッド

  • calculateDisplayedCharacters():

    • 表示されるべき文字数を計算します。経過時間から表示されるべき文字数を推定し、最小値として0を設定します。これにより、文字が一度に表示される速度が制御されます。

  • updateMessage():

    • メッセージを更新して描画します。具体的には、`calculateDisplayedCharacters()` を呼び出して表示されるべき文字数を取得し、`drawMessage()` を呼び出してメッセージを描画します。

  • drawMessage(int32 count):

    • 指定された文字数のメッセージを描画します。メッセージボックスを半透明の黒で描画し、フォントを使用して指定された文字数までの部分文字列を描画します。

  • checkMessageFinished(int32 count):

    • メッセージが最後まで表示されたかどうかを確認します。表示が完了しており、かつマウスがクリックされたか、スペースキーが押された場合は、次のメッセージに切り替えます。

  • switchNextMessage():

    • 次のメッセージに切り替えます。ストップウォッチをリスタートして次のメッセージの表示に備え、全てのメッセージが表示されているかどうかを確認し、表示されていない場合は `messageIndex` を増加させます。

  • update():

    • メッセージボックスの状態を更新します。具体的には、`updateMessage()` を呼び出してメッセージを更新し描画し、`checkMessageFinished()` を呼び出してメッセージの表示が完了したかどうかを確認します。

コード

 #pragma  once #include  <Siv3D.hpp> #include  "../consts/clickable_message_event.cpp"

class MessageSnack
{
private:
    const Font font{FontMethod::MSDF, 20, Typeface::Medium}; // フォントの設定
    const Rect messageBox{0, 400, 640, 80};     // 四角形
    size_t messageIndex = 0;                    // どのメッセージか
    Stopwatch stopwatch{StartImmediately::Yes}; // ストップウォッチ
    Array<String> messages;

    // 表示する文字数を計算する
    int32 calculateDisplayedCharacters()
    {
        return Max(((stopwatch.ms() - 200) / 18), 0);
    }

    // メッセージの描画を更新する
    void updateMessage()
    {
        // 表示する文字数を計算
        int32 count = calculateDisplayedCharacters();

        // メッセージを描画
        drawMessage(count);
    }

    // メッセージを描画する
    void drawMessage(int32 count)
    {
        // メッセージボックスを描画
        messageBox.draw(ColorF{0, 0, 0, 0.8});

        // メッセージを描画
        font(messages[messageIndex].substr(0, count)).draw(20, messageBox.stretched(-140, -10), ColorF{1});
    }

    // メッセージが最後まで表示されたかどうかをチェックする
    void checkMessageFinished(int32 count)
    {
        const bool finished = (static_cast<int32>(messages[messageIndex].length()) <= count);
        if (finished && (messageBox.leftClicked() || KeySpace.down()))
        {
            switchNextMessage();
        }
    }

public:
    bool allMessagesFinished = false; // 現在のテキストが全部表示されたか

    MessageSnack(
        const Array<String>
            &messages = {
                U"あのイーハトーヴォのすきとおった風\n夏でも底に冷たさをもつ青いそら",
                U"うつくしい森で飾られたモリーオ市\n郊外のぎらぎらひかる草の波。",
            }) : messages(messages)
    {
    }

    // コピー代入演算子
    MessageSnack &operator=(const MessageSnack &other)
    {
        if (this != &other) // 自己代入チェック
        {
            // メンバー変数のコピー
            messages = other.messages;
            allMessagesFinished = false;
            messageIndex = 0;
        }
        return *this;
    }

    // 次のメッセージに切り替える
    void switchNextMessage()
    {
        stopwatch.restart();
        allMessagesFinished = (messageIndex == messages.size() - 1);
        messageIndex = (messageIndex < messages.size() - 1) ? messageIndex + 1 : messageIndex;
    }

    // メッセージボックスの状態を更新する
    void update()
    {
        // メッセージの更新
        updateMessage();

        // メッセージが最後まで表示されたかチェック
        int32 count = calculateDisplayedCharacters();
        checkMessageFinished(count);
    }
};

使用例

 #include  <Siv3D.hpp>

void Main()
{
    // ウィンドウを作成
    Window::Resize(640, 480);
    Scene::SetBackground(ColorF{0.8});

    // メッセージの配列を作成
    Array<String> messages = {
        U"あのイーハトーヴォのすきとおった風\n夏でも底に冷たさをもつ青いそら",
        U"うつくしい森で飾られたモリーオ市\n郊外のぎらぎらひかる草の波。"
    };

    // MessageSnack オブジェクトを作成
    MessageSnack messageSnack(messages);

    while (System::Update())
    {
        // MessageSnack を更新
        messageSnack.update();

        // 全てのメッセージが表示された場合は終了
        if (messageSnack.allMessagesFinished)
        {
            break;
        }
    }

    return 0;
}

管理用クラス

MessageSnackクラス単体では、ゲーム内で少々扱いづらいです。そこで、MessageSnackに表示するテキストや、更新のタイミングを調整する管理クラスを作成しました。

機能

  • メッセージイベントの管理

  • メッセージスナックの管理

  • メッセージイベントの追加

  • メッセージスナックの描画

  • マウスによる、メッセージスナックの更新

メソッド

  • addClickableMessageEvent(const Vec2 &pos, const Vec2 &size, const Array<String> &texts = {}):

    • クリッカブルなメッセージイベントを追加します。指定された位置とサイズの領域に、指定されたテキストを持つ ClickableMessageEvent オブジェクトが作成されます。

  • draw():

    • 登録されたメッセージイベントごとにメッセージスナックを描画します。各イベントの状態に応じて、メッセージスナックの表示や非表示を制御します。

  • updateMessageWithMouse():

    • マウスの状態に応じてメッセージスナックを更新します。特に、クリックされた場合には新しいメッセージスナックを表示し、既存のメッセージスナックが表示中の場合には次のメッセージに切り替えます。

コード

 #pragma  once
 #include  <Siv3D.hpp> // Siv3D v0.6.13 #include  "../components/message_box.cpp" #include  "../components/message_snack.cpp" #include  "../consts/clickable_message_event.cpp"

class MessageSnackManager
{
private:
    Array<ClickableMessageEvent> messageEvents; // メッセージイベントの配列
    MessageSnack messageSnack;                  // メッセージスナック
    bool isActive = false;                      // メッセージがアクティブかどうかのフラグ

public:
    void addClickableMessageEvent(
        const Vec2 &pos,
        const Vec2 &size,
        const Array<String> &texts = {})
    {
        messageEvents.push_back(ClickableMessageEvent{
            Rect(pos.x, pos.y, size.x, size.y),
            texts,
        });
    }

    void draw()
    {
        // メッセージスナックの更新処理
        for (auto &event : messageEvents)
        {
            // 各状態ごとの処理
            switch (event.state)
            {
            case EventState::Ready:
                if (not isActive)
                {
                    isActive = true;
                    event.state = EventState::Running;
                    messageSnack = MessageSnack(event.texts);
                }
                break;

            case EventState::Running:
                messageSnack.update();
                if (messageSnack.allMessagesFinished)
                {
                    isActive = false;
                    event.state = EventState::Idle;
                }
                return;
            }
        }
    }

    // マウスによるメッセージスナックの更新
    void updateMessageWithMouse()
    {
        // メッセージスナックの更新処理
        for (auto &event : messageEvents)
        {
            // クリック時
            if (event.rect.leftClicked())
            {
                if (not isActive)
                {
                    event.state = EventState::Ready;
                }
                else
                {
                    messageSnack.switchNextMessage();
                }
            }

            // マウスホバー時
            if (event.rect.mouseOver())
            {
                Cursor::RequestStyle(CursorStyle::Hand);
            }
        }
    }
};

使用例

 #include  <Siv3D.hpp> #include  "MessageSnackManager.hpp"

void main()
{
    // ウィンドウを作成
    Window::Resize(640, 480);
    Scene::SetBackground(ColorF{0.8});

    // MessageSnackManager オブジェクトを作成
    MessageSnackManager manager;

    // クリッカブルなメッセージイベントを追加
    manager.addClickableMessageEvent({100, 100}, {200, 50}, {U"Click me!"});
    manager.addClickableMessageEvent({300, 200}, {250, 50}, {U"Click me too!"});

    while (System::Update())
    {
        // メッセージスナックをマウスの状態に応じて更新
        manager.updateMessageWithMouse();

        // メッセージスナックを描画
        manager.draw();
    }
}少々

使用例


終わりに

共有しましたが、いまだ未完成のコードです。今後も改良を続けてまいります。よろしくお願いします。


この記事が気に入ったらサポートをしてみませんか?