多くの小さなコライダーを大きなものに結合する


13

私は何千ものグリッドの正方形で作られたタイルマップを使用してゲームを作成しています。現時点では、各正方形には衝突をチェックするための正方形コライダーがあります。

ここに画像の説明を入力してください

しかし、何千もの小さなブロックがある場合、それらすべての衝突をチェックするのは非効率的です。タイルマップが事前にこのように見えることを事前に知っていた場合、数千の小さなコライダーではなく、3つまたは4つの大きなコライダーを使用できたはずです。

ここに画像の説明を入力してください

多くの小さな隣接するタイルを最大限に大きなタイルに結合するための何らかの標準的なアルゴリズムはありますか?もしそうなら、誰かがそれをここで説明したり、そのようなアルゴリズムに関する文献を指すことができますか?

あるいは、この方法でタイルコライダーを前処理することは、完全に間違ったアプローチかもしれません。もしそうなら、非常に多くのコライダーの効率に対処するための正しいものは何ですか?


地形を破壊可能にする予定はありますか?
jgallant

@ジョン。私はこれを考えていませんでした。破壊可能性を許可すると、問題が大幅に難しくなると思います(小さなコライダーの1つが破壊される可能性があるため、大きな結合コライダーを再計算する必要がありますよね?)
Craig Innes

はい。これが私が尋ねていた理由です。通常、すべての地形をメッシュに結合します。地形を破壊可能にすることを計画している場合は、使用できる代替方法があります。これは、外側のブロックのみにコライダーを設定します。どのブロックが「エッジブロック」であるかを事前に計算し、それらのブロックにプール可能なコライダーを割り当てます。(jgallant.com/images/uranus/chunk.png- 画像は古くて完璧ではありませんが、テクニックを示しています)ゲームエンジン/プラットフォームに何を使用していますか?
-jgallant

@JonゲームエンジンとしてUnityを使用し、タイル衝突用のBoxCollider2Dコンポーネントを使用しています。この問題に対するより一般的な答えを得るには、ゲーム開発者のスタック交換にもっと役立つかもしれないと思ったので、特定のプラットフォームについては言及しませんでした。「エッジブロック」メソッドに関して、このメソッドのアルゴリズムの正確な詳細を含む回答を送信できますか?または、そのようなテクニックに関するリソースへのリンクがありますか?
クレイグイネス

1
私はこのためにUnityを実装していますが、実際にカットされて乾燥しているわけではないので、書き上げるのに時間がかかります。私は現在仕事中で、ソースコードは家にあります。あなたが答えを今夜まで待つことができるなら。次のようになります。jgallant.com
images

回答:


5

私はこのアルゴリズムをlove2dエンジン(lua言語)に役立てました。

https://love2d.org/wiki/TileMerging

-- map_width and map_height are the dimensions of the map
-- is_wall_f checks if a tile is a wall

local rectangles = {} -- Each rectangle covers a grid of wall tiles

for x = 0, map_width - 1 do
    local start_y
    local end_y

    for y = 0, map_height - 1 do
        if is_wall_f(x, y) then
            if not start_y then
                start_y = y
            end
            end_y = y
        elseif start_y then
            local overlaps = {}
            for _, r in ipairs(rectangles) do
                if (r.end_x == x - 1)
                  and (start_y <= r.start_y)
                  and (end_y >= r.end_y) then
                    table.insert(overlaps, r)
                end
            end
            table.sort(
                overlaps,
                function (a, b)
                    return a.start_y < b.start_y
                end
            )

            for _, r in ipairs(overlaps) do
                if start_y < r.start_y then
                    local new_rect = {
                        start_x = x,
                        start_y = start_y,
                        end_x = x,
                        end_y = r.start_y - 1
                    }
                    table.insert(rectangles, new_rect)
                    start_y = r.start_y
                end

                if start_y == r.start_y then
                    r.end_x = r.end_x + 1

                    if end_y == r.end_y then
                        start_y = nil
                        end_y = nil
                    elseif end_y > r.end_y then
                        start_y = r.end_y + 1
                    end
                end
            end

            if start_y then
                local new_rect = {
                    start_x = x,
                    start_y = start_y,
                    end_x = x,
                    end_y = end_y
                }
                table.insert(rectangles, new_rect)

                start_y = nil
                end_y = nil
            end
        end
    end

    if start_y then
        local new_rect = {
            start_x = x,
            start_y = start_y,
            end_x = x,
            end_y = end_y
        }
        table.insert(rectangles, new_rect)

        start_y = nil
        end_y = nil
    end
end
Here's how the rectangles would be used for physics.
-- Use contents of rectangles to create physics bodies
-- phys_world is the world, wall_rects is the list of...
-- wall rectangles

for _, r in ipairs(rectangles) do
    local start_x = r.start_x * TILE_SIZE
    local start_y = r.start_y * TILE_SIZE
    local width = (r.end_x - r.start_x + 1) * TILE_SIZE
    local height = (r.end_y - r.start_y + 1) * TILE_SIZE

    local x = start_x + (width / 2)
    local y = start_y + (height / 2)

    local body = love.physics.newBody(phys_world, x, y, 0, 0)
    local shape = love.physics.newRectangleShape(body, 0, 0,
      width, height)

    shape:setFriction(0)

    table.insert(wall_rects, {body = body, shape = shape})
