2018年8月14日火曜日

GooglePlay定期購読の解約方法について

Androidでサブスクリプション(定期購読)を提供しているアプリで退会するには、
  1. Google Play ストア Google Play を開き、
  2. メニュー  メニュー 次へ [定期購入] をタップし、
  3. 解約する定期購入をタップし、
  4. [定期購入を解約] をタップして、
  5. 画面の指示に沿って操作(退会理由のアンケートに答える)
という操作をする必要があって、やや面倒くさい。
詳しくは、以下に書かれています。
https://support.google.com/googleplay/answer/7018481?co=GENIE.Platform%3DAndroid&hl=ja

まず、「アプリ上で退会できないの?」と思うかもしれませんが、それは出来ません。
GooglePlayのライブラリが現状そういう機能を提供していないので。
ただ、これは割と正しい処置かもしれません。

GooglePlayの操作で定期購読を集中管理しているから、その方が不要な契約を整理しやすいと考えられるので。
なお、アプリからの解約は出来ませんが、アプリのサーバからの解約(cancel)なら出来ます。なので、仮にアプリに解約機能を持たせたい場合、サーバへ解約リクエストを出してサーバ側で解約処理をすることなら出来なくはなさそうです。ただし、「定期購読は基本GooglePlayで集中管理する」というスタンスからは外れるので、あまり推奨される方式ではないかも。(それが推奨される方式ならそもそもGooglePlay Billing Libraryに解約機能が入っている訳で)
とあるサブスクリプション課金を提供しているアプリのレビューでこのような意見があったのですが、
アンインストールで自動的に解約という機能はあっても良いんじゃないかと思いました。

まぁ、それをアプリに要望するのはお門違いですが。
GooglePlayが持つべき機能なので、Googleへ文句を言うべき事案だと思います。

あと詐欺云々とか言っちゃってますが、少なくとも詐欺罪は成立しないと思います。
詐欺罪とは、
1、人を欺いて財物を交付させた者
2、前項の方法により、財産上不法の利益を得、又は他人にこれを得させた者

に適用される罪で、つまり「欺いた」証拠を立件しなければ成立しないものなので。この場合、詐欺だと言いたいのなら、契約時の各種同意事項の中に嘘が書かれていたことを立件すべきであって、私が見る限り契約の解除に関する方法は正確に明示されており、当然そこに嘘等は無かったように見受けられたから、詐欺罪は成立しない筈です。(契約者が未成年者か被保佐人で法定代理人の許可無く契約を締結したのであれば取消の余地ならあるかもしれませんが)
ただ、これは法律的な話で、実際普通の成年者でも使用許諾などを完全に理解できない人の方が多い。運用レベルでは徹底的にレベルが低い人を基準にすべきなのだが、こういう仕組みを作っているGoogleの人がそもそもレベルが高い人たちしか居ないから、上手く運用レベルで噛み合っていないのではないだろうか。(つまり、詐欺だと叫びたくなるのも分からないでもない)

2018年8月12日日曜日

GooglePlay IAB(subscription)のレシート検証処理の実装 (Node.js)

AndroidのIn-App-Billing API v3でアプリ内課金(subscription)を実装したアプリ+サーバを作ったのですが、アプリの実装は簡単だった反面サーバを作るのに若干難産したので、そのことをネタに備忘録的なものを書いてみます。なお、サーバ言語はNode.js + TypeScriptです。

【①クライアントの実装】
GooglePlay Billing Libraryを使って実装します。
https://developer.android.com/google/play/billing/billing_library_overview
半日ぐらいで簡単に作れました。ライブラリ自体の使い方が(v3だけあって)かなり洗練されていることに加え、ググれば十分に情報が出てくるので、初心者でも簡単に実装できるかと思います。
つまり、クライアントの実装ネタ自体は十分にあるので本記事では割愛します。

【②サーバ実装】
subscription IABでは、購入時に発行されるreceiptをサーバへ送信し、サーバでGooglePlayへのverification(REST API)を行い、有効な状態ならコンテンツ閲覧などの機能を開放するといった形で実装します。

実装時に注意すべき点としては、APIの実行上限が20万件/日に制限されている(引き上げは出来るけど恐らく有料になる)ので、verificationが完了した状態のreceiptデータをRedisやDB等にキャッシュしておく必要がある(要するに無闇にポンポンGoogleへリクエストを投げてはいけない)事ぐらいでしょうか。

【③verification API】
GooglePlay developer APIのPurchases.proudcts:getを用いて検証します。
https://developer.android.com/google/play/developer-api?hl=ja
https://developers.google.com/android-publisher/api-ref/purchases/products/get

Requires AuthorizationなAPIなので、予めGooglePlay developer consoleでサービスアカウントを作成して、JWT認証できるようにしておく必要があります。

APIは普通にHTTP clientで書いても良いかと思いますが、様々なサーバ言語向けライブラリが用意されていて、少なくともauth系のAPIはライブラリを使用することが(Googleから)推奨されています。
https://developers.google.com/android-publisher/libraries

上記ページには、現時点でJavaとPhytonしかありませんが、ページを辿っていくとNode.jsのライブラリもありました。ただし、Alpha版(この時点で嵐の予感)。
https://github.com/google/google-api-nodejs-client

とりあえず、上記ライブラリでJWT認証+purchases APIを実装してみた。ところが、JWT認証は上手くいったのですが、purchases APIが全然上手く動かない。「Invalid Value」というエラーで失敗するのですが、何処がinvalidなのやら...。

