libgdxメモ

このページには、山田がlibgdxを使って得た各種のノウハウをメモしていきます。

ここは ../clan の子ページですが、javaのlibgdxユーザにも有益だと思うので、コード類はなるべくjava表現で書くようにします。

基本

  • http://libgdx.badlogicgames.com/features.html によると、「Libgdx tries not be the “end all, be all” solution. It does not force a specific design on you. Pick and choose from the features below.(以下の表は省略)」との事。この方針によって、同様の他のフレームワークやゲームエンジンよりもずっと自由度が高い。「clojure経由でlibgdxを使おう」みたいな事を考える者にとってはこの自由度は重要な点だ。他の「全部入り」指向のフレームワークは(たとえそれが安全の為であっても)制限が多い。

  • 分からない事があれば、とりあえずWikiに該当記事がないか探してみる。それなりに頻繁に更新されている。

  • 時間がある時に一度、どんな機能があるかをAPIのところから目を通しておいた方がいい。必要な機能が既に実装されている事は多い。
    • 勿論、エディタのリファレンス機能もしくは検索ディスパッチャにも、このAPIからの検索を登録しておく事。頻繁に使う事になる。

ライフサイクル

  • androidのActivity Lifecycleはかなり複雑。しかしandroidアプリを作る上で避けては通れない。

  • libgdxのApplicationLifeCycleは上記androidのlifecycleを扱いやすいようにシンプルにされている(それでいて必要最小限の分離が保たれている)。しかしその代償として分かりにくい部分が増えている箇所があるのと、元々分かりにくい部分も多い為、以下に注意点をまとめてみた。
    • この図ではrender()とひとくくりにされているが、render()の中ではスクリーン等への描画のみではなく、各種の入力値の反映や内部処理も同時に行う必要がある。実際の処理のほとんどはここに書く事になる。
    • PCではpause()→resume()の流れは絶対に起きない為、この部分の動作検証は実機もしくはエミュレータで行うしかない。
      • 0.9.8まではそうだったが、0.9.9になって、PCでもウィンドウのフォーカスが他に移動した時/復帰した時にpause()→resume()の流れが発生するようになったようだ。
        • これによって、ある程度はpause()→resume()の流れがPC上でも検証できるようになったが、GLコンテキストの喪失は起こらないし、pause()→resume()の間であってもrender()がガンガン呼ばれるので完全に同じ処理を行う事はできない。結局自前でandroidとPCで別々の処理を行う必要がある…
    • resume()はこの図を見る限りでは特別な処理はほとんどいらないように見えるが実際はそうではない。詳細は後述の#glコンテキストの喪失を参照。
      • 大雑把には、resume()とpause()は、renderPrepare()とrenderStop()とでも名付けるべきタイミングで呼び出される。
    • この図ではpause()→resume()の間は何も呼ばれないように見えるが、実際にはこの間の中でresize()が呼ばれうる。この為、resize()はresume()に依存してはならないし、resume()もresize()に依存してはならない。
    • この図では分かりにくいが、実機では普通に「ホームボタンを押されてpause()は実行されたものの、続きが再開される前に本体再起動されてdispose()は呼ばれない」のような事が起こりうる。つまりデータの保存はpause()で行わなくてはならない。
    • 後述の#androidのdexキャッシュ問題が起こる。
    • render()内で捕捉されない例外が発生した場合、pause()もdispose()も呼ばれない。これは普通によろしくないので、render()内で例外が発生したら一旦catchし、手でpause()を実行してからrethrowするようにした方がいい。
      • dispose()は前述の通り、正常動作時であっても呼ばれない事があるので、エラー終了する時には実行しなくていいと思う。
      • render()以外での例外の捕捉について
        • create(), dispose()内では当然この処理はいらない
        • resume(), pause()内では例外を捕捉してdispose()を呼ぶ事はできるけれども前述の通り必須ではない
        • resize()は前述の通りpause()→resume()の間に実行されうるので、「例外が出たら常にpause()しとけばいい」という訳にはいかない
          • そんな実装をしてしまったら、例外発生時のみとは言え、pause()が二回連続で実行される状況が起こり、pause()内での処理によっては破滅的な現象が起こりうる
          • 例外発生時でもpause()が一回だけ呼ばれるよう、きちんとフラグ等による判定を行う事。もしそれが面倒なら「resize()内での例外発生時はpause()は呼ばない」実装の方がまだマシだろう

