UnityのGCはどんな実装になっているのか


こんにちは。Aiming エンジニアの久保田です。

僕の携わっているプロジェクトでは、近頃、Unity製クライアントのパフォーマンスの調査や改善を行っている最中です。
プロファイラを眺めていると、僕達が書くアプリケーションレイヤのコードが目立って遅い、ということは珍しいのですが、代わりにC#世界のスパイクとしてよく顔を出すのが、GC実行時間です。

C#は、タイプセーフでありながら人間にやさしく、getter/setter、async/await、Rx、ロケットなラムダ式、他他他…最新型の言語への影響も多大な、ファッション的にも◎な言語です。しかし、闇雲に全ての機能をタダで……というわけにはいかず、ことパフォーマンス面においては、GCというなかなか高い代償を支払うことになりかねないわけですね。

結論としては、UnityのGCは、皆が期待していたほど高性能ではなく、現状では僕達が書くC#が発生させるGCのインパクトは無視できない大きさになります。

そこで、GCのインパクトの少ないコードベースにしていく必要があるわけですが、コードを書く際、内部の実装をある程度理解していると適切な判断がしやすくなります。

今日は、そんなUnityのGCがどんな実装になっているのか、主に3つの方法で調べた結果を解説してみます。

  • 走らせてみる
  • ILコードや、IL2CPP AOTコンパイル後のC++コードを読んでみる
  • IL2CPP のvmをステップ実行してみる

注:ただし、Unityのロードマップには既にGCの差し替えが予定されています。近い将来、ここで紹介しているGCの性能はさらに改善されることと思われます。

Stop the World

さて、リアルタイム性の要求されるアプリケーションでは、GCが性能上の問題になるとかならないとか、よく噂されています。

なぜGCが目の敵にされるのか。これは、GCのスループットが良いとか悪いとかの問題ではなく、たとえどんなに真面目にひたむきにGCが仕事をしていたとしても、彼の仕事が他人のメモリを管理することである以上、管理下のスレッドを全て停止させるタイミングが必ずやってくる、という事情があるためです。

つまり、GCによるメモリ管理を利用しているアプリケーションは、あるとき全ての動作が停止する、これは俗に「Stop the World 」というかっこいい名前で呼ばれ、恐れられています。

Stop the World が発生している間、Unityのゲームはメインスレッドやその他(マネージドな)バックグラウンドのスレッドも実行できず、時が止まってしまいます。これが長引いたり、初動が遅れた処理が間に合わなければ、ユーザが目にするのはカクカクの描画や操作のひっかかり、というわけです。

一方、GCは歴史が古く、競争も激しい分野なため、このStop the World のインパクトを抑える工夫がたくさん編み出されています。
言語組み込みのGCでは、オブジェクトを寿命ごとに分けて管理する世代別GCや、Stop the Worldを段階的に実行するIncremental GC などが搭載されていることが多いようです。Javaに至っては、かなりバージョンアップを繰り返しており、全体のスループットを落とす代わりにGCを並列に動作させるフェイズを増やすコンカレントGCなども搭載されていました。

さて、僕達のUnityのC#ランタイムはどんなGC実装が使われているでしょうか? 気になります。

フルGCが毎回走る

Unity Blog の2015年の記事、Garbage collector integration によれば、Unityでは、Mono/IL2CPP どちらのランタイムも Boehm-Demers-Weiser というGC が使われている、とあります。

Boehm GC は、オープンソースのGC実装で、特定の言語への組み込みを想定したものではない代わりに、移植性が非常に高いつくりだと紹介されています。
ただし、機能面、性能面では言語組み込みGCには及ばないという評判もみかけ、実際、 オープンソースの.NET実装であるMono のGCは、Boehm GCから独自実装のSGenへと乗り換えを行いました。

僕の現行のプロジェクトで使用しているUnity(5.5.3)では、どうなっているのか、ちょっと確認してみましょう。

適当なUnityプロジェクトを作成し、C# の GC.Collect(); をスクリプトの任意の場所に仕込み、それをiOSビルドします。
Xcodeプロジェクトが生成されるので、プロジェクトを開き、Xcodeのシンボル検索で 「GC_Collect_m」を探してみると、

