XNAでは、大きな2D世界地図の一部を動的に読み込むにはどうすればよいですか?


17

巨大な世界地図を作りたい。少なくとも8000×6000ピクセルのサイズ。私はそれを800×600ピクセルのPNG画像の10×10グリッドに分割しました。すべてをメモリにロードしないようにするには、グリッド内のプレーヤーの位置に応じて画像をロードおよびアンロードする必要があります。

たとえば、位置のプレーヤーは(3,3)次のとおりです。

(3,3)のプレーヤー

彼がに向かって右に移動すると、左端(4,3)の3つの画像の割り当てが解除され、右の3つの画像の割り当てが解除されます。

プレイヤーは(4,3)に移動します

おそらく、グリッドの各セル内に、ロードとアンロードをトリガーするしきい値があるはずです。ロードはおそらく別のスレッドで行われるはずです。

そのようなシステムをどのように設計しますか?


1
簡単な提案:ここでは、「ゲームでタイルを動的に読み込む方法」と「XNA for XBox 360でスレッド化を行う方法」という2つの質問があります。この投稿からOS固有のセグメントを削除します(タイル読み込みコードはXB360、Linux、およびNintendo Wiiで同一です)。XBox360でスレッド化するための新しい投稿を作成します。
ZorbaTHut

あなたの実際の問題についての質問に関しては、これはスムーズスクロールマップですか、それとも一連の個々の非スクロール画面ですか?
ZorbaTHut

「スムーズスクロールマップ」が理解できたら、そうです。質問を分離することに関しては、私はそれについて考えましたが、結果的に質問することをちょっとためらいました。しかし、私は今それをやります。
ペック

回答:


10

ここで解決すべき問題がいくつかあります。1つ目は、タイルをロードおよびアンロードする方法です。ContentManagerはデフォルトでは、特定のコンテンツをアンロードできません。ただし、これのカスタム実装は次のことを行います。

public class ExclusiveContentManager : ContentManager
{
    public ExclusiveContentManager(IServiceProvider serviceProvider,
        string RootDirectory) : base(serviceProvider, RootDirectory)
    {
    }

    public T LoadContentExclusive<T>(string AssetName)
    {
        return ReadAsset<T>(AssetName, null);
    }

    public void Unload(IDisposable ContentItem)
    {
        ContentItem.Dispose();
    }
}

2番目の問題は、どのタイルをロードおよびアンロードするかを決定する方法です。次の方法でこの問題を解決できます。

public class Tile
{
    public int X;
    public int Y;
    public Texture2D Texture;
}

int playerTileX;
int playerTileY;
int width;
int height;

ExclusiveContentManager contentEx;
Tile[,] tiles;

void updateLoadedTiles()
{
    List<Tile> loaded = new List<Tile>();

    // Determine which tiles need to be loaded
    for (int x = playerTileX - 1; x <= playerTileX + 1; x++)
        for (int y = playerTileY - 1; y <= playerTileY; y++)
        {
            if (x < 0 || x > width - 1) continue;
            if (y < 0 || y > height - 1) continue;

            loaded.Add(tiles[x, y]);
        }

    // Load and unload as necessary
    for (int x = 0; x < width; x++)
        for (int y = 0; y < height; y++)
        {
            if (loaded.Contains(tiles[x, y]))
            {
                if (tiles[x, y].Texture == null)
                    loadTile(x, y);
            }
            else
            {
                if (tiles[x, y].Texture != null)
                    contentEx.Unload(tiles[x, y].Texture);
            }
        }
}

void loadTile(int x, int y)
{
    Texture2D tex = contentEx.LoadEx<Texture2D>("tile_" + x + "_" + y);
    tiles[x, y].Texture = tex;
}

最後の問題は、いつロードおよびアンロードするかを決定する方法です。これはおそらく最も簡単な部分です。これは、プレーヤーの画面位置が決定されたら、Update()メソッドで簡単に実行できます。

int playerScreenX;
int playerScreenY;
int tileWidth;
int tileHeight;

