LoginSignup
26
16

More than 5 years have passed since last update.

自作クロスプラットフォームライブラリ「rlib」のアーキテクチャ

Last updated at Posted at 2016-12-10

rlibとは

rlibはOpenGL ES2ベースの、クロスプラットフォームゲームライブラリです。
現在は以下のプラットフォームで動作します。

  • Windows
  • Android
  • iOS
  • Emscripten(WebGL)
    • グラフィックス、入力のみ
platform image
Windows rl_sample_windows.png
Android IMG_1916_1.png
iOS IMG_1915_1.png
Emscripten rl_sample_emscripten.png

使用する言語は、ゲームロジック部分はすべてC++で、グルーコード1やプラグインのみ各プラットフォームの言語が併用されます。

本記事は具体的なソースコードの解説ではなく、どうソースを分けるか、などといった設計寄りの話になります。ソースは今のところ非公開ですが、設計についてできるだけ公開しようと思います。
「ライブラリを作ってみたい、けど何から手を付けたらいいかわからない」という方も、何かしらのヒントを持ち帰っていただければと思います。

開発の前に

近年、ゲームエンジン2を用いた開発が一般的になりました。ネイティブアプリを作っているけれども、OpenGLやDirectX、あるいはもっとローレベルなグラフィックスAPIに一生触れることがない、というプログラマーは、これからももっと増えていくでしょう。

自分は趣味でライブラリを開発しています。それ自身が楽しく、また、アプリケーションの挙動や寿命をできるだけコントロールしたいからです。
これからライブラリ開発をしようとしている方は、個人の趣味でやるのか仕事でやるのか、特に仕事でやるなら、それなりに時間をかけてどんなことをしたいのか、を考えておくことをおすすめします。

もしチーム開発などで、アプリケーションの完成に集中しなければならない場合、理由なくオレオレライブラリを使いたいと言う人にはならないように(^^;)
そういう場所ではメリットデメリットをきちんと比較し、合意をとっていきましょう。

