7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Siv3DをAndroidで動かそう

Posted at

はじめに

スマホは、割といつも手にしている割に、ほぼ道具としか使っておらず、BLEとかワイヤレス通信標準装備で絶対面白いはずなのに、実際に活かそうとするとパフォーマンス面の不安や開発のしづらさが気になり、どうしても敬遠してしまっていたんですが。
山で使ったり、仕事で使ったり、いろいろとニーズが積み重なってきたので、いつでも使える様に準備がてらチャレンジしてみました。
それに前からうすうす感じてはいたけど、Termux を使えば、Android 上で自由に使えるC++ のビルド環境を整えるのはそんなに難しくない、、、だろう、多分、、いやイケる!!
その辺の妄想を解決すべく、トライした結果、
できました~ Siv3D for Android ~(*^^)v

※forAndroidブランチをクローンしてビルドします。

具体的には、
boost_1_74_0を入手して、Dependency配下に展開
AndroidStudioのOpenFolderでOpenSiv3D\Androidフォルダを開く、
それだけ。
……と言いたいところですが、実際は 1つ罠があります。

肝心のビルドボタンが押せない問題

いきなり最初の難関ですが、
理由は単純で、Android Studio がしょぼすぎてビルドできない。
と言うか、そもそも肝心のビルドボタンが押せません。

手順通り、
Android Studio でプロジェクトを開く(クローンした中の Android フォルダ)
という流れで行くと、起動直後に Updating indexesが走るのですが、boost がでかすぎるのか、
待てど暮らせど 一向に Indexing が終わらない。
更には、LowMemoryでアプリ閉じるボタンすら押せない。
この状態だと、ビルドボタンが非活性で、Sync もできない状態になります。
image.png

回避方法:boost フォルダを一旦どかしてビルドする

Updating indexes 地獄を回避する方法は、
boost_1_74_0 の直下にある boost/ フォルダを物理的にいったん外す必要あります。
理由は単純で、
Android Studio の「Updating indexes」は プロジェクトを開いた直後の最初の一回だけ強制的に走る仕様なので、

最初に開くプロジェクトのファイル量が少なければ Indexing は終わる
Indexing が終わった後なら、フォルダを戻しても 再Indexing は発生しない
という挙動になるので、これを利用して、以下の手順で突破できる。

・boost_1_74_0/boost をどこかに一旦移動(フォルダごと退避)
・Android Studio で OpenSiv3D/Android を開く
→ Updating indexes が数秒〜数十秒で終わる
・カナヅチアイコンimage.pngでビルドする→Boostが見つかなくてエラーになる。
・Android Studio が落ち着いたら、退避した boost フォルダを元に戻す
(この時点では再Indexingは発生しない)
・もう一度、カナヅチアイコンimage.pngでビルドする→そのままビルドできます

image.png
※ここまでくればboostフォルダを元の位置にもどせます

S3Dの系譜

Android版のSiv3Dはベースが、tanaさん作のラズパイ版Siv3D(バージョンは確かv0.6.3)になっています。
また、tanaさん作のラズパイ版は、おそらくnokotanさん作のWeb版で実現されたGLES3.2の実装使われていたとおもいます。
お二人の実装がなければ、ここまでスムーズに移植は、できなかったであろう、、、
この場を借りて感謝いたします。

Termux由来のLib

取り合えずビルドしてみると分かるように、Fullビルドで2分と比較的短時間でAPKを構築できます。
OpenSiv3D\Android\app\src\main\lib\フォルダにあらかじめビルド済みのライブラリファイルを置いています。
Android版のSiv3Dを作ろうとしたときに、どうしても壁になるのが、大量のOSSをClang17 + BionicLIBCのABIで構築するかという部分です。
Termux は、Android 上で Linux 互換環境を実現するプロジェクトなので、Termux を使うことで、Clang 17 と Bionic を前提としたABIで各種 OSS をビルドできます。

Hello,Siv3D for Android!

Screenshot_20251205-220947.png
実行環境としては、LG-V60(Snapdragon865)というAndroid-12のスマホを使っています。
一見、普通に使えます。

一応動きますが、2点問題があります。

マウスとキーボードが無い

デスクトップ用のSiv3Dをモバイルに持ってくると、マウスとキーボードがありません。
流石に、スマホでマウスとキーボードつなげてできました~と言うのも無理があるので、仮想パッド機能(VPad)を作りましたが、まだ完全ではありません。
作ってみてわかったのですが、マルチタッチって点なんですね。一指一点。
指一本でボタン2つを同時押しができないので、めっちゃ使いにくいです。点→円に換えて範囲で押せるようにしないと実用にならないです。

