KLabGames Tech Blog

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

(本稿はKLab Advent Calendar 2016 の1日目の記事になります)

今年もアドベントカレンダーの季節がやって来ました。お祭り騒ぎと年末進行の勢いで、普段は書かないような変なネタも含めてKLabエンジニア陣が多彩なエントリーをお届けします。

最初は昨年に引き続きKLabアドベントカレンダーの旗振り役をさせていただいてますoho-sからです。 よろしくお願いします。

はじめに 「進歩した科学は魔法と見分けがつかない」

皆さんは不老不死に興味はありますか? 古今東西、人々は伝説に宗教にそして現実にそれを追い求めて来ました。 でも、そもそも「生きている」ってどういうことなのでしょう。 現代の生物学においても、その定義は非常に曖昧です。 特に急進的な主張としては、一個体の生物を構成する物理化学生物的な現象を全て完全に正確に模倣したら、それは生きているということではないか、というものです。 そして、その主張にのっとり、近年のSFで科学的に現実的? な方法としてよく見られるのが、脳のコンピュータへのアップロードです。

これは、人間の脳を構成するあらゆる情報をデータ化したうえで、ものすごい性能のコンピュータにシミュレーションさせることで、人間がコンピュータ上で生きていけるのではないか、という考え方です。 そうすれば、コンピュータ上のあらゆるデータと同じように、バックアップもとれるし、コピーもできるし、電源がある限り動き続けられるし、これ、不老不死じゃん! ということです。

もちろん、人間の神経細胞は140億個以上あるといわれ、それら同士の結合はさらに大変な数になります。当然現在の最高性能のコンピュータをもってしても、シミュレーションは不可能です。 さらに、神経細胞同士の接続の性質も完全にわかっているとはいいがたい状況です。

では、一方で、「その人」を維持するに足る正確さで各神経細胞や、その相互の接続の情報を取得することができるのでしょうか?

生物学の世界では、生物の全遺伝情報、いわゆるゲノムがデータ化できた現在、次のターゲットは、脳の神経細胞とその全結合情報のデータ化となっています。 電子顕微鏡やその他の観測手段を用いてデータ化していくのです。 そして、一個体の全神経細胞とその全接続の情報のことを「コネクトーム」と呼び、一部の生物のコネクトームはインターネットで公開さえされているのです。

つまり、自分自身のコネクトームを作成し、それをシミュレーションできたら不老不死になれそうです。 そう、SFの登場人物たちはきっとこうやって、いとも簡単に脳をコンピュータにアップロードし、コピーし転送しバックアップしアップグレードし、そして進化しているのでしょう。

しかし、ちょっと待ってください。エンジニアなら「それどんなミドルウェア使ってるの?」って気になりませんか? RDBMSのバックアップさえ一仕事なのに、一貫性や同一性やその他諸々は本当に大丈夫なのか。むしろどんなデータ構造なのか。 気になるところはいくらでも出て来ます。 そこで、GraphDBです。

コネクトームとGraphDB 「電気線虫はアンドロイドの夢を見るか?」

まずはコネクトームとはどのようなデータになるのかを具体的にお話ししましょう。 現在、公開されているコネクトームとしては、線虫( C. Elegans )のコネクトームがあります。 線虫は体長1mmほど、全細胞数およそ1000個、神経細胞は300個くらいです。 コネクトームの実態は、神経細胞をノード、それらの接続をエッジとしたグラフデータです。エッジの属性として、電気的・化学的結合の強さが格納されます。 つまり、このようなデータを永続化したり神経活動をシミュレーションしたりなどの様々な処理をするには、グラフデータを扱うのに適したデータ管理システム、そう、GraphDBがあればいいということになります。

GraphDBとは、その名の通りグラフデータ構造を格納して処理するためのDBMSです。 有名なものとしては、Neo4jなどがあります。 ノードとエッジからなるネットワークのようなデータ構造を、テーブルからなるデータ構造を管理するRDBMSのように、管理するものです。 RDBMSでグラフ構造を扱ったことのある人は、その煩雑さと低性能に悩まされたことがあるかもしれません。RDBMSでグラフ構造を扱うのは非常に難しいと言われています。GraphDBは最初からグラフデータ構造を扱うために設計されているために、はるかに速く安全にグラフ構造を管理できるのです。

