彼らはそれをどうしましたか?テラリアの何百万ものタイル


45

私はTerrariaに似たゲームエンジンを主に挑戦として取り組んできましたが、そのほとんどを理解しましたが、何百万もの相互作用/収穫可能なタイルをどのように扱うかについて頭を包み込むように思えませんゲームは一度に持っています。私のエンジンでは、Terrariaで可能なことの1/20である約500.000のタイルを作成すると、フレームレートが60から約20に低下します。気を付けてください、私はタイルで何もしていません。メモリに保存するだけです。

更新:私が物事を行う方法を示すためにコードが追加されました。

これは、タイルを処理して描画するクラスの一部です。犯人は「foreach」の部分で、空のインデックスも含めてすべてを繰り返すと推測しています。

...
    public void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        foreach (Tile tile in this.Tiles)
        {
            if (tile != null)
            {
                if (tile.Position.X < -this.Offset.X + 32)
                    continue;
                if (tile.Position.X > -this.Offset.X + 1024 - 48)
                    continue;
                if (tile.Position.Y < -this.Offset.Y + 32)
                    continue;
                if (tile.Position.Y > -this.Offset.Y + 768 - 48)
                    continue;
                tile.Draw(spriteBatch, gameTime);
            }
        }
    }
...

また、Tile.Drawメソッドもあります。これは、各タイルがSpriteBatch.Drawメソッドの4つの呼び出しを使用するため、更新でも実行できます。これは私のオートタイルシステムの一部です。つまり、隣接するタイルに応じて各コーナーを描画します。texture_ *は長方形であり、各更新ではなく、レベルの作成時に一度設定されます。

...
    public virtual void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        if (this.type == TileType.TileSet)
        {
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position, texture_tl, this.BlendColor);
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position + new Vector2(8, 0), texture_tr, this.BlendColor);
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position + new Vector2(0, 8), texture_bl, this.BlendColor);
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position + new Vector2(8, 8), texture_br, this.BlendColor);
        }
    }
...

私のコードに対する批判や提案は大歓迎です。

更新:ソリューションが追加されました。

これが最後のLevel.Drawメソッドです。Level.TileAtメソッドは、入力された値をチェックするだけで、OutOfRange例外を回避します。

...
    public void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        Int32 startx = (Int32)Math.Floor((-this.Offset.X - 32) / 16);
        Int32 endx = (Int32)Math.Ceiling((-this.Offset.X + 1024 + 32) / 16);
        Int32 starty = (Int32)Math.Floor((-this.Offset.Y - 32) / 16);
        Int32 endy = (Int32)Math.Ceiling((-this.Offset.Y + 768 + 32) / 16);

        for (Int32 x = startx; x < endx; x += 1)
        {
            for (Int32 y = starty; y < endy; y += 1)
            {
                Tile tile = this.TileAt(x, y);
                if (tile != null)
                    tile.Draw(spriteBatch, gameTime);

            }
        }
    }
...

2
あなたはカメラのビューにあるものだけをレンダリングしていることを絶対に確信していますか?つまり、何を正しくレンダリングするかを決定するコードです?
クーパー

5
割り当てられたメモリのために、フレーマーレートは60 fpsから20 fpsに低下しますか?それはめったにありません、何か間違っているに違いありません。どのプラットフォームですか?システムは仮想メモリをディスクにスワップしていますか?
マイクセンダー

6
@Drackirこの場合、タイルクラスさえあれば、適切な長さの整数の配列で十分であり、50万個のオブジェクトがある場合、OOオーバーヘッドは冗談ではないというのは間違っていると思います。オブジェクトを扱うことは可能だと思いますが、ポイントは何でしょうか?整数配列の取り扱いは非常に簡単です。
aaaaaaaaaaaa

3
痛い!すべてのタイル繰り返し、表示されている各タイル Drawを4回呼び出します。間違いなくいくつかの改善が可能です
。...-thedaian

11
表示されているものだけをレンダリングするために、以下で説明する派手なパーティション分割のすべては必要ありません。それはタイルマップです。すでに通常のグリッドに分割されています。画面の左上と右下のタイルを計算し、その長方形にすべてを描画します。
ブレッキ

回答:


56

レンダリング中に500,000タイルすべてをループしていますか?もしそうなら、それはあなたの問題の一部を引き起こす可能性が高いです。レンダリング時に50万個のタイルをループし、「更新」ティックを実行するときに50万個のタイルをループすると、各フレームで100万個のタイルをループします。

明らかに、これを回避する方法があります。レンダリング中に更新ティックを実行できるため、これらすべてのタイルをループする時間を半分に節約できます。しかし、それはレンダリングコードと更新コードを1つの関数に結び付け、一般的には悪い考えです。