Screenshot_20251205-224857.png

VPadの実装例。


# include <Siv3D.hpp>
SIV3D_SET(EngineOption::Renderer::OpenGLES)

void DrawStick(const VPad *vpad,int16 vk, const Font& font, const Font& debugFont)
{
    if (vk != VKLSTICK && vk != VKRSTICK) return;

    auto stickInfo = vpad->GetStickInfo(vk);
    VPad::ButtonStyle style = vpad->GetButtonStyle(vk);

    // 基本円の描画
    stickInfo.baseCircle.scaled(1.2).draw(ColorF{0.3, 0.3, 0.3, 0.2});
    stickInfo.baseCircle.draw(ColorF{0.3, 0.3, 0.3, 0.3});
    stickInfo.baseCircle.drawFrame(2, ColorF{0.6, 0.6, 0.6, 0.8});
    Circle(stickInfo.baseCircle.center, stickInfo.baseCircle.r * 0.15).draw(ColorF{0.2, 0.2, 0.2, 0.2});

    // スティックつまみの描画
    stickInfo.knobCircle.draw(stickInfo.active ? style.activeColor : ColorF{0.5, 0.5, 0.7, 0.7});
    stickInfo.knobCircle.drawFrame(2, ColorF{1.0, 1.0, 1.0, 0.8});

    if (stickInfo.active)
        Line(stickInfo.baseCircle.center, stickInfo.knobCircle.center).draw(3, ColorF{1.0, 1.0, 1.0, 0.5});

    // ラベルの描画
    font(style.label).drawAt(stickInfo.baseCircle.center, ColorF{1.0, 1.0, 1.0, 0.8});

    // デバッグ情報表示
    if (debugFont)
    {
        const String valueText = U"{:.2f},{:.2f}"_fmt(stickInfo.normalizedValue.x, stickInfo.normalizedValue.y);
        debugFont(valueText).draw(
            Arg::topCenter = Vec2{stickInfo.baseCircle.center.x, stickInfo.baseCircle.y - stickInfo.baseCircle.r - 30},
            ColorF{1.0, 1.0, 1.0, 0.8}
        );
    }
}

