KLabGames Tech Blog

KLabは、多くのスマートフォン向けゲームを開発・提供しています。 スマートフォン向けゲームの開発と運用は、Webソーシャルゲームと比べて格段に複雑で、またコンシューマゲーム開発とも異なったノウハウが求められる領域もあります。 このブログでは、KLabのゲーム開発・運用の中で培われた様々な技術や挑戦とそのノウハウについて、広く紹介していきます。

はじめに

先日KLabではパブリッシングパートナー会社様及び共同開発パートナー会社様向けに、 「モバイルオンラインゲーム開発SDK」として、社内で開発・利用を推進しているライブラリ群の提供を開始しました。
今日はこのSDKに含まれているストア課金処理ライブラリおよび仮想通貨管理ライブラリについて、その提供の背景と概要についてお話しします。

ストア課金処理ライブラリ・仮想通貨管理ライブラリ提供の背景

多くのスマートフォン用アプリには、アプリの中から追加で課金を行うことでロックされた機能を有効化したり、新たなコンテンツを追加したりする機能があります。
これを実現しているのがアプリ内課金の機能です。オンラインゲームでは仮想通貨の購入やゲーム内機能をアンロックする目的で利用されるケースが多いようです。

このアプリ内課金は基本的にアプリ(スマートフォン端末)とGoogle PlayやiTunesなどの決済サーバ間で完結します。
つまり運営者が独自にサーバを用意することは仕様上必須ではありませんが、一方でオンラインゲームの運営サーバ(以降、リモートサーバ)側ではコンテンツの付与などをそのサーバ上で間違いなく行う必要があるため、アプリとリモートサーバとの連携処理を適切に実装する必要があります。

構成概念図

このようなリモートサーバにおける課金処理で特に注意しなければならないのが、購入情報の検証すなわち不正対策と、そこで付与されるコンテンツ=仮想通貨の管理に関する実装です。

決済プラットフォームが提供している仕様書の内容だけでは、オンラインゲームに特化したベストプラクティスといったことまではあまり読み取ることができません。
つまりこれらの処理は運用ノウハウといえる領域であり、各社独自に工夫を凝らしている状況ではないかと思います。

前置きが長くなりましたがこのストア課金処理ライブラリおよび仮想通貨管理ライブラリの目的は、このリモートサーバ側の「購入情報の検証」および「仮想通貨の管理」に関するKLabの運用ノウハウを透過的に、つまり容易に利用可能にすることでオンラインゲーム開発者が本来の作業に集中できるようにすることにあります。
なお、これらの機能はモジュール化されており自由に組み合わせることが可能な設計としています。

購入情報の検証 (ストア課金処理ライブラリ)

オンラインゲームのように購入情報をアプリからインターネットを介してリモートサーバに送信し、リモートサーバ上でコンテンツ付与を行う際に注意しなければならないのが不正対策です。
具体的に言うとアプリ内課金に関する処理を迂回するようにアプリそのものを改ざんされたりすることもありますし、偽の購入情報を送りつけるようなツールも横行していますので、このような不正利用者によるただ乗りがないよう考慮して検証を実装する必要があるわけです。

ただしオンラインゲームに関して言うと、アプリ内課金に必ずリモートサーバが介在するためスタンドアロンアプリ=リモートサーバを必要としないアプリに比べると不正対策は比較的容易です。
というのもアプリについてはバイナリ自体やメモリ改ざんが技術的に可能でも、サーバ側を乗っ取ってプログラムを書き換えるようなことはそれと比較してずっと難しいからです。
つまりオンラインゲームにおいてはサーバサイドでの検証を適切に漏れなく行うことで、不正利用をほぼ100%防ぐことができます。

ストア課金処理ライブラリでは次のようなチェックを実施することで不正対策を行っています。

  • 署名あるいは検証サーバを利用した検証の実施
  • 購入情報内の決済IDによる一意性の担保
  • 購入情報に含まれる製品IDや商品IDの存在チェック
  • などなど……