さて、ここでは個人の趣味という立場を明確にした上で、話を進めていきます。
楽しんでいきましょう!(^^

設計の原則

rlib is

  • 趣味
  • ポータブル
    • プラットフォーム、スペック問わずそれなりに動く
    • 近くにいる人の端末で動く
  • OpenGLのナレッジ蓄積
    • 定型処理
    • バグ回避
  • 起動が速い
  • コンパイルが速い

rlib is NOT

  • 仕事
  • 巨大な機能群
    • 保守できる範囲でやる
  • 徹底した最適化
    • 同上
  • 徹底したデータドリブン
    • コードドリブンもアリ

ディレクトリ構成

rlibのディレクトリ構成です。図はFreeMindで書いてみました。

rlibディレクトリ構成.png

基本的には、①commonにC/C++ファイルを格納し、各プラットフォームのビルド設定から参照するようにする、という仕組みです。

Windowsはシンプルで、②windowsディレクトリ以下のVC++のプロジェクトファイルに、対象ファイルをせっせと追加していきます。

iOSも同様に、③iosディレクトリ以下のXCodeのプロジェクトファイルに、対象ファイルを追加していきます。
ソースファイルの拡張子をmmにするとC++とObjective-Cが混在できるので、グルーコードはmmとして記述します。

Androidは、④androidディレクトリ以下のAndroidStudioプロジェクト内、CMakeLists.txtに、対象ファイルを記述してビルドします3
C++とJavaの中継はJNI(Java Native Interface)経由で行います。
実行時にC++部分のライブラリファイル(so)をJavaからリンクし、動作させます。

Emscriptenは⑤emscriptenディレクトリ以下の作業フォルダで、コンパイルを行います。今回はバッチファイルでコンパイルコマンドを呼んでいますが、実際はmakeなどで呼ぶのが無難でしょう。
コンパイルにはemsdkが必要です。公式サイトの手順を参考に「emcc --bind」コマンドでコンパイルします。
注意点として、おそらくEmscriptenのバグと思われますが、cとcppを同一コンパイル単位で扱おうとすると、不明なエラーで終了します。
この場合は、いったんcのみをコンパイルしたものをbitcodeファイルにしておき、それをcppと一緒にコンパイルしてください。

リソースは⑥assetsディレクトリの中に格納し、copy.batで各プラットフォームのディレクトリにコピーします。
今後もっと複雑なことをやるなら、こちらもconv.rbとかにしていくと思います(私の場合は、ツール作成にrubyかC#を利用することが多いです)。

commonの中のモジュールディレクトリ群については、後で詳しく見ていきます。

起動部分

起動部分の設計です。
rlib起動部分.png

青い太枠の内側がライブラリコード、外側がユーザーコードです。
四角の塗りつぶしは、青がC++、緑がJava、灰色がObjective-Cです。

起動の流れ:
1. ユーザーコードのエントリポイント(WindowsならWinMain関数)から、Frameworkクラスの初期化メソッドを呼び出します。
2. Frameworkから、OpenGLの初期化、ApplicationLauncherクラスの初期化が呼ばれます。
Androidのみ、JNIを経由する都合で、同名クラスがC++とJavaに存在します。
3. ApplicationLauncherから、ユーザーが定義したApplicationMain関数が呼ばれます。戻り値はApplicationBaseを継承したインスタンスです。4
4. 3.で返したインスタンスに対して、初期化、フレーム実行、破棄、デバイスロスト関数が呼び出されます。

プラットフォームごとの駆動方法の違いは、Frameworkクラスが以下のようなメソッドやメンバを持つことで、吸収されます。

  • Windows
    • WinMainに対応するメソッド
    • ウィンドウプロシージャ
  • Android
    • Activityのライフサイクルイベント相当のメソッド
    • GLSurfaceView5のインスタンス
    • GLRendererのインスタンス
  • iOS
    • GameViewControllerのメソッドに対応するメソッド
  • Emscripten
    • mainに対応するメソッド

その他の常時動くライブラリ処理は、基本的にApplicationLauncherに記述されます。以下のような処理があります。

  • 入力処理
  • 描画前処理、後処理
  • サウンド処理
  • デバイスロスト処理

デバイスロストについては後述します。

モジュール群

rlib は、いくつかのサブライブラリ的な機能群に分割されています。
ここでは便宜的にモジュールという言葉を使っています。
(実際はただのサブディレクトリ+ネームスペースです)

rlibにあるモジュールの一部を、カテゴリごとに分類した図です。
rlibモジュール.png

本記事では主要カテゴリのモジュール+おまけいくつか、という感じで、以下の流れでモジュールの中身を見ていきます。

  • フレームワーク
    • rlFramework
  • グラフィックス
    • rlcg
    • rlcgShader
    • rlcgDisplay
    • rlcgTree
  • サウンド
    • rlSound
  • 入力
    • rlInput
  • ユーティリティ
    • rlMemory
    • rlContainer
    • rlFiles
    • その他もろもろ

では順に見ていきましょう。ここは長いので、必要に応じて拾い読みでかまいません。

rlFramework

ユーザーが定義するアプリケーションのベースクラスなどがあります。

  • ApplicationBase
    • アプリケーションのベースクラス
    • ユーザが継承クラスを定義する
    • 初期化、破棄、フレーム実行、デバイスロスト、リストアの5つのコールバックがある
  • ApplicationMain()
    • アプリケーションクラスをインスタンス化する関数のプロトタイプ宣言
    • ユーザが定義する
    • メモリマネージャの初期化はここに記述する
  • Framework
    • 各プラットフォームに対応したメソッドがあり、それを呼ぶ
    • プラットフォームによってはcppではなく、javaなど他言語で提供

ユーザーは各プラットフォームでFrameworkのメソッドを実行し、
ApplicationMain関数とApplicationBaseクラスの継承クラスを実装します。
この中に、後述の各モジュールを利用したコードを記述していきます。

Frameworkの中では、OpenGL/OpenGL ES/WebGL の初期化も行われます。WindowsとEmscriptenではglewを利用しています。OpenGL ESそのものではなく、OpenGLのデスクトップ版やWebGLになりますが、ES2とほぼ同等の機能を有しています。

rlcg

低レベル層のグラフィックスリソースや描画を扱います。

  • Context
    • 画面のクリア、描画
    • ステートの変更
  • VertexBuffer
    • 頂点バッファ
    • カスタム頂点フォーマット可
    • 書き込み可/不可
  • IndexBuffer
    • インデックスバッファ
    • uint16_tのみ
    • 書き込み可/不可
  • Texture
    • テクスチャ
    • png/ブランクテクスチャ
    • ミップマップON/OFF
    • 色/ピクセル位置 による抜き色指定
    • 書き込み可/不可(不可でメモリ節約)
    • Lock/Unlock
  • ShaderPass
    • 頂点シェーダとフラグメントシェーダのペア
    • あまり単体では使わず、後述の rlcgShader::ShaderBase を通して使う
  • FrameBuffer
    • フレームバッファ
    • ブランクテクスチャを内部で保持
  • Font
    • TTFフォントをテクスチャにロードし、保持
    • FreeTypeを内部で使用
  • VramResource
    • テクスチャ、バッファオブジェクト、シェーダのベースクラス
  • VramResourcePool
    • テクスチャ、シェーダをインスタンス化しようとした際、ここのハッシュから検索
    • ハッシュになければ登録
    • 参照カウンタによるキャッシュ管理を行う
  • VramResourceManager
    • VramResourceがインスタンス化された際、自動でリストに登録
    • デバイスロスト時の復元処理を行う

ここではOpenGLの初期化以外の、プラットフォーム共通部分を取り扱います。

テクスチャ、シェーダ、バッファはOpenGLで扱うリソースの単位になります。
読み取り専用に設定したテクスチャを除き、メインメモリにコピーを持っています。書き込み用のバッファ確保の速度を稼ぐためと、デバイスロストの対処のためです。
リソースを有効化し、Contextでレンダーステートを変更して、描画します。

カスタム頂点フォーマットは、速度重視でAoS(Array of Structure)形式固定になっていますが、
最近のアーキテクチャではSoA(Structure of Array)形式が速いケースもあるようですね。
詳しくは@emadurandalさんのこちらの記事を。

デバイスロストとは、もともとWindowsのDirect3D環境で発生していた「GPUメモリにロードしたリソースが、特定のタイミングで揮発する現象」のことを指します。これが発生するとリソースの再ロードが必要になります。
Windowsでは以前はDirect3Dを使用していましたが、今はOpenGLを使用しているため、特に呼ばれることはありません。
iOSも特に呼ばれることはありません。
AndroidではOpenGL ESを使用していますが、端末によってはデバイスロスト現象が発生します。onPauseでロスト処理、onResumeで復元処理を行っています。
Emscriptenではもともとデバイスロスト用のコールバックが用意されています。WebGLはWindows上でDirect3D命令に変換されたり、あるいは様々なデバイスで動作するため、同様の現象が起こりうるものと思われます。

rlcgShader

rlcgの上に構築された、シェーダのラッパークラス群です。

  • ShaderBase
    • シェーダを作成する際のベースとなるクラス
    • 変数の宣言&ハンドル取得の統合
    • ES専用の宣言挿入
    • シェーダコードの挿入
    • ShaderPass 複数保持によるマルチパス対応
  • PNCTShaderBase
    • 頂点属性として位置、法線、色、UVを受け取るシェーダのベースクラス
    • ShaderBaseを継承
  • Unlit
    • ライティング無しシェーダ
    • PNCTShaderBaseを継承
    • Depthソート有無、ブレンドモードが選択できる
  • BlinnPhong
    • Blinn-Phoneシェーダ
    • PNCTShaderBaseを継承
    • Depthソート有無、ブレンドモードが選択できる

シェーダの初期化時に、シェーダコードを動的に挿入する機構を設けることで、重複コードを共通化したり、GPU上での分岐を減らしたりすることができます。
生のシェーダコードだと数が爆発的に増えていくため、このような仕組みを用意しました。

以下にPNCTShaderBaseで頂点シェーダとピクセルシェーダの一部を実装、Unlitでピクセルシェーダの残り部分を実装しているソースを示します。

PNCTShaderBase.h
private:
    bool                        mUseColor;
    bool                        mUseTexture;

    int                         aPosition;
    int                         aNormal;
    int                         aColor;
    int                         aTexCoord;

protected:
    virtual rlcg::ShaderPass* OnInitializeShaderPass() override
    {
        // attribute
        aPosition = AddAttribute(ShaderVariable::Type::VEC3, "aPosition");
        aNormal = AddAttribute(ShaderVariable::Type::VEC3, "aNormal");
        if (mUseColor)
        {
            aColor = AddAttribute(ShaderVariable::Type::VEC4, "aColor");
        }
        if (mUseTexture)
        {
            aTexCoord = AddAttribute(ShaderVariable::Type::VEC2, "aTexCoord");
        }

        // uniform
        uModelViewProjectionMatrix = AddUniform(ShaderVariable::Type::MAT4, "uModelViewProjectionMatrix");
        if (mUseTexture)
        {
            uTextureMatrix = AddUniform(ShaderVariable::Type::MAT4, "uTextureMatrix");
            uBaseMap = AddUniform(ShaderVariable::Type::SAMPLER2D, "uBaseMap");
        }

        // varying
        if (mUseColor)
        {
            AddVarying(ShaderVariable::Type::VEC4, "vColor");
        }
        if (mUseTexture)
        {
            AddVarying(ShaderVariable::Type::VEC2, "vTexCoord");
        }

        // vertex main
        if (mUseColor)
        {
            AddVertexMainCode(R"(
vColor = aColor;
    )");
        }
        if (mUseTexture)
        {
            AddVertexMainCode(R"(
vec4 texCoord = uTextureMatrix * vec4(aTexCoord, 1.0, 1.0);
vTexCoord = vec2(texCoord.x, texCoord.y);
    )");
        }
        AddVertexMainCode(R"(
gl_Position = uModelViewProjectionMatrix * vec4(aPosition, 1.0);
    )");
        // fragment main
        if (mUseTexture)
        {
            AddFragmentMainCode(R"(
vec4 baseColor = texture2D(uBaseMap, vTexCoord);
    )");
        }

        return nullptr;
    }
Unlit.h
    virtual rlcg::ShaderPass* OnInitializeShaderPass() final
    {
        PNCTShaderBase::OnInitializeShaderPass();

        // fragment main
        if (IsUseTexture())
        {
            AddFragmentMainCode(R"(
gl_FragColor = vColor * baseColor;
    )");
        }
        else
        {
            AddFragmentMainCode(R"(
gl_FragColor = vColor;
    )");
        }

        return CreateShader(IsUseTexture() ? "UnlitT" : "Unlit");
    }

C++11のRaw Literal Stringによる記述を多用しているため、ちょっと読みにくいかと思いますが、やっていることとしては、以下のような感じです。

  • PNSCShaderBase.h
    • C++側で、GLSL上の変数のハンドルを受け取るためのメンバ変数を宣言
    • C++側で、変数のハンドルを名前で取得すると同時に、GLSL側の変数宣言コードを埋め込む
    • GLSL側の頂点シェーダコードのすべてと、フラグメントシェーダの一部(色取得処理)を埋め込む
  • Unlit.h
    • GLSL側のフラグメントシェーダの残り(色の乗算部分)を埋め込む
    • テクスチャ使用の有無により、C++側の分岐で挿入すべきGLSLコードを変更する

このように、めでたくシェーダの差分実装ができるようになったわけですが…!
いかんせんトリッキーな記述のため、継承階層が深くなったりすると、保守コストが大きく上昇します。
今のところはベースクラスで追加したシェーダ変数等はコメントに明記する、継承回数に制限を設ける、という運用でやっています。

デザイナと共同でやる場合はこのようなコードドリブン形式ではなく、includeなどを活用したファイルで納品する方が、スケールしやすいのではないかと思います。その場合、ハンドル取得方法は別途考慮した上で、プリプロセッサの実装をがんばりましょう。

rlcgDisplay

rlcgの上に構築された、2Dのラッパークラス群です。
リファレンス実装の位置づけで、これを全く使わないということも可能です。

  • Sprite
    • スプライト
    • 生の頂点構造体を4頂点分持っている
    • SetPosition, SetColor, SetRotation等のメソッドを持つ
    • 頂点の直接操作による任意変形も可
    • 位置は仮想キャンバス(後述)上の位置を指定できる(例: 0.0~320.0)
    • UVは画像サイズで指定できる(例: 0.0~256.0)
  • SpriteRenderer
    • Sprite配列を描画
    • 仮想キャンバスサイズを指定できる
    • テクスチャとそのサイズを指定できる
    • IndexBufferの使用/不使用選択
  • DisplaySystem
    • 2Dの描画ツリー
    • SpriteRendererを内部に持つ
    • キューイング、ソートを行う
  • DisplayObject
    • DisplaySystemで描画するノードのベースクラス
  • ImageDisplay
    • 画像を描画するノード
    • Spriteのラッパー
  • NineSlicedImageDisplay
    • 画像を9スライス6で描画するノード

Sprite系と、そのラッパーのDisplay系に分かれています。UIを構築していくならDisplay系、弾幕を作るならSprite系を生で扱うといった感じです。

Spriteは生の頂点構造体を4頂点分持っており、SetPositionなどの関数を通して操作するか、直接頂点を操作します。
任意変形を可能にしたかったのでこの形式にしていますが、多少制限をかけるとデータの節約ができるでしょう。

SpriteRendererは、Spriteをまとめて描画するという役割と、仮想キャンバスやテクスチャのサイズを設定するという役割があります。
仮想キャンバスとは、Spriteを画素ベースで配置するための仕組みです。
例えば、「テクスチャの(0,0)から32x32の領域を、画面の(100,100)から64x64の大きさで描画する」という感じで指定することができます。
内部的には、指定されたサイズをもとに、位置の変換行列とUVの変換行列を保持し、描画時にシェーダに渡すという仕組みです。

Display系では、ツリー上に相対座標でオブジェクトを配置します。
DisplaySystemがツリーの管理クラスで、ここにDisplayObjectを継承したImageDisplayなどを渡すと、Depthソートなどを行った上で描画してくれます。

古いAndroidなどの場合、スプライト描画にIndexBufferを使わない方が、動作が高速になる傾向があります。手持ちのAndroid端末の中でやや古めのもの(Tegra3)で計測したところ、IndexBufferを使用しないほうが50%高速でした。
また、プリミティブ指定におけるTriangleStripとTriangleListについては、あまり速度差は見られませんでした。今のところは原理上ワーストケースに強いであろうTriangleStripに統一しています。この2つの速度差については、HYPERでんちさんの頂点性能の比較が詳しいです。
ただし、モバイル環境ではピクセル性能の影響が大きいため、上記のような頂点性能の違いが見えてくるのは、十分にスプライトの表示面積を小さくしてからになります。

rlcgTree

rlcgの上に構築された、3Dのラッパークラス群です。
rlcgDisplayと同様にリファレンス実装の位置づけで、これを全く使わないということも可能です。

  • Camera
    • カメラ情報
  • Light
    • ライト情報
  • Material
    • マテリアル情報
  • MeshRenderer
    • メッシュ描画
    • 独自バイナリフォーマット
  • PrimitiveRenderer
    • glBegin-glEnd形式の描画
  • TreeSystem
    • 3Dの描画ツリー
  • TreeNode
    • 描画ノードのベースクラス
  • CameraNode
    • Cameraを保持するノード
  • LightNode
    • Lightを保持するノード
  • MeshNode
    • MeshRendererを保持するノード

直接画面に描画するための情報/Renderer系と、ツリー上で描画するためのTree/Node系に分かれています。

MeshRendererは、colladaを独自バイナリ形式にコンバートしたものに対応しています。(コンバータは別途C#で書かれています)
今だと同様にcolladaから作成する、glTFというフォーマットがあるようですね。まだどれくらい枯れてるか見ていないのですが、場合によってはこちらに統一してしまうのもいいかもしれません。

TreeSystemがツリー描画のコアクラスで、子にぶらさげたノードクラスを描画していきます。
座標変換の計算はキャッシュされ、動かない限りは再計算が走らないようになっています。
不透明と半透明のレンダーキューを持ち、ノード単位のDepthソート、複数カメラ、ライティングに対応しています。

rlSound

サウンド用モジュールです。
rlSound設計.png

  • SoundManager
    • BGM、SEの再生管理

アプリケーションからはSoundManager経由で再生を制御します。
他のクラスはrl_innerに隠ぺいされています。

いわゆるStrategyパターンで動かしてます。Windowsではサウンドカードが刺さってないと初期化が失敗するので、その際は、この図にはありませんがNullSoundManagerImplが起動するようになってます。
Android、iOSではサウンドの特性(再生時間、許容レイテンシ)によってどのクラスを使うべきかが異なるので、ロード時にBGMかどうかを指定します。

AndroidはJNI経由でJavaAPIを呼び出しています。JavaAPI以外に、NDKのOpenSL(≠OpenAL)で実装する方法もありますが、ストリーミング再生関連の情報が少なかったので見送ってます。NDKサンプル以外にいい情報があれば、教えていただけると。

rlInput

入力用モジュールです。
rlInput設計.png

  • InputManager
    • キーボード、マウス、パッド、タッチ状態の取得

アプリケーションからはInputManager経由で状態を取得します。
他のクラスはrl_innerに隠ぺいされています。

WindowsではDirectInputやFrameworkが起点となり、WindowsInputManagerImplで状態の管理が行われます。内部でさらにキーボード、マウス、パッドのクラスを保持しています。

その他のプラットフォームでは、Frameworkが起点となり、
各プラットフォーム用のInputManagerImplの状態を更新、取得できるようにしています。
具体的には、AndroidではActivityのonTouchEvent、
iOSではGLKViewControllerのtouchesBegan/touchesEnded/touchesMoved、
Emscriptenではemscripten_set_mousedown_callback/emscripten_set_mouseup_callback/emscripten_set_mousemove_callback
を利用しています。

rlMemory

メモリ管理を行います。
DebugBreak、バッファオーバーランチェックなども一部サポートしています。

  • OSAllocator
    • 通常のmalloc、free
  • PoolAllocator
    • 固定長プールアロケータ
  • TLSFAllocator
    • 外部ライブラリのTLSFを呼び出す4
  • MultiPoolAllocator
    • PoolAllocator複数 + フォールバックでTSLFAllocator
  • MemoryManager
    • ApplicationMainでアロケータを登録する

独自アロケータを使う際に注意すべきなのは、プラットフォームによってメモリ容量を変えなければならない、ということです。
OpenGLやサウンド関連などのAPIが、内部でnewを使うかどうかが違ったりします。あと内部なので解放順も制御できません(単一スタックアロケータとか無理です)。
けっこう茨の道です。どうにもならん時はOSアロケータでお茶を濁します(完)。
まぁそうなった場合でも、特定の状況でデバッグをする際には大変役に立ちます。

rlContainer

固定長コンテナ群です。最初にサイズを指定した後は変更不可で、コンテナ全体のメモリ配置が不変というメリットがあります。

  • FixedVector
  • FixedList
  • FixedHashTable
  • FixedString

STLコンテナの使用については色々な議論がありますが、私自身は、アプリケーション側がよければ使ってよいと思います。
ただし、「判断はアプリケーション側で行うべきであり、ライブラリ側で強制すべきではない」との考えから、ライブラリの中はこれらを使って管理しています。
機能も最小限にとどめています。

rlFiles

ファイル読み書き用のクラスです。

  • Asset
    • アセット専用のディレクトリ以下で動作する、ファイル読み込み専用のクラス
    • rlcg::Texture などリソース系クラスの内部でも使用される
  • File
    • 書き込み可能なディレクトリ以下で動作する、ファイル読み書きのクラス
    • テンポラリと永続ディレクトリのどちらに保存するか選べる
    • Androidのfopenセキュリティ問題対応済

その他の機能

  • 数学関連
    • ベクトル、行列、クォータニオン、三角関数、乱数など
  • 標準ライブラリラッパー
    • コンソール出力、メモリ操作、アサーションなど
  • JNIディスパッチ
    • Javaの関数を呼ぶためのユーティリティ
  • 暗号化
    • 簡単な暗号化のためのユーティリティ
  • apk改竄チェック
    • apkの改竄を検知するとクラッシュ
  • プラグイン
    • 広告とかSNS連携とか

この中での変わり種は、apk改竄チェックでしょうか。

Androidはapkファイル(実行ファイル)を容易に逆コンパイルできるため、ソースに戻した上で改造し、第三者が再度コンパイルするという、海賊版アプリが出回りやすい環境となっています。
rlibでは、自身のapkを丸ごとzip解凍し、中身が書き換えられていないかのチェックを行う仕組みが用意されています。Java側に書くと、そのチェック処理自体を容易に改竄されてしまうので、C++側に記述します。
その先の、書き換えられているかの判定をどうやるか、どこまでやるかは、アプリケーション次第です(逆に、どういうチェックかがばれると対策の意味がありません)。処理時間と安全性を天秤にかけた上で判定を行います。


駆け足で紹介しましたが、結構ボリューム多いですね…。
気になったところは後から読み返して、何かの参考にしていただければと思います。

以上で設計の解説については終わりです。おつかれさまでした。

今後の予定

そのうちやるかもリスト。
必ずしも全部やるわけではありませんが、いったん書いて、いくつか拾う感じで。

  • Emscripten版の続き
    • サウンド、ストレージ周り対応
  • WebAssembly対応
    • Emscripten版は最終的にこっちに移行したい
    • Binaryenもemsdkに同梱されてくれると嬉しい
  • RasPi対応
  • Androidのゲームパッド対応
  • 組み込みスクリプト対応
    • Lua入れたけど、いったん戻した
    • C風インタプリタ作ったけど、いったん戻した
    • WebAssembly組み込めないか再考中
  • rlcgTree拡張
    • たぶん使う上で色々と問題が出てくるので、都度直す
    • エフェクト関連とかもうちょっと足すかも
    • コンポーネントシステム足すかも(いったん消して2度目)
  • GUI連動
    • C#コードと繋いでみたり

終わりに

こんなことやってるよー、という感じで色々書いてみました。

各プラットフォームで空のプロジェクトを作って、OpenGLを初期化さえできれば、まずまずのところで共通化できるのではないかと思います。
あとはどう拡張していくかですが、これは自分が捻出できる時間と、欲しいもののバランスを考えながら、ちょうどいいと思うところを探っていけばよいかなと。

こういうプログラマ的な楽しみもあるよってことで、何かのヒントになれば幸いです(^-^

初めてのQiita投稿ということで、至らない点も多々あったと思いますが、
最後までお読みいただきありがとうございます!

次の方は@hoge1e3さんです。よろしくお願いします!



  1. 糊となるコード。ここではC++と、JavaやObjective-Cなどを繋ぐためのコードを指します。 

  2. ゲームエンジンとライブラリ、両方の単語が出てきますが、ここではあまり厳密に論じません。一般的にはエントリポイントを握っているかが分かれ目でしょうか。 

  3. AndroidStudio2.2以降で、C++プロジェクト作成時にCMakeLists.txtがついてきます。 

  4. ApplicationBaseのインスタンスは、Frameworkの初期化引数でなくApplicationMainの戻り値として指定します。これはプラットフォームにより、C++以外のエントリポイントがあるという事情によるものです。 

  5. AndroidのすべてのOpenGL呼び出しは、JavaのGLSurfaceViewもしくはそこから呼ばれるC++メソッドのみから行うようにしています。Activity起点で呼び出すと、スレッドが違うためクラッシュします。 

  6. 拡大時に四隅以外が拡大する、ウィンドウ枠なんかでよく使われるやつです。 

26
16
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
26
16