Upgrade to Pro — share decks privately, control downloads, hide ads and more …

XFLAG Tech Note Vol.01

XFLAG Tech Note Vol.01

#技術書典5 に出典されたミクシィグループエンジニア有志による技術書です。

当日の詳細はこちら
https://career.xflag.com/report/engineer/xflag-tech-note/

<< 目次 >>
1章:本当にあった、モンスターストライクのギミック実装事例
2章:明日から使える品質向上 Tips 集(モンストの QA チームが意識してることについて)
3章:とある Unity 開発事例(Unity でアーキテクチャの話)
4章:git challenge を支える技術(git challenge という学生向け競技型イベントの裏側の話)
5章:Unity で板野サーカス -誘導ミサイルでクォータニオン入門-
6章:非同期処理の速度問題と解決の試案(Crystal で Ruby の非同期処理の速度改善)

<< TECH NOTE 一覧 >>
mixi tech note #01
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-01

mixi tech note #02
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-02

mixi tech note #03
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-03

mixi tech note #04
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-04

mixi tech note #05
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-05

mixi tech note #06
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-06

mixi tech note #07
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-07

MIXI TECH NOTE #08
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-08

XFLAG Tech Note Vol.01
https://speakerdeck.com/mixi_engineers/xflag-tech-note-vol-dot-01

XFLAG Tech Note vol.02
https://speakerdeck.com/mixi_engineers/xflag-tech-note-vol-dot-02

MIXI ENGINEERS

October 08, 2018
Tweet

More Decks by MIXI ENGINEERS

Other Decks in Technology