上記でおわかりのように一つひとつの処理は技術的に難易度が高いものではありません。
ただしこのような手続きはアプリ毎の依存部分があまりないことや、実際には検証サーバへの接続処理やリトライ処理等エラーハンドリング、各種ログ実装(忘れがちになる部分ですが運用では重要です)など煩雑な対応も付随して多く発生するためライブラリ化するメリットが大きいところといって良いと思います。

仮想通貨の管理 (仮想通貨管理ライブラリ)

日本国内向けに有償で発行が行われた仮想通貨は、発行額や発行形態に応じて資金決済に関する法律(資金決済法)という法律の適用を受けることがあります。
目的は利用者保護にあり、この場合事業者および発行する仮想通貨の名称や単価等を監督省庁に届け出る必要がありますし、事業者に万が一のこと(破産など)があったときに備えて仮想通貨発行額に応じた発行保証金の供託を行わなければならないことになっています。

これを実現するには一貫したルールで仮想通貨の付与・利用を行う必要がありますし、さらにこれらの入出力ログは厳密に管理されなければなりません。
このためKLabでは仮想通貨は種別毎に先入先出法で管理することで入出力を厳格にルール付けした上で残高管理を行っているのですが、このような実装は一定の会計知識も必要ですし、やや複雑な手続きになりがちな上に万が一不具合があった場合にはその損害も計り知れません。
そこでこのような処理をカプセル化し、付随する固有の業務処理におけるトランザクション管理やロック機構がテンプレート化されたライブラリとして提供することで仮想通貨の正確な管理・運用を支援しています。

さいごに

このライブラリの検証処理機能としては現在 python向けにIn-App Purchase(App Store)用とIn-app Billing(Google Playストア)用のものを提供しており、以後ニーズを見ながら他の決済プラットフォーム向けのものを提供予定です(動作環境等詳細につきましては までお問い合わせください)。
ライブラリの開発チームでは常に最新の情報を把握して、プログラムへの反映を継続して行うようにしています。

KLabではこのようにアプリの開発や運用で得られた知見を、各種ライブラリやフレームワークといった共通のコンポーネントとして集約することにより開発業務の効率化を進めています。

9/17から開催される東京ゲームショウでも当社コーナーを出展していますので、興味を持たれた方はぜひ遊びに来てくださいね。


Shimanuki

連載目次

はじめに

@tenntennです。

前回の記事では,spriteパッケージといくつかのイベントについて扱いました。前回の記事を読んでいただいた方は,Android上で画像の描画と移動,回転,拡大/縮小を行うコードをGoだけで書けるようになったのではないかと思います。

なお,前回の記事を執筆後にevent/configevent/sizeに変更されました。確かにサイズに関する情報しかconfig.Eventには含まれていなかったので,納得できる名前変更です。

また,GoチームによってGo Mobileの情報が徐々に整理されてきました。GoのWikiに導入の記事が追加され,リポジトリにもgomobile bindを使ったSDKアプリケーションのサンプルが追加されました。

さて,今回は前回扱わなかった,event/touchevent/lifecycleについて扱おうと思います。この記事を読めば,Go Mobileでタッチイベントを使った簡単なゲームなら実装できるようになるでしょう。

この記事は,2015年8月16日時点の情報を基に執筆しております。これからもしばらくは,event/configのような破壊的な変更が加わる可能性は十分ありますので,ご注意ください。なお,この記事を執筆当時の最新のコミットはbb4db21edcf86f8e35d2401be92605acd79c8d10です。そのため,記事で参照するGo Mobileのリポジトリのリンクは,このコミットを基準にしております。また,この記事にあるソースコードの一部はGo Mobileのリポジトリから引用したものです。

タッチイベント

第1回目の記事で動かしてみたexample/basicを覚えていますでしょうか?このサンプルでは,画面をタッチするとタッチした位置に,緑の三角形が描画されていたと思います。