StackOverflow等で調べてもv2以前の情報しか載っていなくて、一応v3の実装自体は入っているけどexampleとか一切無い状態。
https://github.com/google/google-api-nodejs-client/tree/master/src/apis/androidpublisher
やる気が感じられない...せめてどうやって実装するか切り口となる説明ぐらいREADME.mdに書いて欲しい。なので、JWT認証はライブラリで行いつつnode-rest-clientで直にpurchases APIを実行するのが、現時点で最も無難な選択肢だろうと判断しました。

以下、上記の方式で実装したコードを晒します。

【④必要なパッケージ】
npm install --save googleapis
npm install --save node-rest-client
npm install --save xml2js
※xml2jsを入れないとnode-rest-clientのコンストラクタでモジュール不整合が起きる

【⑤実装】
const RestClient = require('node-rest-client');
const { google } = require('googleapis');
const authClient = new google.auth.JWT({
    email: サービスアカウントのEメール,
    key: サービスアカウントのプライベートキー,
    scopes: ['https://www.googleapis.com/auth/androidpublisher']
});

const restClient = new RestClient.Client();

(略)

authClient.authorize((error, tokens) => {
    if (error) {
        略(認証エラー時の処理)
        return;
    }
    const url = 
        "https://www.googleapis.com/androidpublisher/v3/applications/"
        + receipt.packageName + "/purchases/subscriptions/"
        + receipt.productId + "/tokens/" + receipt.purchaseToken
        + "?access_token=" + tokens.access_token;
    restClient.get(url, {
        headers: {
            'Content-Type': 'application/json'
        }
    }, (data, response) => {
        if (200 != response.statusCode) {
            略(APIエラー時の処理)
        } else {
            receiptExpire = data.expiryTimeMillis;
            略(成功時の処理)
        }
    });
});

2018年4月22日日曜日

東方VGS Lite 1.0.2 (不具合対策版)

不具合対策版の東方VGS Lite 1.0.2 を先程(AM2:30頃)リリースしました。
曲の再生が途中で止まってしまうという連絡を受けて調査していたのですが、結構難航しました。

デベロッパーコンソールで確認した限り、クラッシュレポートが挙がってきているので、アプリ側のバグであることは間違い無いのですが...
色々と厄介そうな感じです。

まず、「発生元不明」ってのが厄介そうですね。これは恐らく、stack traceを吐かずに強制したものと推測できるので、大方ActivityManager辺りにkillされたのだろうと思います。まぁ、それ自体はAndroidならよくある事です。

最も厄介なのは、私が再現環境(Android5.0以降の実機)を持っていないことです。(私物のスマホはiPhoneなんですよねぇ)

何とか知り合いからNexus6を借りることができたので、それで実機確認してみたところ、それらしき現象を確認。再現できればもう勝ったも同然だと思っていたのですが、そこからがまた長かった。

どういう修正をしたのかは、アプリのアップデート内容に無駄に事細かに書いておきましたが、(1)の修正が恐らく決め手です。これで治ってくれる・・・はず。
無駄に事細かに書いておいた修正内容
(500字制限なので実はこれでも結構削った)

2018年4月7日土曜日

東方VGS Liteを公開

GooglePlayで公開しました。
https://play.google.com/store/apps/details?id=com.suzukiplan.tohovgs2
開発着手から約2週間でリリースという超短期間で完成させたのですが、旧東方VGSのソースコードは1行も使っていなくて、完全フルスクラッチで作り直しました。言語はKotlinで。

音源モジュール部分はC言語(下記)ですが。
https://github.com/suzukiplan/vgs-bgm-decoder

言語の構成比率としては下図のような感じです。

ここまで短期間(実働では3人日ぐらい)で作れたのは、Kotlinの言語としての良し悪しというよりは、Android Studio(IntelliJ)の生産効率がメチャクチャ高いからですね。単純に私の開発速度が尋常じゃなく速いのもあるのですが、それでもiPhoneアプリで同じものを作ろうとしたら最低でも3倍(10人日)ぐらい掛かります。

iOS版の東方VGS Liteは多分作らないです。

東方VGS Lite (3)


東方VGSをオーバーホールするこの機会に、一部機種でバックグラウンド再生時に音飛びするあの問題の解消を試みました。

まず、この問題が発生する原因を簡単に解説します。

VGSでは、波形メモリ音源エミュレータが短い間隔で波形データ(PCM)を生成し、それをOSの音源ドライバへ逐次的に書き込むことで音を再生していて、音飛びはこの波形データ生成処理が遅延することで発生します。そして、アプリがバックグラウンド状態に入るとOSがアプリのCPUプライオリティを引き下げることが原因で波形データ生成処理が遅延します。

これを回避するため、AndroidにはWakeLockという仕組みがあります。東方VGSでは、バックグラウンドに入る時にPARTIAL_WAKE_LOCKというCPUプライオリティを維持するWakeLockを取得することで、CPUプライオリティを引き下げられないように要求していますが、それが一部機種で効いていないことが原因で「バックグラウンドにした時に音飛びする」という問題が発生していました。

それ効かない原因としては、WakeLockの権限の取り方が誤っている(アプリ側の問題)か、OSがアプリのWakeLock要求を無視している(OS側の問題)が考えられますが、WakeLock#acquire自体は成功しているので、後者が原因だろうと思っています。

ここまでが既に分かっていたことです。旧東方VGSでは、このアプローチで問題解決を試み続けていた訳です(最近は全くノータッチだったけど)。ただ、疑わしいと思いつつ、結構大変だった為に試せなかった方式があって、それは音源ドライバのAPIをOpenSL/ESからAudioTrackに変更するというものです。

