2017年1月9日月曜日

バーチャルパッドのボタンをUIButton/ButtonでやるのはNG

スマホのバーチャルパッドって操作し難いですね。
知ってたことですが。
改善の余地が色々あって楽しい箇所ではあります。

前々回の記事でファミコンのA/Bボタンの配置について触れましたが、今回はその中身の実装について触れます。
最初、上記のA/Bボタンをそれぞれ UIButton(AndroidならButton)クラスを使って実装したのですが、それだと実際のところかなり操作し難いです。

どうすれば、改善できるだろうか?
まず、昔実機でファミコンをプレイしていた時に、どうやってコントローラを操作していたのか思い出してみることにしました。
そして、スーパーマリオで「Bダッシュをしながらジャンプ」をする時、親指の先でBボタンをホールドした状態で少し右(Aボタン寄り)にスライドして親指の腹でAボタンを圧迫することでジャンプしていた事を思い出しました。

これはiOSやAndroidの標準のボタンでは処理できない操作です。
何故なら、Bボタンをタッチしたイベントは、Aボタンの方でスライドしてもBボタンのイベントとして処理されてしまうので。

そこで、ボタンをUIButtonからタッチ判定の無いUIImageViewに変更して、その上にA/Bボタン両方を覆いかぶせるようにUIViewを配置し、そのUIViewでタッチイベントを処理するようにしてみました。

変更時のcommit
①タッチ判定の変更
②中央付近で両方のボタンを押せるように修正

タッチエリアのイメージは下図のような形です。
処理的には、
・タッチ位置がタッチエリア外ならA/B両方OFF
・タッチ位置が重なるボタンをON
・タッチ位置が中央付近(※若干の遊び領域あり)ならA/B両方ON
という形。

このようにしてみたところ、バーチャルパッドでも「スーパーマリオでBダッシュしながらジャンプ」といった操作ができるようになりました。

スマホのバーチャルパッド自体が邪道だと思っていますが、それでも作るのであれば、可能な限り操作し易いものを作りたい。

ただし、ランドスケープのバーチャルパッド・・・お前はダメだ。

ゲーム画面にバーチャルパッドを重ねるという行為が許せません。
これは、ゲームコンテンツに対する冒涜だとすら思っています。
しかし、実際GooglePlayで公開されているエミュレータアプリのデザインを見てみると、そういう配置のものが多いこと...
そういう配置にせざるを得ないことは理解できますが、それでも何故、そんなクソみたいなデザインにしたのかと。

今回、一応ランドスケープでも動作できるように作っていますが、
このように、バーチャルパッドの無い美しいデザインにしてやりました。
現時点ではランドスケープでは何も操作できません。
観賞用ですね。
もちろん、将来的には外付けのキーボード or ジョイパッドで操作できるようにするつもりです。

ランドスケープの場合、キーボードやジョイパッド必須(持っていないならポートレイトでプレイしてね)って仕様にすべきだと思っています。

そうすれば、
・ユーザーは持っていなければポートレイトでプレイすれば良い
・開発者はクソみたいなバーチャルパッドのデザインを拒否できる
・外付けコントローラを売っているメーカーが潤う
という具合に、ユーザー・開発者だけでなく新たな第三者までもが得をするという素敵な形になります。

更に言うと、そもそもランドスケープだとスマホの画面をタッチ操作し難い(スマホはポートレイトで持つ前提だから縦長になっている)という構造的な問題もあるので、ランドスケープなら外部入力機器を使うべきというのは、すごく理に適ったデザインだと思うのです。

XCodeでsubmoduleでC/C++のソースを持ってくる方法

XCodeのプロジェクトに、GitHub上の別プロジェクトのソースをsubmoduleとして組み込みたいのですが、検索してもあまり良い方法が見つからなかったので、個人的なメモを兼ねてそのやり方を書いておきます。

なお、これは実際LaiNES-iOSで実施している手順です。
https://github.com/suzukiplan/LaiNES-iOS
あと非公開ですがInvader Block 6のリポジトリでも同じやり方でVGSモジュールを組み込んでいます。

まず、XCodeでプロジェクト作成直後のLaiNES-iOSのディレクトリ構成が以下のような感じだったとします。