void Main()
{
    Window::Resize(1280, 720);

    auto* vpad = VPad::getInstance();
    vpad->Init();

    // ボタン領域登録
    const int btnY = vpad->AddRegion(RectF{1000, 300, 80, 80}, VKBTNY);
    const int btnX = vpad->AddRegion(RectF{900, 400, 80, 80}, VKBTNX);
    const int btnA = vpad->AddRegion(RectF{1000, 500, 80, 80}, VKBTNA);
    const int btnB = vpad->AddRegion(RectF{1100, 400, 80, 80}, VKBTNB);
    const int dpadUp    = vpad->AddRegion(RectF{ 200, 300, 80, 80}, VKUP);
    const int dpadLeft  = vpad->AddRegion(RectF{ 100, 400, 80, 80}, VKLEFT);
    const int dpadDown  = vpad->AddRegion(RectF{ 200, 500, 80, 80}, VKDOWN);
    const int dpadRight = vpad->AddRegion(RectF{ 300, 400, 80, 80}, VKRIGHT);
    const int btnR1 = vpad->AddRegion(RectF{100, 100, 200, 80}, VKR1);
    const int btnR2 = vpad->AddRegion(RectF{100, 200, 200, 80}, VKR2);
    const int btnL1 = vpad->AddRegion(RectF{1000, 100, 200, 80}, VKL1);
    const int btnL2 = vpad->AddRegion(RectF{1000, 200, 200, 80}, VKL2);
    const int btnStart  = vpad->AddRegion(RectF{600, 300, 100, 60}, VK_START);
    const int btnSelect = vpad->AddRegion(RectF{600, 400, 100, 60}, VKSELECT);
    const int stickL = vpad->AddRegion(RectF{420, 500, 180, 180}, VKLSTICK);
    const int stickR = vpad->AddRegion(RectF{680, 500, 180, 180}, VKRSTICK);
    const int btnLeft = vpad->AddRegion(RectF{50, 650, 80, 40}, VKBTNL);
    const int btnMiddle = vpad->AddRegion(RectF{150, 650, 80, 40}, VKBTNM);
    const int btnRight = vpad->AddRegion(RectF{250, 650, 80, 40}, VKBTNR);

    vpad->SetButtonStyle(VKBTNA, U"A", ColorF{0.9, 0.2, 0.2, 0.8}, ColorF{0.4, 0.1, 0.1, 0.5});
    vpad->SetButtonStyle(VKBTNB, U"B", ColorF{0.2, 0.9, 0.2, 0.8}, ColorF{0.1, 0.4, 0.1, 0.5});
    vpad->SetButtonStyle(VKBTNX, U"X", ColorF{0.2, 0.2, 0.9, 0.8}, ColorF{0.1, 0.1, 0.4, 0.5});
    vpad->SetButtonStyle(VKBTNY, U"Y", ColorF{0.9, 0.9, 0.2, 0.8}, ColorF{0.4, 0.4, 0.1, 0.5});
    vpad->SetButtonStyle(VKLEFT, U"←", ColorF{0.6, 0.6, 0.6, 0.8}, ColorF{0.3, 0.3, 0.3, 0.5});
    vpad->SetButtonStyle(VKRIGHT, U"→", ColorF{0.6, 0.6, 0.6, 0.8}, ColorF{0.3, 0.3, 0.3, 0.5});
    vpad->SetButtonStyle(VKUP, U"↑", ColorF{0.6, 0.6, 0.6, 0.8}, ColorF{0.3, 0.3, 0.3, 0.5});
    vpad->SetButtonStyle(VKDOWN, U"↓", ColorF{0.6, 0.6, 0.6, 0.8}, ColorF{0.3, 0.3, 0.3, 0.5});
    vpad->SetButtonStyle(VKL1, U"L1", ColorF{0.8, 0.5, 0.5, 0.8}, ColorF{0.4, 0.2, 0.2, 0.5});
    vpad->SetButtonStyle(VKL2, U"L2", ColorF{0.8, 0.3, 0.3, 0.8}, ColorF{0.4, 0.15, 0.15, 0.5});
    vpad->SetButtonStyle(VKR1, U"R1", ColorF{0.5, 0.8, 0.5, 0.8}, ColorF{0.2, 0.4, 0.2, 0.5});
    vpad->SetButtonStyle(VKR2, U"R2", ColorF{0.3, 0.8, 0.3, 0.8}, ColorF{0.15, 0.4, 0.15, 0.5});
    vpad->SetButtonStyle(VK_START, U"START", ColorF{0.7, 0.7, 0.7, 0.8}, ColorF{0.3, 0.3, 0.3, 0.5});
    vpad->SetButtonStyle(VKSELECT, U"SELECT", ColorF{0.7, 0.7, 0.7, 0.8}, ColorF{0.3, 0.3, 0.3, 0.5});
    vpad->SetButtonStyle(VKLSTICK, U"LS", ColorF{0.5, 0.5, 0.9, 0.8}, ColorF{0.2, 0.2, 0.4, 0.5});
    vpad->SetButtonStyle(VKRSTICK, U"RS", ColorF{0.5, 0.9, 0.5, 0.8}, ColorF{0.2, 0.4, 0.2, 0.5});
    vpad->SetButtonStyle(VKBTNL, U"LMB", ColorF{0.8, 0.2, 0.2, 0.8}, ColorF{0.4, 0.1, 0.1, 0.5});
    vpad->SetButtonStyle(VKBTNM, U"MMB", ColorF{0.2, 0.8, 0.2, 0.8}, ColorF{0.1, 0.4, 0.1, 0.5});
    vpad->SetButtonStyle(VKBTNR, U"RMB", ColorF{0.2, 0.2, 0.8, 0.8}, ColorF{0.1, 0.1, 0.4, 0.5});

    const Font font(32);
    const Font debugFont(24);

    while (System::Update())
    {
        // VPadの領域をループして描画
        for (const auto& region : vpad->GetRegions())
        {
            //アナログスティック
            if (region.vk == VKLSTICK || region.vk == VKRSTICK)
            {
                DrawStick(vpad, region.vk, font, debugFont);
            }

            //ボタン
            else
            {
                RectF rect = vpad->GetButtonInfo(region.vk).rect;
                VPad::ButtonStyle style = vpad->GetButtonStyle(region.vk);

                bool isActive = vpad->IsButtonActive(region.vk);
                ColorF color = isActive ? style.activeColor : style.inactiveColor;
                rect.draw(color);
                rect.drawFrame(2, ColorF{1.0, 1.0, 1.0, 0.8});
                font(style.label).drawAt(rect.center(), ColorF{1.0});
            }
        }
        Circle{ Cursor::Pos(), 40 }.draw(ColorF{ 1, 1, 0, 0.5 });
    }
}

デバイスロストの対策が無い