画面上にあるタイルを追跡し、それらだけをループ(およびレンダリング)することができます。タイルのサイズや画面サイズなどに応じて、ループする必要があるタイルの量を簡単に削減でき、処理時間を大幅に節約できます。

最後に、そしておそらく最良の選択肢(ほとんどの大規模な世界のゲームがこれを行います)は、地形を地域に分割することです。ワールドを、たとえば512x512のタイルのチャンクに分割し、プレーヤーがリージョンに近づく、またはリージョンから遠くなるにつれて、リージョンをロード/アンロードします。また、これにより、あらゆる種類の「更新」ティックを実行するために遠くのタイルをループする必要がなくなります。

(明らかに、エンジンがタイルで更新ティックを実行しない場合は、これらに言及しているこの回答の部分を無視できます。)


5
地域の部分は面白いように思えますが、私が正しいことを思い出すと、それがMinecraftのやり方です 編集:もちろん、最後にリージョンがアクティブになってからの経過時間を適用してから、その期間に発生したすべての変更を実行します。それは実際に動作する可能性があります。
ウィリアムマリアガー

2
Minecraftは間違いなくリージョンを使用します(その後のUltimaゲームが使用しました。多くのBethesdaゲームが使用していると確信しています)。Terrariaが地形をどのように処理するかについてはあまりわかりませんが、「新しい」地域に経過時間を適用するか、マップ全体をメモリに保存します。後者は高いRAM使用量を要求しますが、最新のコンピューターのほとんどはそれを処理できます。言及されていない他のオプションもあります。
-thedaian

2
@MindWorX:リロード時にすべての更新を実行することは常に機能するとは限りません。不毛のエリア全体があり、次に芝生があるとします。あなたは何年も離れて行ってから、草に向かって歩きます。芝生のあるブロックがロードされると、追いつき、そのブロック内で広がりますが、すでにロードされているより近いブロックには広がりません。このような物事が進まMUCHいずれかの更新サイクルにタイルの小さなサブセットのみを確認し、システムダウンロードすることなく、ライブ遠い地形を作ることができる-が、ゲームよりも遅いです。
ローレンペクテル

1
リージョンがプレーヤーに近づいたときに「追いつく」ことの代替手段(または補足手段)として、代わりに、離れた各リージョンを個別のシミュレーションで反復し、より遅い速度で更新することができます。このフレームごとに時間を確保するか、別のスレッドで実行することができます。そのため、たとえば、プレイヤーがいる地域に加えて、フレームごとに6つの隣接する地域を更新するだけでなく、1つまたは2つの余分な地域も更新します。
ルイスウェイクフォード

1
気になる人のために、Terrariaはタイルデータ全体を2D配列(最大サイズは8400x2400)に保存しています。そして、いくつかの簡単な計算を使用して、レンダリングするタイルのセクションを決定します。
FrenchyNZ 14年

4

私はここで答えのいずれかによって処理されない1つの大きな間違いを見ます。もちろん、必要以上に多くのタイルを描いたり繰り返したりしないでください。それほど明白ではないのは、実際にタイルを定義する方法です。あなたがタイルクラスを作ったのを見ることができるように、私もいつもそれをしていましたが、それは大きな間違いです。おそらく、そのクラスにはあらゆる種類の関数があり、多くの不必要な処理を作成します。

処理に本当に必要なものだけを反復処理する必要があります。そのため、実際にタイルに必要なものについて考えてください。'mを描画するには、テクスチャのみが必要ですが、実際の画像は処理が大きいため、繰り返したくないです。int [、]またはunsigned byte [、]を作成することもできます(255を超えるタイルテクスチャを期待しない場合)。必要なのは、これらの小さな配列を反復処理し、スイッチまたはifステートメントを使用してテクスチャを描画することだけです。

それでは、何を更新する必要がありますか?タイプ、健康、ダメージは十分なようです。これらはすべてバイト単位で保存できます。更新ループ用に次のような構造体を作成してください:

struct TileUpdate
{
public byte health;
public byte type;
public byte damage;
}

実際にタイプを使用してタイルを描画できます。したがって、構造体からその配列をデタッチ(配列を独自に作成)して、描画ループの不要なヘルスフィールドと損傷フィールドを反復処理しないようにすることができます。更新の目的では、ゲーム世界がより生き生きと感じられるように(画面の外でエンティティが変化するように)、画面よりも広い領域を考慮する必要があります。

上記の構造を保持する場合、タイルごとに3バイトしかかかりません。したがって、保存とメモリの目的にはこれが理想的です。処理速度については、intまたはbyteを使用するか、64ビットシステムを使用する場合はlong intを使用するかは実際には重要ではありません。