$ pwd
/Users/suzukiplan/programs/LaiNES-iOS
MacBook-Pro:LaiNES-iOS suzukiplan$ ls -l
total 208
drwxr-xr-x  34 suzukiplan  staff   1156  1  9 08:40 LaiNES
drwxr-xr-x   5 suzukiplan  staff    170  1  8 17:30 LaiNES.xcodeproj
MacBook-Pro:LaiNES-iOS suzukiplan$ 

このディレクトリ上で、git submodule add をします。

$ git submodule add https://github.com/suzukiplan/LaiNES.git cpp

※本当はそのままの名称にしたかったのですがXCodeで作成したプロジェクトのディレクトリと被ってしまうのでC++モジュールのリポジトリはcppという名称にリネームしています。

そして、Finderでそのcppディレクトリを開き、コンパイル対象のモジュールをドラッグ&ドロップでXCodeのプロジェクトに持っていきます。
この時、「Copy items if needed」のチェックを外すことで、プロジェクト内からcppを参照する形にします。
このやり方で追加すれば、同じリポジトリ内で相対パス参照でモジュールを参照する形になります。

試しに、project.pbxprojファイルの内容を覗いてみると、確かに相対パス参照になっていることが分かります。

$ cat LaiNES.xcodeproj/project.pbxproj | grep apu.cpp
74B47A4C1E1F748200B5ADFE /* apu.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 74B47A451E1F748200B5ADFE /* apu.cpp */; };
74B47A451E1F748200B5ADFE /* apu.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = apu.cpp; path = cpp/src/apu.cpp; sourceTree = SOURCE_ROOT; };
74B47A451E1F748200B5ADFE /* apu.cpp */,

74B47A4C1E1F748200B5ADFE /* apu.cpp in Sources */,

あとは、このリポジトリを git clone した時、何もせずにXCodeでプロジェクトを開くと参照切れでエラーになってしまうので、忘れずに git submodule init と git submodule update をする必要があります。

この手順で困るケースとしては、参照するモジュールがディレクトリ階層を意識する構成になっている時です。その場合、何か色々とやる必要があって面倒くさいです。(だから、私はXCodeで扱う前提のC/C++リポジトリを作る時、いつもソースコードは src ディレクトリ以下に階層を掘らないようにしたり、ファイル名が被らないように気をつけているつもり)

まぁ、本格的に複雑な構成のプロジェクトにするなら、cocoa podsを使うべきですが、cocoa podsを使うまでもない簡易的なモジュール組み込みをしたい場合のTIPSということで。

しかし、cocoa podsは極力使いたくない... 遅いしわざわざフレームワークとして作るのが面倒だし、ついでに podfile や podspec の書き方に謎が多い(それでいて公式ドキュメントも中途半端なものしかない)ので、どう書けば良いのか分からず色々と試行錯誤 → 動作がイチイチ遅いので無駄に時間を浪費 というコンボが頻発するので、苦手です。

余談ですが、XCodeの公式ツール類が充実してなさっぷりは酷すぎる。
cocoa pods的なものは公式で準備すべきなのに未だに無いとか。
それでいて、非公式プラグインはバッサリ切ってくるというね。
Android Studioと並行して使っているとXCodeの酷さが色々と目立ちます。
XCodeも公式にIDEAベースとかにした方が、色々と良くなったりしそうな気がする。

iOS版のLaiNESを作成

先日Androidで作成したLaiNESをiOSにも移植してみました。
https://github.com/suzukiplan/LaiNES-iOS

前の記事で書いているように、最初Android版と同様OpenGLを使う方向で作っていたのですが、如何せんパフォーマンスが悪いと感じたので、CoreAnimationLayerで映像バッファをダイレクトに書き込む方式(VGSと同じ方式)で実装するように作り直していたら丸1日掛かってしまった...(その間、デレステのイベントが走れずランキングが500位台から2000位のボーダーギリギリまで落ちてしまった)

結果的に、パフォーマンスはOpenGLよりも良くなりました。
知っていたことですが、やはりGPUレンダリングはピクセルバッファの描画に弱い。
CPUレンダリング最高です。
(※レンダリングにはピクセルバッファしか使わない勢限定ですが)

Android版と若干異なる点として、バーチャルパッドのデザインを変えました。

Android版のバーチャルパッドは、「普通に考えればこうなるよね」という形。
エミュレータだけでテストしていると、こういうダメなデザインになります。