androidのdexキャッシュ問題

  • 二回目起動時に、以前に起動したプロセスの変数の内容の一部が残っている。android本体の電源を落とすとなくなる
    • 具体的には「アクティビティのクラスに属するstatic変数およびインスタンス変数」は初期化され、そうでないクラスのstatic変数およびインスタンス変数は残るようだ。
    • これは単にandroidでは、「戻る」ボタンでの終了時はdispose()が呼ばれるだけでプロセス自体はまだ生きていて、二回目起動時はそのプロセスを再利用するからのようだ。task killer系のアプリで真の生存状況が確認できる。
    • この為、二回目起動時には、clojureの初期化処理等は非常に早く終わる(PCよりも早い)。
      • なお、clojure等で動的にdexコンパイルした内容も残る為、動的にevalして関数の処理を変更した場合、それも残っている事に注意!
        • デバッグ時を除いて、そういう破壊的更新を行わないようにしなくてはならないし、デバッグ時もこまめにtask killer等でkillした方がいいかも
  • 上とは逆に、変数が<cinit>か何かによってクリアされる現象も起こるらしい。ただ、これはライフサイクルのpause()→resume()の時限定のようだ。

まとめ

  • ライフサイクルの図にはないが、「dispose()→create()→…」的なプロセス再利用が起こりうるので、以下を守るようにする事
    • 状態を持つ(つまり副作用を使って更新する可能性のある)変数はすべて、ライフサイクルのcreate()の時点(もしくは最初にアクセスする段階)に必ず初期化を行うようにする。
    • 手動でのファイナライズ処理が必要なものは必ず、ライフサイクルのpause()もしくはdispose()時にファイナライズ処理を実行するようにする。これをさぼってはいけない。
  • clojureの初期化処理等、初回起動時にのみ行えばよいものについては、「非アクティビティのクラス変数」に「初期化済かどうか」等を記憶させる事ができるので、そこを参照して判定すればいいだろう。
  • clojureにてshutdown-agentsを行ってはいけない!行うと二回目以降の起動時にagentが使えなくなってしまう。future等、暗黙の内にagentを使っているものは結構あるので危険。
    • androidではshutdown-agentsを実行しなくても問題ないようだ。ただしdesktop向けではshutdown-agentsを実行しないとまずいので、実行環境を見てshutdown-agentsするかしないかを決めた方がよい

GLコンテキストの喪失

  • android実機にて、アプリ起動中にホームボタンを押したり別アプリを開いたりして、その後にまた戻ってきた時には上のライフサイクルでの「pause()→resume()」が発生する。この時に、AndroidゲームプログラミングA to Zのp.270に書いてある「GLコンテキストの喪失」が起こる。この現象ではPCでは起こせない。
    • 要はOpenGL内に反映させたデータが全部消えるという現象。
    • この時に、ファイルからコンストラクトしたTextureは自動的に再生成されるようになっているが、そうではない、Pixmap等から動的に生成したTextureは再生成されない。
    • またこの時に、Texture内部idの再配布(再利用あり)が行われる為、前述の「再生成されなかったTexture」は、再読み込みされたTextureのどれかが割り当たる現象が起こる
      • 正確には違うかも。しかしとにかく内部のtexture idがおかしくなる
    • どうするのが正解かというと、これらの「GLコンテキストの喪失」によっておかしくなるインスタンスは全て「resume()時に生成」し「pause()時に破棄」するようにすべき。そして上のライフサイクル図を見てもらえば分かるが、起動直後はresume()は呼ばれないので、create()の最後に自分でresume()を呼んでしまうのがよい。

データのセーブとロード

  • Preferences のデータの保存先
    • androidでは、androidのSharedPreferences内。
      • 仕様上では最大8kとなっているが、実質的には最大2k程度。
      • 詳細は、後述の#preferencesについてを参照。
    • windowsでは、 C:\Users\{USERNAME}\.prefs\{PREFNAME}\ 内。
      • androidとは違い、他のlibgdx利用アプリと共通なので、 PREFNAMEはきちんとuniqueな名前にする必要がある!
    • windows以外のdesktopでは、 ~/.prefs/{PREFNAME}/ 内。
      • PREFNAMEについてはwindowsと同じ注意が必要!
  • 上記の通り、Preferencesの保存先はandroid以外では面倒事があるので、PC向けデプロイを考えているなら、androidデプロイ時はPreferencesに保存するが、desktopデプロイ時はPreferences全く使わずに自前でセーブファイルに保存する、等のようにした方がいいと思う。
  • 自前でセーブファイルを扱う場合、何も考えずに Gdx.files.local() 等に書き出すと、PCでは(jarのあるディレクトリではなく)jarを起動した時のカレントディレクトリに書き出されてしまい位置が固定にならないので、結局、 Gdx.app.getType() を見た上で、自分でセーブロードの保存先を適切に判定する必要がある。
    • androidは、そのまま Gdx.files.local() でよい
    • windowsは、jarの置いてあるディレクトリに。 System.getProperty("java.class.path") もしくは `` あたりを元にディレクトリを求める
    • windows以外のPCは、 System.getProperty("user.home")+"/.appname/" あたりがいい?それともwindowsと同じ扱いとすべきか?迷うところ
      • windowsかどうかの判定は System.getProperty("file.separator") から判定するのが手軽(“os.name”だと微妙に不安がある)
  • android実機では「電話がかかってきて中断→そのまま電源オフ」の即死コンボがある為、「セーブポイントでのみセーブ可能」のような状況はまずい!
    • ほとんどすべてのゲーム用データを保持している変数/構造体は、pause()時にシリアライズして上記通りのディレクトリに書き出し、create()/resume()時に読み込むぐらいの気がまえが必要なようだ。