// System.Void System.GC::Collect()
extern "C" void GC_Collect_m2249328497 (Il2CppObject * __this /* static, unused */, const MethodInfo* method)
{
    {
    int32_t L_0 = GC_get_MaxGeneration_m1986243316(NULL /*static, unused*/, /*hidden argument*/NULL);
    GC_InternalCollect_m479047119(NULL /*static, unused*/, L_0, /*hidden argument*/NULL);
    return;
    }
}

ありました。
今見ているものは、C#標準ライブラリの GC.Collect が実機で実行されるときの姿です。元は System.dll に含まれるC#のバイトコードだったものが、C++コードへ姿を変えています。

iOSビルドはじめとしたUnityのIL2CPP ランタイムでは、C#コンパイラが吐いたバイトコードをそのままVMで実行するわけではなく、このようにあらかじめILコードを力技でC++コードに変換しています。つまり、実機上で走っているのはネイティブC++コードなんですね。

この機能のおかげで、ブラックボックスなはずのVMの動きをそこそこ深くまで読むことができ、Xcodeのデバッガもフルに使えるため、中身の動きを知るためにとても役に立ちます。

まずは ここの GC_Collect_m2249なんちゃらかんちゃらにブレイクポイントを張り、ステップ実行してみましょう。

何度かステップインしていくと、以下の場所へ来ます。