そして今回、この変更を試みたのですが、かなりアッサリとバックグラウンド再生時の音飛び問題が解消されました。

オーバーヘッド的には、OpenSL/ESよりもAudioTrackの方が重いです。

ただ、OpenSL/ESの場合、バッファリングと再生の処理を全てネイティブスレッド(Cスレッド)で動かしているので、JavaスレッドのCPU使用率が上がらないため、OSが「CPU使ってないならWakeLock要らないよね?」的な感じでWakeLockを無効化させていたのではないかなと。対してAudioTrackの場合、バッファリングはJNI経由でJavaスレッド、再生はJavaスレッドのみで実行されるので、JavaスレッドのCPU使用率が上がるため、音飛びがしなくなったと。

ひとまず、一番大きな問題を解消できたので、後は細かい所を片付ければリリースできそう。
(今日中にはリリースできるかも)

2018年4月5日木曜日

東方VGS Lite (2)

ランドスケープ用のデザインを雑に作ってみました。
VGSだと割と面倒だし、どうせバックグラウンド利用が大半だからと Portrait 専用にしてましたが、ネイティブUIだと楽ですね。

画面構成要素としては、LandscapeもPortraitも全く同じで、
・上半分のヘッダー、ピアノ、コントローラの領域を PlayContainer
・リストのタブ(TabLayout)とリスト(ViewPager)を ListContainer
みたいな形で定義しておき、Landscapeの時はそれらを左右に並べるだけという感じ。
(5分で作れました)

2018年4月4日水曜日

Android版 東方VGS Lite(開発中)

東方VGSのAndroid版が新しいAndroidで動かなくなったという報告を頂いたので調べた所、VGSではなくネイティブUIで作り直してしまった方が手っ取り早そうだったので、作り直し中。
旧来の東方VGSはそのまま残しておき、東方VGS Lite(別アプリ)として公開予定です。
完成度は7割前後なのですが、ここから先が長い...
でも、4月中にはGooglePlayで公開できるかと思います。

2018年3月14日水曜日

サーバサイドのお勉強

スマホのソシャゲで使えそうなアカウントサーバを作ってみました。
https://github.com/suzukiplan/simple-account-server
https://github.com/suzukiplan/simple-ap-server

ただし、飽くまでも試作です。
実用的かは未知数。
現在の本職はクライアント畑の人間なのですが、サーバのことも把握しておこうかなと思いまして。要するに単なる興味本位です。

まずは、アーキテクチャ設計。

アカウントサーバというと、Web時代のソシャゲであれば、メアド(ID)とパスワードで作るのが一般的ですが、スマホ向けということで、POSTすれば自動生成され、アプリ側では preference / userDefaults にそれを記憶して使うのだろうと思います。この辺は実際にスマホのソシャゲを幾つか遊んでみた限り、だいたいそんな感じだろうと想像で作成。
初回起動時(アカウント生成)

自動生成されたIDとtoken(パスワードに相当するもの)を用いてログインするイメージは下図のような感じ。
ログイン

DBですが、アカウント情報というとRDB一択だろうとは思いつつ、スキーマ定義が面倒臭いというただそれだけの理由で MongoDB(NoSQL DBMS)にしてみました。MongoDBというと、どちらかというとログ用途というイメージなのですが。

どうしてもHiRDBなどのRDBじゃないとマズイのであれば、置き換えは割と簡単にできます(実際、試しにMariaDBに置き換えてみたのですが、コードの書き換えよりもスキーマ定義等の下準備の方が面倒だった)。なので、まぁこのまま良いかなと。

DBがMongoDBだから速いので、キャッシュとか要らないのではないだろうかと思ったのですが、実測してみた限りRedisの方が最大10倍ぐらい早かったので、Redisでキャッシュ(Read cache)も作ってみました。なお、Write cacheは現時点では実装してません。アカウントの情報更新量が多いとかで必要であれば、RabbitMQを使う感じでしょうか。

最後に言語。
言語の定番は PHP かな?と思いつつ, TypeScript + Node.js で。
理由としては、
・豊富なパッケージ(使おうとしたミドルのパッケージが全てnpmにあった)
・情報を見つけやすい(≒情報が多い?)
あたり。

サーバサイドの言語事情はよく分かりません。Goとかも興味ありましたが、習得するのも面倒だしNode.jsなら少し知っているから楽なので。実のところ、TypeScriptにする必要すら無い(=ダイレクトJSで良い)んじゃないかなと思ったのですが、modelの定義にclassが使えるのが便利だし、tscも色々良い感じになっていてコンパイル手間も無いに等しい状態だったから、ダイレクトJSはやめておきました。

ひとまずこんな感じで、ユーザ登録、ログイン、ユーザ情報参照、ユーザ情報更新といったアカウントサーバの基本的な仕組みを作ってみました・・・が、コレ、実用に耐えられるシロモノなんですかね?正直良くわからないので、誰かコレでソシャゲ作ってサービスインしてみて下さい。(責任は取らない)

2018年3月5日月曜日

NES emulator for iOS

先日作ったnes-emulator-androidをiOSにも移植。
https://github.com/suzukiplan/nes-emulator-ios

機能レベルでAndroidと同等にすることを目標にしてますが、この記事執筆時点ではサウンドキャプチャ機能が未実装です。(save stateとload stateが未実装であることはAndroid版と同じ)
一応以前、コア部分にLaiNESを使ったiOS用のインタフェースも作ったのですが、若干(というかかなり)作りが悪かったので、内部実装はかなり書き直しました。恐らくインタフェース的には実用に耐え得るレベルになったと思います。

