34

概要

私は、iOS 向けのかなり単純な 2D タワー ディフェンス ゲームに取り組んでいます。

これまでは、Core Graphics だけをレンダリングの処理に使用してきました。アプリには(まだ)画像ファイルはまったくありません。比較的単純な描画を行っていると、いくつかの重大なパフォーマンスの問題が発生しました。OpenGL に移行する前に、これを修正する方法についてのアイデアを探しています。

ゲームのセットアップ

UIView大まかに言えば、ゲーム ボードを表すBoard クラスがあります。これは のサブクラスです。ゲーム内の他のすべてのオブジェクト (タワー、クリープ、武器、爆発など) も のサブクラスでありUIView、作成時にサブビューとしてボードに追加されます。

オブジェクト内のビュー プロパティからゲームの状態を完全に分離し、各オブジェクトの状態をメインのゲーム ループ (NSTimerゲームの速度設定に応じて 60 ~ 240 Hz で起動) で更新します。ビューを描画、更新、またはアニメーション化することなく、ゲームは完全にプレイ可能です。

CADisplayLinkネイティブ リフレッシュ レート (60 Hz) でタイマーを使用してビューの更新を処理します。これsetNeedsDisplayは、ゲーム ステートの変化に基づいてビュー プロパティを更新する必要があるボード オブジェクトを呼び出します。ボード上のすべてのオブジェクトをオーバーライドdrawRect:して、フレーム内に非常に単純な 2D シェイプをペイントします。たとえば、武器がアニメートされると、武器の新しい状態に基づいて再描画されます。

パフォーマンスの問題

ボード上に合計約 20 個のゲーム オブジェクトがある iPhone 5 でテストすると、フレーム レートは 60 FPS (目標フレーム レート) を大幅に下回り、通常は 10 ~ 20 FPS の範囲になります。画面上でのアクションが増えると、ここから下り坂になります。iPhone 4 では、事態はさらに悪化します。

インストゥルメントの使用 実際にゲームの状態を更新するために費やされているのは、CPU 時間の約 5% だけであることがわかりました。その大部分はレンダリングに費やされています。具体的には、CGContextDrawPath関数 (私の理解では、ベクター パスのラスタライズが行われる場所です) は、膨大な量の CPU 時間を消費しています。詳細については、下部にあるインストゥルメントのスクリーンショットを参照してください。

StackOverflow や他のサイトに関するいくつかの調査によると、Core Graphics は私が必要としているタスクに対応していないようです。どうやら、ベクター パスのストロークは非常にコストがかかるようです (特に、不透明ではなく、アルファ値が 1.0 未満のものを描画する場合)。私は OpenGL が私の問題を解決してくれるとほぼ確信していますが、それはかなり低レベルであり、それを使用しなければならないことにあまり興奮していません。

質問

Core Graphics からスムーズな 60 FPS を引き出すために検討すべき最適化はありますか?

いくつかのアイデア...

誰かが、CALayer各オブジェクトを個別に持つのではなく、すべてのオブジェクトを 1 つのオブジェクトに描画することを検討することを提案しましCALayerたが、Instruments が示している内容に基づいて、これが役立つとは確信していません。

個人的にCGAffineTransformsは、アニメーションを実行するために使用する理論があります(つまり、オブジェクトの形状をdrawRect:一度に描画し、その後のフレームでレイヤーを移動/回転/サイズ変更するために変換を行います)。 OpenGL。しかし、OpenGLを完全に使用するよりも簡単だとは思いません。

サンプルコード

私が行っている描画のレベルを感じてもらうためdrawRect:に、武器オブジェクトの 1 つ (タワーから発射される「ビーム」) の実装例を次に示します。

注: このビームは「再ターゲット」することができ、ボード全体を横切るため、簡単にするために、そのフレームはボードと同じ寸法になっています。ただし、ボード上の他のほとんどのオブジェクトのフレームは、可能な限り最小の外接長方形に設定されています。

- (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);

}

楽器の実行

ゲームをしばらく実行させた後のインストゥルメントの外観を次に示します。

このTDGreenBeamクラスには、drawRect:上記のサンプル コード セクションに示されている正確な実装があります。

フルサイズのスクリーンショット 最も重いスタック トレースが展開された、ゲームの計測器の実行。

4

3 に答える 3

20