example/basicをAndroidで動かしてみた

Go Mobileでは,画面をタッチするとtouch.Eventが送られてきます。このサンプルでは,送られてきたtouch.Eventをハンドルしてタッチした位置を取得し,その位置に三角形を描画しています。

前回の記事で説明した通り,イベントをハンドルするには,app.AppEventsメソッドで取得されるチャネルからイベントを受信します。

func main() {
    app.Main(func(a app.App) {
        for e := range a.Events() {
            var sz size.Event
            switch e := app.Filter(e).(type) {
                case size.Event:
                    sz = e
                case touch.Event:
                    fmt.Println(e.X, e.Y)
            }
        }
    })
}

タッチイベントを表すtouch.Eventは以下のように定義されています。

// Event is a touch event.
type Event struct {
    // X and Y are the touch location, in pixels.
    X, Y float32

    // Sequence is the sequence number. The same number is shared by all events
    // in a sequence. A sequence begins with a single TypeBegin, is followed by
    // zero or more TypeMoves, and ends with a single TypeEnd. A Sequence
    // distinguishes concurrent sequences but its value is subsequently reused.
    Sequence Sequence

    // Type is the touch type.
    Type Type
}

XYはご想像の通り、タッチした座標です。これらの値はピクセル単位なので,spriteパッケージで使われるgeom.Pt単位にするには,size.Event.PixelsPerPtを基に計算してやる必要があります。

Sequenceは,一連のタッチイベントに付けられるシーケンス番号です。指を同時に画面にタッチすると、タッチイベントが同時に複数発生します。そのため,シーケンス番号は、それらを区別するために利用します。

Typeには,タッチイベントの種類を表す値が入っています。タッチイベントの種類は,TypeBeginTypeMoveTypeEndの3種類が存在し,以下のように定義されています。

const (
    // TypeBegin is a user first touching the device.
    //
    // On Android, this is a AMOTION_EVENT_ACTION_DOWN.
    // On iOS, this is a call to touchesBegan.
    TypeBegin Type = iota

    // TypeMove is a user dragging across the device.
    //
    // A TypeMove is delivered between a TypeBegin and TypeEnd.
    //
    // On Android, this is a AMOTION_EVENT_ACTION_MOVE.
    // On iOS, this is a call to touchesMoved.
    TypeMove

    // TypeEnd is a user no longer touching the device.
    //
    // On Android, this is a AMOTION_EVENT_ACTION_UP.
    // On iOS, this is a call to touchesEnded.
    TypeEnd
)

1本の指を画面に置いてから,離すまでの間に複数のタッチイベントが発生します。それらの一連のタッチイベントは同じシーケンス番号が振られ,指がどの状態にあるかはTypeで取得します。画面に指を置いた瞬間にTypeTypeBeginのタッチイベントが発生し,指を動かす度にTypeTypeMoveのイベントが発生します。画面から指を離すと,TypeTypeEndのイベントが発生し,一連のシーケンスのタッチイベントが終了します。

なお,シーケンス番号は常に一意である訳ではなく,そのアプリの起動中に何度も同じシーケンス番号のイベントが送られてきます。現在の実装では,画面に指を置いた順にシーケンス番号を振られ,指を離すとそのシーケンス番号は開放され、再度別の指を置くと同じシーケンス番号が使いまわされます。つまり,新しくシーケンス番号を振る場合は,現在どの指にも振られていない0以上のもっとも小さい数を振るように実装されています。

タッチイベントは基礎的な情報しか提供していませんが,これらをうまく使うことでドラッグやフリックなどを実装することができます。
Webフロントエンドのタッチイベントのライブラリなどを参考にして、高レベルなタッチイベントを行うライブラリを実装しても面白いでしょう。

ライフサイクル