一方、iOS版はこんな感じ。
一見すると野暮ったい。
ですが、実機で動かしてみると明らかにコチラの方が操作し易い筈です。

Android版のデザインのダメなところは、まず、左右のカーソルキーとA/Bボタンを同じ平行線上に並べている点ですね。
これだと、右カーソルキーを入れながらA/Bボタンを押すのがツライ。
具体的には、左親指が邪魔でA/Bが押し難くなる形になります。
この「右カーソルキーを入れながらA/Bボタンを押す」という操作は、例えば、横スクロールのアクションゲーム(スーパーマリオとか)で頻繁に使います。

そして、A/Bを横並びにしているのもマズイ。
Bを押そうとしてAを押してしまう頻度が高くなってしまうので。
この状態だと、例えば、スーパーマリオでBダッシュで助走をつけてジャンプといった操作で頻繁に誤操作が発生することになります。
縦並びにしてみたところ、誤操作はほぼ発生しなくなりました。
(ただし、スーパーファミコンのように4ボタンだと悩ましい)

2017年1月8日日曜日

iOSでもOpenGLを使ってみる

現在公開している LaiNES Android を単純に iOS へポーティング中。
https://github.com/suzukiplan/nes-view-ios/tree/open-gles-20
(追記: 諸般の事情で消しました)

Androidでは今回、描画をOpenGLにしてみたので、折角だからiOSも同じ方式を試しておこうと考えました。

iOSでOpenGLを使う場合、Viewのinterface宣言を

@interface NESView : GLKView <GLKViewDelegate>

という風にしました。
・UIViewではなくGLKView(#import <GLKit/GLKit.h>)にする
・GLKViewDelegateを設定

また、このNESViewのイニシャライザ(一応全部)の延長処理で、

self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:self.context];

という風にOpenGL/ES 2.0のコンテキストを設定。

あとは、AndroidのC++部分の実装と同じ形で、
・映像の頂点バッファを準備
・シェーダープログラムの作成
GLKViewDelegateのプロトコル(※)でピクセルバッファに対応するポリゴンの色を変更
などの処理を実装。

※これ
-(void)glkView:(GLKView *)view drawInRect:(CGRect)rect

OpenGLのインタフェースはC規約で、Androidのものと全く同じです。
だから、ソースコードはAndroidと共有できて良い感じなのですが、なんか微妙に遅い気がする。まだ、iOS固有の部分(core animation layerやUIView)にパフォーマンスの改善余地があるので何とも言えませんが。

あと、glViewportが想定通りに(というかAndroidと同じように)動いていない気がする。(これはコチラのバグかもしれません)

iOS的にはOpenGLよりもMetal推しだろうから、頑張ってOpenGL化するモチベーションがあまり沸かないんですよね。生Metal(?)を叩くのは結構面倒くさそうですが、Unityとかを使っていればAutomatic Graphics APIの項目をチェックするだけで簡単に使えたりします。

OpenGL自体結構deprecate方向に倒れつつある感じですしねぇ。
iOSならMetal、AndroidならVulkanにシフトしている流れです。
(Windowsについては相変わらずDirectXを魔改造する流れ)

Androidは例の如くOSバージョンのフラグメンテーション化が阻害要因になって普及は遅い筈です(本当、Androidなんか無くなれば良いのに)。Vulkanについては、そういった事情もあって聞き慣れない人も多いかもしれないので、(私もあまり詳しく無いですが)念のため補足すると、OpenGLと同じクロノスグループが開発しているもので、OpenGLは元々特定のハードウェアに依存しないように設計されたものですが、それが現代の最新のGPUには合わない(陳腐化した)ものになってしまったとかいう経緯で作られたものだったかと思います。「結局ハードウェアに依存してるじゃねーか」と突っ込みたくなりますね。しかし、実際その通りなので、その結果、より上位層のゲームエンジン(Unityなど)が直にOpenGLなどを叩きプラットフォーム依存を吸収する責務を負うという現代の潮流が出来上がったものと考えられます。その為、VulkanやMetalといった次世代のグラフィックスAPIはOpenGLと比べてローレイヤー寄りの設計になっているので、直に叩こうとするのはかなりツライですが、変態プログラマ諸氏にとっては格好の玩具だったりもします。

2017年1月6日金曜日

マリオランの価格考察

