Combinando molti piccoli collisori in quelli più grandi


13

Sto creando un gioco usando una mappa a tessere composta da molte migliaia di quadrati della griglia. Al momento, ogni quadrato ha un collider quadrato per il controllo delle collisioni.

inserisci qui la descrizione dell'immagine

Tuttavia, con molte migliaia di piccoli blocchi, controllarli tutti per verificare eventuali collisioni è inefficiente. Se avessi saputo che il tilemap sarebbe stato in questo modo in anticipo, avrei potuto usare solo 3 o 4 big collider anziché migliaia di piccoli:

inserisci qui la descrizione dell'immagine

Esiste una sorta di algoritmo standard per combinare molte piccole tessere adiacenti in quelle di massima dimensione? In tal caso, qualcuno potrebbe descriverlo qui o indicare la letteratura su tali algoritmi?

In alternativa, forse preelaborare i raccoglitori di piastrelle in questo modo è un approccio completamente sbagliato. In tal caso, qual è quello corretto per gestire l'efficienza di un numero estremamente elevato di collider?


Stai pensando di distruggere il terreno?
Jgallant,

@ Jon. Non avevo considerato questo. Immagino che consentire la distruttibilità renderebbe il problema significativamente più difficile (perché uno dei piccoli collider potrebbe essere distrutto, il che significa che i grandi collider combinati dovrebbero essere ricalcolati, giusto?)
Craig Innes

Sì. Questo è il motivo per cui stavo chiedendo. In genere, unisci tutto il terreno in una mesh. Se prevedi di consentire la distruzione del tuo terreno, puoi utilizzare un metodo alternativo che imposta i collider solo sui blocchi esterni. Dovresti pre-calcolare quali blocchi sono "blocchi di bordi" e quindi assegnare quei blocchi con un collider raggruppabile. ( jgallant.com/images/uranus/chunk.png - L'immagine è vecchia e non perfetta, ma dimostra la tecnica) Cosa stai usando per un motore / piattaforma di gioco?
Jgallant,

@Jon Sto usando Unity come motore di gioco, con i componenti BoxCollider2D per le collisioni di piastrelle. Non ho menzionato la mia piattaforma specifica poiché pensavo che potesse essere più utile allo scambio di stack di sviluppo del gioco per ottenere una risposta più generale a questo problema. Per quanto riguarda il tuo metodo "edge block", potresti inviare una risposta con dettagli precisi dell'algoritmo per questo metodo? O hai un link a risorse su tali tecniche?
Craig Innes,

1
Ho un'implementazione di Unity per questo, mi ci vorrà un po 'di tempo per scrivere un articolo, dato che non è davvero semplice. Al momento sono al lavoro e il codice sorgente è a casa. Se puoi aspettare fino a stanotte per una risposta. Ecco come appare: jgallant.com/images/landgen.gif
jgallant

Risposte:


5

Ho trovato utile questo algoritmo per il motore love2d ( linguaggio 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

Qui segui l'esempio di love2d sul mio progetto attuale. In rosso puoi vedere i miei collider da parete.

inserisci qui la descrizione dell'immagine


Esiste una versione C #? Esiste una versione con commenti sulla documentazione? Questo algoritmo può essere adattato per il 3D?
Aaron Franke,

3

Se stai cercando di creare un terreno distruttibile, il modo in cui l'ho fatto in Unity è quello di mettere i collider solo sui bordi del tuo mondo. Quindi, ad esempio, questo è ciò che vorresti realizzare:

I blocchi verdi indicano le tessere che contengono un collider

Tutti quei blocchi verdi contengono un collider e il resto no. Ciò consente di risparmiare un sacco di calcoli. Se distruggi un blocco, puoi attivare i collider su blocchi adiacenti abbastanza facilmente. Tieni presente che l'attivazione / disattivazione di un collider è costosa e deve essere eseguita con parsimonia.

Quindi, la risorsa Tile si presenta così:

Risorsa piastrella in unità

È un oggetto di gioco standard, ma è anche raggruppabile. Si noti inoltre che il box collider è disabilitato per impostazione predefinita. Attiveremmo solo se si tratta di una tessera bordo.

Se stai caricando staticamente il tuo mondo, non è necessario unire le tue tessere. Puoi semplicemente caricarli tutti in un colpo, calcolare la loro distanza dal bordo e applicare un collider, se necessario.

Se si carica dinamicamente, è meglio usare un pool di tessere. Ecco un esempio modificato del mio ciclo di aggiornamento. Carica le tessere in base alla vista corrente della videocamera:

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;
}

Idealmente, scriverei un post molto più dettagliato, dato che c'è molto altro dietro le quinte. Tuttavia, questo può aiutarti. Se ci sono domande, sentiti libero di chiedere o contattarmi.


Ha accettato la risposta di dnkdrone in quanto risponde più direttamente alla domanda originale posta. Tuttavia, ho votato a favore di questa risposta in quanto fornisce una direzione preziosa verso un'alternativa efficiente
Craig Innes,

@CraigInnes Nessun problema amico. Mi piace dare una mano. I punti non contano :)
jgallant,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.