https://www.youtube.com/watch?v=ZQ5_u8Lgvyk
1 comment | 1 point | by WazanovaNews 約4時間前 edited
Casey MuratoriのGameTech Conference 2004での講演。
コードを再利用したいと皆言うけれども、いざ実践となるとなかなかうまくいかない。
ことの背景の解説とその解決策の方向性について、キャラクターアニメーションパッケージの開発を通じてCaseyが学んだことをシェアしてくれています。10年経っても変わらず、またゲーム以外の開発においても、当てはまることが多いかと。
インテグレーションのオプション
あるコンポーネントをAPIを用意してゲームに組み込むインテグレーション作業と、その作業がどれだけゲームの完成に効果がある(ゲームプレイの完成という意味だけでなく、インテグレーション作業が進むと、「メモリがしっかり管理できるようになる。」「パフォーマンスが向上する。」など、技術的に完成度があがることを指す。)かどうかという相関関係を考えてみる。最初はどのように組み込むかという選択肢の幅は大きい。その中で自然と、作業量が少なく、かつ効果が大きいオプションで進める。しかしその後、インテグレーションの作業が後半になってくると、取りうる技術的な選択肢は少なくなり、かつ、その工数/難易度に比して享受できるメリットは少なくなる。つまり、目の前の問題を解決するのに、大きな苦労を要するようになる。ゲームの開発が進むにつれて、インテグレーション作業はマイルストーンごとにしっかり右肩上がりに完成に近づいても、その作業がゲームにもたらす効果は、かなり緩い右肩上がりでしか上がってこないというギャップがある。最後に近づくと、障害の回避策を考えたり、時にはやり直しも伴うので、徒労感が増す。
よって、コンポーネントを組み込むためのAPIの設計においては、このギャップを減らすことがゴールになる。APIを利用する開発者が、そのAPIを利用して次にできることを実現するためにやらなくてはいけないと思うことだけを提供できるかどうかがポイントになる。利用者にとっては、手前で手間を減らせたことよりも、リリース直前で相当な苦労させられたことが記憶に残るものだし、APIを設計する自分自身も最後に壊滅的な状況に陥りたくはないはず。
概念図(ビデオ 9:10-14:50)
API設計の考慮ポイント
1) 粒度
- 一つのまとまったAPIを用意するか、それとも複数に分けるか。例えば、オリエンテーションを変更する場合、一番シンプルなのは、更新後の情報を取得するだけのパターン。また、もしかしたらステップに分けて、まず現在のオリエンテーションを取得し、変更後のオリエンテーションをセット、そして変更を適用するというかたちにするかも。これは、どこかのステップを修正したいケースがある場合。それ以外にも、更新後の情報はそのまま取得したいが、他の操作、例えば実行のタイミングをコントロールしたいというパターンがあるかも。ゲーム開発においては、スレッドが走っていたり、フレームが終わるまで保持していたいなどという状況がありうる。粒度とは、柔軟性とシンプルさのトレードオフである。
2) 冗長性
- 例えば、パラメータの違いのみのような、複数の類似のAPIを用意するかどうか。同様に、オリエンテーション変更のケースで説明すると、例えば、そのフレームが3x3の決まったサイズになるか、クオータニオンをセットできるか、どちらでも選択できるようにするというパターン。また、オリエンテーションを変更するときに、いつも追加でセットしたいアクションとバンドルして、複数パターン用意するかどうかという、上記1) の粒度と絡むような意思決定もありうる。冗長性は、利便性と独立性のトレードオフになる。
3) 密結合
- APIを呼び出すと、必ず別のこともやらなくてはいけないという関係。オブジェクトを量産して整理できてないときによく起こる悪いパターン。典型的なのは物理シミュレーター。「追加したい操作があるので何がシミュレーションされるかコントロールしたいと思ったときに、APIがそれをやらせてくれないかもしれないので、思った通りになるように他の一連のアクションも同じタイミングで実行しよう。」としてしまう。また、あるステートに依存するAPIを利用した際、タイミングをセットしてしまうと、他のあらゆるアクションの実行順が問題になり、隠れた密結合ができてしまうケース。他にも、メモリのアロケーションと初期化のアクションを続けて実行するAPIを用意してしまい、それぞれ独立してコントロールできる自由を許さないかたちにしてしまうケース。特定のフォーマットしか許容しないAPIを用意しているが、システム内には様々なフォーマットのデータが存在してしまっているケースなどがありうる。密結合は、常にないにこしたことはない。
4) データのリテンション
- 自分がコントロールしたい値をセットしても、APIがその値を保持して利用する仕組みになってしまっているので、結局自分で好きなタイミングで変更をかけることができなくなるパターン。例えば、APIが呼ばれたら保持している値を返すかもしれないというコールバックのかたちになっているケース。リテンションは、タイムリーなデータの同期と自動化のトレードオフになる。
5) フローコントロール
- スタックトレースと同じだと考えてもらえばよい。誰が誰を呼び出すのか。ほとんどの場合は、ライブラリが常にスタックのトップにあり、ゲームが一番下にあるかたちになる。時には、コールバックが入ると、ライブラリとゲームの呼び出し順が混在するケースもありえる。フローコントロールにおいては、コールバックを考えないでゲーム側でコントロールできるのがシンプルなので理想型。
各パターンを説明するためのコードサンプル(ビデオ 17:05-26:40)
インテグレーション作業の進捗と期待されるAPI設計の関係
API設計を考慮するうえでのトレードオフは、インテグレーション作業のフェーズによって、常に一定のバランスになるわけではない。ゲームにコンポーネントを組み込む作業の初期段階では、自由度が欲しいので、粒度が荒く、データのリテンションが高いAPIが好まれるだろう。一方、終盤にさしかかると、ゲームの完成にむけてしっかりコントロールを強めたいので、粒度が細かく、データのリテンションが低いAPIが利用できるとよい。
実例
各パターンを説明するためのコードサンプル(ビデオ 30:00-42:40)
ゲームが提供するサービス
- コールバックを絡めてデータを読込むAPIを用意することがあるかもしれない。その場合、コールバックに起因するフローコントロールの問題だけでなく、まずデータを読込み、次に当該オブジェクトのためにデータを解釈するという二つの作業が密結合している。もし、これをユーザができると許容したとしても、少なくともいつ実行されるかはコントロールさせたくない。
- 疎結合なかたちを追求すると、まず必要とするデータのサイズを入手し、次にメモリを割当て、圧縮ファイルを解凍して、最後に必要なアクションを指示するという4段階にわけることもできる。完全にコントロールできる。
- 更にもっと突き詰めて、データを使う前に何らかの呼び出しをしなくてはいけない状況を無くしたいとすると、データの構造がわかっていれば、APIに頼らず、ファイルに対して必要なアクションを実行するだけにすることもできる。
- ポイントとしては、やりたいことを実現するには、複数の手段があるということ。コールバックのリスクを理解したうえで、最初の事例を採用することもあるだろうし、最後の事例に行きつくことも多いだろう。ただし、最初の事例だけしか手段がないというのはまずい。
パラメータの冗長性
- 例えば、データを変換する場合、変換方式の違うものを利用しなくてはいけなくなり、仕方なくデータを渡して戻したりという無駄に長いコードになってしまうケースが実際の現場では散見される。理想的には変換バージョン別にAPIを用意して、適切なものだけを選択するかたちに落ち着けたいはず。
粒度の変化
- リテインモードを利用して、ノードを更新してレンダリングするだけというシンプルで粒度の荒いAPIから、更新/レンダリングのノードをそれぞれ各ステップに細分化したものまで用意することは可能。しかし、それぞれのノードをどのレベルまで細分化するのがよいか、つまり、片方ではそこそこコントロールできる範疇を保持し、もう片方ではお任せするといった、粒度のレベルを実情にあわせて差をつけるという、気遣いの細かい設計をしているAPIは少ない。
リテンションのミスマッチ
- リテインモードのAPIは、想定しているアクションが常にそのまますぐ順次実行されるのであれば問題ないが、実際には、「ユーザがボタンを押したら、この動きがシミュレーションされる。」という条件がつくケースが多い。そうなると、そこからさらに場合わけされた細かい条件式付きのコードがゲーム中に溢れることになる。よって、データをリテインしないで、「ボタンが押されたら、そこから個別データを計算して、シミュレーションを実行。」というモードのAPIが必要になる。
最適なAPIの条件
粒度の階層が段階的に変化し、疎結合であり、最も粒度の細かい階層ではデータのリテンションがなく、常にAPIでなくゲームがフローをコントロールをできる。
まとめ
- まずはAPIを利用する状況を想定し、その箇所のコードを最初に書くこと。他人のつくったAPIを評価するときも、まず完璧なものだと仮定して、自分のユースケースのコードを書く。それにより、相手の課す制約を確認するのでなく、自分の理想とする要件をはっきり認識することができる。
- データをリテインするのが前提なAPIがあれば、リテインせずに使えるタイプも用意する。
- コールバックや継承を使うAPIは、そのいずれも利用しないタイプも用意する。
- 専用のデータタイプを必要とするAPIを提供しない。当該業界で一般的に使われているものには全て対応する。
- 自分の開発をするにあたって、アトミックでないと思われるAPI機能は、accessorを除いて、2-4個のAPIに分ける。
- データ構造がわかるようになっていてはいけない明確な理由がなければ、あらゆる手段(construction, access, I/O, etc.)を使って透明性を高めること。基本的には、開発者はデータの構造がどうなっているのかまったく気しないで作業を進められるようにするべきであるが、リリース前のいざというときは自分で深部を操作できる手段がないと困ることになる。
- コンポーネントのリソース管理(メモリ、ファイル、string, etc.)は気にしなくてよい仕組みにすること。
- コンポーネントのファイルフォーマットは気にしなくてよい仕組みにすること。
- runtimeが全てのソースを利用できる状態にしておくこと。
#api