あと、今回のnes-emulator-iosはcocoapodsでの配布もするようにしたので、Podfileに
pod 'NESView', '~1.0.0'
と書くだけで利用できます。
まぁ、利用するとそのアプリのライセンスをGPL3.0(か互換性のあるOSSライセンス)にする必要があるので、事実上商業利用不可みたいなものですが。

なお、READMEに補足してますが、ライセンス調整版(※ただしコア無し)というものも一応あります(そちらは private repo なので一般公開していませんが)。

2018年2月17日土曜日

NES Emulator for Android - version 1.5.0

先日公開したCycloaをAndroidで動かしたライブラリを色々と更新しました。
https://github.com/suzukiplan/nes-emulator-android
現時点の最新バージョンは1.5.0。

主な機能追加内容としては、以下3つ。

  1. マルチtick送信(n倍速)に対応
  2. 映像と音声のキャプチャインタフェースを追加
  3. ステートセーブ/ロードのインタフェースを追加

この他にも色々と細かい修正はしていますが。
これらの機能を全てテストアプリで触れるようにしておいたので、大分テストアプリの画面のガジェット感が上がりました。

1. マルチtick送信(n倍速)

これは普通のエミュレータでもよくある倍速プレイとかですね。
tickの内容を流すことでリプレイ再生することもちゃんとできるようにしておきました。

2. 映像と音声のキャプチャインタフェース

これは普通のエミュレータにはあまり無い機能ですが、映像と音声をキャプチャするというもの。例えば、プレイしている内容をライブストリーミングに流したりといった機能を作る時に役立ちます。
もっとも、実際に配信するにはこれだけだと不十分で、別途MediaCodecを使ってエンコードして、ライブストリーミング・プロトコルでの配信する機能を(これはAndroidの標準機能ではないので自前で)実装する必要があり、少し大変ですが。

3. ステートセーブ/ロードのインタフェース

Cycloaにはステートセーブ/ロードの機能は無いので、NES Emulator for Android側のインタフェースのみですが、とりあえず作っておきました。余力があれば、Cycloaにステートセーブ/ロード機能を追加した魔改造バージョンをforkして作るかもしれません。(作らないかもしれません)

ここ一週間でもりもりアップデートしてましたが、これで私が当初作ろうと思った機能は(少なくともインタフェースレベルでは)一通り揃ったかなという感じ。

2018年2月13日火曜日

Kotlinで100行以内で書けるファミコンエミュレータ

前回の記事で書いたnes-emulator-androidというライブラリをjcenterで公開しました。

jcenterに公開したので、Android projectの build.gradle の dependency 区に
implementation 'com.suzukiplan:nes-emulator-android:1.2.1'
と1行追加してあげるだけで、Androidアプリで簡単にファミコンエミュレータを動かすことができます。

Androidアプリでファミコンエミュレータを動かすには, nes-emulator-android の NESView という View派生モジュール を使います。
NESView は 通常のAndroidのViewと同様に、レイアウトXMLで配置できます。

(レイアウトXMLの記述例)
<com.suzukiplan.emulator.nes.core.NESView android:id="@+id/nes_view" android:layout_width="match_parent" android:layout_height="match_parent" />

ファミコンの表示解像度は256x240pixel(アスペクト比16:15)なので、ConstraintLayoutを用いて配置してあげても良いですが、Viewを配置したレイアウト内へ自動的にアスペクト比を保った状態で最大限に拡大してセンタリングした状態で表示されます。(余った領域は黒く塗りつぶされます)

このNESViewに対して、
    nesView?.load(romByteArray)
とすればROMファイルをロードできます。

そして、
    nesView?.tick(keyP1.code, keyP2.code)
とすれば1フレーム実行することができます。

NESView#tickを実行すると自動的に垂直同期の待機が行われ、また、サブスレッド(非UIスレッド)から実行できるので、これを連続的に実行してあげればゲームが動きます。

実際のNESViewを用いて実装したエミュレータアプリの本体コードが以下にありますが、Kotlinで100行未満で記述できることが分かります。
https://github.com/suzukiplan/nes-emulator-android/blob/master/test/src/main/java/com/suzukiplan/emulator/nes/test/MainActivity.kt

なお、このNESViewを用いて作成したアプリはGPLv3またはGPLv3と互換性のあるライセンス(Apache v2, MIT, 修正BSD等のOSSライセンス)で公開する必要があるので、ご注意ください。(OSSで公開されているエミュレータのソースコードはだいたいGPL系なので)

2018年2月12日月曜日

CycloaをAndroidで動かしてみた

OSSのファミコンエミュレータのソースを眺めていたところ、Cycloaというエミュレータが中々良くできていそうだった。
https://github.com/ledyba/Cycloa

ただし、quick-save等は実装できていないし対応mapperも少ないので、実用的かというと結構微妙なところですが、内部の実装が結構キレイに書かれていて、フロント実装も結構し易そうな感じだったので、「とりあえずファミコンエミュでも作ってみっかー」みたいな軽い気持ちで作ろうとした時に、最短時間で動くものを作ることに適していそう。

論より証拠ということで、CycloaをAndroidで動かしてみたフロントを書いてみました。
https://github.com/suzukiplan/nes-emulator-android

