Game Programming Patterns

https://gameprogrammingpatterns.com/contents.html

ゲームプログラミングでよく使われるデザインパターンを解説しているサイト。

Command

Flyweight

Observer

Prototype

Subclass Sandbox

  • 浅く広い階層にして継承する?
  • ほとんどのふるまいはsubclassに入る
  • オーバーライドしないと使えないことを明確にする

SuperPower

  それぞれの具体クラスで使うメソッド()
  サウンド()
  パーティクル()
  移動()
  座標取得()
  
SuperPower < SkyLaunch    
SkyLaunch
  superpowerのメソッドを用いる()

多くなって来た場合、さらに分割する。サウンド関係で分けるとか…。

SuperPower
  SoundPlayer()
  ...
SoundPlayer  
  sound()
  stop()
  setvolume()
  ...

みたいな。ベースクラスは、とにかくシンプルに保つ。

Component

再利用可能な部品に分割する。

Component: PhysicsComponent, GraphicsComponent

  • decorationはGraphicsComponentだけを持つGameObject。
  • zoneはPhysicsComponent, GraphicsComponentを持つGameObject(当たり判定があるので)。

このようにして分割。

GameObjectを継承して、各Componentのインスタンスを生成することで再利用が可能。

GameObjectはComponentを集めるだけで、実際にはほとんど何もしない。

Event Queue

その都度すべて実行していては遅くなるのでキューに入れて実行する方式にする。 音やダメージなど、イベントをキューに入れて、処理していく。たとえば音が同時にたくさんなった場合(敵を2体同時に倒したときの断末魔)、ある数に達するとキューに追加しないなど。これによって遅くなるのを防ぎ、ゲーム体験もよくする。

キューを作るには?環状キューが便利。headとtailを用意しておいて、実行が終わったらheadを前にすすめて前の中身を削除。maxの長さに達したらheadに戻るようにする。

動的割当とコピーがなく、シンプルでキャッシュの使いやすい方法である。

イベント(event,notification)、メッセージ(message,request)、は似たような意味で使われるが、微妙な概念の違いがある。

  • イベント→既におこったこと。(モンスターが死んだ)
  • メッセージ→これから起こること。(音を再生する)

リスナーによって、シングルキャストとブロードキャストがある。

ガベージコレクションがある言語であれば、使わないキューのアイテムを削除するのをあまり考えなくてよい。

Service Locator

いつでも使えるように便利にしたい。便利で柔軟になったsingletonパターンという感じ?

service、service provider、locater。

service providerに登録する。locatorで実装と呼び出しを対応づけする(電話帳みたいに)。 他で実装を知ることはない。高度な分離。

登録されてないやつが呼ばれたときは、Null Objectを使う。ないものを呼び出してもクラッシュすることなく安全にできる。 プログラマーでなくても動作を安全に変更できる。まさにフレームワークでよくあることだな。適当にコピペしてれば、認証機能をつけれたりする。プログラマーではないんですねw(悲)。

サービスを提供することがロケーターの仕事?

実装していないサービスはnullを返すようにしておけば、そのたびに止まらない。 これは大きなチームでの開発では役に立つことである。

Data Locality

箱と黒魔術の例。黒魔術を使った仕事は一瞬で終わるのだが、運ぶために仕事のほとんどの時間を使い、一日が終わってしまう。 運ぶ倉庫係はあまり頭がよくない。遅い。作業スペースは限られているので直しにいかないといけない。

コンピューターではこれと同じことが起こる。 つまりCPU処理は早いがデータを運ぶのに時間がかかる。早くするには、一気にまとめて運ぶとか、同時に運ぶとか工夫する必要がある。

キャッシュの話。

要するに、必要となるデータをまとめようという話。AI, render, physicsで使うエンティティはそれぞれまとめると、アクセスが効率的になる。隣り合っているので、あちこち行く必要がない。キャッシュに入る。

なんかよくわからないなぁ…。

一つにまとめたやつ(=particle?)を、ソートしたりとりあえず使わないヤツはフラグつけたりする。いちいちソートするのは非効率なので追加するときに交換する。

Dirty Flag