Transcript

  1. まえがき 本書「XFLAG Tech Note」は、XFLAG™ スタジオに所属する有志達によって執筆・制作された 初の技術書です。実際にスマホアプリ「モンスターストライク(以下モンスト) 」などのプロダクト 開発の現場で実践されている今すぐ試してみたい実⽤的な内容から、個⼈的に研究・実装してみた 事など、各⾃が思い思いに執筆いたしました。そのため、各章それぞれで完結している内容になっ ていますので、好きな章から好きな順番でお楽しみください。

    また、本書は、XFLAG スタジオにある技術的知⾒やアイデアを積極的に共有・公開していくこと で、よりワクワクドキドキするようなプロダクトが世の中にもっともっと溢れだすことを願って刊 ⾏されています。掲載されている情報は、執筆者⾃⾝の環境で検証し執筆されたものですので、ご 参考にされる際は、ご⾃⾝の責任で判断しご活⽤ください。 ディベロッパーリレーションズチーム⼀同 ◆本書に関するお問い合わせ先   https://twitter.com/xflag_engineers ◆ XFLAG スタジオについて   https://career.xflag.com/ ※ʠモンスターストライクʡ 、 ʠモンストʡ 、 ʠXFLAGʡ 、 ʠXFLAG ロゴʡ は、株式会社ミクシィの 商標または登録商標です。また、各社の会社名、サービス及び製品の名称は、それぞれの所有する 商標または登録商標です。 i
  2. ⽬次 まえがき i 第 1 章 本当にあった、モンスターストライクのギミック実装事例 1 1.1 始めに

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.2 友情コンボ「エナジーハート」 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.3 SS「パンドラボックス・クライシス」 . . . . . . . . . . . . . . . . . . . . . . . . 7 1.4 SS「フェイト・オブ・ザ・ゴッズ」 . . . . . . . . . . . . . . . . . . . . . . . . . 11 第 2 章 明⽇から使える品質向上 Tips 集 15 2.1 始めに . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 2.2 コミュニケーションについて . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 2.3 テストについて . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 2.4 QA チームについて . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 2.5 品質について . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 2.6 終わりに . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 第 3 章 とある Unity 開発事例 25 3.1 始めに . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 3.2 MV(R)P アーキテクチャ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 3.3 設計の⼀例 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 3.4 Unity 開発におけるテストの導⼊ . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 3.5 最後に . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 第 4 章 git challenge を⽀える技術 47 4.1 git challenge とは何か . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 4.2 問題の例 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 4.3 インフラの構成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 4.4 スコアボード . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 4.5 今後とまとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 第 5 章 Unity で板野サーカス -誘導ミサイルでクォータニオン⼊⾨- 65 5.1 始めに . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 5.2 今回作成したデモ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 iii
  3. ⽬次 5.3 オイラー⾓とクォータニオンとマトリックス . . . . . . .

    . . . . . . . . . . . . . . 66 5.4 板野サーカスを⽬指す . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 第 6 章 ⾮同期処理の速度問題と解決の試案 79 6.1 問題の内容 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 6.2 解決試案 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 6.3 検証環境 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 6.4 検証 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 著者紹介 93 iv
  4. 第 1 章 本当にあった、モンスターストライクのギ ミック実装事例 1.1 始めに 本章は、モンストのギミックを実際に実装しているクライアントエンジニアの⾓⿓徳(かくたつの り) *1が、担当した案件の発⽣から実装完了までを⼤まかに紹介する章である。読もうとしていただ

    けるのはありがたい限り!……なのだが、読み終わってから後悔することが少なくなるように、先 にある程度の留意事項を記載しておこうと思う。 • モンストについて、ある程度プレイしたことある知識や経験が前提となる • 実装を担当したクライアントエンジニアの⽬線のみの話しかない • いろいろな都合で省略されているところがあり、期待している部分がないかもしれない • フランクな⽂章 • これはフィクションではない ギミックはほとんどがゲーム性と密接に関わるので、ゲームそのものを知らないと理解が難しい かもしれない。無料で DL できるので実際に遊んでみるか、すでに遊んでいる友⼈に読ませて内容 を伝えてもらうか、動画検索をして閲覧すると良いだろう。 もっと詳しく話を聞きたいな、とか、気になったので質問してみたい、とかがあれば、おそらく巻 末に載っているであろう SNS のアカウント宛てに聞いてほしい。 *1 ⽇本⼈ 1
  5. 第 1 章 本当にあった、モンスターストライクのギミック実装事例 1.2 友情コンボ「エナジーハート」 1.2 友情コンボ「エナジーハート」 図 1.1:

    オリーブ 発端 エナジーハートを実装しようと案件が来た時のことは⽐較的鮮明に覚えている。企画の⽅から 「オリーブ*2を実装するから友情コンボに使うエナジーハートも実装したい」とギミック担当のクラ イアントチームに話が来たのだ。 「オリーブってどんなキャラでしたっけ?」 「どんな感じの挙動に なるんです?」など雑談交じりに軽く確認していくと、要点は以下の 2 点だった。 • 基本は、すでに実装されているエナジーサークル*3に準拠する • 軌道が円環状ではなく、ハート型 エナジーサークルについてはゲーム中に⾒たことあるぐらいの認識しかなかったが、オリーブの ⾒た⽬が中々かわいかった*4ので、気付いた時には実装担当に⽴候補していた。 *2 3DS 版モンストに登場するキャラクタ *3 発動者を中⼼としたリング状にエネルギーを発⽣させ、敵にダメージを与える友情コンボ *4 それまでに実装したものは⼥の⼦に実装されなかった! 2
  6. 第 1 章 本当にあった、モンスターストライクのギミック実装事例 1.2 友情コンボ「エナジーハート」 図 1.2: エナジーサークル 要件解析

    エナジーハートはエナジーサークルの挙動に基本的に準拠するので、まずはエナジーサークルの 仕様を知る必要がある。実際の挙動を眺めたり、コードを解析したり、周囲の⼈達から話を聞いた りして、以下のような挙動が分かってきた。 • 衝突判定⽤の円が、リングの形状を描くように移動している*5。 • 移動する衝突判定⽤の円は 2 つある。12 時から 6 時、6 時から 12 時にそれぞれ反時計周 り。 *6 • 移動は毎フレーム更新されている リングの形状は三⾓関数を利⽤して算出されている。 x = r * cos θ y = r * sin θ これをハートの形状を描く関数に変えてやればいけそうだ。そんな関数があるのかと思うかもし れないが、数学は偉⼤なもので、調べてみると結構ある。 *5 敵も円形の衝突判定を持っていて、重なるとダメージを与える *6 移動開始付近の敵と移動終了付近の敵でダメージ発⽣のタイミングがズレてしまうので、その差を半分に抑える為 3
  7. 第 1 章 本当にあった、モンスターストライクのギミック実装事例 1.2 友情コンボ「エナジーハート」 ハート⽅程式 図 1.3: さまざまなハートの⽅程式*7

    これらの関数を利⽤するだけでもそれらしいハートを形作ってくれる。これで問題解決といけば 良いのだが、そうはいかない。実際のゲーム画⾯で表⽰されるハートはこれらの関数通りではなく、 VFX*8の⽅が⽤意するエフェクトに合わせないといけない。その為にはハートの軌跡を変更できる パラメータが必要になる。 お察しの通り、 そう簡単に関数ができあがるものではない。試⾏錯誤を繰り返している間に、 VFX から表⽰想定の共有があった。 *7 出典元  http://mathworld.wolfram.com/HeartCurve.html *8 平たく⾔うと、⾒た⽬の演出や表現を作る役職 4
  8. 第 1 章 本当にあった、モンスターストライクのギミック実装事例 1.2 友情コンボ「エナジーハート」 図 1.4: VFX からの演出想定

    これで形や⼤きさが分かった。もちろん、この想定とまったく同じにしないといけない、という わけではない。関数の作成に思った以上に⼿間どってちょっと焦っていたこともあって、演出に合 わせるのが難しければ相談する、と前もって確認もしておいた。 幸い別件がなかったので、ハート関数の作成に注⼒した。結果的に 3 ⽇ほどかかってしまったが、 ⼤きさや形をパラメータで制御できるハート関数を⽤意できた。 図 1.5: ハート関数*9 A = ⼤きさ (調整可) B = ⾼さ係数 (調整可) P = 丸み係数 (調整可) θ = 底の⾓度 (調整可) T_MIN = 最低位相⾓ T_MAX = 最⼤位相⾓ t = 位相⾓ *9 出典元  http://www.geocities.jp/nyjp07/index_heart2.html 5
  9. 第 1 章 本当にあった、モンスターストライクのギミック実装事例 1.2 友情コンボ「エナジーハート」 i) [-π /2 !=

    t かつ 3 π /2 != t] r = a * SQRT( (5 - 3 * SIN(t)) * (1 + SIN(t)) ) z = a * ASIN( a * (1-SIN(t)) * COS(t) / r) ※ [-π /2 ≦ z ≦ π /2] ii) [t = -π /2] r = 0; z = π /2; iii) [t = 3 π /2] r = 0; z = -π /2; f = -θ * z / PI() + PI()/2 x = r * COS(f) y = b * r * SIN(f) + p * ABS(x) 以上のコードは、実際に僕がコード内に残しているコメントで、この通りに実装がされている(は ずだ) 。単純なハート関数に⽐べて調整可能な値が多い為、VFX から共有された形に合わせること が⽐較的簡単だ。この処理を利⽤して、エナジーサークルをハート型に変えていこう。 実装 ハートの軌跡を描ければもう⼼配するものはない。早く実機で確認できる様に実装を急ぐ。表⽰ できるようになったら VFX や企画の⽅に確認してもらう。ここで修正や調整が⼊ることも少なく ないので、早めに⾒せておきたいところだ。今回の案件に関しては特に問題なかったが、普段は⾒ た⽬の確認を優先する為に多少強引に書いたコードがあったり、パフォーマンスを無駄に落とす様 な処理があったりするので、それらを直す。 作成したハートの関数は、パラメータで制御しやすい反⾯、負荷は低くない。この負荷が実機での プレイでも問題ないかどうかは、QA チーム*10に申し送りしておく。実装が完了したら、QA チェッ クに通して問題がないか確認が⾏われる。期待通りに機能するかどうかはもちろん、ゲームプレイ 中の状況によっても不具合が浮き彫りになることも多い。モンストは⻑期に渡るプロダクトになっ ているので、思わぬケースで不都合が出て修正することも少なくない。 いろいろ不具合が直ったら実装完了だ。リリースの時を待とう。 リリース後 リリースした後は数多くのユーザーの⼿によって実際に使⽤されることになり、 実装担当者にとっ て喜ばしい瞬間を迎えることができる。SNS が当たり前のように使われる今⽇では感想をダイレク トに知る機会も多く、褒められた内容ならうれしくなってしまうのはしかたのないことだ。 が、それだけではない。リリース後にも不具合が発⾒されることもある。不具合の内容、原因の 究明、修正にかかる時間など、いろいろな要素から企画と相談して対応を決める。実装したのが⾃ *10 QA は品質保証を意味する「Quality Assurance」の略。今回の場合、エナジーハートが実際のプレイで発⽣するさ まざまなケース、パターンでも問題なく使⽤できるかチェックする。些細なバグも露わにされるぞ! ありがとう! 6
  10. 第 1 章 本当にあった、モンスターストライクのギミック実装事例 1.3 SS「パンドラボックス・クライシス」 分だったかどうか忘れるくらい数ヵ⽉経ったころに⾒つかる不具合もあるぞ。そのころの⾃分がど んな思想でコードを組んだのかさっぱり忘れているので、どのような経緯があったのかしっかりコ メントを残しておくと、未来の⾃分や他の誰かの⼤きな助けになる。修正を⼿早く終わらせて、み んながあっと驚く様なギミックの実装に時間を注ぎたいのは僕だけじゃないはずだ。

    1.3 SS「パンドラボックス・クライシス」 *11 図 1.6: パンドラ(進化) 発端 これも企画から来た案件だ。仕様は以下の通りとなる。 • ショット⽅向の敵全員にそれぞれパンドラの箱を投げる • 箱は放物線を描いて着弾し、ダメージを与える • ダメージを与えた敵は複数の状態異常になる 放物線は敵の位置で軌道が変わるのでコード側で処理する必要がありそうだが、すでにあるもの を参考にすれば良い。状態異常の処理もすでにあるので、実装は然程難しくなさそうだ。既存の処 理を参考にしたり合わせたりすることは⼤切で、同じことを実現するのに違う処理になっていると 保守がたいへんになってしまうからだ。 ちなみにこの時は、というよりたいていの場合はキャラ絵の作成も同時進⾏であることが多く、 どのような容姿のキャラが使⽤するか分かっていない。オリーブの時のようにあらかじめ容姿が決 まっていることの⽅が珍しいのだ。同じ気持ちを味わってもらおうと最後の⽅にキャラの絵を載せ *11 「SS」は「ストライクショット」の略語。僕はそのまま「えすえす」と呼称している。 7
  11. 第 1 章 本当にあった、モンスターストライクのギミック実装事例 1.3 SS「パンドラボックス・クライシス」 ようと思ったが、僕だったら先に⾒てしまうので⼿間にならないよう先に載せておいた。せっかく なので実際に投げるパンドラの箱も載せておこう。 図 1.7:

    パンドラの箱 放物線 仕様から実装は難しくないと踏んでいたので⼼持ちラクだったが、既存の放物線の処理を確認し ている時にさっそく気になる部分を⾒つけてしまった。それは放物線軌道がフレーム単位で移動し ていたことだ。フレーム単位だと処理落ちが発⽣した時に軌道が違うものになってしまう。最終的 な着弾位置は指定されているので⼤丈夫だが、ダメージの発⽣タイミングがズレてしまう。性能の 違うさまざまな端末でプレイされるので、処理落ちは発⽣するものとして考慮しなければならない。 修正⽅法は秒単位にすることだ。特別な解決⽅法ではなく、広く⼀般的に取られる⼿段だと思う。 結局新しく作らないといけなくなった。 放物線の軌道を描く処理を作ることになったので、どうすれば実現できるかを考える。放物線と 聞いて思い浮かべたのは⼆次関数だったのでそれを使っている。ただ放物線を描くだけでは⽬標の 場所に着弾しないので、考えた⽅法はまず敵の⽅向へ⼀直線に向かう移動量と、放物線の Y 座標成 分を加算するものだった。さっそくできあがったコードを⾒てみよう。 // Y 座標に⼆次関数の変異を加算する // (-1, 0) と (1, 0) を通り、頂点が (0, H)、下に開いた関数を使う // 導出すると以下の通り。 // y = -H*x^2 + H // H は最⼤の⾼さになる。 // なお、定数から時間と距離が決まるので、速度も⼀意に決まる const f32 H = 290.0f; // ⾼さの最⼤ f32 dx = 2*t -1; // x の変異 [-1 ≦ x ≦ 1] f32 offsetY = -H * dx*dx + H; _pos._y += offsetY; *12 これも実際に僕がソースコードの中に記述したものを抜粋した。何秒後に着弾するか、最⼤の⾼ さはどれくらいか、を指定できる。定数は、VFX の⽅を交じえて実際に⾒ながら良さそうな値に調 *12 f32 は float 型のエイリアス 8
  12. 第 1 章 本当にあった、モンスターストライクのギミック実装事例 1.3 SS「パンドラボックス・クライシス」 整していくのだ。着弾までの時間が遅ければ秒数を減らすだけで済むし、⾼さが低ければ値を増や すだけで反映できる。 この放物線の処理は 1

    つの⼿段に過ぎない。時間と⾼さが指定できる⽅が都合が良かったので今 回の場合はこうなっただけだ。 ガタガタ揺らす 放物線挙動を実装して何度も試している内に、1 つ思いついたことがあった。パンドラの箱なら、 ガタガタ揺れている⽅がそれっぽくなるのではないだろうか。具体的には回転⾓を± 30 度ぐらいで 往復させるイメージだ。VFX の⽅に提案して OK をもらったのでさっそく組んでみることにする。 この時の僕は短絡的に正弦波とか余弦波を使えば良いと思っていた。往復させる処理によく使わ れるものだが、箱を揺らす処理としては不適切であることに実装してから気付いた。往復する際に 速度が落ちてしまうので、ガタガタというよりゆらゆらと揺れている印象が強くなってしまったの だ。これなら実装しない⽅が良いぐらいだ。動画でお⾒せしたいところだがそうもいかない。 図 1.8: 正弦波、余弦波*13 サインやコサインはなだらかに変化するので、ギザギザに変化するものがあれば解決すると考え た。それが三⾓波だ。 *13 出典元  https://en.wikipedia.org/wiki/Sine_wave 9
  13. 第 1 章 本当にあった、モンスターストライクのギミック実装事例 1.3 SS「パンドラボックス・クライシス」 図 1.9: 三⾓波*14 三⾓波の値を算出してくれる機能はないので作ることになる。作り⽅としては、まずノコギリ波

    を作ってから、その半分を折り返すことで三⾓波を実現する。 図 1.10: ノコギリ波*15 // 三⾓波を利⽤して回転させ、ガタガタ揺れているように⾒せる const f32 AMPLITUDE = 15.0f; // 波⻑。⾓度に⽐例する。⼤きいほど傾く。 const f32 FREQUENCY = 3.0f; // 周波数。振動数に⽐例する。⼤きいほど早く振動する f32 tri = _time * FREQUENCY - sn_floorf(_time * FREQUENCY); //< 0.0f ~ 1.0f (のこぎり波) // 0.5f 以上を折り返して三⾓波にする if (0.5f < tri) { tri = 1.0f - tri; } tri *= 2.0f; //< 0.0f ~ 1.0f tri -= 0.5f; //< -0.5 ~ 0.5 tri *= 2.0f; //< -1.0f ~ 1.0f f32 deg = tri * AMPLITUDE; *14 出典元  https://en.wikipedia.org/wiki/Triangle_wave *15 出典元  https://en.wikipedia.org/wiki/Sawtooth_wave 10
  14. 第 1 章 本当にあった、モンスターストライクのギミック実装事例 1.4 SS「フェイト・オブ・ザ・ゴッズ」 _sprite.setRotation(deg); これも実際に実装してあるコードの抜粋だ。調整可能な値として波⻑と周波数があるが、要は揺 れる⾓度とその速さのこと。放物線軌道を描いているのであまりにゆっくりだと気付かれないし、 かといって激しくして⾃⼰主張を強めても良くない。最終的な良き値は、VFX

    の⽅と画⾯を⾒なが ら少しずつ調整して決めていく。 リリース後 パンドラは⽐較的⼈気の⾼いキャラ*16で、ストライクショットも僕が想像していた以上に強いも のだった。ユーザーの⼿元で⾒れる様になってから発⾒される不具合もあって悲しい思いもしたが、 ストライクショットを使う度に「放物線、うまく実装できてるわ」とか「ガタガタ揺らすやつ提案し て良かったな」とかうれしくも思っている。もしこのストライクショットを⾒る機会があれば、ダ メージとか状態異常の結果のほかにも、ぜひ放物線やガタガタ揺れる動きを堪能してほしい。 1.4 SS「フェイト・オブ・ザ・ゴッズ」 図 1.11: ラグナロク(神化) 発端 エナジーハートやパンドラボックスの様に、これも企画から作ってほしいとやってきた案件…… ではない! 僕が同僚とモンストをプレイしながら興じる他愛のない話の中の⼀つが実装されたもの *16 出典元  https://www.monster-strike.com/news/20171018_1.html 11
  15. 第 1 章 本当にあった、モンスターストライクのギミック実装事例 1.4 SS「フェイト・オブ・ザ・ゴッズ」 なのだ。そう、クライアントエンジニア側から提案したギミックが採⽤され、リリースされた事例 の⼀つである。 友⼈とゲームをプレイしている時の会話が、たいていの場合はくだらないが中々おもしろいもの であることに⾝に覚えがある⽅も多いと思う。このギミックが発想された時の会話はこんなものだ。

    •「ゲームで良く、⽳に落ちたら上から降ってくるのあるじゃないですか」 •「ありますね」 •「あれみたく、この(モンストの)壁を突き抜けて反対側に出てくるのってどうよ」 •「動きは楽しそうですけど使い勝⼿悪そう」 •「楽しいならええやろ!」 こんなどうでもよい会話はほかにもたくさんある。ほとんどの場合は思い出になるだけだが、ギ ミック担当のクライアントエンジニアをしていると⾃分だけのリソースで「とりあえず組んで⾒て 動きを⾒せる」ということが可能だ。 ⽇々の業務中に思いついたギミックの構想をメモしておきながら、⼿の空いた時間を狙って実装 するのだ。当然、どのキャラが持つかなんて分からないので「壁貫通ループショット」という汎⽤ 的な技名を名付けておいた。後に、ラグナロクがこのストライクショットを使⽤するとは、この時 の僕はまだ知らない。 実装 コーディングをする前に、仕様を決める必要がある。企画から上がってくる案件と違って仕様は ⾃分で決めないといけないし、良くも悪くも期限がない*17。 デタラメな仕様はもちろんダメなのだが、今回の場合はとってもシンプルだ。 • 壁にあたったら、反対側の壁から出てくる 以上だ。これだけなので、実を⾔うとソースコードを掲載するほどの処理は記載していない。モ ンストは四⽅を壁に囲まれているので、上の壁に当たったら下の壁から出てくる、右の壁に当たっ たら左の壁から出てくる、逆も然り。それだけの処理なのだ。なので、もう少し細かく実装までの フローを紹介する。 クライアントエンジニアは⾃分で実装できるがゆえに、サッと作って企画の⽅へ「こんなの作っ て⾒たんですけどどうでしょう」と実際に⾒せることができる。OK なら現在開発中のバージョン や、その次のバージョンで本格的に実装することになる。NG の場合は意⾒をもらいつつ、ああした ⽅がいいんじゃないか、こうした⽅がいいんじゃないか、と試⾏錯誤を繰り返す。 OK でも NG でも考えるべき問題は残っていて、ほかのギミックやパターン、ケースとの兼ね合 いである。壁にダメージウォール*18があったらどうなるか、反対側の壁にワープ*19があったらどう なるか、などを考慮して、現状の挙動を確認する。ほかのギミック案件でもそうだが、企画と相談 *17 ⼈間は守るモノがあると強くなれる。〆切とか。 *18 触れるとダメージを受けてしまう壁 *19 触れると吸い込まれて、別のワープのある位置に移動させられる 12
  16. 第 1 章 本当にあった、モンスターストライクのギミック実装事例 1.4 SS「フェイト・オブ・ザ・ゴッズ」 してどの挙動を仕様とするかを決めていく。 ただ位置が変わるだけだと何が起きたのかプレイ中のユーザーに分かりづらいので、エフェクト を⼊れることになる。エフェクトといえば VFX

    チームだ。VFX の⽅が作ったエフェクトを、位置 が変わる前と後の場所に表⽰させるだけで済む。と思っていたが、キャラが⾼速に壁から壁を移動 し続けるパターンがあり、エフェクトが出すぎてしまう問題があった。エフェクトが出すぎると処 理落ちが酷くなるので、リングバッファを使って⼀定数以上表⽰する場合は古いものを消す処理が ⼊っている。⼀定数というのは実際に使って⾒て感覚的に不⾃然さを感じづらい値にしている。す でに使ったことのある⽅はお気付きになられただろうか。 QA チェックももちろん⾏った。 リリース後 かくして、この「壁貫通ループショット」は実装された。しかし実装されたからといってすぐに 使われるわけではない。事実、このストライクショットが使われたのは数ヵ⽉後で、⾃分も実装し たことを忘れているころだった。 新キャラとしてラグナロクが登場し、ストライクショットが「壁貫通ループショット」だったこ とを知った時はうれしいものだった。何せ初めて⾃分が発案し、⾃ら実装したストライクショット が世に出るのだから。 余談だが、こんな経緯があるので、このストライクショットやそれを使っているラグナロクには 思い⼊れが強い。 13
  17. 第 2 章 明⽇から使える品質向上 Tips 集 2.1 始めに モンストの QA*1は、⽐較的スリムな体制で⾏われています*2。なぜ⼩さい規模で収まっているの

    か考えてみたところ、QA チームと開発チームが良好な関係を築けているという点が⼤きいと思い ます。 「良い関係性」という⼟台のおかげで、テストプロセスの改善が進み、テストが効率化されて いき、短い QA スケジュールでも⼗分な品質を保ってプロダクトを世に出すことができています*3。 私の章では、QA として、開発チームと良い関係性を構築し、品質の良いプロダクトを世に届け るために⼼がけていることを、つらつらと書いていきます。私⾃⾝プログラマーの経験もあるので、 なるべくは、QA にもプログラマーにも有意義な内容になるように⼼がけました。難しい内容は何 もなく、その気になれば、明⽇から試せるものばかりです。 Let’s Try! 2.2 コミュニケーションについて ミーティングをしよう 開発チームと QA チームの間でテストについて、品質について、話し合う機会を設けていますか? 極端なことを⾔うと、開発チームと QA チームで対話の機会がなくてもテストは可能です。ビル ドを作り、QA チームに渡し、不具合管理システムを通してやりとりを⾏うことで、バグは取り除か れていくでしょう。しかし、質の良いテストをし、サービスの品質を向上させるためには、開発チー ムと QA チームで定期的にミーティングを⾏う必要があります。 実は QA チームからは、開発チームに伝えたいことがたくさんあります。ツールの要望、セルフ チェックを徹底してほしい*4、修正コメントの書き⽅、などなど。それらを⼀個⼀個解消していくこ *1 Quality Assurance の略。品質保証と訳されることが多い。⼤雑把に表現すると、開発前のプロダクトからバグを取 り除くことと、そもそもバグを作りこませないことがお仕事です (個⼈の感想です。会社によって QA の役割には差 があるように⾒受けられます) *2 他社の QA の⽅からも「思ったよりも少ない⼈数ですね」と⾔われることが多いです *3 また、ゲーム業界につきものの開発の佳境時期を、お互い笑顔で乗り切ることができるという副産物もあります *4 ⽴ち上げたら無条件でクラッシュする、などの発⾒が容易な不具合を減らすだけで、QA プロセスの効率はだいぶ変 わります 15
  18. 第 2 章 明⽇から使える品質向上 Tips 集 2.2 コミュニケーションについて とで、少ない時間で効率よくテストができるようになっていきます。そして、当然開発チームから も

    QA チームに対して、⾔いたいことの 1 つや 2 つ、あるでしょう。バグ報告書の内容をもう少し ちゃんとしてほしい、とか、なぜこんなにテスト期間が必要なのか、とか。こういった意⾒をお互 いに伝えあうことで、効率の良いテストプロセスができあがっていきます。 改善案もテキストでやりとりすればよいじゃないか、と思われるかもしれません。しかし、テキ ストベースのやりとりは誤解がつきもので、それがきっかけで関係性がこじれることも珍しくない ので、他部署に対する意⾒を伝えるときは⼝頭が良いと思います。 開発も QA も、ミーティング嫌いな⼈が多い職種ですので、まずは、⽉に 1 回、QA リーダーと メインプログラマーの 2 ⼈でミーティングを設定してみてはいかがでしょうか。それがうまく⾏っ たら、もっと⼈を増やして⾏くと良いでしょう。 ちなみに、モンスト開発チームでは、週に⼀回、メインプログラマー、メインプランナー、QA リーダーで、ミーティングを⾏っています。加えて、QA リーダーはメインプログラマーやメインプ ランナーと定期的に 1on1 も実施し、意⾒交換しています。 顔を覚えよう (相⼿に直接会っておこう) 開発組織と QA 組織が違うオフィス (違う会社) にある、ということは珍しくありません。QA 担 当者の名前は知っているけど顔は知らない、という状況で仕事をしたことがある⼈もいらっしゃる のではないでしょうか。チャットツールやクラウドツールの発達のおかげで、直接顔を合わせずと も業務ができる環境が整っています。ですが、そんな状況でも、開発と QA は、お互いがお互いの 顔を知っていることがとても重要だと感じています。 QA からのコミュニケーションは、不具合報告と作業依頼*5が⼤部分を締めています。加えて割り 込みだったりするので、開発者にとって QA からの連絡というのは、基本的には、あまり好ましく なかったりするのが実際のところでしょう。 また、開発者に関しても、⼝下⼿な⽅が少なくないので、QA チームは開発者に対して過度に萎縮 してしまうケースがあります。 そのような状況ですので、 「誰から⾔われているのか」ということは、結構、無視できない要素で す。顔を知らない⼈間から、不具合報告とタスク依頼を受け続けたらイライラが募るでしょうし、顔 を知らない⼈間から、素っ気ないコミュニケーションをされたら、萎縮するか腹が⽴つでしょう。 私の事例としては、テレビ会議でお互いの⾃⼰紹介をしただけで、関係性が改善したこともあり ます。テスト部隊が外部の会社である場合も、⼀度、外部会社を訪れて 1 時間ほど懇親会をしただ けで、その後の仕事が⾏い易くなった、ということがありました。 お互いの顔をしっているだけで、⾔葉から受け取るネガティブな印象は、だいぶ緩和されます。ぜ ひとも、顔を覚えましょう。 *5 サーバを再起動してほしい、とか、指定のキャラクタを付与してほしい、とか 16
  19. 第 2 章 明⽇から使える品質向上 Tips 集 2.2 コミュニケーションについて NO MORE

    「全体的にテストして」 たまにあるのが、 「全体的に問題ないか⾒てほしい」という依頼です。この依頼が来たとき、QA チームには間違いなく、緊張感が⾛ります。 全体を⾒て、さらに、問題がないことを保証する、というのは、事実上無理です*6。 そのため、 「全体的に⾒てほしい」という依頼の「全体」とはどこを指しているのか、その全体の 中でも特に重要なことは何か、といったことをすり合わせる必要があり、それなりのコミュニケー ションコストが掛かってしまいます*7。 ただ本当に、全体を⾒る必要がある場合もあります。たとえば、OS や SDK をバージョンアップ したり、メモリ管理などの根幹部分を改修したときなどです。そのような場合は「全体を⾒てね」と いうのではなく、 「1 名で 2 時間くらいユーザーが良く触れるところを触ってみて、挙動が重いとか レイアウトがおかしいとか、そのような違和感を感じたら報告してほしい」というように、なるべ く具体的に依頼を出しましょう。 可能であれば、テストの範囲をある程度指定する 前項からの続きになりますが、たとえば、あなたが外来を受け持つ医者だったとして、以下の 2 つ の患者のどちらが時間をかけずに正確な診断をくだせるでしょうか。 • 具合が悪いから診てほしい • お腹の調⼦が悪いから診てほしい おそらく後者になると思います。テストする範囲が抽象的であるほど、効率的・効果的なテスト が難しくなるのも、これと同様です。テスト範囲の指定は、できる限り、具体的にしましょう。 テキストのコミュニケーションは丁寧にやりましょう チャット中⼼の会社につとめて、10 年近く経ちますが、QA と開発者間のチャット上の揉め事を 多く⾒てきました。そして、その原因の⼤半が、 「⾔葉⾜らず」に起因しています。 たとえば、たまに⾒かける開発者からの「◦◦の機能って、テストしたんですか?」という⼀⾔で す。ここでちょっと考えてほしいのですが、上記のメッセージを受けた QA 組織は、どのような⾏ 動を取るでしょうか。 QA の⽴ち位置が開発よりも弱い場合*8、それなりの⾼確率で、 「開発者が怒っている。もしかし たら、該当の機能で不具合が出ているのかもしれない。とりあえず不具合が出ているかどうかを確 認して、⾃分たちに否がないかどうかを確かめよう」という⾏動に出ると予想します。 *6 無限の予算と時間があれば、もしかしたらできるかもしれませんが *7 そして、この⼿の依頼を出してくる⼈は、QA という仕事の性質を知らないことが多いので、タフなコミュニケーショ ンになることも少なくありません *8 よくあるのが、QA 組織の多くが業務委託で構成されている場合などです 17
  20. 第 2 章 明⽇から使える品質向上 Tips 集 2.3 テストについて ⼀⽅、開発者は、ただ気軽に、 「テストした?

    機能を改修したいから、もしテストを開始していな いのであれば、改修が終わるまで待ってほしい」と⾔いたかったのかもしれません。この場合、QA の取った⾏動は、まるごと無駄になります*9。 無駄なコストをなくすためには、 チャット上では、 過不⾜のない情報を書くように⼼がけましょう。 開発者と QA チームの環境の差異を意識しよう 「⾃分の環境ではバグっていない」という開発者の⾔葉は、ときに QA チーム vs 開発チームのゴ ングになることがあります。 仮に QA チームから共有された再現⼿順の通りに操作しても、不具合が再現しないときは、⾃分 ⾃⾝の確認環境を QA チームに伝えるようにしましょう。 ビルドのバージョン、確認した OS の種類、サーバ環境(開発⽤とか staging とか)あたりを、QA に伝えることを忘れないようにしてください*10。 2.3 テストについて QA 担当者を早めにジョインさせる 早くから QA 担当者を⼊れておくメリットは、プロダクトに「テスト容易性」という観点が追加 されることです。テスト容易性とは、ゲーム開発の世界でいうなら、デバッグコマンドが充実して いること、となるでしょうか。 デバッグコマンドがあることで、テストは劇的に効率化されますが*11開発の終盤だとプログラム の構造が複雑になる事もあって、QA チームが望むようなデバッグコマンドを実装することが、難 しくなるケースが少なくありません。 ですので、開発フェーズの早い段階からテスト担当者を参加させ、プログラムに柔軟性が残って いるタイミングでデバッグコマンドの要望をもらうと良いでしょう。 ビルドの丸投げはやめましょう QA にとって⼼の折れる代表的な瞬間は何かというと、出社して最新のビルドをインストールし て、さぁ、テストをするぞ! とやる気満々で起動した瞬間に、クラッシュしたときです。まったく テストを進めることができません。 他には、不具合の修正対応がされたはずのビルドを確認したところ、まるで治っていなかったりだ とか、A 機能を実装したのでテストしてくださいと連絡が来たのに、その A 機能が⼊っていなかっ たりとかする場合です。 *9 ⼤げさな例のように思われるかもしれませんが、ありがちな光景です *10 よくあるパターンは、QA は端末で確認し、開発者は PC のエミュレータ等で確認してた、とか。お互い確認してい るサーバの環境が違う、とか *11 テスト費⽤も少なく抑えられる、ということでもあります 18
  21. 第 2 章 明⽇から使える品質向上 Tips 集 2.3 テストについて この⼿のことは、QA の時間を無駄にしてますし、開発関係者にとっても⼆度⼿間です。QA

    にビ ルドを渡す際は、必ず⼀度は⾃分⾃⾝の⼿で問題ないかどうかを確認したうえで*12、渡すようにし ましょう。 直しやすいところからではなく重要度の⾼いところから治す ゲームプログラマーをやってたころは、開発が佳境になり、不具合修正のタスクが増えてくると、 とりあえず⽬に⾒えるタスクの数を減らすために、直しやすい不具合を優先して対応しがちでした。 しかし、QA の⽴場になり、対応すべき不具合にも優先度がある、ということが⾒えてきました。 まず、再現頻度の⾼いクラッシュバグは最優先で直しましょう。クラッシュすることでいろんな 業務が⽌まるため、開発進捗に影響を及ぼします。 また、ゲームの根幹を担う部分も早めに対応に取り掛かりましょう。たとえば、メモリ管理、描 画、通信などです。これは、修正したときの影響範囲が広いため、修正後の再テストに時間がかか るためです。 不具合を修正したあと QA チームに修正確認依頼を出す際は、何をどう直して影響範囲がどこまで及ぶのかを、伝えるよ うにしましょう。 そうすることで、QA チームは修正された箇所だけでなく、関連するところもテストでき、 「バグ 修正をしたことによって新たに⽣まれるバグ」を防ぐことができます。 また、不具合の原因と修正内容の詳細を書いておくことは、開発者⾃⾝のためにもなりますし、未 来に類似の不具合が出たときの対応のヒントになることもあります。 「修正しました」の⼀⾔だけで、QA チームに戻すのはもったいないと思います(急いでるときは しょうがないですが) 。 ツールを作ってあげましょう テストが効率的に進むようなツールを提供してあげましょう。ツールが充実しているチームは、 そうでないチームと⽐べて、⽣産性は何倍も変わります。 たとえば、ゲームのログインボーナスのテストをしているときは、サーバ時間を何度も書き換え る必要があります。このとき、毎回 QA チームがサーバ側に依頼してサーバ時間を書き換えるのと、 QA チームがツールを⽤いて任意にサーバ時間を書き換えられるのとでは、QA チームにとっても開 発者にとっても、効率は違うはずです。 サポート系のツールを作ることは、なかなかモチベーションが上がりにくいかもしれませんが、地 *12 とりあえず動くかどうか軽くチェックすることを、ソフトウェアテストの世界では「スモークテスト」と呼んだりしま す。ハードウェアの世界から流れてきた⾔葉のようで、 「スイッチ⼊れてみて、発⽕したり煙がでたりしないかどうか を確認する」といった意味合いです。スモークテストは、品質を上げ、QA コストを下げるのに、⾮常に効果がありま す 19
  22. 第 2 章 明⽇から使える品質向上 Tips 集 2.3 テストについて 道に確実に、品質とテスト効率を向上させます*13*14。 ちなみに、私が最も感動したツールは、テストユーザー⼀⼈⼀⼈が、それぞれサーバ時間を設定で

    きる、という機能です。同⼀環境で A というユーザーは 8 ⽉ 10 ⽇の AM10 時、B というユーザー は 9 ⽉ 30 ⽇の PM6 時みたいに設定できます。 たいていは、サーバ時間はすべてのユーザーに影響するものですので、誰かがログインボーナスの テストをしているときは、期間限定のガチャのテストはできなかったりします。しかし、この機能の おかげで、時間が関係する機能のテストが並⾏にできるようになり、かなりの効率化が進みました。 どれだけ些細な不具合でも、それを QA チームが⾒つけると、それなりのコストが 掛かる たとえば誤字脱字のような、サクッと修正できる不具合であったとしても、テストフェーズで⾒ つかった場合は、 1. QA チームが⾒つける 2. 不具合を起票する 3. 開発者が修正する 4. 修正版のビルドを作る 5. QA チームが修正確認する というプロセスを踏むことになり、トータルで 30 分くらい対応の時間が取られることになりま す*15。 これが、テストプロセスの前であれば、 1. 該当箇所をセルフチェックする 2. 不具合を⾒つける 3. コードを修正する という、5 分程度の作業時間で済むかもしれません。 些細な不具合も、積もり積もれば⼤きな⼯数となります。QA ⽤のビルドを QA チームに回す前 に、今⼀度、⾃分の担当領域に不具合がないかどうかを確認する習慣をつけましょう*16。 ある程度のバグは許容するとテストのコストは⼤幅にさがる かなり⼤雑把な感覚値ですが、バグを 80% 取り除くのに要した労⼒を 100 とすると、そこから 90% に持っていくまでの労⼒は 500 くらいになります。そして、99% を⽬指す場合は、労⼒は 1000 くらいになるでしょうか。 *13 テストツール開発者を、QA は陰で神として崇めています *14 最近は、QA エンジニアという、テスト環境を整備する職種も⽿にしますが、まだマイナーな印象 *15 どんな軽微な不具合でも最低 30 分の修正コストがかかる、ということです *16 「ビルドの丸投げはやめましょう」の項⽬も参照ください 20
  23. 第 2 章 明⽇から使える品質向上 Tips 集 2.4 QA チームについて 後半になるにつれて、発想が降りてくる時間が必要になるため、コストが急激に増⼤していきま

    す。バグゼロを⽬指すあまりに、QA コストをかけすぎないように、注意が必要です。 バグを⾒つけられるかどうかは、時間にほぼ⽐例する。 たとえば将棋だと、1 分で考えた⼿と 1 時間で考えた⼿は、後者の⽅が有効である可能性が⾼いと 思います。 このとき⼤事なポイントは 2 つあって、時間をかけた⽅が有効である可能性が⾼いということと、 かといって、必ずしも時間をかけた⽅が有効であるとは限らない、ということです。あくまで、統 計的に時間をかけたほうが優れている傾向にある、というくらいにしか過ぎません。 ソフトウェアテストにも同じことが⾔えます。時間をかければ、たいていの不具合を洗い出すこ とはできますが、しかし、時間を賭けたからと⾔って、⾒逃される不具合がゼロになるわけではあ りません。 ⾒逃された不具合というのは、再現に複雑な条件が必要になるものが多く、テストでそれを⾒つ けるためには、論理的な思考というよりは、発想⼒が必要となる作業です。そして、発想が降りて くるかどうかは、与えられた時間に⽐例しますが仮に時間を無限にもらえたとしても、100% 発想が 降りてくるわけではないことに注意が必要です。 まとめると、テストで得られる品質は、時間を変数とした値ですので、テストの時間はなるべく たくさん確保しましょう。ただし、時間が⼗分にあったからといって、すべての不具合が⾒つかる とは限らないので、不具合が流出しても「あんなに時間があったのに、なにやってんだ?」と QA チームを責めるのではなく、なぜその条件が⾒つからなかったのかを、冷静に振り返りましょう。 2.4 QA チームについて QA チームが⼀番詳しい? スマートフォンゲームもリッチになり、複雑になってきています。そんな状況ですので、開発チー ム内でゲーム全体の仕様を詳細に把握しているのは、もはや QA チームだけかもしれません。 暗黙の仕様にも詳しかったりするので、ゲームの仕様に関して、何かわからないことがあれば、 QA チームに聞いて⾒ると良いでしょう。 また、障害が起きやすいコンポーネントについても詳しかったりするので*17、機能改修のリスク を事前に知りたい場合も、QA チームから良い意⾒が聞けるかもしれません。 *17 「この機能に改修が⼊ったら要注意リスト」みたいのを、QA チームで持ってたりする 21
  24. 第 2 章 明⽇から使える品質向上 Tips 集 2.4 QA チームについて ゲーム内パラメータを公開しましょう

    QA チームにゲーム内のパラメータを公開することをお勧めします。可能であれば、編集もでき るようにしておいたほうが良いでしょう*18。 テスターは、プログラムは書けなくても、パラメータの因果関係を理解することはできます。パ ラメータを理解することで⾼度なテストができますし、不具合の原因を⾒つけてくれることもあり ます。 ちなみにモンストでは、テスターがほぼすべてのパラメータを操作することが許されています。 そのおかげで、創造性のあるテストをすばやく⾏えるようになっています。そして、 「思いついた⼿ 順をすばやく試せる」という環境が、テスターのモチベーション維持にも影響していると感じてい ます。 QA チームとは問題を⾒つける部署ではなく、問題がない事を確認する部署である この意識の差はとても⼤きいです。 恥ずかしながら、私⾃⾝、ゲームプログラマーをやってたころ、 「バグは QA チームが⾒つけてく れるっしょ」という意識で仕事してました。その結果、⾃分担当の不具合チケットが 100 を超えた ことがあり、ある⽇、QA リーダー、プロデューサー、メインプログラマー(つまり上司)に会議室 に呼び出され、全員からいかに⾃分が品質の妨げになっているかを、⼩⼀時間説教されたことがあ ります。 プログラマーやプランナーが、QA チームのことを「ミスを⾒つけてくれる組織」というふうに 考えていると、品質は上がっていきません。凡ミスが原因の不具合チケットが⽬⽴つようであれば、 QA の役割を勘違いしている⼈がいる可能性が⾼いでしょう。 *19 QA チームの役割を定義する テストを始める前に、QA チームとミーティングを開き、QA チームに求める役割をすり合わせて おきましょう。 開発側とテスト側で、QA チームの役割の認識に相違があるシーンを結構⾒かけますし、それが原 因でトラブルになることもあります。 たとえば、 • テストケースをどこのチームが作るのか • テストにかかる予算を誰が決めるのか • バグさえ出してればよいのか、それとも、開発プロセスにも意⾒を求められているのか *18 編集が容易になるツールも提供できると、なお良しです *19 これは QA チームの課題でもあります。QA チームの役割を普通は知らなくて当たり前ですので、積極的に開発者や プランナーとコミュニケーションを取るべきでしょう。 22
  25. 第 2 章 明⽇から使える品質向上 Tips 集 2.5 品質について といったようなことは、事前に認識を合わせておかないとお互いに不幸な結果となります。 認識の相違がどこから起こるのかというと、会社によって

    QA チームの役割が異なるため、 「以 前、⾃分が所属した会社ではこうだった」という思い込みからきていることが多いようです。 2.5 品質について 品質を定義しよう 品質とはとても抽象的な⾔葉です。そのため、 「良い品質を⽬指す」と⼀⼝に⾔っても、⼈それぞ れ思い描いているものがバラバラだったりします。 たとえば、挙動が軽いこと、ゲームとしておもしろいこと、不具合が少ないこと、いずれも「良 い品質」として定義できます。ですので、議論の際に「品質を⾼めたい」という⾔葉が出てきたら、 「それってつまりどういうこと?」と、ちゃんと確認するようにしましょう。 「良い品質」というのは、 「性格の良い⼈」と同じくらい、ふんわりした⾔葉ですので、使うときは 注意が必要です。 品質は何で決まるのか 品質とは、何で決まるのでしょうか。f(X) = Quality とするなら、変数 X は何なのか。 QA という職種は、いろんなチームを⾒ることができます。良い品質を提供するチームも、そうで ないチームも、たくさん⾒ます。おそらく、プログラマーやプランナーの何倍も、チームを⾒てき ているでしょう*20。 私個⼈の考えとしては、X は「開発に関わっているすべての⼈(の品質に対する意識) 」と思って います。ツールや採⽤技術や開発フローとかも当然⼤事なのですが、それは枝葉で、結局は「⼈」な んじゃないかなと。 品質を上げるために、レビューのプロセスを強化したり、⾼価な UI ⾃動テストツールや静的解析 ツールを導⼊することがあるかもしれません。しかし、何のためにそれらを導⼊しているのかを開 発メンバーが理解していないと、いずれ形骸化し、品質は思ったように上がっていきません。品質 を⾼めるためには、品質への意識が⾼い⼈を採⽤するか、開発に関わるメンバーの品質への意識を ⾼めるしか⽅法はない気がしています。 品質向上の取り組みをやるのなら、このことを軸に添えていないと、何をやったとしても「仏像 作って魂込めず」という状況になりがちです。 ⼩医は病を癒し、中医は⼈を癒し、⼤医は国を癒す 中国のことわざだそうです。QA にも同じことが⾔えると思います。 バグを⾒つけるのが仕事の中⼼であるうちは、まだスタートライン。バグを作り込ませないよう *20 私も 30〜40 チームくらいは⾒てきたでしょうか 23
  26. 第 2 章 明⽇から使える品質向上 Tips 集 2.6 終わりに な働きかけができて、道半ば。関係者全員が品質に対して正しい考え⽅を持つことができれば、よ うやくゴール。

    どうすれば⼤医になれるのか、いつも考えています。もし⼤医の領域までたどり着けたのであれ ば、もはや、その組織には QA チームは必要ないかもしれません*21。 バグを憎んで⼈を憎まず 開発スケジュールの後期に深刻なバグが出たときや、サービスリリース後に不具合が流出してし まったときは、プロセスの改善やチームメンバーの品質への意識を⾼める材料として扱いましょう。 なぜそれが作り込まれたのか、今後、同種のバグが作り込まれないためにはどうしたらよいのか。 なぜそれがテストフェーズで⾒つけきれなかったのか、⾒つけるためにはどうしたらよかったのか。 チームの問題を振り返る良いきっかけになります。 このとき、特定のプログラマーや QA スタッフを個⼈攻撃するのは効果的ではありません。不具 合の理由は、たいてい、複数の理由が組み合わさっているので、特定の誰かを⼀⽅的に責めて改善 するものではありません*22。個⼈の課題ではなく、チームの課題として、取り組むべき内容です。 反省することは⼤切ですが、犯⼈探しに熱中しないようにしましょう。 2.6 終わりに 読んでいただき、ありがとうございました。QA の⽴場から、開発やプランナーやプロデューサー に知っておいてほしいな、という内容を書いたつもりです。 品質を⾼めるには、関係者全員が品質に関⼼を持ち、QA チームを正しく効果的に使うことが⼤切 です。その第⼀歩を、明⽇から踏み出してみませんか? *21 もしかするとクビになるかもしれませんが、ほかの会社が⾼待遇で雇ってくれるはずなので⼼配しなくてよいでしょう *22 不具合が⾒つけきれなかったことを理由に、QA チームを過剰に攻め⽴てると、QA チームはどんどん保守的になっ ていきます。⾔い訳がましくなり、 「品質を上げる」というよりは、怒られないことに時間を使い始めます 24
  27. 第 3 章 とある Unity 開発事例 3.1 始めに 昨今の Unity

    開発では MVP や MVVM、または MV(R)P などのソフトウェアアーキテクチャ を導⼊することが増えてきました。ここに上げたアーキテクチャは最低限のルールを提⽰していま すが細かい役割の切り出し⽅はチームに委ねるため、どう設計していくか頭を抱えることも多いで しょう。筆者も悩み、Web 上で具体的な事例を探しましたが中々⾒つけることができませんでした。 本章では、筆者が実際に業務で MV(R)P を導⼊した事例、またテストを含んだ開発事例を紹介し ます。 3.2 MV(R)P アーキテクチャ アーキテクチャがなぜ必要か 複数⼈での開発、運⽤にはつらくならない設計が必要です。ここで⾔うつらい設計とはどういう ものでしょう。たとえば筆者は次のように考えます。 • ⼀つのクラスの責務が⼤きい • どこに何が置かれるべきか定まっていない これらは複数⼈で開発していくと⼤きな問題となり開発速度を著しく下げる危険性があります。 前者は UI とロジックなどが密結合だったりと関⼼の分離ができていないことから発⽣する問題で す。後者はきちんとルールを明⽰しないことによる問題です。ルールがないと⼈々は⾃分の持って いるノウハウから⾏動しがちで、さらにちぐはぐなプロダクトになってしまいます。 実際の開発でそこまで凄惨なことにはならないと思います。どのプロダクトもまずは基本的に開 発ルールと全体設計を決めるでしょう。それがソフトウェアアーキテクチャです。各プロダクトご とに特化したアーキテクチャを設計していると思います。たとえば STG では弾幕を出すために⼤ 量のデータをさばく必要があるので、パフォーマンスを重視したデータ指向設計なアーキテクチャ を⽬指したりなど。また、MVC や MVP など、先⼈たちが築き上げたアーキテクチャを採⽤するの も良いでしょう。 25
  28. 第 3 章 とある Unity 開発事例 3.2 MV(R)P アーキテクチャ アーキテクチャを導⼊するにあたって重視すべきはなんでしょうか。筆者は次の

    2 点だと考えて います。 • 作りたいアプリケーションに向いているか • 学習しやすいか まずは作りたいものの内容とマッチしているかは最重要だと考えます。こうなってくるとその都 度特化したアーキテクチャを設計するのが正解のように思えてきますが、それらはかなりのコスト がかかります。また、チームメンバーが学びやすいかという点も重要です。社内ドキュメントを充 実させれば可能ではありますがこれもまた⼤きなコストです。世に存在する既存のアーキテクチャ を採⽤した場合、調べれば理論や考察記事などが公開されているのでいくぶんか楽です。 とはいえ、アプリケーションの性質を理解しないまま「流⾏っているから」と採⽤するとまった く良いことはないので、しっかりと時間かけた選定を⾏うことをお勧めします。特にゲームは要件 によって求められることがあまりにも違うのでうまく適合できない可能性は⾼いです。 MVP アーキテクチャについて 本章で説明するプロダクトでは MV(R)P アーキテクチャを採⽤しています。MV(R)P の説明を ⾏うまえに、その前⾝となる MVP アーキテクチャについて説明します。 MVP アーキテクチャは各コードの責務を Model-View-Presenter に切り分ける設計です。 図 3.1: MVP アーキテクチャ それぞれのレイヤは以下の役割を持ちます。 Model Model 層はいわゆるビジネスロジックを内包します。ビジネスロジックはデータを取ってきたり 何かの計算をしたりなど多岐に渡るため、Model という名前だけで分別することは難しいです。実 際導⼊する場合は Model 層の中でさらに役割で名前を切ることが多いです。たとえばデータを取っ てきたり更新したりするインタフェースを Repository と名付けたり、実際のデータの保存先である API やローカルストレージなどに具体的なアクセスを⾏うクラスを DataSource と名付けたりとさ 26
  29. 第 3 章 とある Unity 開発事例 3.2 MV(R)P アーキテクチャ まざまな実装⽅法があります。

    Model は View と Presenter の存在を知りません。 View ユーザーに表⽰する画⾯や⼊⼒を受け付ける処理がここに該当します。ユーザーのインプットを 受け付け、何らかの処理が⾛って画⾯に表⽰する、というのが基本的な動きになります。Model に 依存してはいますが、直接 Model 層にアクセスすることはなく、後述の Presenter に処理を委譲し ています。 Presenter Model と View の間を取り持つ層です。View のイベントを受け取り Model を操作する、または Model の変更を受け取り View に変更を通知します。View と Model の橋渡しとしての存在ですの で、Presenter にビジネスロジックはあまり含みません。Model のメソッドを呼ぶ、View のメソッ ドを呼ぶ、くらいにとどめておくのが望ましいです。 Unity における MVP たとえば、アイテムの表⽰と、クリックしたらアイテムを⼀つ消費する、といった機能を実装す るとき MVP で分割して書くと以下のようになります。 public interface IItemView { void ShowItem(Item item); } public class ItemView : MonoBehavior, IItemView, IPointerClickHandler { public int itemId; private ItemPresenter _presenter; public Text _itemName, _itemCount; void Start() { _presenter = new ItemPresenter(this); _presenter.FetchItem(itemId); } public void ShowItem(Item item) { _itemName.Text = item.Name; _itemCount.Text = item.Count.ToString(); } public void OnPointerClick(PointerEventData eventData) { _presenter.UseItem(1); } } 27
  30. 第 3 章 とある Unity 開発事例 3.2 MV(R)P アーキテクチャ public

    class ItemPresenter { private IItemView _view; private Item _model; public ItemPresenter(IItemView view) { _view = view; } public void FetchItem(int itemId) { _model = Item.GetById(itemId); _view.ShowItem(_model); } public void UseItem(int useCount) { var callback = item => _view.ShowItem(item); ItemApi.Request(_model, useCount, callback); } } アイテムを表⽰、⼊⼒を受け付ける部分を ItemView に任せ、アイテムのデータ取得、更新は ItemPresenter に委譲しています。ItemPresenter は具体的なロジックを持たず、ItemModel から 取得して結果を ItemView に渡したり、ItemApi を使ったサーバとの通信の結果を View に返して います。 Presenter は具体的な View を知らずに IItemView というインタフェースに依存しています。つ まり Presenter は View を知ってはいるが、MonoBehavior は知りません。このやり⽅の良いとこ ろは Presenter の EditModeTest が書けるという点です。EditModeTest では MonoBehavior を扱 うことができませんが、IItemView を実装したモッククラスを⽤意することで Presenter の挙動を テストできます。 Unity において MVP アーキテクチャを採⽤する場合、MonoBehavior がどのレイヤに相当 するのかというところを考える必要があります。View はユーザーに表⽰する役割を持つため MonoBehavior を継承して問題はないでしょう。Model は⼊⼒の受け付けや表⽰とは遠い世界に いるので継承する必要はないでしょう。Presenter が継承しているかどうかはケースバイケースで す。今回の⼀例では PureClass として定義することでテスタブルになるメリットがありましたが、 Text ⼀つを操作したり画像を 1 枚表⽰するだけでも必ず View のインタフェースと実体の実装が必 要であったりと、若⼲⼿軽さを失うデメリットもあります。ですので Presenter が MonoBehavior を継承することで Button や Text などの View を直接参照する⽅法も有効です。こちらの場合は Presenter のテストが⾏いづらくなるのでどちらが良いかは作りたいものに合わせて判断すると良い でしょう。 MV(R)P アーキテクチャ この MVP から派⽣して @neuecc さんが提唱したのが Model-View-(Reactive)Presenter、 MV(R)P アーキテクチャです。 28
  31. 第 3 章 とある Unity 開発事例 3.2 MV(R)P アーキテクチャ 図

    3.2: MV(R)P アーキテクチャ 基本の考え⽅は MVP と同様ですが、Presenter が View と Model を取り持つ⽅法が変わります。 UniRx を⽤いて View の変更と Model の変更を Observable で購読できるようにしています。先ほ どのアイテムの例だと次のように表せます。 public class ItemView : MonoBehavior { public Text _itemName, _itemCount; public void ShowItem(Item item) { _itemName.Text = item.Name; _itemCount.Text = item.Count.ToString(); } } public class ItemPresenter : MonoBehavior { public int itemId; private ItemView _view; private ReactiveProperty<Item> _model = new ReactiveProperty<Item>(); void Start() { 29
  32. 第 3 章 とある Unity 開発事例 3.3 設計の⼀例 // View

    のイベントを購読してモデルを更新する _view.OnClickAsObservavle() .SelectMany(_ => ItemApi.Request(_model, 1)) .Subscribe(newModel => _model = newModel); // モデルのイベントを購読して View を更新する _model.Subscribe(view.ShowItem); _model.value = Item.GetById(itemId); } } View は Presenter を知らず、イベントを流すだけの存在です。Presenter は Model と View の データのやりとりを Reactive につなぐだけです。 3.3 設計の⼀例 現在進⾏中のゲームタイトルの開発では MV(R)P を採⽤しています。 View View は MV(R)P の思想と同じで、 毎回必ず作るのではなく必要であれば View クラスを作る、 く らいの塩梅で定義しています。具体的に⾔うと画像を1枚表⽰する、テキストを1つ表⽰する、くら いの規模であれば View クラスは定義せず、Presenter が Text や Image を直接参照するようにして います。ですので、画⾯数に対して View はそこまで数が増えていない状況です。独⾃ View を定義 するときは、Text や Image など個々の要素は外部に公開せず、必ず各イベント通知を IObservable にして公開しています。 Presenter Presenter も MV(R)P の思想をそのまま適⽤しています。基本的には View の IObservable を 購読して Model を操作し、Model の IObservable を購読して View を操作するという役割のみで すが、ストリームが複雑化しないような注意は必要です。たとえば View から送られてくるデータ が Model を操作するのに適切でないので Select で変換したくなったり、特定の条件だけ欲しくて Where でフィルタリングしたくなったりという状況はありがちですが、それらをすべてオペレータ に直書きしてしまうと可読性が下がる懸念があります。1 ⾏ラムダ式が追加される分にはまだそこま で気になりませんが、もしオペレータが膨れ上がりそうな兆候を感じたら、変換処理を別の Model に切り出したり⼊⼒を検証する Validation クラスを⽤意したりなど、各責務を逃してあげた⽅が将 来的に幸せになります。もともと Presenter は肥⼤化しがちなのでここはコードレビューで補って いくのが良いでしょう。 30
  33. 第 3 章 とある Unity 開発事例 3.3 設計の⼀例 Model Model

    層以下は最低限の分割を⾏っています。 • Entity • Repository • UseCase • Factory Entity Entity の定義はさまざまですが、プロダクトでの Entity の定義は「ロジックを持たないデータを 保持する役割のクラス」としています。たとえば通信のレスポンスを格納するクラス、マスタデー タの値を保持するクラスなどです。Entity はあくまでデータと構造を保持するだけですので、そ こにコンテキストは含まれていません。たとえば⼀般的な RPG のキャラクタのデータを保持する Entity は以下のようになります。 public class CharacterEntity { public int id; // キャラクタ ID public string name; // 名前 public int level; // レベル public int hp; // ヒットポイント public int mp; // 必殺技ポイント的な public int attack; // 攻撃⼒ public int defence; // 防御⼒ public int exp; // 経験値 } これらはキャラクタの詳細を⾒る画⾯ではすべて必要不可⽋なパラメータです。しかし、所有 キャラ⼀覧を⾒るような画⾯などではここまで詳細な情報は不要かもしれません。プロダクトでの Entity はただのデータを保持するだけに過ぎず、それをどう使うのかというコンテキストは含めま せん。コンテキストを含めたクラスは次のコードのようにそれぞれ定義します。 // キャラクタ詳細を持つモデル public class CharacterDetail { private CharacterEntity _entity; public string name => _entity.name public int level => _entity.level public int hp => _entity.hp public int mp => _entity.mp 31
  34. 第 3 章 とある Unity 開発事例 3.3 設計の⼀例 public int

    attack => _entity.attack public int defence => _entity.defence public int exp => _entity.exp public int nextLevelUpExp => /* _entity.exp で計算したやつ */ } // キャラクタ⼀覧の要素を持つモデル public class CharacterListContent { private CharacterEntity _entity; public string name => _entity.name public int level => _entity.level } Presenter、View の世界に Entity が漏れ出すことはありません。Model 層の外には上記のように どのような使われ⽅をするかというコンテキストを持ったクラスを返します。例で上げたコードだ け⾒ると、CharacterDetail は CharacterListContent が持つ要素をすべて内包しているのでわざわ ざ分けるのは無駄なのでは、と⾒えるかもしれません。しかしこれらはドメインが異なるのでコー ドの修正理由が⼤きく変わってきます。たとえばキャラ⼀覧画⾯では、詳細画⾯では使うことのな い⼩さいキャラアイコンを使いたくなるかもしれません。その際にクラスが同じでは詳細画⾯まで 影響を受けてしまう恐れがあります。クラスの共通化は変更理由を考えた上で⾏うべきです。 Repository Repository は、Entity の保持、操作を⽬的としたクラスです。次のコードは CharacterEntity を 保持する Repository です。 public interface ICharacterRepository { CharacterEntity GetById(int id); void AddCharacter(CharacterEntity newCharacter); } public class CharacterRepository { private List<CharacterEntity> _entities; public CharacterRepository(ILoader loader){ _entities = loader.Load<CharacterEntity>(); } public CharacterEntity GetById(int id) { return _entities.FirstOrDefault(c => c.id == id); } public void AddCharacter(CharacterEntity newCharacter) { _entities.Add(newCharacter); } } 例では Entity ⼀覧を保持し、取得したり追加したりしてます。また、キャッシュの役割も担って 32
  35. 第 3 章 とある Unity 開発事例 3.3 設計の⼀例 います。⼀般的に定義されている Repository

    は次のようなものです。 • どこにデータが永続化されているか知っている • Repository の利⽤者側にはデータの永続化先を教えない たとえばこの CharacterEntity のデータがローカルに保存されているとしたらこの Repository はローカルストレージにアクセスする処理が書かれるし、外部に保存されているとしたら API サー バと通信を⾏うでしょう。そして Repository を呼び出す側は保存先を知ることなく追加したり取得 したりできます。 しかし、本プロダクトでの Repository は外部保存先を知らず、オンメモリ上での保存・更新のみ を役割としています。API 設計との兼ね合いが理由です。 ⾮ゲームでない通常の Web アプリケーションやスマートフォンアプリでは RESTful な設計がス タンダードです。ゲームで⾔うところの Character というリソースを単体で CRUD 可能な設計で す。ですのでリソース単位での Repository 分割は API の設計と同じであり、⾮常に相性が良いと ⾔えるでしょう。しかし、スマートフォンゲームではやや事情が異なり、ゲーム中での通信回数を 極⼒減らす⽂化が⾒られます。キャラクタ⼀覧画⾯に⾏くたびに 100 体のキャラクタ情報を API か ら取ってきたり、クエスト画⾯に⾏くたびに数百を超えるクエスト情報の通信が発⽣すると快適さ を損なうからです。できれば最初にすべて保持してそれらを使いまわしたくなります。 それでも、クエストをクリアしたときや、キャラクタを強化したなどゲーム中に通信が⾏われる のは避けられません。たとえばクエストをクリアしたときという例で考えてみます。クエストをク リアしたときの仕様は次の通りです。 • クエスト報酬アイテムがもらえる • 次のクエスト情報が出てくる QuestRepository から次のクエスト情報を Get して、そのあとクエスト報酬アイテムも Item- Repository から Get するということは可能ですが、それでは通信が⼆回⾛ってしまいます。通信回 数を減らしたいのであれば、クエストクリア時の API のレスポンスに報酬アイテムと次のクエスト 情報を含めたりするのが無難です。そうなってきた場合、Repository と API のリソースが 1 対 1 の 関係性を保つことが難しくなってきます。しかしここで親の Repository を作って QuestRepository と ItemRepository を操作させるのは組み合わせが凄まじいことになり複雑化してしまいます。そ れぞれの Entity を保持する存在は必要だが、リソースを保持する側の都合をあまり考慮したくな かったという背景から、プロダクトでの Repository はオンメモリ上での保持や更新の役割に抑え て、通信の事情は後述の UseCase に任せるという⽅針になっています。 UseCase UseCase はビジネスロジックを内包したクラスです。かなり具体的なドメインを含んでいるのが 特徴です。たとえば次のコードはキャラクタの名前変更を⾏う UseCase です。 33
  36. 第 3 章 とある Unity 開発事例 3.3 設計の⼀例 public class

    ChangeCharacterNameUseCase { private IApiClient _apiClient; private ICharacterRepository _repository; public ChangeUserNameUseCase(IApiClient client, ICharacterRepository repository) { _apiClient = client; _repository = repository; } public async Task<bool> Run(int characterId, string newName) { var entity = _repository.GetById(characterId); var response = await _apiClient.ChangeNameRequest(_entity, newName); if(response.IsSuccess) { _repository.UpdateCharacterName(_entity, newName); return true; } else { return false; } } } public class CharacterNamePresenter : MonoBehavior { private Text _characterName; private InputField _newNameField; public int characterId; public void Initialize(IApiClient client, ICharacterRepository repository) { _newNameField.OnEndEditAsObservable() .Subscribe(async newName => { var useCase = new ChangeCharacterNameUseCase(client, repository); if(useCase.Run(characterId, newName)) { _characterName.Text = newName; } else { // 失敗ダイアログなど } }); } } UseCase は名前変更のためにリソース取得、通信、リソースの更新など必要な処理をすべて内包 しており、Presenter は Run(newName) を呼ぶだけですべて解決するようなインタフェースになっ ています。Presenter が Model 層にアクセスするときは、直接 Repository や Entity を参照させず、 すべて UseCase を通してアクセスするようにしています。ですので UseCase は Facade パターンに 近い役割とも⾔えるでしょう。 34
  37. 第 3 章 とある Unity 開発事例 3.3 設計の⼀例 図 3.3:

    Presenter から UseCase を処理するフローの⼀例 プロダクトにおける UseCase は以下の⽅針で実装されています。 • 使いまわさない • 処理は1つのみ Presenter の受け⼝となるため、UseCase はキャラ詳細を表⽰する画⾯、名前を変更する画⾯、 クエストを選択する画⾯など、 「何をしたいのか」という情報が多分に含まれます。なので⼀つの UseCase を再利⽤するということは、まったく同じコンテキストが複数ヵ所に存在するというこ とです。現実的には場所が違えばコンテキストは微妙に異なるし、もしまったく同じならそれは Prefab 単位で共通化されるでしょう。すなわち UseCase は常に同じ Presenter から呼ばれること 35
  38. 第 3 章 とある Unity 開発事例 3.3 設計の⼀例 を想定すべきです。そのような理由から、UseCase は絶対に使いまわさない⽅針で実装しています。

    もしロジックが同じであればそれは UseCase よりも下の階層でクラスを共通化します。 また、コンテキストが常に同じということは具体的に役割が決まっているので処理は1つのみと しています。ChracterUseCase.ChangeName というようにスコープを広げることはしません。ク ラス名からできることがすべて伝わるのと、単⼀処理ですので依存関係が最低限で済み、テストコー ドを書きやすいのがメリットです。ただし、ファイル数がかなり膨れ上がってしまうのがデメリッ トです。 Zenject の併⽤ 私の開発チームでは Zenject*1を利⽤した DI*2を⾏っています。 DI についてはあまり特殊なことはしていませんが、数点やったことだけ説明します。 Model 層では Zenject を臭わせない フィールドインジェクション、メソッドインジェクションはたいへん便利ですが、Inject 属性を付 けるとそれは Zenject に依存することになってしまいます。Zenject を使っているのでおかしいこと ではありませんが、private なメンバー変数にフィールドインジェクションを使っているため⼿でイ ンスタンスを作ることができないという状況は好ましくありません。Model 層はすべて PureClass なのでコンストラクタを呼ぶことができます。よほど特別な理由がなければコンストラクタで渡す べきでしょう。 public class BadPureClass { [Inject] private Foo _foo; // Zenject が無いと Foo を渡せない! } public class GoodPureClass { private Foo _foo; public GoodPureClass(Foo foo) { _foo = foo; } } 同様に、DiContanier を PureClass に渡すのも極⼒避けるべきです。Resolve できて便利ではあ りますがそれはサービスロケータパターンとなり、Conainer そのものへの依存が発⽣します。メ ソッドインジェクションや Container を渡すのは MonoBehavior を継承している Presenter、View の層までに抑えておくのが無難でしょう。 ただし、後述の Zenject Factory を使っているので完全に Zenject が存在しないわけではありま せん。これは Model そのものが依存しているわけではないので許容範囲と考えています。 *1 Unity で DI を⾏うためのライブラリです。https://github.com/svermeulen/Zenject *2 Dependency injection(依存性の注⼊) の略で、外部から依存するオブジェクトを注⼊するデザインパターンです。 36
  39. 第 3 章 とある Unity 開発事例 3.3 設計の⼀例 Repository の

    Bind Repository は性質上どこの画⾯でも利⽤される可能性があったので、シーンだけにバインドする ということができませんでした。この問題は、現状は ProjectContext の Container に Bind するこ とで対応しています。 public class MasterRepositoryInstaller : Installer<MasterRepositoryInstaller> { public override InstallBinding() { Container.Bind<IMasterDataLoader>().To<MasterDataLoader>().AsCached(); Container.Bind<IMasterFooRepository>().To<MasterFooRepository>().AsCached(); Container.Bind<IMasterBarRepository>().To<MasterBarRepository>().AsCached(); } public void UnInstall() { Container.SafeUnBind<IMasterFooRepository>(); Container.SafeUnBind<IMasterBarRepository>(); } } 任意のタイミングで上記の MasterRepositoryInstaller を ProjectContext の Container に Install しています。Installer として切り出しているのでテストを書いたり、インストール先を切り替えた りできます。どこでもアクセスできるので利便性は⾼い反⾯、 グローバルに展開しているので保守性 の⾯で不安が残ります。現状問題は起きていませんが規模が⼤きくなった時の対応が必要そうです。 ちなみに、 UnBind メソッドはバインドされていなかった場合例外を出すので、 HasBinding<T>() で確認する必要があります。ですので、拡張メソッドを定義しておくと少し楽になるのでお勧め です。 public static class ZenjectExtensions { /// <summary> /// Bind されているかチェックして UnBind する。Bind されてない場合は何もしない /// </summary> /// <typeparam name="T"></typeparam> /// <param name="self"></param> /// <returns></returns> public static bool SafeUnbind<T>(this DiContainer self) { if (self.HasBinding<T>()) { return self.Unbind<T>(); } return true; } } 37
  40. 第 3 章 とある Unity 開発事例 3.3 設計の⼀例 UseCase は

    Zenject Factory で⽣成する 動的に⽣成したインスタンス、GameObject に Inject したい場合は Zenject Factory を使います。 次のコードでは、名前変更する UseCase を Factory で⽣成しています。 public class ChangeCharacterNameUseCase { private IApiClient _apiClient; private ICharacterRepository _repository; public ChangeUserNameUseCase(IApiClient client, ICharacterRepository repository) { _apiClient = client; _repository = repository; } public async Task<bool> Run(int characterId, string newName) { var entity = _repository.GetById(characterId); var response = await _apiClient.ChangeNameRequest(_entity, newName); if(response.IsSuccess) { _repository.UpdateCharacterName(_entity, newName); return true; } else { return false; } } class Factory : PlaceholderFacotry<ChangeCharacterNameUseCase> { } } public class CharacterNamePresenter : MonoBehavior { private Text _characterName; private InputField _newNameField; public int characterId; public void Initialize(ChangeCharacterNameUseCase.Factory useCaseFactory) { _newNameField.OnEndEditAsObservable() .Subscribe(async newName => { var useCase = useCaseFactory.Create(); if(useCase.Run(characterId, newName)) { _characterName.Text = newName; } else { // 失敗ダイアログなど } }); } } public class FooSceneInstaller : MonoInsaller<FooSceneInstaller> { public override InstallBinding() { Container.BindFactory<ChangeCharacterNameUseCase, ChangeCharacterNameUseCase.Factory>(); } 38
  41. 第 3 章 とある Unity 開発事例 3.4 Unity 開発におけるテストの導⼊ }

    Factory 経由でインスタンスを作るので、Presenter は UseCase が依存しているオブジェクトを ほとんど知る必要がありません。ですので UseCase を⽣成するために依存オブジェクトを持つと いうことはなくなります。仮に内部実装に変更があっても Presenter が影響を受けにくくなりま す。また、Installer にはこのシーンで⾏われるすべての UseCase が Bind されることになるので、 Installer を⾒ればこのシーンで⾏われる処理がすべてわかります。これは地味にうれしいです。プ ロダクトでは Instantiate などもできる限り Factory で⾏うような⽅針です。コンストラクタを呼 ぶ、もしくは⾃前の Initialize メソッドを呼ぶということは⽣成したオブジェクトの依存関係を理 解する必要があるため、依存関係解決のためのオブジェクトを⼿で引っ張りまわすことになります。 それらはすべて Container に任せてしまい、各クラスは⾃分の責務のコードだけを書くようにして いければ複雑性を減らせるのではないかと考えています。また、Zenject Factory に対応していれば 今後パフォーマンスチューニングが必要になった段階で Zenject MemoryPool への移⾏が簡単にで きるというのもメリットです。 3.4 Unity 開発におけるテストの導⼊ 私の開発チームではユニットテスト、UI テストを書くようにしています。 なぜテストコードを導⼊するのか 往々にしてプロダクトの要件は常に変化します。⼀ヵ⽉前の仕様が変わることも珍しいことでは ありません。仕様が変わればコードの修正は必要です。つまり、コードは必ず変化するという前提 で書く必要があります。 コードが変化すると、その周辺のコードに少なからず影響を与えます。場合によっては全然関係 ないと思っていたところに⾶び⽕することもあります。それらをあらかじめ⼈間がすべて把握する のは経験則からくる職⼈技に近いものですので、⾮常に難易度が⾼いです。 テストコードを書くと、変更を加えた際に起きる影響をある程度可視化してくれます。 プロダク トの規模が⼤きくなるほど開発速度は落ちていきますが、テストコードはその減速率を下げるのに 役⽴ちます。 テストコードを書くメリットは以下の2点です 品質を把握する テストそのものが品質を上げてくれるわけではありません。テストコードは現状のプロダクトの 品質がどんなものなのかを可視化してくれます。この部分が適切に動いているのか、この部分に変 更を加えるとどうなるのか、など。品質の可視化は機能追加、リファクタリングの判断材料となり、 すばやい意思決定が⾏えます。 39
  42. 第 3 章 とある Unity 開発事例 3.4 Unity 開発におけるテストの導⼊ テストがないコードは品質が悪いのではなく品質がわからないので、どこを直すべきか、どこの

    処理を⼿厚くすべきかという判断が難しくなり、結果的に開発効率が落ちてしまいます。 精神的障壁の排除 影響範囲の⾒えないコードの修正はメンタル的によろしくないです。 根本原因を解決すべきとわ かっていても、影響範囲が⾒えない場合、リスクを減らし最⼩⼯数で抑えるためにいったん場当た り的な対応を取りがちです。これはもちろん有効に働く場合もありますが、上記のネガティブな理 由から選択すると、未来で同じ問題にぶつかってしまいます。 テストコードにより影響範囲が可視化されていると、根本原因の解決はどのくらいの影響を与え るのかというのがある程度⾒えることで「よくわからないがたいへんそう・・」という精神的障壁 を超えた上で判断ができます。 ユニットテストのよかったところ Model 層以下は基本的にユニットテストを書くようにしています。ロジックが含まれない Entity は省いていますが、UseCase、Repository やそのほかの Model は基本的に書きます。当初はアウト ゲームのみの想定でしたが、インゲームも MV(R)P アーキテクチャを導⼊したのでユニットテスト できるところは極⼒テストを書いています。テストを書き始めて⼤きくメリットを感じた点は次の 3つです。 トライアンドエラーの速度が上がった エディタ上ですぐに再⽣して確認できるのは、実際にプレイする場合と⽐較してかなり早いで す。特にインゲームはアウトゲームと⽐較してさまざまな計算ロジックが⼊りがちです。クリア 時の点数計算だったりダメージ計算だったりなどがそうでしょう。これらのロジックを修正して その都度ゲームをプレイするのはかなり⼿間がかかる上に⾒間違えたりなど凡ミスも起こります。 EditModeTest ですぐに調べられるのはそれだけでエンジニアのストレス軽減になっています。ま た、テストしたいので Presenter からロジックを積極的に剥がしていくようなことも時折⾒られま した。 コードレビューの負担が減った コードレビューではもちろん実装を⾒るのですが、これが本当に動くのか、正しいのかというの をコードのみで判別するのは中々たいへんです。テストコードが⼀緒に出されていると、少なくと も「正常に動作するか」という判断はしなくてよくなります。レビュアーは実装⽅法など本質的な 部分に集中できるようになったので開発効率が上がりました。また、どのように使うかという例が テストコードで書かれているので特別にドキュメントが必要ということもありません。 40
  43. 第 3 章 とある Unity 開発事例 3.4 Unity 開発におけるテストの導⼊ リファクタリングのきっかけを作った

    テストを書いていると、すぐにテストコードが書けるクラスと⾮常に書きづらいクラスがあるこ とに気付きます。なぜこんなにも差が付くのでしょうか。理由はいろいろありますが、よくあるも のとしては下準備が多いテストは書きづらいです。下準備とは、そのクラスのメソッドを実⾏する ために必要な依存関係が多いため、事前に⽣成するインスタンスが多いということです。依存する インスタンスを作るためにまたそのインスタンスの依存しているインスタンスを⽣成したりと膨れ 上がっていきます。Zenject を使えばある程度解決はできますが、それでも Bind する数が多いこと には変わりません。プロダクトで実際に発⽣したのは UserData という、あらゆるユーザーデータ を保持する神のようなクラスが原因でした。 public class UserData { public UserFoo Foo { get; } public UserBar Bar { get; } public UserData(UserFoo foo, UserBar bar) { } } 例では2つですが実際はもっとありました。この UserData は⽣成時に⼤量の引数を要求するの でテスト時に⼤量の依存関係を解決しないといけません。UserData の1メソッドをテストするため に関係ないけど依存しているインスタンスを⽤意しないといけなかったのです。また、UserData に 依存しているオブジェクトのテストを⾏うときも同様に UserData を⽣成する必要がありユニット テストの課題となっていました。このクラスの問題点は誰もが気付いていましたが、 プロダクトを動 かすうえでは必ず存在しているシングルトンインスタンスとして振る舞っていたのでそこまで⼤き な問題と認識するのが難しかったのです。テストを書いていく上でこのクラスの問題提起が⾏われ、 ついにリファクタリングが⾏われたのでした。今は各データは Repository に分割され、UserData は本当に最低限の情報だけを持つクラスに変わりました。 このように、設計上問題があるクラスはテストの段階で違和感に気付くことができるので、実際 にリファクタリングを⾏うかは別にしてどうやっていくかの判断ができるようになったのは⾮常に ⼤きなメリットです。 通信を含むユニットテストをどう書くか ユニットテストの問題として、通信が挟まるパターンを書けないという点があります。たとえば ログインに成功したら何かをする、失敗したら何かをするという UseCase があったとして、テスト を⾛らせるたびに本当に通信をするわけにはいきません。通信するテストは書かないという判断も ありですが、それではあまりテストの恩恵を受けられないので、通信を良い感じに誤魔化す⽅法を 考えなければなりません。 Unity 標準では通信部分を誤魔化す機能はないので、モックができる API クライアントを作るこ 41
  44. 第 3 章 とある Unity 開発事例 3.4 Unity 開発におけるテストの導⼊ とにしました。

    Client の流れを分解する もともと作っていた API Client は次のような使い⽅でした。 var client = new ApiClient(); client.StartRequest(new FooRequest()); // => Result<FooResponse> APIClient に RequestBase を継承したリクエストを投げると通信してくれるようなしくみです。 これをインタフェースとして考えると次の図のようになります。 42
  45. 第 3 章 とある Unity 開発事例 3.4 Unity 開発におけるテストの導⼊ ⼀番理想的なのは

    IHTTPClient 部分を差し替えられることです。最も適切なモックだと思いま す。しかし内部実装的に少し⼿間がかかり過ぎるので、今回は IApiClient と IRequest 間を誤魔化 す⽅法を選択しました。 ApiClientMock を作成 C#ではテストでモックをするときは Moq というライブラリが使えます。Moq は Zenject の Option として同梱されているのでそれを使ってクライアントを作りました。 var mock = new ApiClientMock(); var mockResponse = new TResponse(); mock.Mock<TRequest, TResponse>().Success(mockResponse); mock.StartRequest(new TRequest()); // => Result<TResponse> 通信が成功したことにする場合は Success に任意のレスポンスを渡します。 var mock = new ApiClientMock(); var response = new FooResponse(); mock.Mock<FooRequest, FooResponse>().Success(response); var result = mock.StartRequest(new FooRequest()); result.Response.Content; // => FooResponse サーバエラーとして返すなら ErrorResponse を渡します。 43
  46. 第 3 章 とある Unity 開発事例 3.4 Unity 開発におけるテストの導⼊ var

    mock = new ApiClientMock(); ErrorResponse error = new ErrorResponse(); mock.Mock<FooRequest, FooResponse>().Failure(error); var result = mock.StartRequest(new FooRequest()); result.Response.ErrorResponse; // => ErrorResponse ネットワークにつながらなかったなど、サーバに届く前のエラーを起こす場合は Error を呼ぶだ けです。 var mock = new ApiClientMock(); mock.Mock<FooRequest, FooResponse>().Error(Error.Network); var result = mock.StartRequest(new FooRequest()); result.ErrorCause; // => Error.Network 例として、次のコードのテストを書いてみます。 public class FooUseCase { private IApiClient client; public FooUseCase(IApiClient client) { this.client = client; } public string Run() { var result = client.StartRequest(new FooRequset()); if(result.IsSuccess) { return result.Response.Content.Foo; } else { return "NG"; } } } この UseCase は通信結果を返すのでそのままではテストできません、ApiClientMock を使えば このようなテストを書くことができます。 public class FooUseCaseTest { private IApiClient client; [Test] public void Run_Success() { client = new ApiClientMock(); var response = new FooResponse { Foo = "OK" }; client.Mock<FooRequest, FooReponse>() .Success(response); 44
  47. 第 3 章 とある Unity 開発事例 3.5 最後に var useCase

    = new FooUseCase(client); Assert.AreEqual("OK", useCase.Run()); } } これで必ず通信に成功した扱いにできます。また、モックしたリクエストと違うリクエストが⾶ んできたりモックしたが呼ばれなかったなどの場合は⾃らテストを落とすようにしています。 常に本番と同じ状態を⽤意するというのは⾮常にたいへんですので、テストを書くときは何をテ ストしたいかというスコープをしっかりと絞る必要があります。その際にスコープ外の処理は正常 である、という前提を担保するためにモックライブラリを作るのは⾮常に有効ですので多少⼿間で も作ることをお勧めします。しかし、本来テストしないといけない部分をモックしてしまっては本 末転倒ですので、使うときはなぜモックしないといけないのかをしっかり考えるようにしましょう。 3.5 最後に ⽉並みですが、開発に絶対の正解はありません。チームに合わせた⼿法や設計を採⽤するのがベ ターでしょう。とはいえ Unity 開発でそれらの事例が出てくることはあまりありません。各所の開 発者たちが同じ失敗をしてしまうのは⾮効率だと思うので、少しでもノウハウが公開され Unity 開 発の集合知がたまっていくことを願っています。 45
  48. 第 4 章 git challenge を⽀える技術 弊社で催している、学⽣向け競技型イベント git challenge の裏側についての紹介・解説をしま

    す。しかし git challenge で実際に使われているコードのほとんどはプライベートリポジトリとなっ ています。そのため現⾏のコードを使うのではなく、現⾏のコードを参考にしつつスクラッチに⾃ 作したものを⽤いて紹介・解説を⾏います。 4.1 git challenge とは何か git challenge とは、弊社が催している学⽣向け競技型イベントで、ひとことで⾔ってしまえば git で回答するプログラミングコンテストのようなものです。参加する学⽣は git コマンドを駆使して、 コンフリクトの起きるブランチどうしをマージしたり、コミットの順番を並び替えたりして、こち らが要求するコミットを作ります。 図 4.1: 記念すべき第 10 回を 2018 年 12 ⽉ 1 ⽇に開催予定です! 基本ルール 次のようなルールでポイントを集め競い合う競技となっています。 • 学⽣らはふたり⼀組のチームとなり、各チームには GitHub の組織アカウントを割り当てら 47
  49. 第 4 章 git challenge を⽀える技術 4.2 問題の例 れる •

    それらのアカウントには問題⽤のリポジトリが 20 個弱ほど⽤意されており、リポジトリに回 答として指定通りのコミットができれば正解となる • 正解することでポイントを得ることができ、時間内により多くのポイントを得たかどうかで 順位を競う • 問題には難易度があり、難しいほどポイントが⾼いしくみになっている イベントはいつも弊社オフィスで、休⽇を丸⼀⽇使ってやっています。ただし競技時間そのもの は 3 時間ほどです。ちなみに、難易度は 1~6 までありますが、難易度 5,6 を解けた学⽣は今のとこ ろ現れていません。 4.2 問題の例 問題はチュートリアルも含めて 3 つだけ mixi-git-challenge*1 という GitHub 組織アカウン トで公開しています。ここではそれらの問題を簡単に紹介したいと思います。 tutorial チュートリアルです。この問題は実際の git challenge でもチュートリアルとして使われてい ます。git-challenge-tutorial と⾔うリポジトリ名で、ブランチは全部で readme・master・ task-1・task-2 の 4 つです*2。このリポジトリに限らず問題のリポジトリには readme と呼ばれ るブランチがあり、問題設定や正答条件が README.md に書いてあります。基本的には master ブランチに適切なコミットらをプッシュできれば正解です。 どの問題にも users.csv と⾔うファイルがあります。中⾝は次のようになっています。 103,419e0896-52f1-4913-a43c-ec3ae62b66b6, 渡辺 ⼼愛,[email protected],... 48,f6fc18cb-b7d3-4e6c-b4f2-2c9422933b9b, ⽥中 美⽻,[email protected],... 6,64157d6b-d84d-417f-b44d-31775d6a134f, 斎藤 蓮,[email protected],... ... 名前っぽいものやメールアドレスっぽいものが書いてありますが、正直⼤して重要ではありませ ん。重要なのは、それらのユーザー情報っぽいものが各⾏で固有だと⾔うことと、最初の列の数字 です。最初の数字はそのユーザーが持っているチップの数です。ユーザーはユーザーへチップを渡 したりもらったりでき、users.csv ではそれらの情報を管理している、と⾔う設定です。たとえば⼀ つのコミットを⾒てみましょう。 *1 https://github.com/mixi-git-challenge *2 https://github.com/mixi-git-challenge/git-challenge-tutorial 48
  50. 第 4 章 git challenge を⽀える技術 4.2 問題の例 $ git

    show 85afeb1 commit 85afeb146104cc0d3f539ce34e3ec1ab6f8dcf99 (HEAD -> task-1, origin/task-1) Author: Kuniwak <[email protected]> Date: Fri Oct 9 19:24:20 2015 +0900 A さんの変更 diff --git a/users.csv b/users.csv index 9d6ba74..2320034 100644 --- a/users.csv +++ b/users.csv @@ -1,5 +1,5 @@ -103,419e0896-52f1-4913-a43c-ec3ae62b66b6, 渡辺 ⼼愛,[email protected],... -48,f6fc18cb-b7d3-4e6c-b4f2-2c9422933b9b, ⽥中 美⽻,[email protected],... +113,419e0896-52f1-4913-a43c-ec3ae62b66b6, 渡辺 ⼼愛,[email protected],... +38,f6fc18cb-b7d3-4e6c-b4f2-2c9422933b9b, ⽥中 美⽻,[email protected],... 6,64157d6b-d84d-417f-b44d-31775d6a134f, 斎藤 蓮,[email protected],... 134,4048db0f-2454-477a-870b-a007ea21df2b, 松本 ⼼愛,[email protected],... 275,9f2fe7d1-a5b0-41b7-802c-8a7ed93baf48, 斎藤 結愛,[email protected],... このコミットでは「⽥中さん」から「渡辺さん」にチップを 10 枚渡しています。さて、チュート リアルの正答条件ですが、それは master に task-1 と task-2 をマージすることです。察しの良 い⼈はすでにお気付きだと思いますが、このようなデータの扱いをしていた場合、かなりの頻度で コンフリクトがおきます。もちろんチュートリアルでもおきます。なのでコンフリクト解決し、正 しくマージしたものをコミットしてくださいと⾔うのがこのチュートリアル問題の趣旨です。 is order an adding ここからはさらっと紹介します。この問題は git-challenge-is-order-an-adding と⾔うリ ポジトリ名です*3。問題はとてもシンプルで、users4.csv をプッシュしたいが、なぜかできない のでとにかくプッシュしてくださいというものです。users4.csv そのものは Gist に上がってい ます。 minesweeper この問題は git-challenge-minesweeper と⾔うリポジトリ名です*4。問題は master にテス トに落ちるコミットが含まれているので、それらのコミットをすべて直してください、というもの です。数コミット程度なら⼀つ⼀つチェックアウトして確かめても良いでしょうが、全部で 44 コ ミットあります。さぁどうすれば簡単に問題のコミットを調べることができますかね? ちなみに、 特定のコミットだけ落ちるテストというのは Perl のスクリプトとして書かれており、README に リンクがある DropBox からダウンロードできます。 *3 https://github.com/mixi-git-challenge/git-challenge-is-order-an-adding *4 https://github.com/mixi-git-challenge/git-challenge-minesweeper 49
  51. 第 4 章 git challenge を⽀える技術 4.3 インフラの構成 4.3 インフラの構成

    さて、ここから「⽀える技術」っぽい話をします。しかし、前述した通り現⾏のコードはプライ ベートです。ですので、筆者がスクラッチで⾃作したものを紹介します*5。 現⾏の構成 実は現⾏の構成についてはすでに  mixi engineer blog で紹介されています*6。しくみは簡単で、 AWS 上に⽴てた Jenkins が GitHub からの Webhook を受け取り採点スクリプトを⾛らせる、と いった感じです。詳しくは mixi engineer blog の記事を⾒てみてください。 新しい構成 次に⾃作した新しい構成を紹介します。新しい⽅では Jenkins の代わりに Drone という OSS の CI ツールを使いました。構成は次のようになります。 図 4.2: 新しいインフラの構成 *5 つまり、まだ「git challenge を⽀える技術」ではないですが、いずれ移し替えるかもしれません。 *6 http://alpha.mixi.co.jp/entry/2017/08/03/113000 50
  52. 第 4 章 git challenge を⽀える技術 4.3 インフラの構成 問題のリポジトリには問題のためのブランチや回答のブランチ、採点スクリプトが含まれるブラ ンチなどが存在します。実は現⾏では、問題のリポジトリと回答のリポジトリは別の上、採点スク

    リプトは Jenkins が持っていました。新しい構成では、すべて問題のリポジトリに集約させていま す。Haskell 製のデプロイツールを使って、この問題のリポジトリから回答のブランチや採点スクリ プトのブランチなどを除いたブランチを複製し、新しいリポジトリをチームごとに作成します。採 点のフローは次のようになっています。 1. 各チームがチームのリポジトリに回答をプッシュ 2. すると Haskell 製の⾃作 App サーバに Webhook され 3. App サーバは問題のリポジトリの採点スクリプトのブランチに空コミットし 4. そのコミットが Drone によって Webhook され 5. 採点スクリプトが⾛る 採点スクリプトは、各チームのリポジトリをクローンしスクリプトを⾛らせます。なんでこんな ⾯倒くさいフローにしているのかと⾔うと。 。 。 • なぜチームリポジトリから Drone に Webhook しないのか? : 採点スクリプトをチームリ ポジトリに置きたくない • なぜ App サーバから直接 Drone を⾛らせないのか? : Drone はブランチベースなので厳 しい • なぜ App サーバで直接採点スクリプトを⾛らせないのか? : 結果を保持したりが⾯倒く さい 要するに Drone や問題リポジトリに状態の管理を任せることで、App サーバの開発をサボった のでした。また、スコアボードも App サーバから返しています。スコアボードのための情報、採点 結果などは Drone に App サーバから Drone の API を叩いて取得し、必要なデータにして返して います。ちなみに、まだやっていませんが、この App サーバは Docker イメージ化しているので Kubernetes とかで良い感じに分散したいですね。 Drone Drone は OSS の CI ツールで、Go ⾔語によって書かれています*7。コンテナネイティブで、 Docker との親和性が⾼いです。⼿元で起動する場合も docker-compose up というコマンドだけ で⽴ち上げることができます。 Haskell 以降では⾃作したデプロイツールと App サーバについて紹介します。しかし、その前に デプロイツールと App サーバを記述した Haskell というプログラミング⾔語について紹介 *7 https://drone.io/ 51
  53. 第 4 章 git challenge を⽀える技術 4.3 インフラの構成 しましょう*8。ちなみに、Haskell 製の⾃作したデプロイツールと

    App サーバのコードは matsubara0507/git-plantation と⾔うリポジトリにあります*9。執筆時点でのバージョンを 保持するために teckbookfest5 というタグを付けたのでそちらを参照してください。 Haskell とは? Haskell は純粋関数型プログラミング⾔語で、プログラミングに関する研究のための⾔語として 1990 年ごろに多くの研究者の協⼒によって作成されました。近年では Facebook*10 や GitHub*11 が Haskell を⼀部で採⽤するなど実⽤としても注⽬を集めています。 Haskell の特徴は強⼒な型システムにあります。最近では多くのプログラミング⾔語で静的型検 査が注⽬を集めていますが*12、Haskell のそれはそのどれよりも⽐べ物にならないぐらい強⼒です。 たとえば Haskell では RESTful API のインタフェースを型として表現したりもできます(これに ついては後述します) 。 関数定義 わざわざここで説明する必要はないのですが、以降でちょくちょく書くので念のため説明してお きます。たとえば階乗を求める関数は次のように記述できます。 factorial :: Int -> Int factorial n = foldl (*) 1 [1..n] Haskell は Python と同じようなオフサイドルールです。⼀⾏⽬には関数の型を記述し、⼆⾏⽬ には実装を記述しています。Haskell は強⼒な型推論を持っているので、実は⼀⾏⽬の型定義を記 述する必要は基本的にないのですが、可読性のために記述するのがデファクトスタンダードとなっ ています。Int -> Int が型です。Int 型の引数を受け取り、Int 型を返すという意味の型です。 foldl はいわゆる畳み込みの関数で、次のような型を持ちます(厳密には違いますが) 。 foldl :: (b -> a -> b) -> b -> [a] -> b a や b と⾔うのはジェネリックスな型で任意の型を受け取れます。もちろん factorial の場合 はどちらも Int 型にしていますが。また、[a] は型 a を要素にもつリスト型です。つまり foldl は b -> a -> b と⾔う型の 2 引数関数と b 型の初期値、a 型のリストを受け取って畳み込んだ結 *8 さぁここがメインパートですよっ! *9 https://github.com/matsubara0507/git-plantation *10 https://github.com/facebook/Haxl *11 https://www.theregister.co.uk/2018/08/16/github_rails_microsoft/ *12 Python には型ヒントと⾔うのが導⼊され、Ruby では 3 系で何らかの型検査の導⼊が検討されています。また最近 流⾏りの Go や Rust は静的型付き⾔語ですね。 52
  54. 第 4 章 git challenge を⽀える技術 4.3 インフラの構成 果を返します。 型を記述する

    Haskell で型を記述するには次のように書きます。 data Maybe a = Just a | Nothing = の左側の Maybe a が定義した型の名前になり、その要素が Just a か Nothing になります。 Maybe a や Just a の a は関数のときと同じようにジェネリックスな型です。Maybe Int と記述 することで取りうる値は Just 10 や Nothing となるのです。ちなみに、この Maybe a 型は組込 みで存在する型で、いわゆるオプショナル型です。 また、次のように記述することで別名の型を定義できます。 type UserId = Int 特殊な型の記述 以降ではときおり次のように型を記述します。 type User = Record ’[ "id" >: UserId , "name" >: Text , "active" >: Bool ] これは Haskell の⼀般的な型の記法ではありません。拡張可能レコードと⾔われるものです。こ れを⽤いると user^.#name のようにオブジェクト指向プログラミング⾔語のようにフィールドに アクセスできるなど、いろいろと便利なので重宝しています。詳しくは「Haskell 拡張可能レコー ド」などで調べてみてください。 Stack Haskell プログラムをビルドする場合は Stack*13 と呼ばれるツールを使うのが簡単です。Stack は 2015 年ごろにリリースされた割と最近に出てきたビルドツールです。パッケージマネージャーも *13 https://docs.haskellstack.org/ 53
  55. 第 4 章 git challenge を⽀える技術 4.3 インフラの構成 付随しており、Stackage*14 として管理しています。パッケージそのものは

    Hackage*15 と⾔うまっ たく別のサービス上に保存されていますが、Stackage は各パッケージのバージョンの組み合わせが ビルドできるリゾルバーを提供してくれます。 デプロイツール 今のところデプロイツールには設定ファイルを読み込んで問題のリポジトリからチームのリポジ トリを⽣成する機能しかありません。他にも便利機能があると良いのですが、それはおいおい追加 していきます。チームリポジトリを⽣成するコマンドは次のように使います。 $ GH_TOKEN=XXX stack yaml -- \ git-plantation-tool -c .git-pantation.yaml --work .temp new_repo team_name -c .git-pantation.yaml で設定ファイルを指定しています。設定ファイルは次のようになっ ています。 problems: - problem_name: tutorial repo_name: matsubara0507/git-challenge-tutorial difficulty: 1 challenge_branches: - readme - master - task-1 - task-2 ci_branch: ci teams: - name: sample github: sample-hige member: - matsubara0507 基本的に今のところは問題の設定とチームの設定を記述しています。ちなみに、次のような Haskell の型を定義し、yaml パッケージの decodeFile などを使うだけで定義した型にマッピング してくれます。すばらしいですよね。 type Config = Record ’[ "problems" >: [Problem] *14 https://www.stackage.org/ *15 https://hackage.haskell.org/ 54
  56. 第 4 章 git challenge を⽀える技術 4.3 インフラの構成 , "teams"

    >: [Team] ] type Problem = Record ’[ "problem_name" >: Text , "repo_name" >: Text , "difficulty" >: Int , "challenge_branches" >: [Branch] , "ci_branch" >: Branch ] type Team = Record ’[ "name" >: Text , "github" >: Text , "member" >: [User] ] git-plantation-tool new_repo コマンドでは次のようなフローで新しいチームリポジトリを ⽣成しています。 1. GitHub API でチームリポジトリを作成 2. 作成したチームリポジトリをクローン(この時点では空リポジトリ) 3. 問題リポジトリをリモートに追加してフェッチ 4. challenge_branches のブランチのみを問題のリポジトリからチームのリポジトリにコピー 5. それらのブランチをプッシュ チームリポジトリの作成⾃体はこれで完成ですが、採点スクリプトを実⾏するために問題リポジ トリの CI ブランチから新しくブランチを切っておきます。 1. 問題リポジトリをクローン 2. ci_branch のブランチからチーム名でブランチを作成 3. チームリポジトリの名前を書き込んだ REPOSITORY と⾔うファイルを作成 4. そのファイルをチームブランチにコミットし問題リポジトリにプッシュ 採点スクリプトはこの作成したチームブランチで実⾏されます。ちなみに、これらの作業をする ディレクトリを --work .temp で指定しています。また、GH_TOKEN 環境変数には GitHub トーク ンを指定し、GitHub API をたたくときや、問題リポジトリやチームリポジトリにプッシュするの に使います。 App サーバ 次に⾃作した Haskell 製 App サーバについて紹介します。現状の App サーバは次のようなルー ティングを持っています。 55
  57. 第 4 章 git challenge を⽀える技術 4.3 インフラの構成 [GET] /

    # index.html を返す [GET] /static # 画像や JS などの静的ファイル [POST] /hook # GitHub Webhook の Ping と Push Event を拾える [GET] /api/teams # Team 型のリストを JSON 形式で返す [GET] /api/problems # Problem 型のリストを JSON 形式で返す [GET] /api/scores # 全てのチームのスコアを JSON 形式で返す さて、これを Haskell でどう実現しているのかというと、Haskell の Web フレームワークはいく つかありますが、僕は Servant と呼ばれるものを愛⽤しています。彼の有名な Ruby on Rails のよ うなオールインな Web フレームワークというよりは、Sinatra のような RESTful API のためのコ ンパクトな DSL といった感じです。 Servant Sevant の特徴は RESTful API の定義を型として書けてしまう点です。たとえば今回の App サーバの API 定義の型は次のようになっています。 type API = Get ’[HTML] H.Html :<|> "static" :> Raw :<|> "hook" :> WebhookAPI :<|> "api" :> CRUD type WebhookAPI = GitHubEvent ’[ ’WebhookPingEvent ] :> GitHubSignedReqBody ’[JSON] PublicEvent :> Post ’[JSON] () :<|> GitHubEvent ’[ ’WebhookPushEvent ] :> GitHubSignedReqBody ’[JSON] PushEvent :> Post ’[JSON] () type CRUD = "teams" :> Get ’[JSON] [Team] :<|> "problems" :> Get ’[JSON] [Problem] :<|> "scores" :> Get ’[JSON] [Score] 驚くことに Haskell では GHC 拡張と⾔われる機能を駆使することで "static" のような「値」 (この場合は⽂字列) を型の中で扱うことができるのです*16。この型に対して、HTTP リクエスト を受け取ったときの振る舞いを関数で定義します。 -- Plant のところは気にしないで *16 このように「値」を記述できる型を「依存型」と⾔います。興味のある⼈は調べてみてください。 56
  58. 第 4 章 git challenge を⽀える技術 4.3 インフラの構成 server ::

    ServerT API Plant server = indexHtml :<|> serveDirectoryFileServer "static" :<|> webhook :<|> crud indexHtml :: Plant H.Html indexHtml = ... webhook :: ServerT WebhookAPI Plant webhook = pingWebhook :<|> pushWebhook pingWebhook :: RepoWebhookEvent -> ((), PublicEvent) -> Plant () pingWebhook _ (_, ev) = ... pushWebhook :: RepoWebhookEvent -> ((), PushEvent) -> Plant () pushWebhook _ (_, ev) = ... crud :: ServerT CRUD Plant crud = getTeams :<|> getProblems :<|> getScores getTeams :: Plant [Team] getTeams = ... getProblems :: Plant [Problem] getProblems = ... getScores :: Plant [Score] getScores = ... Plant の部分は基本的に気にしないでください。これは関数の中で IO などの好きな副作⽤を 利⽤するためのコンテナ型です。それぞれの API に対応する関数、たとえばチームのリストを返 す /api/teams の関数では [Team] 型を返していますね。もしここで、違う型の値を返す実装を getTeams 関数でした場合は静的型検査でエラーとなります。もちろん、getTeams 関数の型そのも のが [Team] 型を返すようになっていなければ、それもエラーとなります。このように Servant は 定義したルーティングに対して正しく実装できているかを型検査して、実際に試す前に確認してく れるのです*17。 Webhook 後の振る舞い 残りの App サーバの節では Webhook を受け取ったあとの振る舞いを紹介します。前述した通 り /hook API は GitHub Webhook の ping イベントとプッシュイベントを拾えます。ping イベ ントは GitHub Webhook を設定したときにテストで⾶ばされるイベントで、今のところログの出 ⼒しかしていません。なので実際に利⽤するのはプッシュイベントだけです。プッシュイベント後 の動作は次のようになっています。 *17 すばらしい!! 57
  59. 第 4 章 git challenge を⽀える技術 4.4 スコアボード 1. プッシュイベントを受け取る

    2. プッシュイベントの payload からチームと問題を検索する 3. 問題のリポジトリをクローン 4. 採点⽤のチームブランチにチェックアウト 5. 空コミットをしてプッシュ 採点⽤のブランチはデプロイツールで作ったものです。単純に問題のリポジトリに空コミットを プッシュしているだけですね。この空コミットによって Drone による採点スクリプトが実⾏されま す。Drone で動作するスクリプト⾃体は問題リポジトリの ci ブランチの .drone.yml ファイルに 書いてあります。 workspace: base: /root pipeline: clone_target_repo: image: buildpack-deps:16.04-scm environment: - GH_TOKEN=$$GH_TOKEN commands: - pwd - export PATH=$${PATH}:‘pwd‘/bin - (ls | grep --silent target) || \ git clone https://[email protected]/‘cat REPOSITORY‘.git target - cd target && ../verify.bash when: branches: exclude: [ ci ] Drone 側はデプロイツールで作成した REPOSITORY ファイルの中⾝を⾒て採点スクリプトを回す べきリポジトリをクローンします。採点スクリプトそのものは verify.bash というスクリプトで す。たとえばチュートリアルの採点スクリプトは matsubara0507/git-challenge-tutorial の ci ブランチに置いてあります。ここでは説明を割愛しますが、興味のある⽅は覗いて⾒てください。 4.4 スコアボード 次にスコアボードの話をします。もちろん、これも現⾏とは 全然違います 。 現⾏のスコアボード 現⾏のスコアボードは TypeScript によって記述されており、 さらに Isomorphic TypeScript と呼 ばれるデザインパターンを利⽤しています。これは、 サーバ ・ クライアントの双⽅を TypeScript で実 装することで、⼀部の実装を共有するデザインパターンのことを指すようです。筆者は TypeScript について詳しくないのでこれ以上の説明はしませんが、サーバとクライアントでデータ型や API を 58
  60. 第 4 章 git challenge を⽀える技術 4.4 スコアボード 共有化できるというの魅⼒的ですよね。 新しいスコアボード

    新しいスコアボードも基本は同じような⾒た⽬にしていますが、Elm*18 というプログラミング⾔ 語で記述しました。実は Haskell を弊社で利⽤している例はありませんが、Elm であればいくつか の社内ツールに利⽤されている実績があります*19。 図 4.3: 新しいスコアボード Elm Elm は純粋関数型プログラミング⾔語で、JavaScript にコンパイルされる、いわゆる Alt. JS で す。お察しの通り、Haskell から強い影響を受けていますが、Haskell に⽐べてかなり機能を削ぎ落 としています。たとえばアドホック多相を表現するための「型クラス」がありません*20。Elm は汎 ⽤プログラミング⾔語と⾔うよりは、The Elm Architecture というフレームワークのためのドメイ ン特化⾔語と⾔えるでしょう*21。 Elm の構⽂⾃体は、Haskell によく似ています。たとえば、Elm で階乗を求める関数を定義する と次のようになります。Haskell のときのものとすごく似ていますね。 factorial : Int -> Int factorial n = List.foldl (*) 1 (List.range 1 n) *18 公式サイト http://elm-lang.org/ *19 Elixir を採⽤しているプロダクトの社内ツールです。https://career.xflag.com/interview/engineer/xflag-career- blog27/ *20 もちろん、Haskell の⽬⽟機能であるモナドもありません。 *21 このあたりの話は @arrow さんの Qiita 記事「Elm はどんな⼈にオススメできないか」が参考になります。 59
  61. 第 4 章 git challenge を⽀える技術 4.4 スコアボード Elm のソースコードはリポジトリの

    elm-src/Main.elm です。このプログラムでは、2018 年の 8 ⽉にリリースされたばかりの Elm 0.19 を使っています。Elm 0.19 ではかなり破壊的な変更をさ れたので、インターネットを検索して得られるサンプルコードがうまく動かないと思います。Elm 0.19 の変更点については elm/compiler リポジトリの upgrade-docs/0.19.md に詳しく書いてあ ります*22。⽇本語であれば @jinjor ⽒の記事「Elm 0.19 の主な変更点」が参考になるでしょう*23。 本書では Elm の設計思想や細かい構⽂についての説明は省きます。興味のある⽅は Elm 公式のド キュメント guide.elm-lang.org がかなりよくまとまっていてお勧めだそうなので読んで⾒てく ださい*24。 Elm と Haskell さて、 ここからが Elm を採⽤したキモになります。現⾏のスコアボードは Isomorphic TypeScript というデザインパターンにより、サーバとクラインアントで実装を共有していました。実は新しい スコアボードでも Haskell の実装から Elm の実装を⾃動⽣成することで、同様に実装の共有をして います。Haskell から Elm の実装を⽣成するには elm-export*25 と⾔うパッケージを使います*26。 これを使うことで、Haskell の型から Elm の型を⽣成できます。たとえば、Team という型を Elm で⽣成して⾒ます。 type Team = Record ’[ "name" >: Text , "github" >: Text , "member" >: [User] ] instance ElmType Team where toElmType = toElmRecordType "Team" Elm の コ ー ド を ⽣ 成 し た い 型 を ElmType と い う 型 ク ラ ス*27の イ ン ス タ ン ス に す る こ と で ⽣ 成 で き ま す 。た だ し 、toElmRecordType 関 数 は 筆 者 が ⾃ 作 し た も の で す 。実 装 は src/Language/Elm.hs にありますが、ここでは割愛します。そして、次のような main 関数を 書きます。 spec :: Spec spec = Spec ["Generated", "API"] $ concat *22 https://github.com/elm/compiler/blob/master/upgrade-docs/0.19.md *23 http://jinjor-labo.hatenablog.com/entry/2018/08/23/142026 *24 ちなみに、これの⽇本語版も絶賛 Elm-jp というコミュニティで翻訳中です。https://guide.elm-lang.jp/ *25 http://hackage.haskell.org/package/elm-export *26 残念なことに少なくとも執筆当初は Elm 0.19 に対応していなかったので、筆者が⾃分で直したものを使っています。 https://github.com/matsubara0507/elm-export/tree/elm-0.19 *27 型クラスがわからない⼈は、後付けできる Java のインタフェースという認識で⼗分です。 60
  62. 第 4 章 git challenge を⽀える技術 4.4 スコアボード [ [defElmImports]

    , toElmTypeAll (Proxy @ Team) ] toElmTypeAll :: ElmType a => Proxy a -> [Text] toElmTypeAll proxy = [ toElmTypeSource proxy , toElmDecoderSource proxy , toElmEncoderSource proxy ] main :: IO () main = specsToDir [spec] "elm-src" 細かいところは気にしなくて良いです。spec のような設定を書き、specsToDir 関数を呼び出す だけで次のような elm-src/Genarated/API.elm を⽣成してくれる。 module Generated.API exposing (..) import Http import Json.Decode exposing (..) import Json.Decode.Pipeline exposing (..) import Json.Encode import String type alias Team = { name : String , github : String , member : List String } decodeTeam : Decoder Team decodeTeam = Json.Decode.succeed Team |> required "name" string |> required "github" string |> required "member" (list string) encodeTeam : Team -> Json.Encode.Value encodeTeam x = Json.Encode.object [ ( "name", Json.Encode.string x.name ) , ( "github", Json.Encode.string x.github ) , ( "member", Json.Encode.list Json.Encode.string x.member ) ] 61
  63. 第 4 章 git challenge を⽀える技術 4.4 スコアボード さらに、servant-elm*28 を使うことで

    Servant で書かれた API の型から、HTTP リクエストを ⾏う Elm の関数を⽣成してくれます。前述した spec 関数へ次のように generateElmForAPI 関 数を書き加えます。 spec :: Spec spec = Spec ["Generated", "API"] $ concat [ [defElmImports] , toElmTypeAll (Proxy @ Team) , generateElmForAPI (Proxy @ ("api" :> CRUD)) ] CRUD という型は Servant に関する節で定義したものです。これを同じように実⾏すると Team 型のほかに次のような Elm の関数も⽣成してくれます。⻑くなるので /api/teams に対応する関 数しか書きませんが、 /api/problems と /api/scores に対応する関数も⽣成されます。 getApiTeams : Http.Request (List Team) getApiTeams = Http.request { method = "GET" , headers = [] , url = String.join "/" [ "" , "api" , "teams" ] , body = Http.emptyBody , expect = Http.expectJson (list decodeTeam) , timeout = Nothing , withCredentials = False } これらによって現⾏の TypeScript によるスコアボード同様、サーバとクラインアントの実装を 共通化でき、また API の整合性を保証することもできました*29。 *28 http://hackage.haskell.org/package/servant-elm *29 すばらしいっ。 62
  64. 第 4 章 git challenge を⽀える技術 4.5 今後とまとめ 4.5 今後とまとめ

    今回は git challenge をより多くの⼈に知ってもらうことを⽬的として、新しい git challenge の システムを開発し、それについて執筆しました。このシステムは Drone や Haskell、Elm といった ⽐較的利⽤されていないツール・⾔語を使って開発しています。Haskell・Elm を採⽤したことによ り強⼒な型システムに守られた安全なシステムを容易に作ることができます。Drone を利⽤したの は最終⽬標でもある Kubernetes 化を⾒据えてです*30。最終⽬標と述べたように、執筆時点にでき ているシステムは現⾏のシステムに⽐べるとまだまだで、とても置き換えることはできません。今 後はデプロイツールに機能を追加したり、エラーハンドリングをちゃんとしたり、現⾏からの置き 換えを⽬標に開発を続けていきます。 *30 参加者は限られているので、わざわざ Kubernetes で分散する意味があるかどうかは置いといて。 63
  65. 第 5 章 Unity で板野サーカス -誘導ミサイルで クォータニオン⼊⾨- 5.1 始めに 板野サーカスという⾔葉をご存じでしょうか?

    アニメのマクロスなど板野⼀郎⽒が⼿がけるミサ イルの弾幕シーンの総称です。⼤量の誘導ミサイルが襲いかかり、ヴァルキリー (ロボット) が華麗 にかいくぐって撃破していくシーンです。 男⼦プログラマーたるもの板野サーカスにあこがれます。さらに Unity を使っているなら作りた くなるでしょう。その欲求のまま板野サーカスを作り上げたときには、クォータニオンを使いこな しているでしょう。 想定読者 • 誘導ミサイルにロマンを感じている⼈ • Unity でゲームをなんとなく作れる⼈ • クォータニオンがよくわからない⼈ 読者が得られる知⾒ • それっぽい物を作るまでの過程 • オブジェクトの回転制御 • オイラー⾓とクォータニオンの⽐較 • Unity でクォータニオンを使⽤した回転制御 • Unity で RigidBody を "使⽤しない" 当たり判定 65
  66. 第 5 章 Unity で板野サーカス -誘導ミサイルでクォータニオン⼊⾨- 5.2 今回作成したデモ 読者が得られない知⾒ •

    クォータニオンについての数学的な知識 理 解 を 深 め た い ⽅ は「Quaternion に よ る 3 次 元 の 回 転 変 換 」(https://qiita.com/ kenjihiranabe/items/945232fbde58fab45681) を参照してください。丁寧に解説してありま した。 5.2 今回作成したデモ 敵と⾃分がいます。ミサイルを撃ち合います。 図 5.1: スクリーンショット • 動画はこちらにあります。 https://www.YouTube.com/watch?v=V40_mCYeqZ4 • Unity の Project を公開しています。 https://github.com/toymany/missile 5.3 オイラー⾓とクォータニオンとマトリックス 回転の表現⽅法は「オイラー⾓」と「クォータニオン」と「マトリックス」の 3 種類あります。そ れぞれ得⼿不得⼿があります。 ざっくり結論を先に書きます。 1. 計算はクォータニオン 2. ⼈間が触わるところはオイラー⾓ 3. クォータニオンでもオイラー⾓でもできないときはマトリックス 66
  67. 第 5 章 Unity で板野サーカス -誘導ミサイルでクォータニオン⼊⾨- 5.3 オイラー⾓とクォータニオンとマトリックス オイラー⾓ (x,y,z)

    のように書きます。x 軸,y 軸,z 軸それぞれの軸で x 度 y 度 z 度回転するというように表現 します。やっかいなことにオイラー⾓は (x,y,z) が同じ値であっても、回転させる軸の順番によって 結果が異なります。xyz,zyx,yxz... というようにさまざまなパターンがあります。 クォータニオン (x,y,z,w) のように書きます。クォータニオンは複素数を拡張したものです。それぞれの値はオイ ラー⾓のように簡単ではありません。ざっくりいうと、x,y,z は回転軸であり、w はその周りの回転 量です。 値の感覚をつかむために Quaternion.AngleAxis を使⽤して、数字の変化を確認します。Quater- nion.AngleAxis は、回転軸とその軸周りの回転⾓度を指定してクォータニオンを⽣成する関数 です。 リスト 5.1: TestAngleAxis() for (var f = 0; f <= 360; f += 90) { var q = Quaternion.AngleAxis(f, new Vector3(1, 0, 0)); Debug.LogFormat("{0} {1}", f, q); } リスト 5.2: TestAngleAxis の結果 0 (0.0, 0.0, 0.0, 1.0) 90 (0.7, 0.0, 0.0, 0.7) 180 (1.0, 0.0, 0.0, 0.0) 270 (0.7, 0.0, 0.0,-0.7) 360 (0.0, 0.0, 0.0,-1.0) 出⼒結果を⾒ると、 このクォータニオンは x と w が変化してます。回転⾓度が 0 のとき、 (0.0, 0.0, 0.0, 1.0) です。これは回転がない「単位クォータニオン」です。回転⾓度が 0 以外のときは、x 軸 周りの回転なので x に値が⼊っており y と z は 0 です。この x は、θ =回転⾓度とすると sin(θ /2) です。w は cos(θ /2) です。クォータニオンの数値から回転を想像できればデバッグに役⽴ちます。 マトリックス 3DCG では 4 ⾏ 4 列 16 要素のマトリックスを使⽤します。回転と位置とスケールをまとめて表 現します。回転のみであれば 3 ⾏ 3 列で表現できます。アプリケーションが回転の計算にオイラー 67
  68. 第 5 章 Unity で板野サーカス -誘導ミサイルでクォータニオン⼊⾨- 5.3 オイラー⾓とクォータニオンとマトリックス ⾓やクォータニオンを使っても、Unity などのシステムは最終的にマトリックスに変換して描画し

    ます。 オイラー⾓とクォータニオンを⽐較する 表 5.1: オイラー⾓とクォータニオンの⽐較 項⽬ オイラー クォータニオン 難易度 簡単 難しい ジンバルロック ある ない 球⾯補間 できない できる 180 度以上の回転 できる できない 難易度 クォータニオンは、オイラー⾓に⽐べて回転の結果をイメージするのが難しく計算も複雑 です。とはいえ、使うだけなら Unity が⽤意している関数を使⽤するだけです。Unity では、 Transform.rotation がクォータニオンですが、インスペクタで表⽰されている⾓度はオイラー⾓で す。このように⼈間が理解しやすいのはオイラー⾓ですので、回転量を指定するときなど⼈間が触 れる部分はオイラー⾓を使⽤します。 ジンバルロック ⾶⾏機やミサイルの制御をオイラー⾓を使ってやろうとすると、どうにもならない事態に遭遇し ます。9 割の状況で⼤丈夫だとしても、ひねって、宙返りなど、x 軸,y 軸,z 軸それぞれの回転が絡み 合った時に「ジンバルロック」は発⽣します。ジンバルロックがおきると意図した⽅向に回らなく なります。x 軸で回しても z 軸で回しても同じ⽅向に回ってしまいます。しかし、計算をクォータニ オンで⾏えばジンバルロックは発⽣しません。 球⾯補間 ある回転からある回転に補間したいというのはよくあります。オイラー⾓を使って単純に (x1,y1,z1) と (x2,y2,z2) をそれぞれの軸で線形補間すると、ぐにゃぐにゃと遠回りするような回転 になります。クォータニオンには球⾯補間があります。これを使⽤すると普通に期待する最短の回 転で補間ができます。Unity の Quaternion.Slerp です。 180 度以上の回転 オイラー⾓で (720 度,0 度,0 度) とすれば、x 軸で 2 回転することが簡単に表現できます。しか し、クォータニオンは 180 度を超える回転は表現できません。w = cos(θ /2) というように三⾓関 68
  69. 第 5 章 Unity で板野サーカス -誘導ミサイルでクォータニオン⼊⾨- 5.4 板野サーカスを⽬指す 数を通るためです。0 度も

    360 度も 720 度も同じ値になります。回転速度を扱う時はクォータニオ ンではなく、回転軸 (x,y,z) と回転速度 (degree) と分けて管理してそこからクォータニオンを⽣成 します。 5.4 板野サーカスを⽬指す クォータニオンとオイラー⾓をざっくりと理解したところで、板野サーカスを⽬指します。 テスト環境を作る アルゴリズムに取り組む前にテスト環境を整えます。テストを作ると同時にクラスの設計もざっ くり⾏います。アルゴリズムをいろいろ試⾏錯誤するのでテスト環境がないと始まりません。 テストがしやすいプログラムは、機能が分離されて役割がはっきりしているので良い設計です。 オブジェクトの配置 オブジェクトを配置します。地⾯、ミサイル、ランチャー、ターゲット、カメラです。括弧内は (クラス名) です。 • 地⾯ (Ground) : 移動している距離感がほしいのでまずは平⾯を地⾯として追加します。 • ミサイル (Missile) : 今回の主役のミサイルです。 • ランチャー (Launcher) : ミサイルを発射するものです。 • ターゲット (Target) : ミサイルが当たりにいく攻撃⽬標です。 図 5.2: テスト環境 クラスの追加 先に配置したオブジェクトにつけるクラスを追加します。空っぽのまま、実装はまだ始めません。 クラスに⼊れるべき機能は単独では決まらず、ほかのクラスとの関係性で決まります。 69
  70. 第 5 章 Unity で板野サーカス -誘導ミサイルでクォータニオン⼊⾨- 5.4 板野サーカスを⽬指す 実装の前にクラスとクラスの関係を眺めて、プログラム全体をスケッチする感覚で進めます。オ ブジェクトごとに⼀通り追加したら、クラス設計の沼にはまらないうちに引き上げましょう。

    最低限の動きをつける ミサイルを前進させる ミサイルを単純に等速で前進させます。頭の中にあるすばらしい誘導アルゴリズムの実装に着⼿ したい、そんな気持ちをぐっとおさえます。 前進させた先にターゲットを置いて必ず重なるようにします。この段階ではただすり抜けます。 リスト 5.3: Move1() void Update() { var step = Vector3.forward * this.speed * Time.deltaTime; transform.position = transform.position + step; } ターゲットにあたったら爆発させる ミサイルがターゲットをすり抜けるのを⾒ていると、爆発してほしくなります。その欲求に従っ て当たり判定を作りましょう。 いつもは RigidBody を使⽤して OnCollisionEnter で衝突を検知させます。しかし、物理挙動を ⼊れると誘導が難しくなります。今回は単純に座標を transform.position に代⼊して動かしたいの で、RigidBody は使⽤せずに Physics.BoxCast を使⽤して判定します。 リスト 5.4: CheckHit1() // RigidBody を使わないで CheckBox を使って当たり判定をする。 void CheckHit1() { var t = transform; var isHit = Physics.CheckBox(t.position, t.localScale, t.rotation); if (isHit) { Explosion(t.position); } } この CheckHit1 では 2 つ問題が発⽣しました。 1 つ⽬の問題は、ミサイルがすり抜けました。移動速度が上がって 1 フレームに移動する距離が、 70
  71. 第 5 章 Unity で板野サーカス -誘導ミサイルでクォータニオン⼊⾨- 5.4 板野サーカスを⽬指す ターゲットの⼤きさを超えるとすり抜けてしまいます。Physics.CheckBox はチェックした瞬間に

    重なっていないと当たりません。 そこで、すり抜け防⽌に Physics.BoxCast を使⽤します。Physics.BoxCast は始点と⽅向と⻑さ を指定してチェックします。これにより 1 フレームの移動距離を⻑さにすることで、速度が上がっ ても必ず当たります。 2 つ⽬の問題は、爆発の発⽣がずれました。爆発は当たった時のミサイルの位置にでます。そこ は、ターゲットから少し離れた位置です。当てた快感が物⾜りません。 そこで、爆発をターゲットに密着させるため RaycastHit hitInfo を返す Physics.BoxCast を使 ⽤します。hitInfo.collider が当たったコライダです。コライダとミサイルの最近点に爆発を発⽣さ せます。 それらを修正したコードが CheckHit3 です。 リスト 5.5: CheckHit3() // 爆発が真ん中に来るように最近点を計算 void CheckHit3() { var t = transform; RaycastHit hitInfo; var direction = t.position - lastPosition; var length = direction.magnitude; var isHit = Physics.BoxCast( lastPosition, transform.localScale, direction, out hitInfo, t.rotation, length); Debug.DrawRay(lastPosition, direction, this.color, 1.0f); if (isHit) { var point = hitInfo.collider.ClosestPoint(lastPosition); Explosion(point); } } 当たった相⼿側のコライダの最近点に爆発を出すことでやってやった感が出ます。 71
  72. 第 5 章 Unity で板野サーカス -誘導ミサイルでクォータニオン⼊⾨- 5.4 板野サーカスを⽬指す 図 5.3:

    スクリーンショット ミサイルを誘導させる ターゲットの位置をずらす ミサイルがターゲットにあたらないようにします。ミサイルがまっすぐターゲットの横を⾶んで ⾏く様を⾒ていると、ぐいっと曲げて誘導させたい気持ちがふつふつと湧き上がってきます。 ミサイルをターゲットに即時に向かせる まずは補間せずにターゲットの⽅向に即時に反映させます。 リスト 5.6: CreateVector() // ターゲットと⾃分のポジションを引き算して⽅向ベクトルを作ります。 var v1 = targetPosition - myPosition; // その⽅向を向くクォータニオンを⽣成します。 var to = Quaternion.LookRotation(v1, Vector3.up); LookRotation はほとんどの場合はうまくいくのですが、特定の⽅向で安定しないのでやめまし た。特定の⽅向とは、v1 が Vector3.up に近付くときです。そこで、 FromToRotation を使⽤し ます。 リスト 5.8: FromToRotation() var to = Quaternion.FromToRotation(Vector3.forward, v1); 72
  73. 第 5 章 Unity で板野サーカス -誘導ミサイルでクォータニオン⼊⾨- 5.4 板野サーカスを⽬指す ミサイルをターゲットに徐々に向かせる 今のミサイルの回転を

    from, ⽬的の回転を to として、補間します。迷わず球⾯補間 (Quater- nion.Slerp) を使⽤します。線形補間 (Quaternion.Lerp) ではありません。球⾯補間のために Quaternion を使うと⾏っても過⾔ではありません。 リスト 5.8: FromToRotation() transform.rotation = Quaternion.Slerp(transform.rotation, to, 0.05f); これでミサイルの⽅向が 1 フレームに 5% ずつ補間されます。このときに Z 軸周りの回転も⼊り ます。ミサイルは問題ないのですが、キャラだと困ることがあります。 ⽅向の変化を割合ではなく⾓度で指定する 5% ずつ回転するということは、ミサイルの前⽅向とターゲットへの⽅向との差によって回転量が 変化するということです。1 フレームに 90 度ずれているときは 4.5 度回転し、30 度ずれているとき は 1.5 度回転します。これはバネで引っ張られるように回転します。そういう動きを⽬指すならそ れで良いのですが、今回は⼀定の誘導性能をもたせます。ミサイルの誘導性能を x 度/秒 で指定を します。つまり、 「Slerp に指定する割合 = 誘導性能 / ミサイルの前⽅とターゲット⽅向のなす⾓」 です。 ミサイルの前⽅とターゲット⽅向のなす⾓を求めるのにちょうど良い、 Quaternion.Angle(from, to); という関数がありました。コードにしたのが CalcRotateRatio1() です。 リスト 5.9: CalcRotateRatio1() // ⽬標⾓度と現在⾓度と回転能⼒から回転する割合を計算する。 static float CalcRotateRatio1(Quaternion from, Quaternion to, float degreeSpeed) { var r1 = degreeSpeed * Time.deltaTime; var r2 = Quaternion.Angle(from, to); var ratio = 0.0f; // ゼロ除算、微⼩な値での割り算を避ける if (r2 < Mathf.Epsilon || r2 <= r1) { ratio = 1.0f; } else { ratio = r1 / r2; } return ratio; } 73
  74. 第 5 章 Unity で板野サーカス -誘導ミサイルでクォータニオン⼊⾨- 5.4 板野サーカスを⽬指す これを作っているとき、Quaternion.Angle 関数でハマりました。まったく誘導できませんでし

    た。分かってしまえば原因は単純で、Quaternion.Angle の戻り値が Radian だと思いこんでいたの ですが、Degree でした。Unity の Quaternion.Angle のドキュメントには ‘2 つの回転 a と b 間の ⾓度を返します。‘ とあります。しかし、ドキュメントを英語に切り替えると "Returns the angle in degrees between two rotations a and b." と、はっきり degree と書いてあります。 当たり前かもしれませんが、⾓度の単位は 2 種類あります。 1. Degree 1 周が 360 度という⼩学校で習う馴染み深い単位 2. Radian 1 周が 2 πという⾼校の数学 2B で習う、Sin,Cos の引数に渡す単位 ミサイルっぽくするために加速させる 物理的な条件を真⾯⽬に考えてみます。 1. 発射のときの速度は 0 です。 2. 推⼒により加速します。 3. 速度に⽐例して空気抵抗が強くなります。 4. 空気抵抗により加速度が落ちます。 5. 空気抵抗と推⼒が釣り合うところまで加速したら速度が⼀定になります。 以上を真⾯⽬に計算する場合もありますが、制御しやすくするためと計算量を抑えるためにアル ゴリズムはできるだけ簡略化します。 今回は下記のように簡略化しました。 1. 発射のときの速度は 0 です。 2. 指定速度まで加速します。 3. 指定速度に達したら⼀定にします。 ミサイルが遅いときに回転すると不⾃然 加速機能を⼊れたことで新たな問題がでてきました。そこで CalcRotateRatio1 を修正します。 CalcRotateRatio1 の結果の回転速度に、その時の移動速度の最⼤速度に対する割合を乗算します。 これで移動開始時は回転せず、加速とともに誘導性能が上がります。 リスト 5.10: CalcRotateRatio2() static float CalcRotateRatio2(Quaternion from, Quaternion to, float degreeSpeed, float speed, float maxSpeed) { 74
  75. 第 5 章 Unity で板野サーカス -誘導ミサイルでクォータニオン⼊⾨- 5.4 板野サーカスを⽬指す var r1

    = degreeSpeed * Time.deltaTime; var r2 = Quaternion.Angle(from, to); var ratio = 0.0f; if (r2 < Mathf.Epsilon || r2 <= r1) { ratio = 1.0f; } else { ratio = r1 / r2; } var adjusted = ratio * (speed / maxSpeed); return adjusted; } 板野サーカスを⽬指していろいろ動かす この記事のメインである誘導ミサイルが動きました。ここからはこの誘導ミサイルを⼤量に放ち、 敵も味⽅も動き回り、カメラがそれらを画⾯に納めるように動かします。 設計の変更 ここまでターゲットとランチャーを別々に使っていましたが、敵も味⽅も撃ちまくるのでプレイ ヤーとして統合しました。 ミサイルが狙うターゲットはいろいろ存在できるように、ITarget としてインタフェースに切りだ しました。このインタフェースを実装すれば誘導ミサイルが狙えます。 リスト 5.11: ITarget public interface ITarget { Vector3 Position { get; } Vector3 Velocity { get; } } Velocity は今回使⽤していません。将来ミサイルを賢くするときに先読みさるために使います。 ミサイルの⼤量発射 ミサイルの発射に緩急が欲しくなったので、 「連射する状態」 「待機状態」を繰り返します。 75
  76. 第 5 章 Unity で板野サーカス -誘導ミサイルでクォータニオン⼊⾨- 5.4 板野サーカスを⽬指す カメラの移動 プレイヤーから少し遅れてついていくように移動させます。位置関係をみて加速や減速の制御は

    奥深いのですが、 Vector3.SmoothDamp を使えばだいたいよい感じに動きます。 リスト 5.12: CameraContoller.Update() void Update() { var t = transform; // ターゲットの位置を少し遅れてついていくように注視点を動かす。 { var now = targetPosition; var to = this.target.position; targetPosition = Vector3.SmoothDamp( now, to, ref this.targetVelocity, this.targetPower); } // プレイヤーの位置とターゲットの位置からカメラの位置を決める { var now = t.position; var target_p = targetPosition; var player_p = this.player.position; var to_target = target_p - player_p; var q1 = Quaternion.LookRotation(to_target, Vector3.up); var to_p = player_p + q1 * positionOffset; t.position = to_p; } // 位置を決めてからターゲットを向かせる t.LookAt(targetPosition); } 最後に 今回作成したデモは、板野サーカスっぽい雰囲気は出せたと⾃負しています。昔から何回か挑戦 しては挫折していたことが、簡単にできてしまって驚きました。Unity が複雑な計算を実装してく れているので、活⽤⽅法さえ理解さえすれば、難しい数学の知識がなくてもよい感じに作れます。 昔ベーマガというプログラム投稿雑誌に、 「なんかわからないけど Sin を使うと曲線が書けるんだ よね」って書いてありました。同様に「クォータニオンもよくわからないけどよい感じで回転がで きるよね」と思ってもらえれば、この記事の⽬的は達成されます。 現状の誘導ミサイルは単純です。必要であればもっとリアリティを⾼められます。⾓度から速度 を直接計算するのではなく、重⼒や空気抵抗や推⼒から加速度を計算して速度を間接的に計算しま 76
  77. 第 6 章 ⾮同期処理の速度問題と解決の試案 本章では、Ruby プログラムの⾮同期処理の速度問題とその解決案を紹介します。 6.1 問題の内容 ゲームの開発現場でよくありがちな課題の⼀つに⾮同期処理の速度改善があります。たとえば、 同時接続ユーザー数と処理対象によっては⾮同期処理のキューが詰まるというようなケースです。

    6.2 解決試案 アプリケーション側の対処⽅法として以下の案を考えました。 • Resque を Sidekiq に変更する • Sidekiq クライアントは Ruby 版の Sidekiq を使⽤し、Sidekiq サーバは Crystal 版を使⽤ する 理由として第⼀に Crystal の強⼒な処理性能が挙げられます。プログラムの仕様によってパ フォーマンスに幅がありますが、最⼤で 20 倍もの⾼速化を実現することも可能です。それにより キューの処理を⾼速化が期待できるからです。 そして Crystal ⽤の Sidekiq や O/R マッパなどの使いやすいライブラリが豊富であることも理由 として挙げられます。 6.3 検証環境 実際に検証するにあたって以下の環境を構築します。 • Ruby on Rails 5 • Sidekiq(Ruby) • Sidekiq(Crystal) • MySQL 5.7 79
  78. 第 6 章 ⾮同期処理の速度問題と解決の試案 6.4 検証 また本稿での Ruby、および Crystal のバージョンは以下の通りです。

    • Ruby 2.5 • Crystal 0.26.0 6.4 検証 サンプルのコードとして、ゲームアプリのミッションとその達成状況の判定を⾮同期で⾏うもの を作成します。実際のコードはもっと条件が複雑ですが、今回はベンチマークが⽬的なので簡略化 しています。 Rails アプリケーションの作成 以下のテーブル構成で Rails のアプリケーションを作成します。 テーブル名:users ユーザー ID 以外にも、ニックネームの情報がありますが今回は ID とランクのみ残して除外し ます。 カラム名 型 説明 id serial ID rank integer ユーザーランク テーブル名:characters キャラクタの情報です。 カラム名 型 説明 id serial ID name text キャラクタ名 element integer 属性(5 ⾊の⾊) rarity integer レアリティ テーブル名:user_characters ユーザー保有のキャラクタの情報です。 カラム名 型 説明 id serial ID user_id integer ユーザー ID character_id ingeter キャラクタ ID level integer キャラクタレベル luck integer 運 80
  79. 第 6 章 ⾮同期処理の速度問題と解決の試案 6.4 検証 テーブル名:stages クエストのステージ情報です。 カラム名 型

    説明 id serial ID difficulty integer ステージ難易度 (1~6) テーブル名:user_stages ユーザーがクリアしたステージの情報です。 カラム名 型 説明 id serial ID user_id serial ユーザー ID stage_id serial ステージ ID cleared_at timestamp クリアした時間 テーブル名:user_achievements ミッションの達成状況です。 カラム名 型 説明 id serial ID user_id serial ユーザー ID achievement_id serial ミッション ID 達成すべきミッションとして以下を想定します。 • character_id 10 を運極達成 (Luck が 99) • character_id 20 をレベル最⼤にする • stage_id 4 のクエストをクリアしている • stage_id 7 のクエストをクリアしている • stage_id 9 のクエストをクリアしている • 超絶ステージ以上の難易度を 5 回以上クリアしている (stages の difficulty が 5 以上) 各々のミッション達成判定の発⽕条件は、キャラクタ関連ならキャラクタ合成時、クエストの達 成はクエストクリア時に⾏われますが、今回は⼀律で⾏います。 Rails アプリケーションの作成 以下⼿順で検証⽤のアプリケーションを作成します。 rails new td5demo -d mysql --skip-bundle bundle install --path vendor/bundle 81
  80. 第 6 章 ⾮同期処理の速度問題と解決の試案 6.4 検証 各種テーブルを作成していきます。 bundle exec rails

    g migration CreateUser rank:integer bundle exec rails g migration CreateCharacter name:text element:integer rarity:integer bundle exec rails g migration CreateUserCharacter user:references character:references \ level:integer luck:integer bundle exec rails g migration CreateStage difficulty:integer bundle exec rails g migration CreateUserStage user:references stage:references \ cleared_at:timestamp bundle exec rails g migration CreateUserAchievement user:references \ achievement_id:integer 作成後に実⾏します。 bundle exec rails db:create db:migrate Sidekiq を追加します。またバルクインポート⽤のツールも追加します。 gem ’sidekiq’ gem ’activerecord-import’ インストールします。 bundle install コンフィグを以下設定します。 config/initializers/sidekiq.rb Sidekiq.configure_server do |config| config.redis = { url: ’redis://127.0.0.1:6379’ } end Sidekiq.configure_client do |config| config.redis = { url: ’redis://127.0.0.1:6379’ } end ワーカのひな型を作成します。 82
  81. 第 6 章 ⾮同期処理の速度問題と解決の試案 6.4 検証 bundle exec rails g

    sidekiq:worker Reviewer また⾃動ロードするパスの⼀覧にワーカを加えます。 config/application.rb config.autoload_paths += %W(#{config.root}/app/workers) 検証⽤のデータを投⼊します。検証のデータは以下の通りです。 • ユーザー数 : 1000 • キャラクタ数 : 200 • ユーザーが持つキャラクタ : 200 • ステージ : 10 • ユーザーがクリアしたステージ : 10 db/seeds.rb ActiveRecord::Base.transaction do # user 1000 ⼈分作成 (1..1000).each do User.create!(rank: [*90..99].sample) end # character200 種作成 (1..200).each do Character.create!(name: %w(char1 char2 char3 char4).sample, \ element: [*1..5].sample, rarity: [*1..5].sample) end # stage10 種 (1..10).each do Stage.create!(difficulty: [*4..6].sample) end user_ids = User.all.map { |user| user.id }.sort character_ids = Character.all.map { |char| char.id }.sort stage_ids = Stage.all.map { |stage| stage.id }.sort User.all.each do |user| # キャラクタ 200 種持つ (1..200).each do user.user_characters.create!(character_id: character_ids.sample, \ level: [*95..99].sample, luck: [*95..99].sample) end # ステージ 10 種クリアしている (1..10).each do 83
  82. 第 6 章 ⾮同期処理の速度問題と解決の試案 6.4 検証 user.user_stages.create!(stage_id: stage_ids.sample, cleared_at: Time.current)

    end end end 達成判定を⾏います。 app/workers/reviewer_worker.rb class ReviewerWorker include Sidekiq::Worker def perform(id) id = id.to_i user = User.includes([user_characters: :character, user_stages: :stage]).find(id) logger.info "user_id: #{user.id} review start at: #{Time.current}" review(user) logger.info "user_id: #{user.id} review finish at: #{Time.current}" end def review(user) # キャラクタの判定 user_achievements = [] user.user_characters.each do |user_character| # character_id 10 を運極達成 (Luck が 99) if user_character.character.id == 10 && user_character.luck == 99 user_achievements << UserAchievement.new(user_id: user.id, achievement_id: 1) end # character_id 20 をレベル最⼤にする if user_character.character.id == 20 && user_character.level == 99 user_achievements << UserAchievement.new(user_id: user.id, achievement_id: 2) end end # ステージのクリア判定 clear_count = 0 user.user_stages.each do |user_stage| # stage_id 4 のクエストをクリアしている if user_stage.stage.id == 4 user_achievements << UserAchievement.new(user_id: user.id, achievement_id: 3) end # stage_id 7 のクエストをクリアしている if user_stage.stage.id == 7 user_achievements << UserAchievement.new(user_id: user.id, achievement_id: 4) end # stage_id 9 のクエストをクリアしている if user_stage.stage.id == 9 user_achievements << UserAchievement.new(user_id: user.id, achievement_id: 5) end # 超絶ステージ以上の難易度を 5 回以上クリアしている (stages の difficulty が 5 以上) if user_stage.stage.difficulty >= 5 clear_count += 1 84
  83. 第 6 章 ⾮同期処理の速度問題と解決の試案 6.4 検証 end end if clear_count

    >= 5 user_achievements << UserAchievement.new(user_id: user.id, achievement_id: 6) end UserAchievement.import(user_achievements) end end Sidekiq(Ruby) でのパフォーマンス コンソールを⽴ち上げ以下のコードを実⾏します。 ids = User.all.map {|user| user.id} ; ids.each { |id| ReviewerWorker.perform_async(id) } Ruby の Sidekiq を起動します。コンカレンシーを 25 として設定します。 bundle exec sidekiq -c 25 -L log/sidekiq.log ログの⼀番最初と最後は以下の通りです。 2018-08-19T06:29:17.564Z 93963 TID-oum8sv0of ReviewerWorker \ JID-f31af8215084975af303aeb3 INFO: user_id: 22 review \ start at: 2018-08-19 06:29:17 UTC (中略) 2018-08-19T06:30:03.221Z 93963 TID-oum8kqpff ReviewerWorker \ JID-35f78a22ed0a47c7fd29a568 INFO: user_id: 993 review \ finish at: 2018-08-19 06:30:03 UTC 差分を取ると、45 秒程です。 Crystal による Sidekiq サーバの構築 Crystal による Sidekiq サーバを構築します。 85
  84. 第 6 章 ⾮同期処理の速度問題と解決の試案 6.4 検証 crystal init app td5democr

    shard.yml に以下の内容を追記します。 shard.yml dependencies: sidekiq: github: mperham/sidekiq.cr version: 0.7.0 granite: github: amberframework/granite version: 0.13.0 mysql: github: crystal-lang/crystal-mysql 記載後に以下実⾏します。 shards install AmberFramework とその OR/M Crystal の O/R マッパは Granite を使⽤します。AmberFramework プロジェクトの成果物とし て⽇々更新されています。 AmberFramework*1とは Rails ライクな Crystal の Web フレームワークです。現時点で最新の バージョンは 0.9.0(2018 年 8 ⽉ 31 ⽇時点)となっています。 AmberFramework で主に使⽤可能な OR/M は以下の 3 種類があります。 • Granite – https://docs.amberframework.org/granite/ • Crecto – https://www.crecto.com • Jennifer – https://github.com/imdrasil/jennifer.cr Granite は Amber のプロジェクトの派⽣物です。Crecto は Elixir の Web フレームワーク 「Phoenix」で使⽤される OR/M「Ecto」に影響されて作られた OR/M です。 *1 https://amberframework.org 86
  85. 第 6 章 ⾮同期処理の速度問題と解決の試案 6.4 検証 Jennifer はあまり詳しく⾒ていませんが、Migration で DSL

    を使⽤できたりなど、Granite より やや Rails 寄りになっているように思えます。 本稿では公式プロジェクト派⽣の Granite を使⽤していきます。 モデル層の作成 モデル層のファイルを以下⼿動で作成します。 src/td5democr/models/character.cr class Character < Granite::Base adapter mysql has_many :user_characters field name : String field element : Int32 field rarity : Int32 end src/td5democr/models/stage.cr class Stage < Granite::Base adapter mysql has_many :user_stages field difficulty : Int32 end src/td5democr/models/user.cr class User < Granite::Base adapter mysql has_many :user_characters has_many :user_stages has_many :user_achievements field rank : Int32 timestamps end src/td5democr/models/user_achievement.cr class UserAchievement < Granite::Base adapter mysql belongs_to :user field achievement_id : Int32 timestamps end 87
  86. 第 6 章 ⾮同期処理の速度問題と解決の試案 6.4 検証 src/td5democr/models/user_character.cr class UserCharacter <

    Granite::Base adapter mysql belongs_to :user belongs_to :character field level : Int32 field luck : Int32 timestamps end src/td5democr/models/user_stage.cr class UserStage < Granite::Base adapter mysql belongs_to :user belongs_to :stage field cleared_at : Time timestamps end Crystal ⽤の Sidekiq サーバを作成します。 src/td5democr.cr require "sidekiq/cli" require "sidekiq/api" require "granite/adapter/mysql" Granite::Adapters << Granite::Adapter::Mysql.new({name: "mysql", url: "#{ENV[DB_URL]}"}) require "./td5democr/worker/*" require "./td5democr/models/*" class SomeClientMiddleware < Sidekiq::Middleware::ClientEntry def call(job, ctx) ctx.logger.info "Pushing job #{job.jid}" yield end end class SomeServerMiddleware < Sidekiq::Middleware::ServerEntry def call(job, ctx) ctx.logger.info "Executing job #{job.jid}" yield end end 88
  87. 第 6 章 ⾮同期処理の速度問題と解決の試案 6.4 検証 cli = Sidekiq::CLI.new server

    = cli.configure do |config| config.server_middleware.add SomeServerMiddleware.new config.client_middleware.add SomeClientMiddleware.new end cli.run(server) Ruby 同様に Sidekiq ワーカを作成します。感覚的に、ほぼ Ruby と同様であることがわかり ます。 src/td5democr/workers/reviewer_worker.cr class ReviewerWorker include Sidekiq::Worker def perform(id : Int32) user = User.find(id) if user logger.info "user_id: #{user.id} review start at: #{Time.now}" review(user) logger.info "user_id: #{user.id} review finish at: #{Time.now}" end end def review(user : User) user_characters = UserCharacter.all("where user_id = ?", ["#{user.id}"]) user_stages = UserStage.all("where user_id = ?", ["#{user.id}"]) characters = Character.all stages = Stage.all user_achievements = [] of UserAchievement # キャラクタの判定 user_characters.each do |user_character| character = characters.find {|c| c.id == user_character.character_id} if character # character_id 10 を運極達成 (luck が 99) if character.id == 10 && user_character.luck == 99 user_achievements << UserAchievement.new(user_id: user.id, achievement_id: 1) end # character_id 20 をレベル最⼤にする if character.id == 20 && user_character.level == 99 user_achievements << UserAchievement.new(user_id: user.id, achievement_id: 2) end end end # ステージのクリア判定 clear_count = 0 user_stages.each do |user_stage| stage = stages.find {|s| s.id == user_stage.stage_id} 89
  88. 第 6 章 ⾮同期処理の速度問題と解決の試案 6.4 検証 if stage # stage_id

    4 のクエストをクリアしている if stage.id == 4 user_achievements << UserAchievement.new(user_id: user.id, achievement_id: 3) end # stage_id 7 のクエストをクリアしている if stage.id == 7 user_achievements << UserAchievement.new(user_id: user.id, achievement_id: 4) end # stage_id 9 のクエストをクリアしている if stage.id == 9 user_achievements << UserAchievement.new(user_id: user.id, achievement_id: 5) end # 超絶ステージ以上の難易度を 5 回以上クリアしている (stages の difficulty が 5 以上) if difficulty = stage.difficulty if difficulty >= 5 clear_count += 1 end end end end if clear_count >= 5 user_achievements << UserAchievement.new(user_id: user.id, achievement_id: 6) end UserAchievement.import(user_achievements) end end Sidekiq(Crystal) のパフォーマンス 今度は Ruby の Sidekiq を停⽌した状態で Crystal の Sidekiq を実⾏し、以下のタスクを実⾏し ます。 crystal build src/td5democr.cr ./td5democr -c 25 >> sidekiq.log 2018-08-18T17:30:01.393Z 92025 TID-236ow3k JID=33ae9aedb6dda73fcab949b1 \ INFO: user_id: 1 review start at: 2018-08-19 02:30:01 +09:00 (中略) 2018-08-18T17:30:24.586Z 92025 TID-236oum8 JID=a80e4816f47b174fbc0da85f \ INFO: user_id: 998 review finish at: 2018-08-19 02:30:24 +09:00 90
  89. 第 6 章 ⾮同期処理の速度問題と解決の試案 6.4 検証 23 秒程で全件完了しました。Ruby に⽐べるとおよそ 2

    倍ほどの速度になります。 まとめ たしかに⾼速化が達成できていることは確認できました。Crystal 側では DB チューニング(コ ネクションプールなど)をほぼデフォルトのまま⾏ったのでこのあたりのチューニング次第ではさ らに性能が出せるかと思いますが、プログラムの実⾏速度差でいうと期待していたほど(最⼤の 20 倍)ではありませんでした。 また実⾏環境がローカルだったのでアプリケーションと DB との通信は UNIX ソケットで⾏われ ていましたが、ネットワーク環境ではさらに遅延が⽣じ、実⾏速度差はもっと縮まるのではないか と予測されます。 91
  90. 著者紹介 ⾓ ⿓徳 (1 章担当, GitHub: Blasthal, Twitter: @Blasthal) 魂をゲームに捧げているゲームエンジニア。モンストでは主にストライクショットや友情コ

    ンボなどの実装を担当している。エンジニアリングだけでなく、企画、イラスト、モデル、ア ニメーション、サウンドなどなど、ゲーム制作に関係すること全般に頭から⾶び込んでいく。 ⽬下の⽬標は、Blender でキャラを⾃作しゲームリソースとして完成させること。誰かが作 り⽅教えてくれたらなー。教えてくれればなー。   安⾥ ⼤志 (2 章担当) 良い QA チームを作ることをお仕事にしています。格ゲー部の部⻑もやってます。   井本 ⼤登 (3 章担当, GitHub: adarapata, Twitter: @adarapata) ⾮ゲームのサーバサイド、Android アプリを開発してたけど趣味が⾼じて最近 Unity のクラ イアントエンジニアになりました。設計とかチームビルディングとかガルパンが好きです。   松原 信忠 (4 章担当, GitHub: matsubara0507) 所属はモンストサーバチームで Ruby を書いてる。プログラミングが好きで、普段は推し⾔ 語の Haskell で遊んだり、新しいプログラミング⾔語を勉強したりしている。Haskell-jp や Elm-jp でちょこちょこ活動もしている。   ヤマヤタケシ (5 章担当, GitHub: toymany, Twitter: @toyman_jp) 早いものでゲーム業界に⼊って 17 年経ちました。近年はスマホ⽤のゲーム開発プロジェクト に参加しています。趣味でアナログカードゲームを作ったりゲームアプリを作ったりしてい ます。   内⽥ 将之 (6 章担当, GitHub: msky026, Twitter: @msky026) モンストのサーバとか⾒てる⼈。Crystal-JP の中の⼈でもある。最近は Unity に強い関⼼が ある。 93
  91. 付録 著者紹介   ⼟屋 雅 (表紙・デザイン全般, note:miyabt, Twitter:@miyabt_) 普段は web

    やアプリを作っているデザイナーです。UI や UX を考えるのがとっても好きで す。個⼈的な趣味で同⼈誌を作っているので今回デザインを担当させていただきました   杉⽥ 絵美 (プロジェクト推進・制作進⾏等の庶務, GitHub: esugita, Twitter: @semiemi7) 元エンジニアで、ミクシィでは、Perl を触ったり、アプリを触ったり、新規事業の PM をし たりして、今は、DevRel チームで各種イベントを企画・運営したり、技術的な知⾒のアウト プット活動をサポートしたりしています。   喜多 功次 (制作アシスト, Twitter: @kojikita) DevRel チームで勉強会やイベントの運営などをしています。元エンジニアで以前は web フ ロント側のサービス開発やアドテクなどを担当していました。JavaScript が好きで、最近は PWA に注⽬しています。 94
  92. XFLAG Tech Note 2018 年 10 ⽉ 8 ⽇ 初版第 1

    刷 発⾏ 著 者 株式会社ミクシィ XFLAG スタジオ 発⾏所 株式会社ミクシィ 印刷所 ⽇光企画   (C) XFLAG