SampleIL2CPP`il2cpp::icalls::mscorlib::System::GC::InternalCollect:
0x101370cb0 <+0>: pushq %rbp
0x101370cb1 <+1>: movq %rsp, %rbp
-> 0x101370cb4 <+4>: popq %rbp
0x101370cb5 <+5>: jmp 0x101369d90 ; il2cpp::gc::GarbageCollector::Collect at BoehmGC.cpp:60

止まっているコードがアセンブリなのは、いよいよIL2CPPのVM部分のコードに到達したためです。これより下のソースはXcodeプロジェクトに含まれていません。

しかし、よく見てみると、親切にもソースコードのファイル名やシンボルが載っていました。

il2cpp::gc::GarbageCollector::Collect at BoehmGC.cpp:60

実は、この辺の libil2cpp のvm部分のソースファイルは、Unityに含まれています。

/Applications/Unity/Unity.app/Contents/il2cpp/libil2cpp/gc/BoehmGC.cpp (Macの場合) を開いてみると……

void
il2cpp::gc::GarbageCollector::Collect(int maxGeneration)
{
    GC_gcollect();
}

僕の書いた GC.Collect は、 @GC_gcollect@ という関数を引数なしで実行するという実装になっていました。
(上記では、GC.Collect から辿っていますが、メモリの圧迫がトリガーになった場合も同じ関数が実行されていた)

引数に渡ってきた世代番号を無視しているのがちょっと気になりますが、今は目を瞑りましょう。
この Gc_gcollect()関数が置かれているのは以下のファイルです。

/Applications/Unity/Unity.app/Contents/il2cpp/external/boehmgc/alloc.c

boehmgc というディレクトリ名ですね。中身をみると、現行のUnityはやはり Boehm GCを使っていることがわかります。

この、Boehm GC 内のGC_gcollect の先をステップインして掘り進んでいくと、以下の関数に辿り着きます。

/*
* Assumes lock is held. We stop the world and mark from all roots.
* If stop_func() ever returns TRUE, we may fail and return FALSE.
* Increment GC_gc_no if we succeed.
*/
STATIC GC_bool GC_stopped_mark(GC_stop_func stop_func)
{
// 省略
STOP_WORLD();
// 省略
START_WORLD();
// 省略
}

これは、GCが管理しているメモリに対してルートから辿れる参照にマークをつけるフェイズですが、STOP_WORLD というかっこいいマクロによって、スレッドを止める処理が差し込まれていることが確認できます。

さらに、この辺りの処理は、 `GC_Increment` が真のとき、ちょっとずつ実行するという挙動になるようなのですが、Xcodeでこの変数の値を追ってみると、

0が入っています。ということは、Incremental GCは有効になっていません。この値、確認した限りでは、プリプロセッサか、GC_enable_incremental() を呼ぶことでしか変更されていないので、少なくとも今見ている環境では 毎回、フルGCが走っているとみてよさそうです。

  • Unity上でのGC処理は、Boehm GCが使われている
  • 世代別やIncremental の機能は特に使われておらず、毎回フルGCが走っている可能性が高い
  • GCの実行中は Stop the World する

注: 上記の結果は、Unity 5.5.3 でのもの。

アプリケーションのC#が引き起こすGCインパクトは思いのほかでかい

どうやら、Unity の GCによるメモリ回収は、一括して行われるようです。この辺りが、GCが一度実行されると一気にスパイクとして現れる原因と推測できます。

さて、GCがボトルネックになっている場合、GCの首をすげかえる、GCをやめる、といった選択肢をとりあえず除外するならば、アプリケーションレイヤでの対策にどれくらいの意味があるのでしょうか。

Unity上で 何もないシーンを作成し、純粋にアプリケーションのC#で classのnew を繰り返した結果を見てみました。

高々1000回程度のclassのnewで、頻繁に8ms弱のGCが発生しています。

この結果の注目すべきところは、下のグラフ、アプリケーションがヒープを要求しなかった場合にはGCスパイクがまったく発生しなかったという点で、これはアプリ層のC#次第で GC発生頻度が大きく変わることを示唆しています。

また、Unity内のUnityEngine.Object をはじめとしたオブジェクト達は、DestroyしてもすぐにはGC対象にはならず、プーリングされるような振舞いをしているため、実際のゲームでもアプリ層のC#の影響はなかなか大きくなり得ます。

struct vs class

次に気になるのは、GCのマーク&スイープが走っているときを除いた、オブジェクトが管理されること自体のオーバヘッドです。

コンパイル後に生成されるコードを見比べることで、 値型と参照型、sturct と class の違いを比較してみたいと思います。

こんな感じのC#コードをつくり、

//(中略)
var c = new FatClass();
var s = new FatStruct();
//(中略)

以下のコマンドで IL2CPPのAOTコンパイラを走らせます。

$ mcs -target:library ./Hoge.cs
$ mono /Applications/Unity/Unity.app/Contents/il2cpp/build/il2cpp.exe \
--convert-to-cpp \
--enable-symbol-loading \
--development-mode \
--assembly='Hoge.dll' \
--generatedcppdir='/Users/rkubota/tmp/Hoge'

すると、 上記のC#コードは、下のようなコードに展開されました。

//(中略)
FatStruct_t680642026 V_0;
memset(&V_0, 0, sizeof(V_0));
(中略)
FatClass_t2447623899 * V_1 = NULL;
{
Initobj (FatStruct_t680642026_il2cpp_TypeInfo_var, (&V_0));
FatClass_t2447623899 * L_0 = (FatClass_t2447623899 *)il2cpp_codegen_object_new(FatClass_t2447623899_il2cpp_TypeInfo_var);
FatClass__ctor_m162577467(L_0, /*hidden argument*/NULL);
V_1 = L_0;
return;
}
(中略)

これが さきほどの new です。
こうしてみると、structのnewとclassのnewは、C#世界でのシンタックスは同じでも、vm内での仕事は大きく違っていることがわかります。

structのnewは、単純なC++世界の値へ展開されており、スタックへ変数を確保するだけ。
対して class は、管理のためのコードが余分に生成されています。

なかでも、il2cpp_codegen_object_new という関数は、GCへメモリを要求し、初期化する処理になっています。この関数を辿っていくと、Boehm GCによって `pthread_mutex_lock` が行われている箇所があります。

classをnewするたびにロックが発生しているとは、これを見るまで意識していませんでした。structと比較するとnewには少なからずオーバーヘッドがあるということは言えそうです。

値渡し vs 参照渡し

続いて、参照をつけかえることによるオーバーヘッドが存在するか調べてみます。

// ごく単純な値渡しのメソッド
static long PassValue(FatStruct v)
{
return v.M00 + v.M10 + 1;
}

// ごく単純な参照渡しのメソッド
static long PassRef(FatClass v)
{
return v.M00 + v.M10 + 1;
}

これが展開されたものが以下です。

// System.Int64 Hoge::PassValue(FatStruct)
extern "C" int64_t Hoge_PassValue_m3398056464 (Il2CppObject * __this /* static, unused */, FatStruct_t680642026 ___v0, const MethodInfo* method)
{
{
int64_t L_0 = (&___v0)->get_M00_0();
int64_t L_1 = (&___v0)->get_M10_10();
return ((int64_t)((int64_t)((int64_t)((int64_t)L_0+(int64_t)L_1))+(int64_t)(((int64_t)((int64_t)1)))));
}
}
// System.Int64 Hoge::PassRef(FatClass)
extern "C" int64_t Hoge_PassRef_m1560133231 (Il2CppObject * __this /* static, unused */, FatClass_t2447623899 * ___v0, const MethodInfo* method)
{
{
FatClass_t2447623899 * L_0 = ___v0;
int64_t L_1 = L_0->get_M00_0();
FatClass_t2447623899 * L_2 = ___v0;
int64_t L_3 = L_2->get_M10_10();
return ((int64_t)((int64_t)((int64_t)((int64_t)L_1+(int64_t)L_3))+(int64_t)(((int64_t)((int64_t)1)))));
}
}

値渡しと参照渡しは、さほど違いが見られませんでした。参照渡しだから何かGC管理のための特別なコードが生成される、ということはないみたいですね。

これはおそらく、Boehm GCが保守的GCという仕組みを採用しているためで、GC内部で使用中のメモリのうち、参照っぽいものをなるべく保守的に自動判定するアルゴリズムが働いてくれるため、アプリケーションがわざわざ参照が増えたり減ったりをGCに知らせる必要がないようです。

これは、関数への参照渡しだけでなく、プロパティへの代入時も同じでした。

Swiftのような参照カウント方式でメモリを管理する環境では、参照が増えたり減ったりする時点でオーバーヘッドが発生しますが、保守的GCの場合、オーバーヘッドは後から一括で支払う、というわけですね。

わかったこと

ここまででわかったことは以下です

  • UnityのGC は Boehm GC
  • Incremental や Generational の機能はなく、毎回フルGCが走っていると思われる
  • classのnew はstructに比べて遅い( ミューテックスのロックを通る)
  • 参照渡しや、参照型のプロパティ代入は特別なオーバーヘッドはない。(Write barieerもない)

一括してやってくるフルGCによるスパイクへの対策としては、まず毎フレーム生まれるようなゴミを生成しないようにすることが重要です。

この辺の具体的な対策の話は数えるとたくさんありそうですが、長くなってきたので、またの機会に譲りたいと思います。
それではまた!!!!!

参考:


ヴァリアントレギオンで利用しているSlackBOTについて


こんにちは。エンジニアの山内です。

Aimingでは社内のチャットツールとして Slack を使用しており、ヴァリアントレギオン (以下ヴァリレギ) のチャンネルでも、いくつかのBOTが動作しています。

KPT入力を催促するBOT

ヴァリレギのチームでは、Trello に Keep, Problem, Try を思いついた時に入力しておき、スプリントのタイミングで入力された内容を見返してKPTを進行しています。
ついつい忘れがちなため、毎日特定の時間になると Trello へ KPT の入力を要求するBOTがいます。KPTをやかる(※)BOT と呼ばれています。

※やかる:方言。(強引な理由の)文句を言う。訳すなら「KPT入力(されてない事に)文句を言うBOT」。

レビューを催促するBOT

ヴァリレギのチームでは、レビュータイムを設けてレビューが溜まってしまう状況を低減しています。レビュータイムをBOTが通知します。

Pivotal確認を催促するBOT

ヴァリレギのチームでは、PivotalTrackerでストーリーの管理をしています。実装ができて Accept を行うのはプランナーチームになるため、完了しているストーリーが無いか確認を催促するBOTがいます。

bugbot

最近はTODOも通知するようになりました。

担当決めBOT

急ぎで見てほしいレビュー、調査依頼など、誰かに依頼したいけど誰に投げるか決めにくい。かといって、@誰か ではなかなか拾われない。ということで、担当をランダムに抽選するBOTがいます。

これは あなたのチームの「いい人」は機能していますか? というスライドで紹介されていた手法です。

エリス 担当 <人数>

と入力すると担当者をピックアップしてくれます(エリスはヴァリレギ内でプレイヤーに色々教えてくれるキャラクターです)

eris

なぜか作成者が抽選されにくい現象が発生しましたが、コードに不正はありませんでした。
コマンドが日本語で打ちにくいと言う意見があり、エリスの多言語化が要求されています。

これらのBOTのおかげでチーム内のコミュニケーションが円滑になっています。


「ゲーム開発が変わる ! Google Cloud Platform 実践インフラ構築」という本を執筆しました


インフラエンジニアの野下です。

去年の年末から年明けにわたり、人生初めて本を執筆させていただきました。

本の内容については、日本リージョンの開設を発表し、さらなる飛躍を期待しているGoogle社のクラウドサービスである、Google Cloud Platform (以後GCP)へのゲームサーバ移設の話や弊社の様々なところで利用しているGCPの利用状況などについて書かせていただきました。

また、弊社の芝尾も一緒に執筆させていただき、弊社のデータ分析環境で使用しているBigQueryについての環境構築や他の分析環境との比較について紹介しています。

話をいただいたのが11月末で、それから冬休みのほとんどを執筆活動に費やし、2月中旬に校了というかなりタイトなスケジュールでしたが、2016年3月4日にkindle版をリリースすることができました。話をいただいた時は、まさか自分が本を出版することになるとは夢にも思わなかったのですが、誰でも体験できることではないので、貴重な体験をすることができました。

こちらが、本の表紙になります。初めて表紙ができた時は、感慨深いものがありました。現在、Amazonのストアにkindle版オンデマンド版の2種類がストアに並んでますので、是非読んでみてください(オンデマンド版については、4月1日ごろ発売予定)。

gcp表紙

 


第4回Aimingクソゲー開発コンテストを行いました!


 

こんにちは、エンジニアの相良です。

Aiming では1年に1回くらいの頻度で「クソゲー開発コンテスト」というイベントを行っています。
イベントの趣旨としては、
「職種関係無く有志でクソゲーを作って楽しく発表しましょう。 できればこれを機に新しいゲームエンジンとかをイジってみましょう。 クソゲーオブクソゲーには経営陣から賞が出るよ。」
みたいなものです。
普通にプロジェクトをやっているだけでは、なかなか新しいゲームエンジンなり技術に触れる機会って少なくなりますよね? また、人事や企画や運営やデザイナーなど、非エンジニアの方がちらっと Unity とかを触ってるとすごく相互理解が深まったりしますけど、実際のところ普段の会社生活の上でそういうことができる機会ってなかなか無かったりします。 なので Aiming では、こういうイベントを定期的に行うことで、普段使ってないものに触れる機会を積極的に作るようにしています。

今回は、春に UE4 無料化とか Unity 5 の発表とかがあったので、良い機会ということで東京スタジオと大阪スタジオ合わせて5月にクソゲー開発コンテストを開催しました。多少時間があいてしまいましたが、参加したクソゲークリエイターは17名で過去最大規模となり、賞である焼肉も振る舞われましたので、このタイミングにて栄えあるクソゲーたちを紹介させて頂きます!

東京の作品

まず東京の作品から紹介していきます。

kusogame2_ss
東京で社長賞に輝いたのはユニティちゃんが地球上を駆けながら飛んでくる寿司や馬などを倒す 3D シューティングゲームです。
なぜ弾がおしりから出るか、、という疑問には目をつぶるとして、完成度が非常に高い作品でした。

kusogame1_ss
こちらも同じ人の作品。
弾が足から出たり、そもそも弾が当たらなかったり。デフォルメユニティちゃんがたくさん飛んできたりと突っ込みどころ満載でしたw

16cb6d18cf58a9e66641a6d111b49935-1
PhotonとUnityを使った潜水艦対戦ゲームです。
ソナーで探索したりやデコイで撹乱したりしつつ、先に相手に魚雷を打ち込んだほうが勝ち!というゲームです。 当日は2台のiPhoneを使って実際に対戦し、勝敗がつくところまでできていました。 ちゃんとゲームになってたので是非リリースしてほしい。でも一番時間をかけたのはタイトル画面だそうですw

29ec61f1e1d59fdac2f8ba136b3553f8
湧き出るゾンビからバスを守るゲームです。
三日間生き残ったらクリアなのですが、三日目に大量のゾンビが出てきまくって殺される無理ゲーになってましたw 発表時に2回挑戦しましたが開発者もクリアできませんでした…

e7308bb57538504b0f0ea4b810732e01
初音ミクが音楽に合わせて踊りプレイヤーはリズムに載ってやってくるマーカーに合わせてボタンを押す音ゲーで、結果によって1枚の大きな絵が表示されるものでした。
初音ミクのうしろでゾンビが踊っていたりと会場ではとても盛り上がりました。

VR-FPS-SS1
非エンジニアの企画の人が個人で開発したVRホラーゲームです。
アセット買うのに3万くらいかけたそうですw
閉会後も皆声をだして楽しんでいました。 中には立てなくなった人も…

GameScreen_2015-05-20_17.24.30
SLG風の戦闘モックです。

かなり良い感じの戦闘が動いていました!

11289385_382417775299823_5001867642110111451_o-1
Photon作品2つ目です。
開発者は現在学生の方でした(当日は社員の知り合いの開発者の方も発表に参加してました)。
ゲーム内素材の三角形はパワーポイントで作成したとか。 省エネすぎますw

63b50497414a532e99826bbd00989237
弊社ミニ四駆部の社員作のミニ四駆シミュレーターです。
Unity 製で、それっぽい物理挙動を独自実装してます。 パラメータをいじるとミニ四駆が吹っ飛んでいくので楽しいです、、ってゲームにすらなってないじゃん!ってツッコミは置いといて。
技術の無駄遣いが素晴らしいです。

enchant
2Dのフラッピーバードチックなアクションゲームです。 東京でのエントリはほとんどがUnity製でしたがこの作品は cocos2d-x で開発されていました。 でかい猫が迫ってくるので捕まらないようにジャンプしていく姿がシュールすぎます。

大阪の作品

OLYMPUS DIGITAL CAMERA

ここからはところ変わって大阪側の参加作品紹介になります。

cf2a39462ff2d86116c3a04737e76b4a
PhotonとUnityを使った対戦ゲームです。
決まったタイミングで、音声を入力して、相手にダメージを与えることができます。
大きい声で叫ぶほど、多いダメージを与えるので、発表当時はめっちゃ大きい声で
叫んで、発表者がストレス発散できたとか・・・

a2bd5c42c87d133e24d554531e03ada0
こちらは重力の向きを変えながらキャラを動かして、部屋の中のコインを集めてゴールを目指すゲームです。 コンセプトがしっかりしていて、ちゃんとゲームになっていました。 Unity での作品。

aa6d675c54c3025511cb466ece37960d
色んな障害物を排除して、ユニティちゃんを彼氏のいる場所=ゴールまで導くゲームです。 なぜこんな筋肉ガチムチのモデルを選んでしまったのでしょうか… むしろ彼氏の待つ場所に到達してほしくないと思うのは私だけではないでしょう。 制作につかった時間の半分以上はユニティちゃんのモーションをいじる時間に使ったそうです。

10f65ed6c78097b6b443d25befff5188
Unity で作ったナンバーパズルゲームです。
その後個人でGoogle Play にリリースする程度にはちゃんとゲームになっている作品でした。

70514041c42510e517b236a22e598104
人事職の方が、このコンテストをきっかけにUnityを勉強して制作したスネークゲームです。 というか単にちくわを食うと犬が増殖するだけの何かです。 ホラーですね。 本人はゲームだと言い張ってますが。 今回のコンセプトに完璧に沿っている素晴らしいクソゲーだ!と代表の椎葉が絶賛しておりました。

番外編

最後に、クソゲーコンテスト発表向けに制作した作品ではありませんが、
とても作り込んだ作品なので、発表させていただきます。

d08d2e99223e6ed0b22c242c9fbbfd1b
将棋をはじめ、色んなオンライン対戦ゲームが入っているアプリです。
このコンテストに参加した方が趣味で制作したもので、興味ある方はぜひ遊んでみてください。
http://barukaso.dip.jp/~rath/shougi/sawaishougi.html

 

まとめ

OLYMPUS DIGITAL CAMERA

 

今回のクソゲー開発コンテストいかがだったでしょうか?

写真は賞品の焼肉写真ですが、非常に美味しい焼肉でした。次回も賞を取れるように頑張ろうと心に誓いました。

Aimingでは不定期に有志開催の勉強会やこういった開発コンテストを行っていたりします。

また、今回ははじめにも書いたとおりエンジニアだけではなくプランナーや人事まで参加するなど敷居は低くかなり自由な会になっていました。

もし、この記事を読んで興味を持った方がいたら、Aiming 飲み会みたいなイベントも開催しているので、 是非一度遊びに来てみてください!

 

以上、東京・大阪同時開催第4回クソゲー開発コンテスト報告でした。

OLYMPUS DIGITAL CAMERA