Combiner de nombreux petits collisionneurs en de plus grands

13

Je crée un jeu en utilisant une carte de tuiles composée de plusieurs milliers de carrés de grille. À l'heure actuelle, chaque carré a un collisionneur carré pour vérifier les collisions.

entrez la description de l'image ici

Cependant, avec plusieurs milliers de minuscules blocs, il est inefficace de les vérifier tous pour détecter les collisions. Si j'avais su que le tilemap allait ressembler à ceci à l'avance, j'aurais pu utiliser 3 ou 4 gros collisionneurs plutôt que des milliers de minuscules:

entrez la description de l'image ici

Existe-t-il une sorte d'algorithme standard pour combiner de nombreuses petites tuiles adjacentes en de grandes tailles maximales? Dans l'affirmative, quelqu'un pourrait-il le décrire ici, ou pointer vers la littérature sur de tels algorithmes?

Alternativement, peut-être que prétraiter les collisionneurs de tuiles de cette manière est complètement la mauvaise approche. Dans l'affirmative, quelle est la bonne solution pour gérer l'efficacité d'un très grand nombre de collisionneurs?

Craig Innes
la source
Envisagez-vous d'avoir le terrain destructible?
jgallant
@Jon. Je n'y avais pas pensé. J'imagine que permettre la destructibilité rendrait le problème beaucoup plus difficile (car l'un des petits collisionneurs pourrait être détruit, ce qui signifie que les grands collisionneurs combinés devraient être recalculés, non?)
Craig Innes
Oui. Voilà pourquoi je demandais. En règle générale, vous combinez tout votre terrain en un maillage. Si vous prévoyez de permettre à votre terrain d'être destructible, il existe une autre méthode que vous pouvez utiliser, qui définit les collisionneurs uniquement sur les blocs externes. Vous devez précalculer quels blocs sont des «blocs de bord», puis attribuer ces blocs à un collisionneur groupable. ( jgallant.com/images/uranus/chunk.png - L'image est ancienne et pas parfaite, mais illustre la technique) Qu'utilisez -vous pour un moteur / une plate-forme de jeu?
jgallant
@Jon J'utilise Unity comme moteur de jeu, avec des composants BoxCollider2D pour les collisions de carreaux. Je n'ai pas mentionné ma plate-forme spécifique car je pensais que cela pourrait être plus utile à l'échange de pile de développement de jeu pour obtenir une réponse plus générale à ce problème. Concernant votre méthode des «blocs de bord», pourriez-vous soumettre une réponse avec des détails précis sur l'algorithme de cette méthode? Ou avez-vous un lien vers des ressources sur ces techniques?
Craig Innes
1
J'ai une implémentation Unity pour cela, cela me prendra un certain temps pour faire une écriture, car elle n'est pas vraiment coupée et sèche. Je suis au travail en ce moment et le code source est chez moi. Si vous pouvez attendre jusqu'à ce soir pour une réponse. Voici à quoi cela ressemble: jgallant.com/images/landgen.gif
jgallant

Réponses:

5

J'ai trouvé cet algorithme utile pour le moteur love2d ( langue 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

Voici l'exemple de love2d sur mon projet actuel. En rouge, vous pouvez voir mes collisionneurs de murs.

entrez la description de l'image ici

dnk drone.vs.drones
la source
Existe-t-il une version C #? Existe-t-il une version avec des commentaires sur la documentation? Cet algorithme peut-il être adapté pour la 3D?
Aaron Franke
3

Si vous cherchez à créer un terrain destructible, la façon dont je l'ai fait dans Unity, c'est de placer les collisionneurs uniquement sur les blocs de bord de votre monde. Ainsi, par exemple, voici ce que vous aimeriez accomplir:

Les blocs verts indiquent les tuiles qui contiennent un collisionneur

Tous ces blocs verts contiennent un collisionneur, et les autres non. Cela économise une tonne de calculs. Si vous détruisez un bloc, vous pouvez activer les collisionneurs sur les blocs adjacents assez facilement. Gardez à l'esprit que l'activation / la désactivation d'un collisionneur est coûteuse et doit être effectuée avec parcimonie.

Ainsi, la ressource Tile ressemble à ceci:

Tile Resource In Unity

C'est un objet de jeu standard, mais il peut également être mis en commun. Notez également que le collisionneur de boîtes est désactivé par défaut. Nous ne l'activerions que s'il s'agit d'une tuile de bord.

Si vous chargez statiquement votre monde, il n'est pas nécessaire de regrouper vos tuiles. Vous pouvez simplement les charger en une seule fois, calculer leur distance par rapport au bord et appliquer un collisionneur si nécessaire.

Si vous chargez dynamiquement, il est préférable d'utiliser un pool de tuiles. Voici un exemple édité de ma boucle de rafraîchissement. Il charge les tuiles en fonction de la vue actuelle de la caméra:

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

Idéalement, j'écrirais un article beaucoup plus détaillé, car il se passe beaucoup plus de choses en coulisses. Cependant, cela peut vous aider. S'il y a des questions, n'hésitez pas à me contacter ou à me contacter.

jgallant
la source
Réponse de dnkdrone acceptée car elle répond plus directement à la question initiale posée. Cependant, ont voté en faveur de cette réponse car elle donne une orientation précieuse vers une alternative efficace
Craig Innes
@CraigInnes Aucun problème homme. Juste pour aider. Les points n'ont pas d'importance :)
jgallant