Go Mobileにおいて,アプリのライフサイクルはlifecycle.Eventで表わされます。Go Mobileでは,複数のプラットフォームを一元的に扱うためOnStartOnStopなどのAndroidのActivityのライフサイクルとは一致していません。

lifecycle.Eventは以下のように定義されています。

// Event is a lifecycle change from an old stage to a new stage.
type Event struct {
    From, To Stage
}

ライフサイクル中のある状態をStageという型で表現し,lifecycle.Eventは,とあるStageから別のStageに移ることを表しています。Stageには,StageDeadStageAliveStageVisibleStageFocusedの4つがあり,以下のように定義されています。

const (
    // StageDead is the zero stage. No lifecycle change crosses this stage,
    // but:
    //  - A positive change from this stage is the very first lifecycle change.
    //  - A negative change to this stage is the very last lifecycle change.
    StageDead Stage = iota

    // StageAlive means that the app is alive.
    //  - A positive cross means that the app has been created.
    //  - A negative cross means that the app is being destroyed.
    // Each cross, either from or to StageDead, will occur only once.
    // On Android, these correspond to onCreate and onDestroy.
    StageAlive

    // StageVisible means that the app window is visible.
    //  - A positive cross means that the app window has become visible.
    //  - A negative cross means that the app window has become invisible.
    // On Android, these correspond to onStart and onStop.
    // On Desktop, an app window can become invisible if e.g. it is minimized,
    // unmapped, or not on a visible workspace.
    StageVisible

    // StageFocused means that the app window has the focus.
    //  - A positive cross means that the app window has gained the focus.
    //  - A negative cross means that the app window has lost the focus.
    // On Android, these correspond to onResume and onFreeze.
    StageFocused
)

ステージはStageDeadから始まり,アプリをサスペンドしたり,レジュームしたりすると変わります。各ステージがAndroidのライフサイクルのどの部分にあたるのかは、上記のStageの定義にコメントとして書いてあります。

表にまとめると以下のようになります。なお,「順方向」や「逆方向」は,そのステージ「へ」遷移した(順方向)か,そのステージ「から」遷移した(逆方向)かを表しています。詳細は後述のCrossesメソッドの説明で行います。

ステージ名 意味 対応するAndroidのライフサイクル
StageDead 初期状態 なし
StageAlive アプリの起動している 正方向:onCreate,逆方向:onDestroy
StageVisible ウィンドウの表示されている 正方向:onStart,逆方向:onStop
StageFocused ウィンドウが選択されている 正方向:onResume,逆方向:onFreeze

執筆当時はMacとAndroidでステージの遷移の仕方が違っていました。
たとえば,Androidではステージは以下の表のように変化します。

操作 ステージの遷移
アプリを起動する StageDead->StageFocused
ホームボタンを押す StageFocused->StageAlive
アプリに戻る StageAlive->StageFocused

一方,Macの場合は以下のように遷移します。

操作 ステージの遷移
アプリを起動する StageDead->StageAlive->StageVisible->StageFocused
別のウィンドウを選択 StageFocused->StageVisible
アプリのウィンドウを選択 StageVisible->StageFocused
アプリのウィンドウを閉じる StageFocused->StageAlive->StageVisible
アプリを終了(ウィンドウを閉じてる場合) StageVisible->StageDead
アプリを終了(ウィンドウを開いている場合) StageFocused->StageDead->StageAlive

このようにAndroidの場合はStageVisibleになることはなく,Go Mobileのソースコードを見てもイベントを発生させてる箇所は見つけられませんでした。

以下の図のように,ステージは基本的にStageDead->StageAlive->StageVisible->StageFocusedの順(定数の小さい順)に遷移することを前提としています。
lifecycle.Event.Crossesメソッドを使うと,引数で渡したステージを順方向(定数の小さい順)に通り過ぎてるか(CrossOn),逆方向(定数の大きい順)に通り過ぎているか(CrossOff)が取得できます。
たとえば,図の左側のようにイベントがlifecycle.Event{From: StageAlive, To: StageFocused}の場合,Crosses(StageVisible)CrossOnを返します。