Preferencesについて

  • libgdxのPreferencesはandroidのSharedPreferencesらしいけど、他アプリと共有なの?
    • SharedPreferences 自体はアプリ毎に個別です。ただし、明示的に他アプリのContextを指定する事で他アプリのPreferencesを読み書きする事も可能なので「SharedPreferences」という名前になっているようです。
      • ※ただし、desktop向けデプロイでは単にホームディレクトリ上の ~/.prefs/ ディレクトリに保存されるだけなので、libgdxを使うアプリではごちゃまぜになる為、「androidではprefに保存、desktopではファイルに保存」等と処理を分けた方が無難。
    • libgdxでは getSharedPreferences(name, Context.MODE_PRIVATE) として、 MODE_PRIVATE でハードコーディングされている。よって他アプリからデータが読み取られる事はないようだ。

端末の向き

基本的に AndroidManifest.xml で指定するだけだが、ちょっと癖がある。

  • activity の属性として、 android:screenOrientation を portrait もしくは landscape に設定すると縦固定もしくは横固定になる、通常はこれでok
  • 縦横を柔軟にしたい場合は、 android:screenOrientation を unspecified user behind sensor nosensor のどれかにする(詳細)。
    • しかしこれだけでは、画面の向きが変更になる度にlibgdxのcreate()からやり直しになってしまうので、これと同時に、 android:configChanges に screenSize を足す(詳細)。

サウンド回り

  • androidの音回りは、一定時間無音だと、次に何かを再生する時に0.5秒程度の遅延が発生するようだ。なので、SEを適切なタイミングで出したい場合、BGM入れない場合であっても、無音のoggでも再生しておいた方がいい。そうしないとSEに遅延が発生する。

TextureAtlasおよびTexturePackerの使い方

  • https://github.com/libgdx/libgdx/wiki/Texture-packer
  • 要は、テクスチャは、小さいものを何個も登録するよりも、結合したでかい一つの画像にしてそこから矩形指定して使う方が速度コストが良いらしい。その為のクラスがTextureAtlasおよびTexturePacker。

TexturePackerを使ってpackする

  1. libgdx配布物を自分で展開し、そこに入っている gdx.jar および extensions/gdx-tools/gdx-tools.jar にクラスパスを通しておく(あるいはクラスパスの通った場所にコピーする)
  2. 特定ディレクトリに、packしたいテクスチャファイル(つまり*.png)を全部入れる
  3. 上記ディレクトリに、pack.jsonという設定ファイルも一緒に入れる。省略可能
  4. java -cp 'gdx.jar;gdx-tools.jar' com.badlogic.gdx.tools.imagepacker.TexturePacker2 src-dir dst-dir name を実行する
    • src-dir は、上記の特定ディレクトリを指定
    • dst-dir は、生成結果を入れるディレクトリを指定、省略可能
      • この中に、 pack.atlaspack.png が生成される
      • 省略した場合、 src-dir と同じ階層に、 src-dir-packed/ という名前のディレクトリが生成され、その中に上記2ファイルが作られる
    • name は省略可能、これを指定すると上記2ファイルのpack部分に使われる
  • 上記の src-dir 内に、 pack.json という設定ファイルを入れる事により、pack時の挙動等を制御する事ができる。
    • ぬるぽでpackに失敗する時はおそらく、生成後の画像サイズがmaxWidthもしくはmaxHeightを越えてしまっているので、pack.jsonを入れてこれらの値を増やす。今どきの端末なら4096x4096まで、数年前の端末でも2048x2048までは問題なくいけるようだ。