先日話題になったマリオランですが、一応私もプレイしました。
先週ぐらいにクリア済み。
当然ですが、クリアというのは、全ステージのカラーコインをコンプリートして、スペシャルステージやキノピオランの最終目標(ケーキの取得)も完了した状態です。
追加課金の無い完全落とし切りのゲームなので、恐らく追加コンテンツは発生しないのだろうと思います。
公式でもそういう発言をしているらしい記事があったので貼っておきます。
http://heavy.com/games/2016/12/super-mario-run-downloadable-content-additional-new-update-updates-maps-levels-items-nintendo/

このマリオランの価格設定(1200円)が「高すぎる」とかで少し話題になっているらしい。
参考記事:
任天堂マリオランは「高すぎる」 フォーブス記者も怒り爆発
任天堂 スーパーマリオランの1,200円はなぜ高い?

この意見が、ソシャゲなどを無課金でしかプレイしない人たち(いわゆる乞食勢)限定のものであれば、無視して問題ないと思うのですが、実のところ、課金をすることに全然躊躇しない私でも最初「ちょっと高いかな」と思ったりしたので、その原因について考察してみたいと思います。

なぜ、最初「ちょっと高いかな」と思ったのか。

それは、単純にワールドをアンロックすることに魅力を感じなかった為です。
フリーで提供されている3ステージを遊んでみて、これと同じのようなものの繰り返しだろうから、すぐに飽きるだろうなと。
一応、カラーコインを全部(ブラックコインまで)集めてみて、それ自体そこそこやり応えがあると思ったものの、それでも私ならすぐに全ステージ揃えてしまい飽きてしまうだろうと想像しました。
それだけのために1200円ということであれば、100%課金しなかった筈です。

そう思いつつ結局課金したのは、キノピオラリー(※)が楽しかった為です。
※他人のリプレイと競争してコインの獲得数で勝敗を決めるという対戦ゲーム
キノピオラリーは非同期対戦(対戦相手がリプレイ)のゲームなので、同期対戦のゲーム(例えば、デレステのライブパーティーというイベントなど)と比べて飽きがくるのが早そうだと思ったものの、1200円分は楽しめると思うことができたので、購入しました。

このことから、ゲーマー層(ゲームを極めるタイプの人)向けにはキノピオラリー、ライト層(カジュアルゲームなどを遊んで満足するタイプの人)向けにはステージアンロックがマリオランを購入させるための導線として考えられたモノなのだろうと想定できます。(他にも王国の建築要素など色々あったりするのかもしれませんが、そこら辺は専門外なので省略)

つまり、そこそこじっくりプレイしてみれば、1200円支払うことに納得感が得られる程度に楽しめるものだが、そこに至るまでの導線が弱いことが「1200円だと高いな」という印象を私に与えた原因だと言えます。

最初から有料であれば、何も不満無く気持ちよくプレイできた筈。

というのも、お金を最初に支払ってしまえば、それだけで「元を取る分まで遊び尽くそう」というモチベーションが生まれた筈なので。タダで手に入ったものだと、どうしても表面的なところで評価を下してしまいます。(購入導線の「強さ」みたいなことをチラホラ言ってますが、この「表面的なところ」だけでも買いたいと思わせるようなものが、要するに「強い」ということ)

また、フリーであるために、無課金勢によるレビューが沢山付き、購入する上で参考になる有益なレビュー(購入した上でのレビュー)が埋もれてしまうのも良くない。

free to play で出すのであれば、メインとなるコンテンツは全て基本無料でプレイできるようにした上で、サブコンテンツをアドオンにするなどで収益化の手段を作るしか無く、飽くまでもメイン・コンテンツに対して対価を求めるのであれば、基本有料にするのがベストな売り方だと思います。

つまり、今回1200円が高いと私に印象付けた根本的な原因は、本来「基本有料」で売るべき商品を「中途半端な基本無料」で売った為ではないかと考察しました。

pixel bufferのOpenGL/ESのロジックをC++化

昨日書いた記事で、Android版LaiNESの描画ロジックをOpenGL/ES2.0に書き換えることに成功しましたが、OpenGL/ESの実装をJavaで書くというしょっぱい作りになっていました。

AndroidのOpenGL/ESはNDK(C/C++)で書くことができるので、Javaで書くのはナンセンスです。

という訳で、サクッと Java → C++化。
https://github.com/suzukiplan/nes-view-android/pull/6