不必要な仕事を遅らせる。

  • scene graphは描画に必要なすべてのオブジェクトが入っている。位置、角度、…それで位置を求めて描画する。その変換が厄介である。
  • 位置には階層構造がある。海に船、人が描画されてるとするとその順番で下から描画されないといけない。
  • 変換はパフォーマンス的に重要なホットコード上にあり、よくない。解決策は、描画している最中に計算することだ?階層の最上位から辿って計算していく。
  • ほとんどのオブジェクトは動かないので、むだだ。選択的にやる。
  • 変換をキャッシュしておいて描画にそれを用いる。 → オブジェクトが動かない場合はキャッシュに上がることはなく、エコ。
  • 変更と更新の分離。影響を受ける変換を1度だけ再計算する。そのためにフラグをつける。更新が必要なもの(out of date=dirty)につける。したがってdirty flag.
  • いままでは下の方から再帰的に計算していたものが、最初に全部移動させて、それから再計算するようになった。
  • データ保存が関わるとき破滅的になる可能性がある。たとえばエディタは変更があったことを知っていて、未保存ということを知らせる。保存しなかった場合(遅延させまくったとき)は破滅的な結果になる可能性がある。だからエディタの場合は、たいてい自動で変更をどこかべつの場所に自動保存している。
  • 常に保存しないことはパフォーマンスを良好にするが一方で、結果を失う可能性のある欠点も生み出す。
  • ほかの最適化と同様、メモリとCPUの交換である。計算する必要がないがメモリに保存する必要がある。多くの場合メモリは多いので有意義。
  • いつdirty flagをリフレッシュするか?
  • ネットワークのやりとりをするソフトウェアでもこの考え方は使える(データ入出力が最も遅いから)。少しでも通信を減らすため。オンラインゲームとか。

Object Pool

オブジェクトを個別に割当てて解放する代わりに、プールするオブジェクトを再利用することでパフォーマンスとメモリ使用量を改善する。

  • メモリのフラグメンテーション(メモリがバラバラにあって、ある長さのオブジェクトが連続したメモリに入らないこと。パフォーマンス悪化の原因)を避けたい。ゲームでは致命的になるから。
  • さらにメモリの割当てが遅いので、プールでは最初に一気に割り当てて、あとはゲームが終わるまで保持しておく。
  • 使用中と、もう使わない状態のふたつがある。もう使わないになると解放する。
  • 使う時:
    • 頻繁に生成と削除するとき
    • オブジェクトのサイズが同じくらい
    • 割当てが遅い、フラグメンテーションを起こす
    • DBやネットワークなど接続にコストがかかるもの
  • 一般的に、新しく生成されるオブジェクトの不在よりすでにあるオブジェクトの削除のほうが気づかれにくい → プールの数がいっぱいのときは古いものを削除する。音楽にせよ、グラフィックにせよ。
  • ほかの賞でも全体的に実装のフェーズ(後半)になるとよくわからない
  • Flyweightに似ている。再利用可能なオブジェクトの配列を維持するから。Flyweightはオブジェクトの共有によって同時に利用できるようにする。それによってメモリの重複利用をなくす。いっぽうOPは長い期間での共有である。使用者が使い終わったものを再利用する。

Spatial Partition

オブジェクトを位置によって編成されたデータ構造に格納することで効率的に配置。

  • ゲームには現実世界と同様、空間の概念があることが多い。プレイヤーとの距離で音量を調整するサウンドエンジン、近くだけに制限されるチャット、弾丸の当たり判定…。
  • これら場所に関する問い合わせには反応の速さが求められる。が、ふつうにやると多くの計算が必要である…お互いが振り下ろす剣が相手と重ならないか、常に計算する必要がある。A→B B→Aで2重ループにしてそれぞれのオブジェクトの組み合わせを全部やる。オブジェクトが2つだけならまだいいが、増えるたびに計算が2乗増えていく。ボトルネックになる。
  • 位置によって編成するデータ構造に入れておくとすばやく検索できる?
  • 空間があるゲームなら一般的なパターン。
  • ほかの多くの最適化と同様、スピードとメモリのトレードオフ。
  • ユニットをグリッドに格納する。単純にリストに追加していくことで実装できる。
  • 攻撃のとき重なりを比較するという仕組み自体は変わっていないが、グリッドの中にあるユニットとの重なりだけを考えればよくなる。
  • グリッドを踏み越えるためのメソッドを追加する(プレイヤー,AIによって呼び出される)。元のグリッドから削除して、新しいグリッドに追加する。
  • フラットな構造であるグリッドに替えて、階層構造を持つquadtreeとBPSs。オブジェクトの存在によって再帰的に領域が分けられる。これは何もない空間ではメモリを割り当てなくてよくなる。フラットな構造だと何もなくてもメモリを使う。また、密度の高いところでも分割し少ないオブジェクトだけを考えるだけでよくなる。
  • 領域のオブジェクトの数が同じになるように分割する方法もある。オブジェクトの数が同じなのでかかる時間が同じになる。
  • 四分木は再帰的に正方形を4つに分割する。
  • 空間パーティションはルックアップを高速化するための2次キャッシュとみなすことができる。
article/dev_game_pattern.txt · 最終更新: 2020/07/22 17:05 (外部編集)