Core Graphics の作業は CPU によって実行されます。その後、結果が GPU にプッシュされます。あなたが電話するとき、あなたsetNeedsDisplayは描画作業を新たに行う必要があることを示します.

オブジェクトの多くが一貫した形状を保持し、単に移動または回転すると仮定すると、単にsetNeedsLayout親ビューを呼び出してから、そのビューの最新のオブジェクト位置をlayoutSubviewsおそらくcenterプロパティに直接プッシュする必要があります。単に位置を調整するだけでは、再描画する必要はありません。コンポジターは単に GPU に、既に別の位置にあるグラフィックを再現するように要求します。

centerゲームのより一般的な解決策は、初期設定以外の場合はbounds無視することです。おそらくこれらのヘルパーの組み合わせを使用して作成された、frame必要なアフィン変換をプッシュするだけです。これにより、CPU の介入なしに、オブジェクトを任意に再配置、回転、スケーリングできるようになります。すべて GPU の作業になります。transform

さらに詳細な制御が必要な場合は、各ビューにCALayer独自のaffineTransformがありますがsublayerTransform、サブレイヤーの変換と結合する もあります。したがって、3D に非常に興味がある場合、最も簡単な方法は、適切なパースペクティブ マトリックスをスーパーレイヤーにロードし、適切sublayerTransformな 3D 変換をサブレイヤーまたはサブビューにプッシュすることです。

このアプローチには明らかな欠点が 1 つあります。一度描画してから拡大すると、ピクセルが見えるようになります。事前にレイヤーを調整contentsScaleしてそれを改善しようとすることはできますが、それ以外の場合は、GPU が合成を続行できるようにすることの自然な結果が表示されます。magnificationFilter線形フィルタリングと最近接フィルタリングを切り替えたい場合は、レイヤーにプロパティがあります。線形がデフォルトです。

于 2012-11-07T21:32:31.837 に答える
7

たぶん、あなたは過剰に引き出しています。つまり、冗長な情報を描画します。

したがって、ビュー階層をレイヤーに分割する必要があります(前述のとおり)。必要なものだけを更新/描画します。レイヤーは合成された中間体をキャッシュでき、GPU はそれらすべてのレイヤーをすばやく合成できます。ただし、描画する必要があるものだけを描画し、実際に変化するレイヤーの領域のみを無効にするように注意する必要があります。

デバッグ:「Quartz Debug」を開き、「Flash Identical Screen Updates」を有効にしてから、シミュレーターでアプリを実行します。これらの色付きのフラッシュを最小限に抑えたいと考えています。

オーバードローが修正されたら、セカンダリ スレッドで何をレンダリングできるか (例: CALayer.drawsAsynchronously)、または中間表現の合成 (例: キャッシング) または不変レイヤー/四角形のラスタライズにどのようにアプローチできるかを検討してください。これらの変更を行うときは、コスト (メモリーや CPU など) を慎重に測定してください。

于 2012-11-07T19:58:05.217 に答える
0

コードがすべてのフレームで「再描画する必要がある」場合は、毎回コードを呼び出して CALayer を再描画するのではなく、グラフィックス ハードウェアを使用して同じことを実装する方法を検討することをお勧めします。

たとえば、ラインの例では、一連のタイルで構成されるレンダリング ロジックを作成できます。塗りつぶされた中央部分は、CALayer としてキャッシュし、何度も描画して領域を特定の高さまで塗りつぶすソリッド タイルにすることができます。次に、レイのエッジである別の CALayer を用意します。ここで、レイのエッジから離れて透明にアルファ フェードします。この CALayer を上部と下部 (180 度の回転) でレンダリングして、上下に適切にブレンドされたエッジを持つ特定の高さのレイになるようにします。次に、このプロセスを繰り返して、光線を広げてから短くし、最終的に終了します。

その後、グラフィックス カード ハードウェア アクセラレーションを使用してより大きな形状をレンダリングできますが、呼び出しコードは、ループごとに実際に画像データを描画してから転送する必要はありません。各「タイル」はすでに CPU から GPU に転送されており、GPU でのアフィン変換は非常に高速です。基本的に、毎回レンダリングしたくないだけで、レンダリングされたすべてのイメージ メモリが GPU に転送されるのを待つ必要があります。

于 2013-03-24T03:20:25.777 に答える