これで描画周りのパフォーマンスは大分良い感じになったと思います。

ただし、実機で動かした時の最大のボトルネック要因は描画周りではなくAPUエミュレータ(音関連)なので、その部分を何とかしたいところ。(これはLaiNESの問題で、VGSでは問題ないことだからあまり深追いするつもりはありませんが)

2017年1月5日木曜日

OpenGLで pixel buffer を60fps描画する方法

たまには技術的なことを書こう。

かなり以前、Android版VGSの描画をSurfaceViewからOpenGLに変更する試みをしたものの、60fpsのパフォーマンスが確保できず断念したことがあります。

VGSの描画処理は、

  1. 描画API(vgs_putSP等)を発行するとメモリ(システムメモリ)上に確保された8bit仮想VRAMに出力(OS非依存)
  2. VSYNC(垂直同期)の間隔で8bit仮想VRAMの内容を画面に出力(OS依存)
という流れで映像を画面に出力してます。

Android版VGSの上記2(OS依存部分)の実装は、RGB565のBitmapに8bit仮想VRAMの内容を(パレット変換をしながら)書き込み、それをSurfaceViewへCanvas#drawBitmapで描画する形になっています。

当時、これをOpenGL化するに当たって、毎フレームテクスチャを生成して描画するという方法を試しました。

しかし、テクスチャ画像の生成には結構時間がかかります。
また、システムメモリからGPUのメモリへの転送速度は物凄く遅い。
色々工夫して、50fps台ぐらいまで出せるようになったものの、それでは実用上問題がある(フレームレートだけでなく発熱量や消費電力も凄いことになる)ので、当時は結局OpenGL化を断念しました。

今回また、映像処理をOpenGL化することに再挑戦してみようと思います。

今回は、映像をテクスチャで表示するのではなく、1pixelを2ポリゴン(1つの四角形)で表現し、ポリゴンの色を変更することで映像を表示する仕組みを試してみることにします。

また、今回はVGSではなく、先日作ったLaiNESベースのファミコンエミュレータを使います。VGSのアーキテクチャはゲーム機エミュレータと同じ仕組みで作っているので、やっていることはファミコンと大差はありません。

以下、実際にOpenGL対応したAndroid版LaiNESを作成時の Pull Request です。
https://github.com/suzukiplan/nes-view-android/pull/4

ファミコンの場合、画素は256x240なので、pixel数=61,440 になります。
ポリゴン数 = pixel数 x 2 なので, 1フレーム辺り122,880ポリゴン。
60frame で描画するポリゴン数は, 7,372,800(737万2,800ポリゴン/秒)。

初代PlayStationの最大ポリゴン数のSCE公表値は、

  • 演算能力 = 150万ポリゴン/秒
  • 表示能力 = 36万ポリゴン/秒

とのことなので、この方式だと少々キツイ。
まぁ、初代PlayStationで動かす訳ではないですが。
ただ、もう少しポリゴン数を抑えた方が良い感じかなと。

そこで、上記のPull Requestでは、色情報の変化があったpixelのポリゴンのみ色を変更するようにしています。

これで適当なゲーム(某横スクロールアクションゲーム)を動かしてみて、1フレームあたりに更新されたピクセル数を測定した結果が下図です。
画面切り替わり直後などに最大更新が走りますが、ゲームプレイ中は多くても1万pixel未満の更新(これはスクロール時)で、スクロールしていない状態であれば100pixel未満の更新しか走らないようです。

常にスクロールしている状態(1万pixel更新)であれば、120万ポリゴン/秒ということになるので、これなら演算能力的には初代PlayStationでも耐えられそうです。(表示能力は追いつかなそうですが)

ナッツ&ミルクやドンキーコングみたいな固定画面アクションゲームなら余裕です。
まぁ、最近のスマホならフル更新(737万ポリゴン)であっても60fps保てそうな感じかと思います。(Android実機が手元に無くてエミュレータでしか動作確認してません)

合理的ではないものを作りたい

ここ最近、実機版の東方VGSの開発が忙しくて、東方VGSの曲追加が滞っています。 東方VGS(実機版)のデザインを作りながら検討中。基本レトロUIベースですがシークバーはモダンに倣おうかな…とか pic.twitter.com/YOYprlDsYD — SUZUKI PLAN (...