TextureAtlasを使う

  • https://github.com/libgdx/libgdx/wiki/Texture-packer#wiki-TextureAtlas を参照。以下はその解説
    • TextureAtlas atlas; で領域を確保
    • atlas = new TextureAtlas(Gdx.files.internal("path/to/pack.atlas")); を create() 時にでも行う。この指定で "pack.atlas""pack.png" を両方同時に指定するのと同じ効果があるようだ。
    • Sprite sprite = atlas.createSprite("imagename"); で、 テクスチャの一部からSpriteインスタンスを生成する。この場合、元ファイル名はimagename.pngのものが取り出せる。引数指定時に、元ファイルについていた拡張子は含めてはならない。
    • AtlasRegion region = atlas.findRegion("imagename"); 同様に、AtlasRegionインスタンスを生成。AtlasRegionはTextureRegionとして利用可能。
    • NinePatch patch = atlas.createPatch("patchimagename"); 同様に、NinePatchインスタンスを生成。ただし元画像はninepatch splits(.9.png)形式でなくてはならない。
    • atlas.dispose(); libgdxのライフサイクル通り。アプリ終了時に必ずdispose()する。
    • Array<Sprite> sprites = atlas.createSprites("imagename"); hoge_02.png 形式の連番画像をSpriteの配列として取り出す。
    • Array<AtlasRegion> regions = atlas.findRegions("imagename"); hoge_02.png 形式の連番画像をAtlasRegionの配列として取り出す。

自動packing

その他の特性など

  • デフォルトでは、packされた各画像は最外部の1ドットが引き伸ばされた形式で保存される(つまり元画像が16x16なら18x18になる)。これはGL_LINEARでの拡大縮小時の用途。GL_LINEARを使わないならpack.jsonで切ってもいいし、そのままでもサイズ以外には別に問題はない。
  • android実機でのテクスチャの最大サイズは機種によってまちまちらしい。
    • 1024x1024はどの機種でも大丈夫な様子
    • android-2.3以降なら2048x2048いけるか?
    • 4096x4096がokな機種もあるらしい

NinePatchの作り方と使い方

  • ファイル名は hoge.9.png のようにする
  • 画像の一番外側は透過色になるようにする。その後、一番外側に対して以下の加工を行う
    • 左端の中央および上端の中央にて、黒ピクセルで「パッチ伸張を行う部分」の長さをぬりつぶす
    • 右端の中央および下端の中央にて、黒ピクセルで「ウィンドウ内の描画領域」の長さをぬりつぶす
    • この説明でよく分からなければ、android-sdkのtoolsに入っているdraw9patchを実行し、適当にウィンドウ用の画像を開き、いじってみれば理解できると思う。一旦理解した後はgimp等で直に黒ピクセルを設置してしまえるようになる。
  • 使う際は、前述のTextureAtlasに入れておき、元ファイルがhoge.9.pngならばatlas.createPatch("hoge")でpadding等が上記通り設定されたNinePatchインスタンスが取り出せる。黒ピクセルは勿論除去されている。

BitmapFont

BitmapFontは非常にバッドノウハウが濃縮されている。

BitmapFont概要

  • libgdxで利用できるBitmapFontは、大昔より慣用的に利用されている「*.fnt」形式のビットマップフォントである。
    • fnt形式のビットマップフォントはlibgdx以外でも大抵のゲームエンジンで採用されていたりする汎用的なものである。しかし「(RFC的な)標準化された仕様」というものが存在せず、半分「実装だけが仕様です」状態になっており非常に困る。
      • 自分が調べたlibgdxでの実装(仕様)については下の方に書いた。
  • fnt形式のビットマップフォントは、「文字定義情報」と「全文字入りTexture」から構成される。
    • 「文字定義情報」は、通常は*.fntファイルによって定義する。
    • 「全文字入りTexture」は、通常は*.pngファイル。libgdxでは生成済のTextureやTextureRegionを直に指定する事もできる。
  • fnt形式のビットマップフォント(具体的には、上記の*.fnt*.pngのペア)を生成するソフトは結構あちこちで公開配布されている。しかしlibgdxには「Hiero」というビットマップフォント生成ツールが付属しているので、これを使うのが無難。
  • libgdxは内蔵でascii範囲のみのArialフォントが組み込まれている。引数無しでBitmapFontインスタンスを生成するとこれが利用できる。
  • libgdxのBitmapFontは、いわゆるUnicodeの内、いわゆるCJK文字の辺りまでは普通に利用できる(CJK以外のより複雑なUnicode仕様がどこまで完全に満たされているかは不明)。
    • ただし勿論、漢字を一通り使おうと思ったら結構なTexture領域と定義が必要。