1
ええ、私のエンジンの以降の反復はそれを使用するように進化しました。主にメモリ消費を削減し、速度を向上させるため。ByRef型は、ByVal構造体で満たされた配列と比較して低速です。それにもかかわらず、これを答えに追加するのは良いことです。
ウィリアムマリアガー

1
いくつかのランダムなアルゴリズムを探している間に、うんざりしました。コードで独自のタイルクラスを使用している場所にすぐに気付きました。あなたの新しいとき、それは非常に一般的な間違いです。2年前にこの記事を投稿して以来、あなたがまだ活動しているのを見てうれしいです。
-Madmenyo

3
healthまたはは必要ありませんdamage。最も最近選んだタイルの場所とそれぞれの損傷の小さなバッファを保持できます。新しいタイルが選択され、バッファがいっぱいになった場合、新しいタイルを追加する前に、そこから最も古い場所をロールオフします。これは、一度にマイニングできるタイルの数を制限しますが、とにかくこれには本質的な制限があります(大体#players * pick_tile_size)。このリストをプレーヤーごとに保持しておくと、簡単になります。サイズ速度にとって重要です。サイズが小さいほど、各CPUキャッシュラインのタイルが多くなります。
ショーンミドルディッチ14年

@SeanMiddleditchあなたは正しいですが、それはポイントではありませんでした。ポイントは、停止して、実際に画面にタイルを描画して計算を行うために必要なものを考えることでした。OPはクラス全体を使用していたため、簡単な例を作成しました。学習の進行において、simpleは大いに役立ちます。さて、ピクセル密度に関する私のトピックのロックを解除してください。あなたは明らかにプログラマーであり、CGアーティストの目でその質問を見ようとしています。このサイトはゲームafaikのCGでもあります。
Madmenyo

2

さまざまなエンコード手法を使用できます。

RLE:したがって、座標(x、y)から始めて、軸の1つに沿って同じタイルがいくつ並んでいるか(長さ)をカウントします。例:(1,1,10,5)は、座標1,1から始まるタイルタイプ5のタイルが10個あることを意味します。

大規模配列(ビットマップ):配列の各要素は、その領域にあるタイルタイプを保持します。

編集:私はちょうどここでこの素晴らしい質問に出くわしました: マップ生成のためのランダムシード関数?

Perlinノイズジェネレーターは良い答えのように見えます。


エンコード(およびperlinノイズ)について話しているので、質問されている質問に答えているかどうかはよくわかりません。質問はパフォーマンスの問題を扱っているようです。
テダイアン

1
適切なエンコード(perlinノイズなど)を使用すると、それらのタイルをすべてメモリに一度に保持することを心配する必要はありません。代わりに、エンコーダー(ノイズジェネレーター)に特定の座標に何が表示されるかを尋ねます。ここでは、CPUサイクルとメモリ使用量のバランスが取れており、ノイズジェネレーターがそのバランスを保つのに役立ちます。
サムアックス

2
ここでの問題は、ユーザーが何らかの方法で地形を変更できるゲームを作成している場合、単にノイズジェネレーターを使用してその情報を「保存」することはできないということです。さらに、ほとんどのエンコード手法と同様に、ノイズ生成はかなり高価です。実際のゲームプレイ(物理、衝突、照明など)のためにCPUサイクルを節約する方が良いでしょう。Perlinノイズは地形生成に最適であり、エンコードはディスクへの保存に適していますが、この場合はメモリを処理するより良い方法があります。
-thedaian

しかし、非常に素晴らしいアイデアですが、テダイアンのように、地形はユーザーによって操作されるため、数学的に情報を取得することはできません。とにかくパーリンノイズを見て、ベーステレインを生成します。
ウィリアムマリアガー

1

おそらく既に提案されているように、タイルマップをパーティション分割する必要があります。たとえば、Quadtree構造を使用して、不要な(表示されない)タイルの潜在的な処理(単純なループ処理など)を取り除きます。この方法では、処理が必要な可能性のあるもののみを処理し、データセット(タイルマップ)のサイズを大きくしても、実際のパフォーマンスが低下することはありません。もちろん、ツリーのバランスが取れていると仮定します。

「古い」を繰り返すことでつまらない音や何かを鳴らしたくはありませんが、最適化するときは、常にツールチェーン/コンパイラでサポートされている最適化を使用することを忘れないでください。そして、はい、時期尚早な最適化はすべての悪の根源です。コンパイラを信頼してください。ほとんどの場合、あなたよりもよく知っていますが、常に、常に2回測定し、決して推測値に依存しないでください。実際のボトルネックがどこにあるかわからない限り、最速のアルゴリズムを高速に実装することではありません。そのため、プロファイラーを使用して、コードの最も遅い(ホット)パスを見つけ、それらを除去(または最適化)することに焦点を合わせる必要があります。ターゲットアーキテクチャの低レベルの知識は、多くの場合、ハードウェアが提供するすべてのものを絞り出すために不可欠であるため、これらのCPUキャッシュを調べて、分岐予測子とは何かを学習します。プロファイラーがキャッシュ/ブランチのヒット/ミスについて通知する内容を参照してください。また、何らかの形のツリーデータ構造を使用すると、インテリジェントデータ構造とダムアルゴリズムを使用する方が、他の方法よりも優れています。パフォーマンスに関しては、データが最初になります。:)