2つめの問題は、もともとデスクトップ派生のSiv3Dには、デバイスロストの対策が無いのです。
更に、Androidは画面消灯すると、100%デバイスロストします。
どういう事かと言うと、画面消灯するとアプリも落ちます。
その他では、ファイル選択など、AndroidOSのアクティビティに切り替わるとデバイスロストするので、何か対策を入れないと、Helloworld以外の使い道がありません。

対策は、初期化関数を用意する。
です。
初期化関数は、以下の関数が内部でWeakシンボルになっていて、デフォルトの内容は、アプリを終了する記述になっています。
bool Init();

デスクトップ版のコードを使った場合は、Init()関数は無いので、デバイスロスト→Init()→アプリを終了します。
Android用として、Init()関数にアプリが使用するGPUの初期化処理を記述しておくと、デバイスロスト→Init()→初期化処理が実行され、アプリ動作を継続できる仕組みです。

初期化処理の記述例


# include <Siv3D.hpp> // OpenSiv3D v0.6.5
SIV3D_SET(EngineOption::Renderer::OpenGLES)

Optional<Font> g_font ;
Optional<Texture> g_texture ;
Optional<Texture> g_emoji ;
Vec2 g_emojiPos{ 300, 150 };

// 初期化関数
bool Init()
{
    g_font.reset();
    g_texture.reset();
    g_emoji.reset();

    Window::Resize(800, 600);

    // 背景の色を設定 | Set background color
    Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });

    // 通常のフォントを作成 | Create a new font
    g_font = Font{ 60 };

    // 絵文字用フォントを作成 | Create a new emoji font
    Font emojiFont = Font{ 60, Typeface::ColorEmoji };

    // `font` が絵文字用フォントも使えるようにする | Set emojiFont as a fallback
    g_font->addFallback( emojiFont );

    // 画像ファイルからテクスチャを作成 | Create a texture from an image file
    g_texture = Texture{ U"example/windmill.png" };

    // 絵文字からテクスチャを作成 | Create a texture from an emoji
    g_emoji = Texture{ U"🐈"_emoji };

    // 絵文字を描画する座標 | Coordinates of the emoji
    g_emojiPos = Vec2{ 300, 150 };

    return true;
}

void Main()
{
    // 初期化関数を呼び出し
    Init();

    // テキストを画面にデバッグ出力 | Print a text
    Print << U"Push [A] key";

    while (System::Update())
    {
        // テクスチャを描く | Draw a texture
        g_texture->draw(200, 200);

        // テキストを画面の中心に描く | Put a text in the middle of the screen
        (*g_font)(U"Hello, Siv3D!🚀").drawAt(Scene::Center(), Palette::Black);

        // サイズをアニメーションさせて絵文字を描く | Draw a texture with animated size
        g_emoji->resized(100 + Periodic::Sine0_1(1s) * 20).drawAt(g_emojiPos);

        // マウスカーソルに追随する半透明な円を描く | Draw a red transparent circle that follows the mouse cursor
        Circle{ Cursor::Pos(), 40 }.draw(ColorF{ 1, 0, 0, 0.5 });

        // もし [A] キーが押されたら | When [A] key is down
        if (KeyA.down())
        {
            // 選択肢からランダムに選ばれたメッセージをデバッグ表示 | Print a randomly selected text
            Print << Sample({ U"Hello!", U"こんにちは", U"你好", U"안녕하세요?" });
        }

        // もし [Button] が押されたら | When [Button] is pushed
        if (SimpleGUI::Button(U"Button", Vec2{ 640, 40 }))
        {
            // 画面内のランダムな場所に座標を移動
            // Move the coordinates to a random position in the screen
            g_emojiPos = RandomVec2(Scene::Rect());
        }
    }
}

まとめ

Siv3Dの移植みたいな作業は、AIがふつうに使えるので楽になりましたね。
動かすだけ(ハロワ)なら、プロジェクトにSiv3Dのコード突っ込んでビルド通すだけの様な気がします。

問題なのは、もともとオリジナルに存在しない部分をどうするか。。。

仮想Pad(VPad)は、Androidの使い方と、Siv3Dの既存のAPIとの両立とパフォーマンスが悩みで、未だに解決できていない部分あります。

デバイスロストも、結果、付け焼刃な作りなので、手直しは必至な状態です。

カメラについては、デスクトップ版はOpenCVのWebカメラを使いますが、Android版はAndroidOSからのJNI渡しに変更している割に、データ転送がベタなので、いまいちフレームレートが残念な結果になってしまいました。

とまあ、力量不足感もいろいろありますが、ここらで一旦棚上げとしまして。
本家v0.8で、GPUのデバイスロスト対策が実装される事を祈りつつ。

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?