GraphDBや、グラフデータを扱う仕組みは、例えばSNSのユーザー間の関係性であったり、検索対象ドキュメントの間の関係性であったりと、様々な対象が現実にあり、近年利用が進んでいる分野です。

ということで、本稿では、Go言語でグラフデータを格納し永続化できる簡単な仕組みを実際に作成し、そこに線虫のコネクトームを格納して、その線虫を不老不死にしてみましょう。

GraphDBを作る 「Connect! A to B」

まずはバックエンドのデータストアを決めます。もちろんここでグラフに向いた独自データストアを作り始めてもいいのですが、今回はLevelDBを使います。GoでLevelDBを使うに当たっては、公式ドキュメントと、こちらを参考にしました。 LevelDBはいわゆるKVSなので、グラフデータに適したキーを設計しなければいけません。今回は、以下のようなキーの構造にしました。

ノード

Name Size Description
NodePrefix=0x01 1byte プリフィックス
type 2byte タイプ
ID 4byte ID

エッジ

Name Size Description
EdgePrefix=0x02 1byte プリフィックス
FromID 4byte 始点ノードのID
Direction 1Byte エッジの方向
Type 2Byte タイプ
ToID 4byte 終点ノードのID

LevelDBはバイト列の前方一致による検索が可能なので、これで、

  • ノードの全検索
  • ノードのタイプによる検索
  • ノードのタイプとIDによる検索
  • エッジの全検索
  • エッジの始点IDでの検索
  • 始点IDと方向での検索
  • 始点IDと方向とタイプでの検索
  • 始点IDと方向とタイプと終点IDでの検索

ができます。 一方で、このキー構造だと、終点から始点への検索ができないので、エッジを格納するときは、始点から終点までの前向きエッジと終点から始点までの後ろ向きエッジを同時に格納することで解決しました。

こちらが、実際に作ってみたリポジトリです。

以下にノードとエッジのコードを提示します。

また、DBのOpenや検索等のためのコードが以下になります。

公開されている多くのGraphDBには、ロックやトランザクションや各種制約、クエリ言語やサーバーとして動かすための仕組みなどがありますが、今回はLevelDBにグラフ構造を永続化して、簡単なノードの検索とエッジの検索ができるようになったということで、ここまでにしておきます。

線虫のコネクトームを格納する 「発進」

線虫のコネクトームデータはこちらのものを利用しました。オリジナルのデータはdoi: 10.1126/science.1221762.を参照してください。dot形式のデータで取得してテキストエディタ等で置換とマクロでTSV形式の単純なデータに変換しました。

実際のデータはこのようなものです。

これを作成したGraphDBに読み込みます。

読み込みは以下のようなコードを書きました。

// InputFromTSV read two tsv files (node and edge) and insert node and edge into DB
func inputFromTSV(db *graphdb.GraphDB, nodefilename string, nodetype int16, edgefilename string, edgetype int16) {
    nodefile, err := os.Open(nodefilename)
    if err != nil {
        panic(err)
    }
    defer nodefile.Close()

    nodereader := csv.NewReader(nodefile)
    nodereader.Comma = '\t'
    for {
        record, err := nodereader.Read()
        if err == io.EOF {
            break
        } else if err != nil {
            panic(err)
        }

        node := &graphdb.Node{
            Nodetype: nodetype,
            ID:       no2ID(record[0]),
            Value:    []byte(record[1]),
        }
        db.AddNode(node)
    }

    // ==========================================================================

    edgefile, err := os.Open(edgefilename)
    if err != nil {
        panic(err)
    }
    defer edgefile.Close()

    edgereader := csv.NewReader(edgefile)
    edgereader.Comma = '\t'
    for {
        record, err := edgereader.Read()
        if err == io.EOF {
            break
        } else if err != nil {
            panic(err)
        }
        db.AddEdge(no2ID(record[0]), no2ID(record[1]), edgetype, []byte(record[2]+","+record[3]))
    }
}