「早すぎる最適化はすべての悪の根源」の+1私はこれに罪がある:(
thedaian 21

16
-1クワッドツリー?ちょっとやりすぎ?このような2Dゲーム(テラリア用)ですべてのタイルが同じサイズである場合、どの範囲のタイルが画面上にあるかを非常に簡単に把握できます。右下のタイル。
BlueRaja-ダニーPflughoeft

1

ドローコールが多すぎるのではないでしょうか?すべてのマップタイルテクスチャを単一の画像(タイルアトラス)に配置すると、レンダリング中にテクスチャが切り替わりません。また、すべてのタイルを単一のメッシュにバッチ処理する場合、1回の描画呼び出しで描画する必要があります。

動的な調整について...おそらく、クワッドツリーはそれほど悪い考えではありません。タイルがリーフに配置され、非リーフノードがその子からのバッチメッシュであると仮定すると、ルートには1つのメッシュにバッチされたすべてのタイルが含まれている必要があります。1つのタイルを削除するには、ルートまでのノードの更新(メッシュの再バッチ処理)が必要です。しかし、すべてのツリーレベルで、4 * tree_heightメッシュがそれほど多くないはずのメッシュリバッチされたメッシュの1/4しかありませんか?

ああ、このツリーをクリッピングアルゴリズムで使用すると、常にルートノードではなく、その子の一部がレンダリングされるため、ルートまでのすべてのノードを更新する必要はありませんが、(リーフではない)ノードまで現時点でのレンダリング。

私の考えだけで、コードはなく、たぶんナンセンスです。


0

@到着は正しいです。問題は描画コードです。フレームごとに4 * 3000 + ドロークワッドコマンド(24000+ ドローポリゴンコマンド)の配列を構築しています。次に、これらのコマンドが処理され、GPUにパイプされます。これはかなり悪いです。

いくつかの解決策があります。

  • タイルの大きなブロック(画面サイズなど)を静的テクスチャにレンダリングし、1回の呼び出しで描画します。
  • シェーダーを使用して、フレームごとにジオメトリをGPUに送信せずにタイルを描画します(つまり、タイルマップをGPUに保存します)。

0

あなたがする必要があるのは、世界を地域に分割することです。Perlinノイズテレインの生成では共通のシードを使用できるため、世界が事前生成されていなくても、シードはノイズアルゴの一部を形成し、新しいテレインを既存のパーツにうまく継ぎ合わせます。この方法では、プレイヤーが一度に表示する前に小さなバッファー(現在の画面の周りのいくつかの画面)よりも多くを計算する必要はありません。

プレイヤーの現在の画面から遠く離れた場所で成長している植物などの処理に関しては、たとえばタイマーを使用できます。これらのタイマーは、植物やその位置などに関する情報を保存するファイルを繰り返し処理します。タイマー内のファイルを読み取り/更新/保存するだけです。プレイヤーが再び世界のそれらの部分に到達すると、エンジンは通常どおりファイルを読み込み、画面に新しいプラントデータを表示します。

収穫と農業のために、昨年作った同様のゲームでこのテクニックを使いました。プレイヤーはフィールドから遠く離れて歩くことができ、戻ったときにアイテムが更新されていました。


-2

膨大な数のブロックを処理する方法について考えてきましたが、頭に浮かぶのはフライウェイトデザインパターンだけです。わからない場合は、それについて読むことを強くお勧めします。メモリの節約と処理に関しては、多くの助けになるかもしれません:http : //en.wikipedia.org/wiki/Flyweight_pattern


3
-1この回答は一般的すぎるため有用ではなく、提供するリンクはこの特定の問題に直接対処しません。Flyweightパターンは、大きなデータセットを効率的に管理するための多くの方法の1つです。Terrariaのようなゲームにタイルを保存する特定のコンテキストでこのパターンをどのように適用するかについて詳しく説明していただけますか?
グッドグッドポスト
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.