ライフサイクル

少し分かりづらいですね。Crossesメソッドの実装を見る方が分かりやすいかもしれません。なお,Stage型は単なるuint32のエイリアス型であるため,大小比較ができることに注意しましょう。

// Crosses returns whether the transition from From to To crosses the stage s:
//  - It returns CrossOn if it does, and the lifecycle change is positive.
//  - It returns CrossOff if it does, and the lifecycle change is negative.
//  - Otherwise, it returns CrossNone.
// See the documentation for Stage for more discussion of positive and negative
// crosses.
func (e Event) Crosses(s Stage) Cross {
    switch {
    case e.From < s && e.To >= s:
        return CrossOn
    case e.From >= s && e.To < s:
        return CrossOff
    }
    return CrossNone
}

Crossesメソッドを使うことで,プラットフォームによって,とあるステージに遷移しない場合でも,そのステージを「通過」したかどうかが分かります。

たとえば,以下のようにCrossesをメソッドを呼び出した場合,たとえStageVisibleに遷移してなくても,あたかも遷移したかどうかを判断しているように処理ができます。

func main() {
    app.Main(func(a app.App) {
        for e := range a.Events() {
            switch e := app.Filter(e).(type) {
            case lifecycle.Event:
                switch e.Crosses(lifecycle.StageVisible) {
                case lifecycle.CrossOn:
                    // 画面が表示された場合の処理
                case lifecycle.CrossOff:
                    // 画面が表示されなくなった場合の処理
                }
            }
        }
    })
}

Crossesメソッドは一見無駄なように見えますが,Go Mobileでは,複数のプラットフォームの実装を用意する必要があり,Crossesメソッドは,その違いを吸収することができる,よく考えられたメソッドです。

今回は,タッチイベントとライフサイクルについて説明しました。タッチイベントと前回説明したspriteパッケージを使えば簡単なゲームを作ることができるでしょう。また,ライフサイクルイベントを使えば,アプリがサスペンドされたことを検出して,ゲームをポーズさせたりすることができ,より完成度のゲームを作ることができるようになると思います。

次回は,音の再生と各種センサーを扱う方法について説明する予定です。

※この記事は8/10に書かれたものです。現在では最新のGit2.5のWindows用バイナリも公式に公開されていますので、
 自前ビルドせずともGit2.5が利用できますが、次期バージョンでも同様の手順でビルドできると思われます。

@makki_dです。
普段はLinuxを使っていますが、WindowsでGitをビルドしてみたというお話です。
とても簡単にビルドすることができたので紹介したいと思います。

Git 2.5 を使いたいんです。

先日Git2.5が公開されました。
様々な修正のほか機能も多数追加されましたが、個人的には git worktree コマンドに注目しています。

git worktreeとは

あるブランチで作業をしているときそれを中断して、急ぎで別のブランチの修正をしなければならないことってよくありますよね。
そんな時皆さんはどうしていますか?

  1. 今の作業をとりあえずcommitstashしてブランチを切り替える
  2. もうひとつcloneしてきてそこで作業する
  3. git new-workdirで別ディレクトリに作業ツリーを作る
  4. git worktreeで別ディレクトリに作業ツリーを作る ← New!!

git worktreegit new-workdirと同じく、別ディレクトリに作業ツリーを作ることができるコマンドです。
作業ツリーを別に作ることで、今進めている作業をそのまま置いておきながら、同じリポジトリの別ブランチの作業を別ディレクトリで行うことができます。

具体的な使い方はドキュメントの例がわかりやすいです。

これまでもgit new-workdirがありましたが、このコマンドはシンボリックリンクを作成するため、Windowsの場合一般ユーザ権限では利用できませんでした
一方、git worktreeではシンボリックリンクではなく、ファイル中にパスを記録する形で作業ツリーと親リポジトリの関係が管理されるので、一般ユーザでも利用することができます。