少し目新しい点としては、
・Android Studio 2.2の時に対応した cmake スタイルのNDKビルドを適用
・エミュレータコア機能の実装を library module に分割(bintrayとかにデプロイはしてませんが追記: bintrayにデプロイしてjcenterへのリンク申請中
・↑を用いたアプリ本体の実装を Kotlin で書いてみた
ぐらいでしょうか。

少し話しが逸れますが、私はSwiftが大嫌いなのですが、Kotlinは割と良い感じだと思い始めてます。Swiftがキライな理由は、言語的な意味での良さを全て食い潰すバージョン間互換性の欠如とIDE(XCODE)の対応がIDEAとかと比べるとお粗末過ぎるところでしょうか。言語自体は結構良い感じです。しかし、言語の良し悪しというのは、言語自体の良し悪しではなくIDEの良し悪しに左右されるというのが恐らく真理です。(だから、iOS用のコードを書くならObjective-Cにしておいた方が良い。Swiftと比べればまだ安定している方なので。)

2018年2月11日日曜日

東方VGS(iOS)コードレビュー③ VGSView

VGSViewについて解説します。
https://github.com/suzukiplan/tohovgs-ios/blob/master/Touhou%20VGS/VGSView.m

VGSViewはiOS版VGSのグラフィックス描画処理を実装したViewです。
VGSのグラフィックス描画は、vgs2_putBG(BGを描画)やvgs2_putSP(スプライトを描画)といったVGS API(C/C++)を用いて行いますが、それらを用いて描画された情報はVGS VRAMというメモリ上に記憶されています。

VGSViewの責務(役割)を箇条書きで書くと、

  • VGSのメインループを回す(VGS VRAMに1フレームの描画がされる)
  • VGS VRAMを、iOSの描画処理系に合わせた形式に変換
  • 画面に出力(表示)
  • 上記を1秒間に60回の間隔(60fps)で繰り返し実行

となります。

iOSのゲームに適したグラフィックス描画機能としてはOpenGL/ESやMetal(GPU Renderer)がありますが、これらは上述の用途で使うのは不適当です。というのも、GPU Rendererで上記要件を満たそうとすると、

  • 毎フレームVGS VRAMをテクスチャ変換して描画 or
  • 1pixelを2つのポリゴンで表現して、(変化があったpixelの)色情報を設定

といった処理方式で実現することになりますが、前者(テクスチャ変換)は変換オーバヘッドが物凄く大きく、VGS VRAMのサイズ(東方VGSの場合は240x320)のテクスチャを毎フレーム生成することは現時点最新のGPUでも恐らく不可能です。仮にできたとしてもGPUが火を吹く勢いで回り熱暴走を起こします。

後者の処理は前者よりは現実的ですが、その場合全画面のpixelの色情報を変えようとすると最大240x320(76800)回のパレット変更命令をGPUに飛ばす必要があり、GPUへのデータ伝送は物凄く遅いので60fpsではとても動けないようになるでしょう。

つまり、VGSの要件を満たすことができるのは、GPU描画ではなくCPU描画一択になります。

まず、VGSViewを生成するとCALayer(Core Animation Layer)を独自のもの(VGSLayer)へ差し替えます。
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSView.m#L156-L159

そして、CADisplayLinkを用いて垂直同期の発生間隔でsetNeedsDisplayをコールするようにしています。これでVGSLayerの描画更新が1/60秒間隔で行われるようになります。
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSView.m#L170-L171

VGSLayerが初期化されると、まず2枚のCore GraphicsのBitmap Contextを生成します。これが変換後のVGS VRAMバッファになります。
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSView.m#L101-L115
2枚準備している理由は、VGSLayerがダブルバッファリングと呼ばれる方式で描画出力を行っている為です。ダブルバッファリングとは、一方のバッファの内容を表示中にもう一方のバッファ更新を非同期で行うことで、60fpsの描画がスムースに行われるようにするためのものです。(実のところ、最近のiPhoneならこんなことしなくてもシングルバッファで余裕で回せますが...)

その後、ダブルバッファリングを実現するためのGameLoopスレッドを生成して起動しています。
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSView.m#L117-L121

GameLoopスレッドでは、100μs間隔で描画指示が発生するのを待機して、
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSView.m#L34
描画指示が発生したら、VGSのメインループを1回回し、
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSView.m#L35-L42
その後、VGS VRAMの内容をCore GraphicsのBitmap Contextの形式に変換しています。
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSView.m#L48-L61

GameLoopが本来必要な処理は以上ですが、VGSにはグローバルリクエストという機能があり、それに応じてAppStoreのページを開くといった要求処理を実装しています。(東方VGSでは使ってませんが、VGSからのネイティブ広告の表示制御などはこのグローバルリクエストで実現しています)
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSView.m#L63-L88

最後に、CADisplayLinkにより1/60間隔で発生するVGSLayerの描画要求のコールバック(display関数)では、描画指示を出しつつ、描画が確定している方のCore Graphics/Bitmap ContextをViewの表示内容(content)としてセットする処理を行っています。
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSView.m#L131-L139

以上でVGSViewの解説は終わりです。

この部分は東方VGSに限らず、VGSを最初にiOSへ移植した時から一切変更していないので、あまり面白い内容ではないですね。だから、実装内容を淡々と解説しましたが、このVGSViewを最初に作った当初はもの凄く苦労しました。
というのも、市販のiOSアプリ関連の書籍には、役立つ内容が書かれたものが一切無かったので。(仕方が無いのでApple公式の分かり難いCore Graphicsのドキュメントを読むなどして理解しました)
ゲームのグラフィックス描画処理というと、最近ではOpenGL or MetalといったGPU Rendererを用いた方式に一本化されていて、スプライトやラスタといった昔ながらのコンピュータグラフィックスの描画方式は、完全に過去のものになってしまったのかな。

2018年2月5日月曜日

東方VGS(iOS)コードレビュー② ViewController

大規模なアプリの場合、ViewControllerが幾つもあったりしますが、東方VGSのViewControllerはひとつだけです。
https://github.com/suzukiplan/tohovgs-ios/blob/master/Touhou%20VGS/VGSViewController.m

更に言うと、その唯一のViewControllerが管理するViewも最大5つ(iPhone4s以前 or iPadの場合は4つ)だけしか無いので、ViewControllerの実装は物凄くシンプルになっています。
※上図では省略していますが、全てのViewの親(ルートView)としてVGSScreenViewというものがあります。

ViewControllerが生成されるとiOSからデリゲートメソッドのviewDidLoadがコールバックされますが、VGSViewControllerでは、そこで上述のViewを作って並べつつ、VGS特有の初期化処理を行っています。

以下、VGSViewControllerのviewDidLoadに実装されている処理内容を見ていきます。

(1) 多重初期化の抑止
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSViewController.m#L38-L40
これは、iOS8の時にviewDidLoadが2回実行され結果的にアプリが起動時にクラッシュするバグ(※iOS側のバグ)が発生したので、その時に入れた対処コードです。

このバグ自体、発生条件がかなり限定的なのですが、東方VGS(というか私が作った全てのiOSアプリ)は不運にもその条件に該当してしまったので、当時公開していた全てのアプリにこの対処コードを入れてアップデートしました。(物凄く大変でした)

そういえば、この対処バージョンの東方VGSをアップデートするついでに、東方幻想郷から「Bad Apple!!」を追加しましたが、これは当然そういう意図で追加されたものです。

iOS版東方VGSをAppStoreで公開していた約4年間ほどの短いようで長い歴史の中で、ほぼ唯一不評のレビューが付き、総平均レビュー値を4.9から4.8に下げる原因となったのがコレでした。ただ、普通のアプリだったら「起動すると落ちる☆1」と低評価レビューの嵐となる所、「起動すると落ちる☆5」という風に、不満を言いつつも評価を下げないでくれる人が結構居ることに少し驚きました。
このiOS8デグレード事件以外に、もう一つ不評を買ったのが、AppStoreだとアップデート時の更新内容の説明文を最大4000字書けたので、「4000字きっかり埋める説明文を書いてみよう」と思い立ち、少し長めのポエムを書いた時ですね。 
逆に一番評価が高かったのが「神々が恋した幻想郷」と「芥川龍之介の河童」を追加した時のレビューで、平均☆5.0(約300件ついたレビューがオール☆5)でした。オール耳コピで作る必要がある風神録の楽曲全曲収録という無謀な挑戦ができたのは、この時のオール☆5が原動力になりました。
(2) バージョン切り分け
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSViewController.m#L44-L67
この辺のバージョン切り分けは色々と謎ですね。
過去バージョンとの互換性を保つ為に色々な試行錯誤が行われていたようです。(他人事)
iOSの場合、Androidと違ってOSバージョンがフラグメント化しないので、古いバージョンはガンガン切ってしまって良いかと思うのですが、東方VGSではApple側から「おい、やめろ!」と言われるまで最低サポートバージョンをiOS4.3(東方VGSの機能を実現する上で最小限の機能セットが実装されているiOSの最低バージョン)以降にしてました。(最終的には、古すぎるバージョンのiOSをサポートしているとAppleから審査提出を拒否されるようになったので、最低サポートバージョンをiOS6.1にまで引き上げました)

(3) ルートViewの作成
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSViewController.m#L68
ViewControllerのルートViewとしてVGSScreenViewというものを作成して追加しています。VGSScreenViewには「タッチイベントを取得してVGSへ送信する」という役割があります。(その役割が無ければルートViewは単なるUIViewでも良かったとも言えます)
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSScreenView.m
普通のiOSアプリを作った事がある人なら、「タッチイベントの処理は各Viewが担えば良いのでは?」と考えるかもしれません。実際にタッチを受け付けるのはVGSViewなので、わざわざタッチイベントを処理する専用のViewなんて設けるのはナンセンスだと言えます。ただし、UIViewは存在自体が結構重いモノなので、Viewをグルーピングする用途しか持たない空のUIViewをルートに配置するのではなく、そこにタッチイベント処理を集約させる機能を持たせ、VGSViewの責務範囲を減らしつつロジック分散するのは、ある意味(※VGSというスペシフィックなモノに限れば)合理的かもしれません。
ちなみに、このViewのグルーピングという考え方でAndroidとiOSで決定的な違いがあります。Androidの場合、Viewのグルーピングを行う専用の機能(ViewGroup)をViewとは分離して定義した一方、iOSの場合Viewのグルーピング機能をそのままView(UIView)に持たせています。結果的に、Viewグルーピングの実装はAndroidの方がシンプルになった反面融通が効きにくいものになり、iOSの方はかなり融通が効く反面実装コストが高く付くものになりました。何れにせよ一長一短あるのですが、だいたいどのアプリも似たり寄ったりなmaterial designとかいうクソみたいなデザインを採用している現状を勘案すると、この部分のスキームは結果的にAndroidの方が優れたものだったのではないかと思っています。
(4) romdata.binの読み込み
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSViewController.m#L69-L77
VGSの場合、グラフィック、音声(効果音、BGM)、テキストなどのアセットデータは独自形式のバイナリデータとして「romdata.bin」というファイルに集約しています。そのromdata.binファイルを読み込みメモリ上に配置する処理をここで行っています。そして、ここで読み込んだメモリ上のデータ(ROMデータ)は、アプリのプロセスが生存中維持し続けます。これにより、東方VGSでは一般的なiOSネイティブアプリやUnity等で書かれたゲームアプリを作る時に悩まされる「ガーベージコレクト」が一切発生しません。(その代わりに表現力が貧弱になる)

(5) VGSViewとUIImageViewの作成+配置
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSViewController.m#L79-L114
VGSViewというVGSのコア機能を持つViewと、上下の画像イメージの作成と配置をココで行っています。
16:9の端末と4:3の端末で処理が分かれていますね。
iOSアプリを作ったことがある方なら、「これだとiPhone6や6 plusで画面下にスキマが出来てしまうのでは?」と疑問に思うかもしれませんが、東方VGSはiPhone6(4.7")やiPhone 6 plus(5.5")の場合は互換モードで動くので、特に問題なく配置されます。(iPhone Xでどうなるかは知らない)
この部分はちょっと直したいですね...

(6) VGSBarViewの作成+配置
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSViewController.m#L116-L118
最後にVGSBarViewという画面最上部の時計を表示しているViewを作成して配置しています。リリース当初の東方VGSにはこのViewは無くて、iOS標準のステータスバーを表示していました。しかし、iOS側の度重なる仕様変更で嫌気が差し、最終的にはiOS標準のステータスバーを削除して自前のステータスバーを置く形に落ち着きました。
iOSはバージョンアップの都度、かなりラジカルに仕様変更をしてきますが、最も煽りを食うのがこのiOS標準のステータスバーの部分です。ステータスバーはiOS側の持ち物なので、当然といえば当然ですが。(最近では、iPhone Xのあの変態的な形状のステータスバー...ハードレベルで変えてくるから堪ったものではない)

2018年2月3日土曜日

東方VGS(iOS)コードレビュー① AppDelegate

先日OSSとして公開したiOS版東方VGSのアプリ実装を眺めつつザックリと解説するシリーズ。(シリーズが続くかは不明)
本稿は、iOSアプリケーション開発経験者ではなくても意味が分かるように書いていき、最終的にはiOS版東方VGSの全体的な作りが何となく分かるようになることを目標にしています。
第1回の今回はAppDelegateというiOSアプリの一番根っこの部分で何をやっているかを見ていきます。

↓このソースの解説
https://github.com/suzukiplan/tohovgs-ios/blob/master/Touhou%20VGS/VGSAppDelegate.m

(1) AppDelegate(起動時の処理)

iOSアプリの場合、起動が完了するとAppDelegateのdidFinishLaunchingWithOptionsが呼び出されます。(もっと正確に言えば他のUNIXプログラムと同様main関数から起動して、そこでAppDelegateが登録されます。そして、iOS側の制御でアプリの起動処理が完了した時にdidFinishLaunchingWithOptionsがコールバックされます)
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSAppDelegate.m#L23

そこでやっていることは大まかに、
・ViewControllerの登録
・OpenALの初期化
・AudioSessionへのproperty-listenerとinterruption-listenerの登録

ViewControllerはAndroidで言うところのActivityやFragmentのことで、アプリのViewを制御するコードを書きます。ViewとControllerが登場したということは、Modelを作ってMVCのデザインパターンで設計すべしと考えるのが普通ですが、東方VGSはMVCとかそういうデザインパターンでは作っていません。もちろん、DDDとかそういうモノでもなくて、デザインパターン的には好き勝手に作っています。一人でプログラミングする場合、それが一番生産性が高い。

OpenALは音を再生する為に使っています。普通音を再生するにはAudioPlayer等を使いますが、東方VGSの場合、VGSが音声を発生する装置をエミュレーションしてパルス符号データを時系列に吐き出し続け、それを拾って再生するのでAudioPlayerよりも波形を逐次直書きできるOpenALを使っています。(Androidの場合はOpenSL/ESを使っています)

AudioSessionのproperty-listenerは、kAudioSessionRouteChangeReason_OldDeviceUnavailableというハードウェアイベントを拾う為に登録していて、これは、ヘッドセットがデバイスから外された時に発生するイベントで、これを検知した時に音声の再生をポーズするために使っています。これは、アプリ公開後にユーザからそういった要望を受けて実装しました。(iTunesがそういう仕様だったらしく、東方VGSも同じ仕様だと思ってしまったのだとか。学校の休み時間に東方VGSを聴いて授業が始まる時にヘッドセットを抜いて仕舞ったところ、授業中にうっすらとBGMが流れてしまうハプニングがあったと伺ったので、申し訳なく思って速攻で実装しました)
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSAppDelegate.m#L72

interruption-listenerは、東方VGSのオーディオセッションに対して割り込みが発生した時にアプリを停止させる処理を実行しています。これは、バックグラウンドで再生中に電話が掛かってきたり、別の音楽アプリ(iTunesなど)を起動した時にアプリが動き続けると煩わしいので、それなら止めた方が良かろうということで実装しました。この影響で一部アプリ(オーディオセッションを奪うアプリ)とは共存できないという仕様になりました。この仕様については、アプリ公開中に一部ユーザから不満を言われていたので、直せばもっと高い評価が得られたかもしれません(ちなみにiOS版のレビュー評価件数は最終的に、DL数約20万に対して5千件ほどで平均4.8ぐらいでした)が、逆に治してしまうと電話が掛かってきた時にアプリがちゃんと止まってくれるか分からなかった(開発当時はiPhoneではなくiPod touchしか持っていなかった)ので、怖くて修正できなかったと記憶しています。
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSAppDelegate.m#L84

(2) AppDelegate(バックグラウンド関連)

バックグラウンド関連での処理もAppDelegateで行われますが、ココでやっていることはバックグラウンドに入ったフラグのON/OFFぐらいです。
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSAppDelegate.m#L104
https://github.com/suzukiplan/tohovgs-ios/blob/71464aae09f107fbf02e9ce1989d9bfb80381e37/Touhou%20VGS/VGSAppDelegate.m#L119
後付で、音楽がプレイ状態でない時にバックグラウンド遷移する時はアプリをexitするようにしました。(その方が若干程度ではありますが電池が節約できたので)

もっと淡々と書こうと思ったのですが、コードの1行1行にそれなりに思い出が詰まっているので、全然淡々としていないな。

2018年1月21日日曜日

iOS版東方BGM on VGS(再公開)

iOS版の東方VGSを再公開しました。
ただし、AppStoreではなくGitHubでOSSとして。
https://github.com/suzukiplan/tohovgs-ios

※お手持ちのiPhoneへインストールするにはMac+XCODEが必要になります

流石にこれではあんまりなので、代行公開をOKにしようかとも考えたのですが、様々なリスクが考えられるのでそれについては原則NG(正確には事前許諾が必須)としています。

ImageViewで色を乗算方式で変える方法(programmatically)

AndroidのImageViewで色をプログラム的に変えたいと思って調べると、だいたい「元画像が単色」の場合の変更方法しか出てこない。マテリアルデザインの場合、原則アイコンは単色(tint-color)でデザインするのが基本だから仕方ないですね。

ここでは、元画像が複数色のものを全体的に色調を変える方法を書きます。
オリジナル ⇒ 全体的に色調を変更
(素材はいらすとやから借用)
ImageView#setColorFilter を使えば色を変更できるらしいという情報は、ググれば沢山出てくるのですが、全体的に色調を変える場合は、第二引数のModeに PorterDuff.Mode.MULTIPLY を指定してあげるだけで良いです。

上図のような全体的に緑がかった感じにする場合は、

imageView.setColorFilter(0xff00ff00, PorterDuff.Mode.MULTIPLY);

でOK。

実際にAndroid上で動かせるようにしたものをGitHubにアップロードしておきました。
https://github.com/suzukiplan/ImageViewTest

2018年1月14日日曜日

音ゲーの譜面を暗譜する方法

ガルパにせよデレステにせよ、音ゲーの譜面を暗譜するのはほぼ無理だと思っています。

音ゲーなんかより遥かに鍵盤数が多いピアノの暗譜なんて人間技じゃない・・・という風に思うかもしれませんが、ピアノの暗譜は音ゲーのそれと比べると遥かに易しい。
ピアノに限らず楽器全般の暗譜は、楽譜を完全に脳内にコピーしている訳ではなく、単に脳内で音を再現できる状態に過ぎないので。
「脳内で音を再現できる状態」とは、例えば、好きな歌を鼻歌でふふ~んと歌うことができる状態のことを言います。(これなら誰でも出来るはず)
そして、楽器演奏は音に紐づく動作を繰り返す運動行為(例えばギターなら一番低いEの音であれば、フレットを抑えずに1弦を弾くといった感じ)で、鳴らしたい音に紐づく動作が記憶できていれば、脳内で再生している音楽に対応づく動作パターンへの変換を行うだけで暗譜演奏ができるという事になります。(実際に楽器をやってみると分かることですが、これは想像するよりも遥かに簡単に誰でも出来ることです)

スマホの音ゲーで暗譜がほぼ無理な理由は、楽譜自体の形を暗譜しなければいけない為です。もちろん、出来ている人も居るかもしれませんが。(速度1でフルコンできるタイプの人は恐らくそれです)

ゲーム側に次のようなオプションがあれば、暗譜が簡単に出来るようになる筈。

(1)FX音のPANを左側と右側で変える
※FX音 = タップやスライド等で鳴る音のこと

こうすることで、音と動作(左の方を叩くとか右の方を叩く)を紐付けて記憶できるようになるので、音による暗譜が可能になります。

(2)FX音の音程を左側と右側で変える

左側の音程=低い、右側の音程=高いという感じ。
ただし、曲によってはFX音と合わなくなる可能性があるかも。
(その代わり(1)だけの場合と比べて圧倒的に記憶し易くなる筈)

なんでデレステにもガルパにもそういうオプションが無いのか?(特許絡みの何かがあるのか、あるいは単純に開発会社にそういう発想が無いのか)

まぁ、アプリの改修なんて期待してもしょうがない(こうしてネットで晒しておけばワンチャンあるかも程度の期待は無くはない)ので、ローカルフィックス案も一応あります。
それは、
①イヤフォンは使わない
②FX音をoffにする(アプリ設定)
です。
物凄くアナログですが、要するリアル打鍵音でプレイ。
これなら(1)はアナログ的に何となく実現できます。
まぁ、かなり微量ではありますが0とnone 0の間には大きな隔たりがあることを実感できるかと思います。
ついでに、デレステもガルパも実際にデバイスを叩いたタイミングと音が鳴るタイミングに微妙にズレがある(これは恐らく技術的には改修不可能だと思う)のですが、それも完全に無くなるというメリットもあります。

この分野を純粋に突き詰めていくと、曲の音声もOFFにするのがベストだったりする(完全なサイレントにすることでスライドのこすれる音も構成要素として成立するようになる)のですが、そこまでいくと「なんで、このゲームプレイしてるんだろ」という哲学的な感じになってきます。

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

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