end

現在のプロジェクトのlove2dの例を次に示します。赤で私の壁のコライダーを見ることができます。

ここに画像の説明を入力してください


C#バージョンはありますか?ドキュメンテーションコメント付きのバージョンはありますか?このアルゴリズムは3Dに適応できますか?
アーロンフランケ

3

破壊可能な地形を作成する場合、Unityでこれを行った方法は、ワールドのエッジブロックにのみコライダーを設定することです。したがって、たとえば、これはあなたが達成したいことです:

緑のブロックは、コライダーを含むタイルを示します

これらの緑色のブロックにはすべてコライダーが含まれていますが、他のブロックには含まれていません。これにより、計算が大幅に節約されます。ブロックを破壊すると、隣接するブロックのコライダーを非常に簡単に起動できます。コライダーの有効化/無効化にはコストがかかるため、慎重に行う必要があることに注意してください。

そのため、Tileリソースは次のようになります。

Unityのタイルリソース

これは標準のゲームオブジェクトですが、プール可能です。また、ボックスコライダーはデフォルトで無効に設定されていることに注意してください。エッジタイルの場合にのみアクティブにします。

ワールドを静的にロードしている場合、タイルをプールする必要はありません。それらをすべて1ショットでロードし、エッジからの距離を計算し、必要に応じてコライダーを適用できます。

動的にロードする場合は、タイルプールを使用することをお勧めします。更新ループの編集例を次に示します。現在のカメラビューに基づいてタイルを読み込みます。

public void Refresh(Rect view)
{       
    //Each Tile in the world uses 1 Unity Unit
    //Based on the passed in Rect, we calc the start and end X/Y values of the tiles presently on screen        
    int startx = view.x < 0 ? (int)(view.x + (-view.x % (1)) - 1) : (int)(view.x - (view.x % (1)));
    int starty = view.y < 0 ? (int)(view.y + (-view.y % (1)) - 1) : (int)(view.y - (view.y % (1)));

    int endx = startx + (int)(view.width);
    int endy = starty - (int)(view.height);

    int width = endx - startx;
    int height = starty - endy;

    //Create a disposable hashset to store the tiles that are currently in view
    HashSet<Tile> InCurrentView = new HashSet<Tile>();

    //Loop through all the visible tiles
    for (int i = startx; i <= endx; i += 1)
    {
        for (int j = starty; j >= endy; j -= 1)
        {
            int x = i - startx;
            int y = starty - j;

            if (j > 0 && j < Height)
            {
                //Get Tile (I wrap my world, that is why I have this mod here)
                Tile tile = Blocks[Helper.mod(i, Width), j];

                //Add tile to the current view
                InCurrentView.Add(tile);

                //Load tile if needed
                if (!tile.Blank)
                {
                    if (!LoadedTiles.Contains(tile))
                    {                           
                        if (TilePool.AvailableCount > 0)
                        {
                            //Grab a tile from the pool
                            Pool<PoolableGameObject>.Node node = TilePool.Get();

                            //Disable the collider if we are not at the edge
                            if (tile.EdgeDistance != 1)
                                node.Item.GO.GetComponent<BoxCollider2D>().enabled = false;

                            //Update tile rendering details
                            node.Item.Set(tile, new Vector2(i, j), DirtSprites[tile.TextureID], tile.Collidable, tile.Blank);
                            tile.PoolableGameObject = node;
                            node.Item.Refresh(tile);

                            //Tile is now loaded, add to LoadedTiles hashset
                            LoadedTiles.Add(tile);

                            //if Tile is edge block, then we enable the collider
                            if (tile.Collidable && tile.EdgeDistance == 1)
                                node.Item.GO.GetComponent<BoxCollider2D>().enabled = true;
                        }
                    }                       
                }                  
            }
        }
    }

    //Get a list of tiles that are no longer in the view
    HashSet<Tile> ToRemove = new HashSet<Tile>();
    foreach (Tile tile in LoadedTiles)
    {
        if (!InCurrentView.Contains(tile))
        {
            ToRemove.Add(tile);
        }
    }

    //Return these tiles to the Pool 
    //this would be the simplest form of cleanup -- Ideally you would do this based on the distance of the tile from the viewport
    foreach (Tile tile in ToRemove)
    {
        LoadedTiles.Remove(tile);
        tile.PoolableGameObject.Item.GO.GetComponent<BoxCollider2D>().enabled = false;
        tile.PoolableGameObject.Item.GO.transform.position = new Vector2(Int32.MinValue, Int32.MinValue);
        TilePool.Return(tile.PoolableGameObject);            
    }

    LastView = view;
}

理想的には、舞台裏でさらに多くのことが行われているので、もっと詳細な投稿を書くでしょう。ただし、これは役に立つかもしれません。質問がある場合は、お気軽にお尋ねください。


提示された元の質問により直接的に回答するdnkdroneの回答を受け入れました。しかし、この答えをupvotedていることは、効率的な代替に向けた貴重な方向を与えるよう
クレイグ・インズ

@CraigInnes問題はありません。手伝いたい。ポイントは関係ありません:)
jgallant
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.