func no2ID(no string) []byte {
    noint, _ := strconv.Atoi(no)
    id := make([]byte, 4)
    binary.LittleEndian.PutUint32(id, uint32(noint))
    return id
}

そして、まずは永続化した線虫のコネクトームをDOT形式で書き出し、データの確認をします。

func main() {
    db, _ := graphdb.Open("celegans.db")
    inputFromTSV(db, "c.elegans_neural.male_node.tsv", NEURON, "c.elegans_neural.male_edge.tsv", CONNECTION)

    db.PrintGraph2DOT()
}

以下が出力したdot形式ファイルをGraphVizで書き出したものです。

C. Elegans のコネクトーム

次に、ノードに接続するエッジを検索してみましょう。 コードはこんな感じになります。

func main() {
    db, _ := graphdb.Open("celegans.db")

    start := db.GetNode(NEURON, no2ID("0"))

    edges := db.GetNodesEdge(start)

    for _, edge := range edges {
        fmt.Println(graphdb.Byte2string(edge.To))
    }
}

さらに、こうすると、Hop数を変えてノードを羅列できます。

func main() {
    db, _ := graphdb.Open("celegans.db")

    start := db.GetNode(NEURON, no2ID("0"))
    fmt.Println(graphdb.Byte2string(start.ID))

    rec(db, start, 0, 4)
}

func rec(db *graphdb.GraphDB, node *graphdb.Node, curdepth int, maxdepth int) {
    curdepth++
    if curdepth == maxdepth {
        return
    }

    edges := db.GetNodesEdge(node)

    for _, edge := range edges {
        fmt.Println(strings.Repeat(" ", curdepth) + graphdb.Byte2string(edge.To))

        next := db.GetNode(NEURON, edge.To)

        rec(db, next, curdepth, maxdepth)
    }
}

いかがでしょう。これで僕のPC上で線虫が不老不死となりました。

・・・そんなわけない!

まとめ 「未来を予測する最善の方法」

今回はあくまでも実生物の神経ネットワークを使いやすい形で静的に永続化したにすぎず、生物のダイナミックな「生きている」という性質は、シミュレーションを動かしたりしなければ得られません。不老不死ははるか遠い。

今回、なぜこのような題材を選んだかということを最後に説明しないと意味不明になりそうなので、説明しておきます。

まず、GraphDBに興味があったこと、特に、今あるものを利用するのではなく、どう実現されているのかに興味があったので実際に作ってみようと考えました。 また既存のGraphDBはサーバーを立てて使うものが多いのですが、プログラム組み込みで使うものが欲しかったというのもあります。 と言っているところで、EliasDBというものを見つけました。 今回は、それRDBMSでもいいじゃんというレベルしか実装できませんでしたが、GraphDBはとても面白いので、今後も勉強していきたいと思います。

そして、GraphDBを使った題材を考えていて、コネクトームというものを知り、これだ!と思ったわけです。 コネクトームに関しては、ロボットに線虫のコネクトームを元にしたシミュレーションシステムを接続し、電子線虫サイボーグを作る研究など、とてつもなく未来な研究が実際に行われています。例えば、ここです。興味のある方は調べてみると面白いと思います。

それでは、引き続きこの後のKLabアドベントカレンダーをお楽しみください。

はじめに

こんにちは、@tsukimiyaです。シンボリックリンク切り換えによるホットデプロイ、したいですか?シンボリックリンク切り換えによるデプロイはアトミックなデプロイを低コストで実現する手段です。最近はDeployerやCapistranoなどシンボリックリンク切り換えによるデプロイ作業を簡単に行うためのツールも充実し、自分で頑張ってシェルスクリプトを書かずとも低コストでシンボリックリンク切り換えデプロイを行う事が可能です。