Hieroを使ったBitmapFont生成

  • 基本的な使い方は、Hieroのページに書いてある通り。
    • そこのページにはHiero以外のfnt生成ソフトの簡単な紹介もあり、それらを使ってもよい。ただし先に書いたように、fnt形式のビットマップフォントの仕様は標準化されておらず実装依存の部分が多い為、libgdxで使うのであれば、libgdx付属のHieroで生成するのが一番無難だと思う。
  • ../clanではmake hieroで起動できるようにしておいた。
  • 具体的な作成手順
    1. 左上の「Font」項目にて、フォントを選択し、そのサイズ等も設定する。
      • 利用用途にもよるがフォントのライセンスには注意する事(assets#フォントも参照)。
      • RenderingをNativeにするとOSのレンダラによってフォントが描画されるので、より綺麗にレンダリングされる場合がある(OSによる)。しかしGlyphの圧縮詰め込みが行われなくなってしまう(下のRendering表示のところで、Glyph cache表示にしてみると分かる)。なので基本はRenderingはJavaを選択した方がよい。
      • いわゆるJIS第一水準までの漢字全部を含み、Size=16、Rendering=Javaの状態で生成すると、生成Texture画像が1024x1024にギリギリに収まるかどうかぐらいの大きさになる。
    2. 上部中央の「Sample Text」項目にて、使用する文字の一覧を設定する。漢字を含めたい場合は、利用する(可能性のある)漢字全部をここに書く。
    3. 右上の「Effects」項目にて、文字にかけるエフェクトを複数選択できる。最初はColorを白に設定するだけでよいと思う。
      • おすすめは「Distance field」。ただしビットマップ生成にものすごく時間がかかる。また、Distance fieldを使う場合は後述のPadding値も適切に設定した方がよい。
    4. 左下の「Rendering」項目にて、「Glyph cache」表示を選択し、適切に「Page width」と「Page height」の設定を行う。
      • これは要は、生成する*.pngの大きさ設定の事。この縦横サイズよりも実際の文字の方が多い場合は、*.pngが複数に分割されてしまう(つまりPagesが複数になる)。分割されると管理が面倒になるのでなるべく一枚に収まるように設定する。
        • ただしandroid実機で扱えるTextureの最大サイズは、「全ての端末で扱える=512x512」「最初期の端末を除く=1024x1024」「数年前ぐらいレベルの端末=2048x2048」「最近の端末=4096x4096」、みたいな感じ。あまり無茶に大きくしない事。
    5. 右下の「Padding」項目にて、1文字の上下左右に埋め込む空白ピクセルの量と、全文字のサイズに対する文字サイズの修正量を指定できる。
      • Paddingはマイナス値も設定できるがあまりにマイナスにしすぎるとエラー例外が出てHieroを再起動せざるをえなくなる。この辺はあまり無茶な値を設定しない事。
      • 後述するLinear描画を使う事を想定し、最低でも1ずつPaddingしておくの推奨。表示時に文字間隔が空くのが嫌な場合は、Paddingで増やした分だけ、xとyをマイナスすればよい(マイナスした分は右と下から引かれる事になる)。
    6. 全て設定し終わったら、「Rendering」項目内にある「Reset Cache」ボタンを忘れずに押す。これを押さないと、テクスチャ内でも文字の配置位置が最適化されない。忘れないようにする事
    7. 左上の「File」より、「Save BMFont files」を選択する。これで*.fnt*.pngが生成保存される。
    8. 一応この設定内容も、左上の「File」より、「Save Hiero settings file」から保存しておく。これで*.hieroファイルが保存される。
      • 保存されない項目が何個かあるようなので、このセーブロード機能にはあまり頼らない方がよいようだ。
    9. 上記にて生成した*.fntファイルにはいくつかの問題がある為、テキストエディタで開いて以下の加工を行う。この変更内容の詳細は#bitmapfont書式にある。
      • commonの行の、lineHeightのサイズを適切に設定する
        • デフォルトではかなり行間が広く取られている。日本語メインで扱う場合、フォントサイズ16で作ったらlineHeightは16~20ぐらいで良いと思う。
      • lineHeightの変更に伴い、commonの行の、baseのサイズを適切に設定する
        • 行間を狭くしたなら、base値もいくらか減らす必要がある。具体的にどれぐらい減らすべきかはフォントによる。実際に表示させての微調整が必要かも。
        • 本来の意味論的には、lineHeight=base+行間隔になる。具体的には、漢字の高さと同じ値をbaseに設定すべき、という事になる。しかし詳細は後述するが、実際の文字高さはbase等ではなく「M」のheight値によって決定されているので、base値は単に文字全体を上下させるパラメータとして利用しても構わないと思う。
      • char id=32の行のxadvanceの確認
        • id=32は半角スペース。フォントによってはxadvanceつまり横幅が変な値に設定されてしまう事がある。
      • 日本語を扱う場合、char id=12288の行を新規追加する
        • id=12288は全角空白。何故かHieroから認識されず除外されてしまう。前述のid=32の半角スペースの行をコピペし、idを12288に書き換え、xadvanceも半角の倍に設定すればok。
      • char id=12288の行を増やしたのに伴い、chars行のcount値を1増やす
      • 等幅フォントとして生成する場合は、全ての行のxadvanceを適切に書き換える
      • 最後に、char id=77の行(「M」の文字)の設定をいじる事を考慮する。詳細については後述の#bitmapfontバッドノウハウ#bitmapfontチューニングポイントを参照。

BitmapFontの準備

  • *.fntおよび*.pngを、普通にassetsとして含めておく。
    • *.pngの方はTextureAtlasとして、他のTextureと一緒に固めてしまう事も可能。

BitmapFontの描画

  • http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/graphics/g2d/BitmapFont.html辺りを見て、draw()を実行すればok。
    • ※BitmapFontは他のTextureやRectangle等と違い、指定座標を「左上の角」とするrectとして描画される(Texture等はデフォルトでは指定座標が「左下の角」に相当する)。非常に困る。
      • 例えば、座標に(0,0)を指定すると画面の外に描画される事となって何も表示されず、(0,フォントの高さ)としてはじめて左下に表示される。
      • よく分からない場合は、BitmapFontのコンストラクタの第二引数をtrueにしてみるとよい。これをtrueにすると他のTextureやRectangle等と同じ領域に描画されるようになる(つまり指定した座標を左下とするrectとして描画される)。しかし当然上下が逆になる。
        • これを行うのであれば、ビットマップフォント生成時に上下を逆にしておく必要がある、が、Hieroにはそういう機能はついてなかった
    • 複数行を一度に描画する場合はdrawMultiLine()drawWrapped()を使う。
    • BitmapFontの描画は各文字の座標計算が含まれる為そこそこコストがある。このコスト軽減の為にhttp://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/graphics/g2d/BitmapFontCache.htmlがある。実際のコードではほぼこちらを使う事になる。
    • BitmapFontを描画する際に、文字サイズを変更してもジャギーにならないよう綺麗に表示させたい場合は、BitmapFontの文字TextureにLinearフィルタを設定する。
      • 具体的にはhttp://stackoverflow.com/a/18062590みたいな事。勿論描画コストは上がる。またBitmapFontのTextureをTextureAtlasに入れてた場合、他の画像にまでフィルタが設定されてしまう点に注意。
        • ※上記リンクのコードは、テクスチャが一枚だけのフォントでしか有効でない。テクスチャを複数持つフォント(漢字入りだと複数になりがち)では、getRegion()ではなく、getRegions()を使って、全部のテクスチャに設定を行う必要がある。
      • この際には、Linearフィルタ固有の問題として、*.png内での文字同士の間隔が十分に取れていないと、隣の文字からの色漏れが発生してしまう事がある。
        • Hieroでの生成の場合はきちんとPaddingを設定しておく事。
    • 既存の(プロポーショナルな)フォントを等幅フォントとして利用したい場合は、setFixedWidthGlyphs()を使う。
      • 一番横幅が大きい文字に合わせられる。この仕様が困る場合は*.fntをいじるしかない。
      • 具体的には、 http://legacy.e.tir.jp/fnt_japanese_chars.txt を全角と半角とで分割して文字列化し、全角→半角の順でsetFixedWidthGlyphs()を実行する(全部一緒に実行すると、半角文字まで全角文字の横幅になってしまう)。

BitmapFont書式

前述の通り、fnt形式のビットマップフォントは「(RFC的な)標準化された仕様」が存在せず「実装が仕様です」状態になっている。なのでlibgdxの挙動から書式を調べてみた。

  • info行 : この行は、このフォントがどのようなものなのかを示す為の情報行であり、実際の描画の際には全く活用されないようだ(少なくともlibgdxでは)。
  • common行 : このフォント全体に影響するような項目。
    • lineHeight : 「行間を決定する為の」一行の高さ。主にdrawMultiLine()系やdrawWrapped()系に影響する。改行を含まない場合のgetBounds()系で得られる高さには影響しない点に注意。
    • base : 一行の文字のベース高さ。これを上下させる事で全ての文字をまとめて上下にスライドさせられる。
    • scaleW, scaleH : 文字領域全体のサイズ。変更する必要はない。
    • pages : 総ページ数。変更する必要はない。
    • packed : ???
  • page行 : 前述のcommon pagesの数だけ行が存在。
    • id : このページのid。
    • file : このページの元ファイル名。BitmapFontのコンストラクタに*.fntだけ指定した場合は、(同じディレクトリにある)このファイル名からTextureがロードされる。
  • chars行 : char行が何行あるかを示す為だけの行
    • count : char行が何行あるかを示す。Hieroではマイナス1オリジンで出力される(つまり5行あればcount=4になる)。libgdxではこの情報は別に利用してないように思える。
  • char行 : 文字一つの定義行。前述のchars countの数+1だけ行が存在。
    • id : Unicode番号。ucs2範囲(つまり65535まで)は普通に使えるのを確認済。
    • x, y, width, height : この文字の転送元Texture内のrect情報。基本的に切り詰められた領域が指定されている。
    • xoffset, yoffset : 前述のrectを描画する際の、描画先座標に対するオフセット値。前述の通り、転送元が切り詰められている為、その切り詰め分を戻す為に使われる。マイナス値も指定可能。
    • xadvance : この文字の横幅。この文字が描画されると、次に描画される文字はx座標がこの値だけ進んだ位置を基準に描画される事になる。
    • page : この文字がどのページに存在するか。前述のpage行のid値を指定する。
    • chnl : ???
  • kernings行 : kerning行が何行あるかを示す為だけの行
    • count : kerning行が何行あるかを示す。Hieroではマイナス1オリジンで出力される(つまり5行あればcount=4になる)。libgdxではこの情報は別に利用してないように思える。
  • kerning行 : 詳細不明

BitmapFontバッドノウハウ

  • libgdx組み込みのArialフォントを使う際は font.setFixedWidthGlyphs("0123456789") しておくとよい
    • ここにアルファベットまで含めると数値の文字間が空きすぎて見た目が悪くなる
  • BitmapFont描画の基準座標が「左上の角」である事を忘れないようにする(詳細は#bitmapfontの描画参照)。
  • *.fntのcommon lineHeightの値と、getBounds()で得られる高さとの相関関係について
    • #bitmapfont書式にも書いているが、lineHeightは「行間を決定する為の」一行の高さであり、drawする文字列が一行だけの時はこの値は全く使われない。ではその時は何によって一行の高さが決定されるのかと言うと、getCapHeight()の値が使われている。
      • getCapHeight()が返す高さとは、具体的には「半角大文字Mの高さ」。つまりchar id=77のheight値。yoffsetは考慮されない。
    • つまり結果として、*.fntにて「common lineHeight=16」かつ「char id=77 height=14 (つまりMの文字の高さが14)」の時に、三行の文字列をdrawMultiLine()する際のTextBoundsの高さは、16+16+14=46、という事になる。

BitmapFontチューニングポイント

  • Linearフィルタをかける事を前提に、*.png内での文字間隔を十分に取るべき。
    • HieroでPaddingを設定する。
    • 今は別にLinearにしなくてよくても、将来に別のゲーム等にフォントを流用した時にLinearフィルタつけたくなる可能性は高い。だから文字間隔は最初から十分に取っておいて損はない。
      • ちなみに、フォント側のサイズ指定が等倍のままであっても、カメラの方をいじって拡大縮小すると、フィルタが設定してあればそれが適用される。
  • 他の画像と一緒にTextureAtlasにまとめるべきか?
    • ascii範囲のみであれば、まとめてもよい。ただしまとめる場合はLinearフィルタの有無は全体と統一を取る事。
    • 漢字含む場合は、まとめるべきではない。
  • *.fntいじり
    • ゲーム等では、フォントは等幅の方が圧倒的に扱いやすいので、なるべくxadvanceの等幅加工をしておきたい。
      • setFixedWidthGlyphs()でもよいが、横幅が一番大きい文字に揃える加工しかできないので扱いづらい
      • まず cat hoge.fnt | perl -lne 'print $1 if $_ =~ /xadvance\=(\d+)/'|sort|uniq -c みたいにして、xadvanceの値の分布を調べる
        • 少しだけ中途半端なxadvance値がある場合、文字化けみたいな事になってる場合がある。そこは手で、別の文字を表示するように変更する等するとベスト(とは言え通常使わなさそうな文字であれば放置でもok)。
        • 「半角文字系」と「全角文字系」の二つに大体分かれたら、それぞれを一括置換で適切なサイズに揃える。
          • 「全角文字系」はまず問題はないが、「半角文字系」はフォントによっては、ascii範囲のみに限らない(ギリシャ/ロシア系等もこちらに分類されたりする)ので、一応その辺は確認しておく事。とは言え通常はこれらの文字が「半角幅」になってもまず問題はない(顔文字とかで使う際には影響はあるが)。
    • char id=77 「M」の設定
      • フォントによっては「半角Mよりも全角漢字のheightの方が大きい」という事が普通にあり、それが微妙に困る時もあるので、「M」の文字のheightを他の全角漢字レベルに大きくすると同時にyoffsetを減らす事で、getCapHeight()の値の調整を行う。
        • この際には、いじった「M」の文字だけ領域が大きくなる為、Linearフィルタ用の文字間隔がなくならないように注意する必要あり!
          • HieroにてPaddingを設定する際に、この「M」の増加量の半分程度の値を、下方向に余分に増やしておけば大体足りると思う。
        • これも cat hoge.fnt | perl -lne 'print $1 if $_ =~ /height\=(\d+)/'|sort|uniq -c みたいにして、heightの値の分布を調べておいた方がやりやすい。
        • フォントにもよるが、各文字のheight値の大きさは「罫線系」「jやφ等の一部のアルファベット/ギリシャ文字」「漢字系」「大文字のアルファベット等」「その他」の順になっている(これは*.pngを見れば、大きいもの順に並んでるのですぐ分かる)。
          • これだけ見ると「罫線系」「j等」のheight等をいじりたくなるが、これらは敢えてそのままにしておいた方がいい。下手にいじると揃わなくなる。ただしこれらは「M」への基準には使わない。「漢字系」のheight値を使って「M」をいじる。
      • 困る場面はほぼTextBounds回りなので、「TextBoundsはwidthのみ利用し、heightについては常に自分で計算した値を使う」というポリシーにして回避してもよい。

そして完成したBitmapFont

「M+ 1m medium」をベースにした、上記仕様を完全に満たしたビットマップフォントを作った。

パフォーマンスチューニング

http://www.infoq.com/jp/articles/9_Fallacies_Java_Performance の「結論」より

どのようなJavaプロセスを実行 (開発も実運用も) する場合,
最低限でも以下のフラグは常に設定するべきです。
-verbose:gc (GC ログの出力)
-Xloggc: (より包括的な GC ログ出力)
-XX:+PrintGCDetails (詳細な出力用)
-XX:+PrintTenuringDistribution (JVM が Tenured として扱うしきい値を表示する)
  • android上で動かす際にはさすがに設定できないが、とりあえずPC上でデバッグしている間は付けて損はないようだ。

Mavenパッケージ

  • http://search.maven.org/#search%7Cga%7C1%7Cgdx あたりにある。
  • 警告!mavenセントラルリポジトリには「com.badlogicgames.gdx.*」というパッケージ名で登録されているが、実際のクラス名は「com.badlogic.gdx.*」になっている。よって、maven/leinにてパッケージを指定する際には前者を、実際のソース内でimportする際には後者を指定しなくてはならない。Java sucks!

その他

  • desktopのみ対象にしてマウスの右クリックや移動を検知するには、InputProcessorを使う → http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/InputProcessor.html

  • Gdx.files.internal("path/to/dir/").list() で、assets内の特定ディレクトリ内のファイルを取得できるが、これはdesktopでは動かない。理由は、apkではインストール時にファイルが展開されるのでFSとしてアクセスできるが、jarおよびexeではファイルが展開される訳ではない為。
    • 対策としては、単にコンパイルするタイミングで、特定ディレクトリ内のファイル一覧を取得してdumpしておけばよい。
  • desktop向けでは、ウィンドウの移動中やリサイズ中は完全にゲームが一時停止し、その検知はメインスレッドからは行えない(少なくともwindowsでは)。

  • render()内では、まず最初に画面への描画を行い、その後にセンサ類からの入力値取得や更新処理を行うのがいいと思う、多分。
    • GLへの指示を先に行う事で並行作業的なメリットが得られるのではという考え(しかし実際にそうなのかは未確認)。
  • assetsディレクトリに置いたファイルは、apkやjarに固められる際に、jarのrootに配置されてしまうので、クラス名と衝突しなさそうな名前のディレクトリを掘って、そこに入れた方がいい
    • libgdx付属のボイラープレートでは「data/…」に配置するようになっていた
    • 何の事を言っているのか分からない場合は、自分で作ったjarを jar tf path/to/xxx.jar してみて、中身の構造を見てみれば分かる
  • postRunnable()について
    • 単なる遅延実行用途にも使える(が後述の問題があるのでなるべく頼らない方がいいと思う)。
    • postRunnable()で渡したコードの実行時に例外が出ると、スタックトレースは出るがプロセスは死なないようだ。
      • これは意外と困るので、何らかの対策を行った方がいい。
    • postRunnable()の実行自体はrender()の中であっても、postRunnable()で渡したコードが実行されるのはrender()の中ではない!よってtry~catchやdynamic-binding等の、効果範囲が動的スコープなものの効果が出てなくて死亡、という事がよくある。
      • 「注意する」以外に何らかの対策ができればいいが、可能なのか?
  • Gdx.input.justTouched() と Gdx.input.isTouched() は、実機では稀に不整合状態になるタイミングがあるようだ。
    • おそらくだが、どちらかもしくは両方がリアルタイムでの判定の為、実際にそれぞれの結果を取得/変数保存するタイミングにずれがあり、その間に状態が変化すると、不整合状態になるのではと思う。
    • 対策は色々あると思うが「ある一つの判定処理内にて、両方を同時に使う事がないようにコードを組む」のが一番簡単だと思う。
  • libgdxでは基本的に小数は全部floatなので、引数エラーになったらfloat化してみる。

  • assets にも少し関連情報を書いた

余裕があればチュートリアル的なものも書きたいが…そんな余裕はない!

外部リンク


provided by 技情研ネット.