protected override void Update(GameTime gameTime)
{
    // Code to update player position, etc...

    // Load/unload
    int newPlayerTileY = (int)((float)playerScreenX / (float)tileWidth);
    int newPlayerTileX = (int)((float)playerScreenY / (float)tileHeight);

    if (newPlayerTileX != playerTileX || newPlayerTileY != playerTileY)
        updateLoadedTiles();

    base.Update(gameTime);
}

もちろん、タイルも描画する必要がありますが、tileWidth、tileHeight、およびTiles []配列を指定すると、これは簡単なはずです。


ありがとうございました。非常にうまく言いました。あなたが知っているなら、私は提案をしたいと思います:タイルのロードがスレッドでどのように行われるかを説明できますか?3つの800x600タイルを一度にロードすると、ゲームが少し停止することを想像します。
ペック

Texture2Dオブジェクトを作成するとき、グラフィックカードが関与する必要があるため、私が知る限り、2番目のスレッドでロードが行われてもスキップが発生します。
ショーンジェームズ

0

あなたがしたいことはかなり一般的です。このテクニックやその他の一般的なテクニックに関する素晴らしいチュートリアルについては、こちらをご覧くださいこのタイルエンジンシリーズをご覧

以前にこのようなことをしたことがない場合は、シリーズを視聴することをお勧めします。ただし、必要に応じて、最後のチュートリアルコードを入手できます。後で行う場合は、drawメソッドを確認してください。

簡単に言うと、プレーヤーの周りの最小および最大X / Yポイントを見つける必要があります。それができたら、それぞれをループして、そのタイルを描画します。

public override void Draw(GameTime gameTime)
{
    Point min = currentMap.WorldPointToTileIndex(camera.Position);
    Point max = currentMap.WorldPointToTileIndex( camera.Position +
        new Vector2(
            ScreenManager.Viewport.Width + currentMap.TileWidth,
            ScreenManager.Viewport.Height + currentMap.TileHeight));


    min.X = (int)Math.Max(min.X, 0);
    min.Y = (int)Math.Max(min.Y, 0);
    max.X = (int)Math.Min(max.X, currentMap.Width);
    max.Y = (int)Math.Min(max.Y, currentMap.Height + 100);

    ScreenManager.SpriteBatch.Begin(SpriteBlendMode.AlphaBlend
                                    , SpriteSortMode.Immediate
                                    , SaveStateMode.None
                                    , camera.TransformMatrix);
    for (int x = min.X; x < max.X; x++)
    {
        for (int y = min.Y; y < max.Y; y++)
        {

            Tile tile = Tiles[x, y];
            if (tile == null)
                continue;

            Rectangle currentPos = new Rectangle(x * currentMap.TileWidth, y * currentMap.TileHeight, currentMap.TileWidth, currentMap.TileHeight);
            ScreenManager.SpriteBatch.Draw(tile.Texture, currentPos, tile.Source, Color.White);
        }
    }

    ScreenManager.SpriteBatch.End();
    base.Draw(gameTime);
}

public Point WorldPointToTileIndex(Vector2 worldPoint)
{
    worldPoint.X = MathHelper.Clamp(worldPoint.X, 0, Width * TileWidth);
    worldPoint.Y = MathHelper.Clamp(worldPoint.Y, 0, Height * TileHeight);


    Point p = new Point();

    // simple conversion to tile indices
    p.X = (int)Math.Floor(worldPoint.X / TileWidth);
    p.Y = (int)Math.Floor(worldPoint.Y / TileWidth);

    return p;
}

ご覧のとおり、多くのことが行われています。マトリックスを作成するカメラ、現在のピクセル位置をタイルインデックスに変換し、最小/最大ポイントを見つける必要があります)、それからあなたはそれを描くことができます。

チュートリアルシリーズをご覧になることをお勧めします。現在の問題、タイルエディターの作成方法、プレーヤーを境界内に収める方法、スプライトアニメーション、AIインタラクションなどについて説明します。

補足説明として、TiledLibにはこれが組み込まれています。コードも学習できます。