ただ、PHPでのシンボリックリンクの切り換えによるデプロイについてネット上を見ていると「nginx+php-fpm環境でOPcacheを有効にしているとシンボリックリンクを切り換えてもキャッシュされている切り換える前のコードが実行され続ける」という情報がいくつか見つかります。OPcacheが原因となるならApache+mod_php環境下でも同様の問題は発生しそうですが、トラブルに遭遇しているのはnginx-php-fpm環境の人ばかりです。実際、自分は普段Apache + mod_php環境をよく使っているのですが実行するコードがいつまでも更新されない、というトラブルには遭遇していないように思います。

今回はnginx + php-fpm, Apache + mod_phpそれぞれの環境に対し検証コードを実行し、問題の確認・原因の切り分けをしてみました。

準備

実験環境は以下のような構成で準備しました。

ディレクトリ構成

.
|-- 1
|   |-- index.php
|   `-- lib
|       `-- user.php
|-- 2
|   |-- index.php
|   `-- lib
|       `-- user.php
`-- docroot -> 1

プログラム

  • index.php

<?php
require_once(dirname(__FILE__).'/lib/user.php');
echo "index(1):" . __FILE__ . "<br>";
foo();
  • lib/user.php

<?php
function foo()
{
	echo "lib(1):" . __FILE__ . "<br>";
}

ディレクトリ1, ディレクトリ2がソースコードが存在するディレクトリの実体で、docrootがドキュメントルートに設定するディレクトリ1, またはディレクトリ2へのシンボリックリンクになります。
このコードをnginx + php-fpm環境とapache+mod_php環境でシンボリックリンクを書き換えながらそれぞれ実行してみます。

PHPにはOPcacheの他にもrealpath cacheという仕組みがあり、開いたファイルの実パスを一定時間(php.iniのrealpath_cache_ttlに設定した秒数の間。標準は120秒。)キャッシュするという仕組みがあります。コード中に存在する__FILE__は実行しているスクリプト自身のパスが入るPHPが自動的に定義する定数ですが、これがRealpath cacheの影響を受ける事を考え、念のためディレクトリ1のコードには(1)、ディレクトリ2のコードには(2)と直接記述しました。
realpath cacheとシンボリックリンク切り換えリリース時にrealpath cacheが引き起こす問題について興味のある方は「PHPにおけるシンボリックリンクを使ったデプロイの危険性について(「realpath_cache」和訳)」で詳しく説明されているので、興味のある方はこちらもご覧ください。

検証

検証はphp-fpm, mod_phpそれぞれの環境に対して以下の手順で進めました。

1. ディレクトリ1に対してシンボリックリンクを張りindex.phpを実行しOPcacheにキャッシュさせる

まずはindex.phpを実行しOPcacheにキャッシュさせます。この時点ではディレクトリ1に対してシンボリックリンクを張り、そのまま実行しているだけなので出力は以下のようになり結果に差違は見られません。

nginx+php-fpm Apache + mod_php
出力 index(1):/var/www/1/index.php
lib(1):/var/www/1/lib/user.php
index(1):/var/www/1/index.php
lib(1):/var/www/1/lib/user.php

2. docrootへのシンボリックリンクを2に張り替えindex.phpを実行する

シンボリックリンクを張り替え、すぐにindex.phpを実行した結果です。

nginx+php-fpm Apache + mod_php
出力 index(1):/var/www/1/index.php
lib(1):/var/www/1/lib/user.php
index(1):/var/www/1/index.php
lib(1):/var/www/1/lib/user.php

この時点ではどちらの環境でも 1/index.php が実行されています。これはrealpath cacheが「docroot/index.php => 1/index.php」という情報をキャッシュしているから、と考えられます。

3. 120秒経過した後、もう一度index.phpを実行する

今回調べたいのはOPcacheがシンボリックリンク切り換えに与える影響なので、PHP本体が作成したrealpath cacheの有効期限が切れるのを待ちます。今回はphp.iniを編集せずに検証しているので、realpath_cache_ttlの標準設定である120秒が経過するのを待ちもう一度実行します。

nginx+php-fpm Apache + mod_php
出力 index(1):/var/www/1/index.php
lib(1):/var/www/1/lib/user.php
index(2):/var/www/2/index.php
lib(2):/var/www/2/lib/user.php

Apache + mod_php環境では 2/index.php が実行されました。対して、nginx + php-fpm環境は 1/index.php のままです。念のために時間をあけてもう一度実行したりもしましたが、nginx + php-fpm環境では 1/index.php が実行され続けました。
一体何が起きているのでしょうか?

古いコードが実行され続ける原因

OPcacheがキャッシュしているのはPHPのopcodeだけではありません。realpath cacheの情報も一部OPcacheが管理する共有メモリ上に保存します。この「realpath cacheのキャッシュ」はphp-fpm, mod_phpどちらの環境でも作られるものですが、このキャッシュが存在している状態でrealpath cacheの有効期限が切れた場合の挙動がphp-fpmとmod_phpで違うのです。Apache + mod_phpの場合、realpath cacheの有効期限が切れるとOPcacheが保持しているrealpath cacheのキャッシュも作り直されるのですが、nginx + php-fpm環境の場合realpath cacheの有効期限が切れてもOPcacheは新しく作られたrealpath cacheを無視して古いrealpath cacheの情報を保持し続けます。実際、3)を行った際にrealpath_cache_get()関数を使いrealpath cacheが保持している内容を見ると
「docroot/index.php => 1/index.phpというキャッシュはexpireされ、docroot/index.php => 2/index.php というキャッシュが作られているにも関わらず 1/index.php が実行され続けている」
という状態を確認することが出来ます。

OPcacheのキャッシュをクリアする

今回の検証でOPcacheがシンボリックリンク張り替え前のrealpath cacheをOPcacheがキャッシュし、nginx + php-fpm環境の場合古いキャッシュを保持し続ける事でシンボリックリンクを切り換えても古いコードが実行され続ける可能性があることがわかりました。原因はOPcacheがキャッシュしているrealpath cacheなのでそのキャッシュを適切にクリアする事が出来ればシンボリックリンクを張り替えた後のコードが実行できるはずです。

OPcacheのキャッシュは「プロセスを再起動する」か「opcache_reset()関数を呼ぶ」ことでクリアすることが出来ます。このうち、プロセスの再起動はダウンタイムが発生する代わりにopcache, realpath cache共に消えるためダウンタイムの発生が許容出来るなら最も確実な方法です。
では、opcache_reset()関数はどうでしょうか。

実は、PHP7.0以降なら一定の解決策になりえます。PHP7.0以降のopcache_reset()はOPcacheのキャッシュ内容をクリアするのとあわせて、全てのプロセスPHP本体側のrealpath cacheをクリアするので、不整合が発生することはありません。

一方でPHP5.6までのopcache_reset()関数はPHP本体側のrealpath cacheにはノータッチでした。よって以下のような問題が引き起こされます。

  1. 新しいコードをデプロイしシンボリックリンクを切り換え
  2. opcache_reset()実行
  3. realpath cacheがexpireする前にプログラムが実行される
  4. OPcacheが古いrealpath cacheをキャッシュしてしまう
  5. nginx + php-fpm環境だと以前のコードが実行され続ける

この問題を解決するには「realpath cacheがexpireされてからopcache_reset()関数を実行する」事になり、現実的ではありません。

opcache_reset()を実行するまでは以前のコードが実行される可能性があり完全な解決策とは言えませんがロードバランサー切り換えほど大げさな仕組みを用意しづらい環境で、かつPHP7以降を使っているならopcache_reset()を使う事を考えても良いかもしれません。
ただ、この方法だとopcache_reset()を叩くためのPHPコードを用意しなければならず、本番環境に適応する場合opcache_reset()を叩くためのPHPコードはローカルアクセスしか出来なくする、と一手間付け加える事が必要になります。そこで検討したいのが次の方法です。

Webサーバにシンボリックリンクからrealpathへの解決を任せる

nginxを使用している場合、nginxにrealpathの解決を任せる事が可能です。
nginxの設定で

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

としている箇所を

fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;

に変更します。こうすることでシンボリックリンクからrealpathへの解決がnginxで行われ、PHPは常にrealpathで動作する事になりシンボリックリンクを切り換えた際に発生するOPcacheやrealpath cacheによる問題は起こらなくなります。この方法ならopcache_reset()を使用する方法とは違い管理用のプログラムを別に用意することもないですし、アトミックなデプロイも完全な形で行えます。

おわりに

OPcacheを有効にしているnginx + php-fpm環境でシンボリックリンク切り換えによるデプロイを行うとopcodeが更新されず古いコードが実行され続けるらしい、という漠然とした情報から興味を持ち調べてみたのですが、実際にnginx + php-fpm環境では古いコードが実行され続ける事が確認出来ました。自分としては実行している環境がOPcacheの挙動に影響を与えるとは考えていなかったため、実際に挙動が変わる事が確認出来たのは面白い発見でした。
また、opcache_reset()によってキャッシュをクリアすれば解決する、という記事も何件か見たのですがopcache_reset()はPHPのバージョンにより挙動が違いPHP7.0以降なら一定の解決策になり得ると言う事がわかったのも自分としては新しい発見でした。

  • OPcacheを有効にしているnginx + php-fpm環境ではOPcacheが原因で古いコードが実行され続ける可能性があること
  • その環境でもnginxにrealpathの解決を任せたりPHP7.0以降のopcache_reset()関数を使えば便利さを損なわずデプロイツールの恩恵を得られること

が伝われば幸いです。

はじめに

オンラインゲームでは、金貨やコインといった ゲーム内で通貨のように利用可能なアイテム が必ずといって良いほど登場しますよね。
先日もこのTech Blogにて、ゲーム内で発行する仮想通貨のデータ分析業務についての記事が掲載されましたが、ゲーム内通貨の運用は各種法令等も絡むことからKLabでも特に注意して取り扱いを行っている業務の一つです。

そこで今日はゲーム内通貨の運用に関する業務やKLabとしての考え方を、会計上の観点からもう少し掘り下げてご紹介します。

法令におけるゲーム内の金貨等の位置づけ

オンラインゲーム内でアイテムの購入に利用できる金貨やコインといったトークン、上記ではゲーム内通貨と言いましたが、これは資金決済に関する法律(以下資金決済法、とします)で各種の規定がされています。そして上記のようなゲーム内の概念は、この法律の中では前払式支払手段という名前で定義付けがされています。

前払式支払手段の定義について条文にはちょっと難しく書いてあるのですが、できるだけウソにならない範囲で簡単に書くと以下のようになります。

  1. 発行の際にその金額・数量等が紙の証票(例としてチケットなど)またはデジタルデータとして記録されていること
  2. 1が対価と引き替えに利用者に対して発行されていること、つまり有償であること
  3. 1の内容と紐付く、IDや番号などが発行されていること
  4. 物品の購入やサービスを受ける際などに利用できるものであること

※ただし上記を満たす場合でも使用期限が6ヶ月以内のものは原則として対象外

上記はゲーム内の金貨やコインだけでなく、デパートなどで発行される商品券や交通機関などで使える回数券やプリペイドカード、英会話教室の前売りチケットなども上記の特徴を持つことが多く、つまりこれらは法律上同じ立て付けのものということになります。

ところで今年5月に改正資金決済法が成立し、bitcoinといった近年出てきた新しい決済手段に関する条文が追加されました。そこで このbitcoinのような新しい概念は、条文の中では 「仮想通貨」 という名称で定義づけされました。
「仮想通貨」というとどこかで聞いたような名前ですが、ここでいう仮想通貨とは前述の特定のサービスやゲーム内でのみ利用可能な前払式支払手段とは法律上別物でして、ここでは不特定の相手に対して利用可能なものを指します(正確にはそのほかにも仮想通貨を構成する要件があります)。従って法令改正後も前述の前払式支払手段に関する条文はほぼそのまま残っています。

なお冒頭でご紹介した記事のように、我々KLab内部ではゲーム内の金貨やコイン=前払式支払手段のことは 仮想通貨 と呼んでいるのですが、上記の法令改正の絡みもあり誤解を招くためこの記事の中では、法令に則り正確に 前払式支払手段 と呼ぶことにします。

オンラインゲームでの前払式支払手段の会計処理

当たり前の話ですが、会計において一番大事なことは売上を正確に勘定することです。
会計上、売上を計上するタイミングは物販業においては引渡基準と言って 原則的には役務提供(顧客に対してサービス提供や商品の引き渡しを行うこと)を完了した時点とすること になっています。
例えば自動車の販売を行った場合、売上を立てるのは契約日や入金日ではなく、この引渡基準に基づいて登録日あるいは実際に顧客に対して車の引き渡しを行った日とする運用にするのが一般的です。

この点でオンラインゲームにおける前払式支払手段は次のように若干特殊な性格を持ちます。
※ゲームにおける金貨やコインなどは無償でも配布することもありますが、ここでは有償で販売された物で無償配布のものは含まない(本来の意味での前払式支払手段)前提で説明します

オンラインゲームにおける前払式支払手段の流れ

上記で示すようにオンラインゲームにおける前払式支払手段とは、iOSのApp StoreやAndroidのGoogle Playストアのようなスマートフォンでの「決済プラットフォーム」によるアプリ内課金や、その他クレジットカード決済などで販売される「商品」です。しかし、顧客がその「商品」(例えばコインや金貨)を購入する動機は、これそのものにあるわけではなく、これと引き替えに何かアイテムを購入したい、とかサービスを受けたい、というさらに別の目的にあるわけです。
逆にコンテンツ提供者=”前払式支払手段の提供者”の視点からすると、前払式支払手段を販売しただけの段階では商品を引き渡す義務がまだ残っている状態だと考えることもできます。
つまり前払式支払手段は債権を表章した有価証券のようなものと考えると、前払式支払手段販売時点ではゲーム内における役務提供は完了していないため、この時点では売上計上は まだ できない、と考えるのがもっとも合理的なのです。最初にプリペイドカードや英会話前売りチケットのことを書きましたが、これと同じと考えればわかりやすいと思います。

これを会計的に整理して言うと、顧客が「決済プラットフォーム」で代金を支払った時点でまもなくコンテンツ提供者への入金は発生するもののこの時点では役務提供の終了(=売上発生)とは見做さず、 前受金処理(負債として計上すること)を行う 、ということになります。つまり顧客が実際に前払式支払手段を利用してアイテムを引き替えた時が前受金を取り崩すタイミング=すなわち売上として計上を行うタイミングになるわけです。

帳簿イメージ

この項の最初に書いた通り、会計において一番重要なことは売上を正確に計上することです。従って前払式支払手段を利用した業務においては、販売・発行時の情報と同じくらい、 消費した際の情報が大事 だということがおわかり頂けると思います。

前払式支払手段の残高管理と売上計上

多くのゲームにおいて前払式支払手段はまとめ売りの際に値段を下げることがあります。
例えば、金貨100枚だと120円(@1.2円)だが1,200枚まとめて買うと1,000円(@1.0円)になってお得!……といった販促方法は一般的にもよく見られるものですよね。

どの価格で購入した前払式支払手段であっても同様に利用できるという前提であれば、アイテム引き替えの実装だけを考えた場合、その前払式支払手段の総残高さえ分かればよいことになります。つまりそれぞれの単価を気にする必要は無いわけです。
一方で、上記で述べたように「売上を正確に計上する」という観点ではその実装だけでは問題が発生します。

  • それぞれ「単価いくらの前払式支払手段が消費されたか?」を記録しておかないと売上が正確に計上できない
  • 単一アプリにおける同一の前払式支払手段でも、Google PlayストアとKindleアプリのように購入元の「決済プラットフォーム」が混在することがある(売上は「決済プラットフォーム」毎にそれぞれ勘定するのが妥当であるため)

……ということがあるためです。そのため単に総残高を記録するだけでなく、前払式支払手段の種類別の 預入・払出のルールが厳密に運用 されていなければなりません。
このような要件は実は一般的な商品の在庫管理の考え方そのものでして、これに倣って前払式支払手段の残高を管理していくことになります。

在庫管理の基本的な方式は先入先出法、後入先出法、移動平均法といったものがありますが、「古いものから消費」というルールが顧客から見て最も自然でわかりやすいこと、預入・払出記録の1:1の突き合わせができるため詳細なトレース・分析が行いやすいなどの理由からKLabでは現状ほとんどのケースで先入先出法を採用しています。

さて、会計上一般的に在庫の管理を行う場合には 商品有高帳 という帳簿を作成します。前払式支払手段の内部管理においてもこれとほぼ同様のことを行うことになります。
例として、とあるゲームの金貨(=前払式支払手段)の預入・払出のサンプルを以下に示します。

金貨預入・払出サンプル

上記の例では、顧客であるユーザAは日を分けて金貨をそれぞれ100枚(単価 @1.2円)、1,200枚(単価 @1.0円)と購入し、その次の日にアイテムショップで500枚消費しています。ということで残高は (100+1,200)-500=800枚になる計算です。
肝心の内訳としては、先入先出で古いものから消費していくルールですから、単価1.2円の金貨全部と単価1.0円の金貨一部が取り崩され、後者が800枚だけ残ることになります。

このデータさえあれば売上を導出するのは簡単ですね。上記の「払出」に当たるログを全顧客分サマリーすれば良いわけです。
さらにここから前払式支払手段の残高も明らかなので、これをエビデンスとして資金決済法で定められた供託(後述します)を行うことができます。

その他資金決済法で定められた業務

これまで前払式支払手段の会計業務についてご説明してきましたが、KLabは資金決済法で定められる自家型の前払式支払手段発行者に該当しますので、上記の他にも法で定められた業務が存在します。
以下に簡単にご紹介します。

前払式支払手段の発行届出

資金決済法では基準日(毎年3月末・9月末)未使用残高が1,000万円を越える自家型発行者は、管轄の財務局長に前払式支払手段に関する情報(前払式支払手段の名称やその単価等)を書面で届出することが定められています。
KLabもこれに当たるため、関東財務局に前払式支払手段に関する届出を行っています。

発行保証金の供託

資金決済法では、保証金として発行済みの前払式支払手段の1/2以上の金銭を供託することが定められています。上述の発行届出と同様に、基準日残高でこれを計上します。
これは利用者保護のための運用で、万が一発行者が破綻してしまったような場合にはここから優先的に配当を行うことで、支払い済みの代金を丸損しないようにしているというわけです。

前払式支払手段の払戻し

銀行法や出資法という法律では、免許なしに事業として他人のお金を預かったり、その払戻しを行ったり、または送金をしたりすることを禁止しています(これらは社会的影響が大きな業務だからです)。
もし前払式支払手段がいつでも自由に払戻しができてしまうと、一旦それを購入後、好きなときに現金化ができるということになります。これは上述したお金の預入と実質同じことができることになってしまうため、発行済みの前払式支払手段を払戻しすることは資金決済法で禁止されているのです。この規定があるため、前払式支払手段の発行業者は滅多なことでは払戻しに応じることはありません。

一方で発行者が前払式支払手段を廃止した場合には、上記の例外として利用者に対して購入済みの前払式支払手段について払戻しの対応を行わなければならないことになっています。
利用者は発行者が設定した申し出期間(法令上最低60日間)内に申し出を行うことで払戻しが受けられることになります。
ちなみに、払戻しの期間を超えた場合は除斥(当該利用者を通常の払戻し手続きから除外して、相当する額の前払式支払手段残高を控除すること)することが可能になります。除斥された場合でも民法上の債権が消滅するわけではありませんが、この期間を過ぎてしまうとお金の回収が面倒になってしまいますので、未使用分の前払式支払手段があるサービスが終了してしまった場合は、事業者の告知を見たらぜひ早めに手続きをしてくださいね。

おわりに

アイテム課金型オンラインゲームというジャンルは、比較的新しい業態であるため日々利用者を保護するための新たな法令やガイドラインが検討されています。一方で、バックエンドの業務は通常の商品の在庫管理や売上管理等と考え方に大きな差はありません。
みなさんが前払式支払手段に関する業務を行うことがありましたら、この記事を何かの参考にしていただければ幸いです。


Shimanuki

↑このページのトップヘ