公式のWindows用バイナリは1.9.5 (※8/10時点)

早速 Git 2.5 をWindowsでも使ってみたいところですが、公式のDownloadページには、Windows向けバイナリは1.9.5までしか用意されていません。
そして「新しいバージョンが使いたかったら、ソースからビルドしてね」とさらっと書かれています。

Windows上でOSSのビルド環境を整えるというと多くの場合苦行となるような印象がありますが、Gitの場合は驚くほど簡単に開発環境がセットアップできました。

WindowsでのGitビルド環境

Windows用のバイナリと開発環境は、Git for Windowsで入手できます。
紛らわしいですが、「git for windows」等で検索すると上位に出てくる msysGit は 1.9.5 までしかありません。
特に2系を使いたい場合は、間違えずに後継プロジェクトであるGit for Windowsを見てください。

Git for Windows SDKのダウンロード・インストール

インストーラを実行すると、指定したインストールディレクトリ以下に gcc を始めとした開発ツール・環境・ライブラリ一式がダウンロード・インストールされます。
64bit版SDKのデフォルトのインストールパスは C:\git-sdk-64 なのでそのまま記載しますが、変更した場合は適宜読み替えてください。

インストールパス
ネットワークインストール

インストールが終わるとそのまま Git のビルドが始まりますが、私の環境では gettext 関連のビルドでエラーとなりました。

ビルドエラー

エラーとなったコマンドプロンプトを一旦終了し、msysのシェル (C:\git-sdk-64\mingw64_shell.bat) で改めて開き直します。
gitのソースディレクトリ (/usr/src/git) でmakeを実行すると、先ほどのエラーに引っかからずにビルドできます。

ビルド成功

ビルドできたらmake instalすると C:\git-sdk-64\mingw64\bin に実行ファイル一式がインストールされます。
ここにPATHを通すことで、gitコマンドがコマンドプロンプトやcygwinから使えるようになります。

バージョン2.4.6??

執筆時点のmasterブランチでビルドすると、git のバージョンが2.4.6になっていました。

version

これではgit worktreeが使えない!?と一瞬焦りましたが、よくよくコミットグラフを確認すると、v2.5.0タグのブランチがまるごとmergeされてるため、使いたかったgit worktreeコマンドも使えました。

worktree

ちょっとパッチを当ててみる

git worktreeで作った作業ツリーのサブディレクトリの中でalias登録したgitコマンドを呼び出すと、次のようなエラーが出てしまいます。

makiuchi-d@PC-1034 MINGW64 ~/Projects/test/work1/a (work1)
$ git br
fatal: internal error: work tree has already been set
Current worktree: C:/Users/makiuchi-d/Projects/test/work1
New worktree: C:/Users/makiuchi-d/Projects/test/work1/a

これでは不便なので、エラーを出している部分に即席パッチをあててみようと思います。

diff --git a/environment.c b/environment.c
index fb4eda7..8cf1442 100644
--- a/environment.c
+++ b/environment.c
@@ -226,7 +226,7 @@ void set_git_work_tree(const char *new_work_tree)
 {
        if (git_work_tree_initialized) {
                new_work_tree = real_path(new_work_tree);
-               if (strcmp(new_work_tree, work_tree))
+               if (strncmp(new_work_tree, work_tree, strlen(work_tree)))
                        die("internal error: work tree has already been set\n"
                            "Current worktree: %s\nNew worktree: %s",
                            work_tree, new_work_tree);

※あくまで応急措置です

この修正を加えた上でmakemake installすることで、動くようになりました。

動作確認

まとめ

GitはWindowsでも簡単に開発環境をセットアップできます。
いち早く新機能を試したいときや、ちょっとした修正をしたい場合など、ぜひ試してみてください。

P.S.
8/18、Git 2.5のWndows用バイナリが公式に公開されました。

↑このページのトップヘ