私はiOS用のかなり単純な2Dタワー防衛ゲームに取り組んでいます。
これまで、レンダリングを処理するためだけにCoreGraphicsを使用してきました。アプリには(まだ)画像ファイルはありません。私は比較的単純な描画を行う際にいくつかの重大なパフォーマンスの問題を経験しており、OpenGLに移行する以外に、これを修正する方法についてのアイデアを探しています。
大まかに言うと、ゲームボードを表すUIView
のサブクラスであるBoardクラスがあります。ゲーム内の他のすべてのオブジェクト(タワー、クリープ、武器、爆発など)もUIView
のサブクラスであり、作成時にサブビューとしてボードに追加されます。
ゲームの状態をオブジェクト内のビュープロパティから完全に分離し、各オブジェクトの状態をメインのゲームループで更新します(ゲームの速度設定に応じて、60〜240HzでNSTimer
によって起動されます)。ゲームは、ビューを描画、更新、またはアニメーション化することなく、完全にプレイできます。
ネイティブリフレッシュレート(60 Hz)でCADisplayLink
タイマーを使用してビューの更新を処理します。このタイマーは、ゲームの状態の変化に基づいてビューのプロパティを更新する必要があるボードオブジェクトでsetNeedsDisplay
を呼び出します。 。ボード上のすべてのオブジェクトはdrawRect:
をオーバーライドして、フレーム内に非常に単純な2D形状をペイントします。したがって、たとえば武器がアニメートされると、武器の新しい状態に基づいてそれ自体が再描画されます。
ボード上に合計約2ダースのゲームオブジェクトがあるiPhone5でテストすると、フレームレートは60 FPS(目標フレームレート)を大幅に下回り、通常は10〜20FPSの範囲になります。画面上でより多くのアクションが発生すると、ここから下り坂になります。そしてiPhone4では、事態はさらに悪化します。
Instrumentsを使用して、実際にゲームの状態を更新するためにCPU時間の約5%しか費やされていないと判断しました。その大部分は、レンダリングに向けられています。具体的には、CGContextDrawPath
関数(私の理解では、ベクターパスのラスタライズが行われる場所です)は、膨大なCPU時間を費やしています。詳細については、下部にあるInstrumentsのスクリーンショットを参照してください。
StackOverflowや他のサイトに関するいくつかの調査から、CoreGraphicsは私が必要としていることを実行できないようです。どうやら、ベクターパスをなでるのは非常にコストがかかります(特に不透明ではなく、アルファ値が1.0未満のオブジェクトを描画する場合)。 OpenGLが私の問題を解決することはほぼ確実ですが、それはかなり低レベルであり、それを使用する必要があることにそれほど興奮していません-私がここで行っていることに必要ではないようです。
コアグラフィックスからスムーズな60 FPSを引き出すために検討すべき最適化はありますか?
誰かが、各オブジェクトを独自のCALayer
にするのではなく、すべてのオブジェクトを単一のCALayer
に描画することを検討することを提案しましたが、これがInstrumentsの表示内容に基づいて役立つとは確信していません。
個人的には、CGAffineTransforms
を使用してアニメーションを実行する(つまり、オブジェクトの形状をdrawRect:
で一度描画してから、変換を実行して後続のフレームでレイヤーを移動/回転/サイズ変更する)という理論があります。それらはOpenGLに直接基づいているので、私の問題を解決するでしょう。しかし、OpenGLを完全に使用するよりも、それを行う方が簡単だとは思いません。
私が行っている描画のレベルの感触を与えるために、これは私の武器オブジェクトの1つ(塔から発射される「ビーム」)のdrawRect:
実装の例です。
注:このビームは「リターゲット」でき、ボード全体を横切るため、簡単にするために、フレームはボードと同じ寸法になっています。ただし、ボード上の他のほとんどのオブジェクトでは、フレームが可能な限り最小の外接長方形に設定されています。
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
// Draw beam
CGContextSetStrokeColorWithColor(c, [UIColor greenColor].CGColor);
CGContextSetLineWidth(c, self.width);
CGContextMoveToPoint(c, self.Origin.x, self.Origin.y);
CGPoint vector = [TDBoard vectorFromPoint:self.Origin toPoint:self.destination];
double magnitude = sqrt(pow(self.board.frame.size.width, 2) + pow(self.board.frame.size.height, 2));
CGContextAddLineToPoint(c, self.Origin.x+magnitude*vector.x, self.Origin.y+magnitude*vector.y);
CGContextStrokePath(c);
}
ゲームをしばらく実行した後のInstrumentsの外観は次のとおりです。
TDGreenBeam
クラスには、上記のサンプルコードセクションに示されている正確なdrawRect:
実装があります。
コアグラフィックスの作業はCPUによって実行されます。その後、結果はGPUにプッシュされます。 setNeedsDisplay
を呼び出すと、描画作業を新たに行う必要があることを示します。
オブジェクトの多くが一貫した形状を保持し、単に移動または回転すると仮定すると、親ビューでsetNeedsLayout
を呼び出すだけで、そのビューのlayoutSubviews
内の最新のオブジェクト位置をおそらく直接にプッシュする必要があります。 center
プロパティ。位置を調整するだけでは、再描画する必要はありません。コンポジターは、GPUに、すでに別の位置にあるグラフィックを再現するように要求するだけです。
ゲームのより一般的な解決策は、初期設定以外でcenter
、bounds
、およびframe
を無視することです。必要なアフィン変換をtransform
にプッシュするだけです。おそらく、 これらのヘルパー の組み合わせを使用して作成されます。これにより、CPUの介入なしに、オブジェクトの位置を変更、回転、スケーリングすることができます。すべてGPUで動作します。
さらに詳細な制御が必要な場合は、各ビューに独自のCALayer
を持つaffineTransform
がありますが、サブレイヤーの変換と組み合わせるsublayerTransform
もあります。したがって、3Dに非常に興味がある場合、最も簡単な方法は、適切な遠近法行列をスーパーレイヤーにsublayerTransform
としてロードしてから、適切な3D変換をサブレイヤーまたはサブビューにプッシュすることです。
このアプローチには、一度描画してからスケールアップするとピクセルが表示されるという明らかな欠点が1つあります。レイヤーのcontentsScale
を事前に調整して改善を試みることができますが、それ以外の場合は、GPUが合成を続行できるようにすることの自然な結果を確認するだけです。線形フィルタリングと最も近いフィルタリングを切り替えたい場合は、レイヤーにmagnificationFilter
プロパティがあります。線形がデフォルトです。
たぶん、あなたは過剰に描いています。つまり、冗長な情報を描画します。
したがって、ビュー階層をレイヤーに分割する必要があります(前述のとおり)。必要なものだけを更新/描画します。レイヤーは合成された中間体をキャッシュでき、GPUはそれらすべてのレイヤーをすばやく合成できます。ただし、描画する必要があるものだけを描画し、実際に変更されるレイヤーの領域のみを無効にするように注意する必要があります。
Debug it:「QuartzDebug」を開き、「Flash Identical Screen Updates」を有効にしてから、シミュレーターでアプリを実行します。これらの色付きのフラッシュを最小限に抑えたいと考えています。
オーバードローが修正されたら、セカンダリスレッドで何をレンダリングできるかを検討します(例:CALayer.drawsAsynchronously
)、または中間表現の合成(キャッシュなど)または不変のレイヤー/四角形のラスター化にどのようにアプローチできるか。これらの変更を実行するときは、コスト(メモリやCPUなど)を慎重に測定してください。
コードをすべてのフレームで「再描画する必要がある」場合は、コードを呼び出してCALayerを毎回再描画するのではなく、グラフィックハードウェアを使用して同じことを実装する方法を検討することをお勧めします。
たとえば、ラインの例では、一連のタイルで構成されるレンダリングロジックを作成できます。塗りつぶされた中央部分は、CALayerとしてキャッシュし、何度も描画して領域を特定の高さまで塗りつぶすソリッドタイルにすることができます。次に、光線のエッジである別のCALayerを作成します。これは、光線のエッジから離れるにつれてアルファフェードして透明になります。このCALayerを上下(180度回転)でレンダリングして、上下に素敵なブレンドエッジを持つ特定の高さの光線が作成されるようにします。次に、このプロセスを繰り返して、光線を広くしてから短くして、最終的に終了します。
その後、グラフィックカードハードウェアアクセラレーションを使用してより大きな形状をレンダリングできますが、呼び出しコードは、すべてのループで画像データを実際に描画して転送する必要はありません。各「タイル」はすでにCPUからGPUに転送されており、GPUでのアフィン変換は非常に高速です。基本的に、毎回レンダリングする必要はなく、レンダリングされたすべての画像メモリがGPUに転送されるのを待つ必要があります。