これは非常に役立ちますが、タイルマップのレンダリングのみをカバーします。関連するタイルのロードとアンロードはどうですか?100個の800x600タイルをメモリにロードできません。ビデオチュートリアルを見ていきます。ありがとう。
ペック

ああ、あなたの質問を誤解しました。それで、あなたは伝統的なタイルマップを使用していますか、それともあなたがあなたのレベルのためにあなたがバラバラにした大きな画像を持っていますか?

考えてみてください... タイルの名前を配列に保存し、同じテクニックを使用して何をロードして描画するかを知ることができます。オブジェクトを再利用しようとするか、大量のゴミを作成する可能性があることに留意してください

2番目:マップは8000x6000で、800x600ピクセルの10x10タイルに分割されます。再利用に関しては、私はすでにそれを担当するコンテンツマネージャーを持っています。
ペック

0

現在のプロジェクトで非常によく似たものに取り組んでいます。これは私がそれをしている方法の簡単な要約であり、あなた自身で物事を少し簡単にする方法に関するいくつかの副次的なメモがあります。

私にとって、最初の問題は、世界をその場でロードおよびアンロードするのに適した小さなチャンクに分割することでした。タイルベースのマップを使用しているため、その手順は非常に簡単になります。レベル内の各3Dオブジェクトの位置を考慮するのではなく、すでにレベルがタイルにうまく分割されています。これにより、ワールドをX×Yタイルチャンクに単純に分割してロードできます。

手作業ではなく、自動的にそれを行いたいと思うでしょう。XNAを使用しているため、レベルコンテンツのカスタムエクスポーターでコンテンツパイプラインを使用するオプションがあります。再コンパイルせずにエクスポートプロセスを実行する方法をご存知でない限り、正直なところ、これをお勧めします。C#はC ++のようにコンパイルが遅くなることはほとんどありませんが、マップに小さな変更を加えるたびにVisual Studioをロードしてゲームを再コンパイルする必要はありません。

ここで別の重要なことは、レベルのチャンクを含むファイルに適切な命名規則を使用することを確認することです。チャンクCをロードまたはアンロードすることを知り、実行時にロードするために必要なファイル名を生成する必要があります。最後に、あなたが道を進むのに役立つかもしれない小さなことを考えてください。チャンクの大きさを変更し、再エクスポートして、すぐにパフォーマンスへの影響を確認できるのは本当に素晴らしいことです。

実行時には、それはまだかなり簡単です。チャンクを非同期的にロードおよびアンロードする方法が必要になりますが、これはゲームまたはエンジンの動作に大きく依存します。2番目のイメージは正確です。どのチャンクをロードまたはアンロードする必要があるかを判断し、適切でない場合は、そのケースを作成する必要があります。一度にロードしたチャンクの数に応じて、プレーヤーがチャンクから次のチャンクに境界を越えるたびにこれを行うことができます。いずれにせよ、最悪の(合理的な)ロード時間であっても、プレーヤーがそれを見ることができる前にチャンクがロードされるのに十分な量をロードしたいのです。おそらく、パフォーマンスとメモリ消費のバランスが取れるようになるまで、この数値を何度も試してみたいと思うでしょう。

実際のアーキテクチャに関して言えば、何をロード/アンロードするかを決定するプロセスから、実際にメモリにデータをロードおよびアンロードするプロセスを抽象化する必要があります。最初のイテレーションでは、ロード/アンロードのパフォーマンスについても心配せず、動作する可能性のある最も単純なものを取得し、適切なタイミングで適切なリクエストを生成するようにします。その後、ごみを最小限に抑えるためのロード方法の最適化を検討できます。

使用していたエンジンのために、さらに複雑になりましたが、それは実装固有のものです。私がしたことについて質問がある場合は、コメントしてください。できる限りのことをしてお手伝いします。


1
msbuildを使用して、Visual Studioの外部でコンテンツパイプラインを実行できます。2番目のWinFormsサンプルでは、creators.xna.com
アンドリューラッセル

@Andrew !!!!! どうもありがとうございます!!!まさにその理由で、コンテンツパイプラインを完全に放棄しようとしていました!
ペック
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.