27 Eylül 2026 Pazar

Rilevamento delle collisioni 2D in MonoGame: dai rettangoli di base ai sistemi Pixel-Perfect ad allocazione zero




Rilevamento delle collisioni 2D in MonoGame: dai rettangoli di base ai sistemi Pixel-Perfect ad allocazione zero

Quando si creano giochi 2D con motori visivi come Unity o Godot, il rilevamento delle collisioni spesso sembra una serie di caselle di controllo dell'ispettore: si schiaffeggia un "BoxCollider2D" o un "CircleCollider2D", si collega un "Rigidbody" e si spera che il passaggio della fisica interna non balbetti sui dispositivi mobili.

In MonoGame e C#, tuttavia, hai il controllo totale. Non c'è alcun sovraccarico fisico nascosto, nessuna inerzia rotazionale indesiderata e nessun misterioso picco di raccolta dei rifiuti (GC) che ruba il budget del frame.

In Arar Games, quando abbiamo creato Blocked: Pixel Panzer e Paint Trek, i loop dei nostri giochi arcade dovevano elaborare centinaia di proiettili nemici ad alta velocità, griglie di mattoni esplosivi, torrette di carri armati rotanti, sorvoli di aerei da combattimento e scudi antiparticellari a una velocità compresa tra 60 e 120 FPS su PC Windows e dispositivi Android. Un motore fisico generico era fuori discussione: avevamo bisogno di un'architettura di collisione a più livelli appositamente costruita.

In questa guida completa basata su codice, inizieremo dalle basi assolute della collisione MonoGame ("Rectangle.Intersects" con sprite "Bullet" e "Enemy" reali) per poi arrivare ai controlli avanzati dei cerchi, al clamping misto, ai raycast anti-tunneling, alla collisione Pixel-Perfect di livello produttivo e alle griglie spaziali ad allocazione zero ottimizzate per la sopravvivenza del GC mobile.


1. Le basi: una semplice gerarchia di "Sprite" per monogioco

Prima di rilevare le collisioni, abbiamo bisogno di entità di gioco pulite. In MonoGame, un'entità possiede fondamentalmente una posizione, una trama e un rettangolo di delimitazione.

Ecco l'architettura delle entità di base utilizzata nei nostri giochi:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class Sprite
{
    public Vector2 Position;
    public Texture2D Texture;
    public Color Tint = Color.White;
    public bool IsActive = true;

    // The raw Axis-Aligned Bounding Box (AABB)
    public virtual Rectangle Bounds => new Rectangle(
        (int)Position.X,
        (int)Position.Y,
        Texture != null ? Texture.Width : 0,
        Texture != null ? Texture.Height : 0
    );

    public virtual void Draw(SpriteBatch spriteBatch)
    {
        if (!IsActive || Texture == null) return;
        spriteBatch.Draw(Texture, Position, Tint);
    }
}

Ora creiamo classi concrete Player, Enemy e Bullet:

public class Bullet : Sprite
{
    public Vector2 Velocity;
    public int Damage = 25;

    public void Update(GameTime gameTime)
    {
        Position += Velocity * (float)gameTime.ElapsedGameTime.TotalSeconds;
    }
}

public class Enemy : Sprite
{
    public int Health = 100;

    public void TakeDamage(int damage)
    {
        Health -= damage;
        if (Health <= 0)
        {
            IsActive = false;
        }
    }
}

public class Player : Sprite
{
    public int Health = 100;
    public bool IsInvulnerable = false;

    public void TakeDamage(int damage)
    {
        if (IsInvulnerable) return;
        Health -= damage;
    }
}

Una volta attivate queste entità, esploriamo come rilevare le collisioni tra di loro, partendo dal metodo più semplice.


2. Livello 1: La collisione più semplice – Rectangle.Intersects (AABB)

Il controllo di collisione 2D più fondamentale in MonoGame è il test Axis-Aligned Bounding Box (AABB). Il termine "allineato all'asse" significa semplicemente che i bordi del rettangolo sono completamente paralleli agli assi \(X\) e \(Y\) dello schermo (nessuna rotazione).

MonoGame fornisce un metodo veloce e integrato: Rectangle.Intersects(Valore rettangolo).

Come funziona Rectangle.Intersects dietro le quinte

Sotto la superficie, MonoGame esegue quattro confronti di numeri interi:

public bool Intersects(Rectangle value)
{
    return value.Left < this.Right &&
           this.Left < value.Right &&
           value.Top < this.Bottom &&
           this.Top < value.Bottom;
}

Se tutte e quattro le condizioni sono soddisfatte, i rettangoli si sovrappongono. Se anche una sola condizione fallisce, un asse vuoto le separa e non è possibile alcuna collisione.

Codice di gioco reale: "Bullet" contro "Enemy" in "Game1.Update"

Ecco come controlli le collisioni tra un elenco di proiettili attivi e un elenco di nemici attivi all'interno del ciclo principale di "Aggiornamento" di MonoGame:

public class Game1 : Game
{
    private List<Bullet> _bullets = new List<Bullet>();
    private List<Enemy> _enemies = new List<Enemy>();

    protected override void Update(GameTime gameTime)
    {
        // 1. Update bullet and enemy positions
        foreach (var bullet in _bullets) bullet.Update(gameTime);

        // 2. Collision Check: Bullets vs Enemies
        for (int b = _bullets.Count - 1; b >= 0; b--)
        {
            var bullet = _bullets[b];
            if (!bullet.IsActive) continue;

            for (int e = _enemies.Count - 1; e >= 0; e--)
            {
                var enemy = _enemies[e];
                if (!enemy.IsActive) continue;

                // The AABB check!
                if (bullet.Bounds.Intersects(enemy.Bounds))
                {
                    // Collision occurred!
                    enemy.TakeDamage(bullet.Damage);
                    bullet.IsActive = false;

                    // Remove inactive bullet immediately
                    _bullets.RemoveAt(b);
                    
                    if (!enemy.IsActive)
                    {
                        _enemies.RemoveAt(e);
                    }

                    // A bullet can only hit one enemy; break the inner loop
                    break;
                }
            }
        }

        base.Update(gameTime);
    }
}

Suggerimento per le prestazioni: Nota che iteriamo all'indietro (for (int i = list.Count - 1; i >= 0; i--))! Se usi foreach e provi a chiamare _bullets.Remove(bullet), C# lancia un'eccezione "InvalidOperationException: Collection was modification". L'iterazione all'indietro consente la rimozione sicura degli elementi senza problemi di reindicizzazione della memoria.

Il segreto di Arcade: "Hitbox discreti" tramite Gonfia

Nei giochi retrò come Blocked: Pixel Panzer, le texture degli sprite spesso includono margini trasparenti o punte di antenne. Se il carro armato del giocatore esplode perché un proiettile ha toccato un angolo vuoto e trasparente della sua trama, il giocatore si sentirà ingannato.

Per rendere la collisione reattiva ed equa, i giochi utilizzano un Hitbox più piccolo all'interno dello sprite utilizzando Rectangle.Inflate:

public class EnemyTank : Enemy
{
    // Shrink the bounding box by 6 pixels on all sides for fair collision
    public override Rectangle Bounds
    {
        get
        {
            Rectangle raw = base.Bounds;
            raw.Inflate(-6, -6); // Reduces width and height by 12px
            return raw;
        }
    }
}

3. Livello 2: collisione da cerchio a cerchio (immunità rotazionale)

I rettangoli funzionano benissimo per i blocchi statici e le tessere della griglia, ma falliscono quando gli sprite ruotano. Quando un'astronave non quadrata ruota in Paint Trek, un riquadro di delimitazione allineato all'asse deve espandersi per racchiudere gli angoli rotanti, provocando frustranti "collisioni fantasma" nell'aria vuota.

Per gli asteroidi circolari, le sfere energetiche a ricerca e i veicoli spaziali rotanti, i Bounding Circles sono la soluzione ideale.

La trappola della radice quadrata

Due cerchi si scontrano quando la distanza tra i loro centri è minore o uguale alla somma dei loro raggi:

\(\text{Distanza}(C_A, C_B) \le r_A + r_B\)

Nel codice, il calcolo della distanza euclidea utilizza "Math.Sqrt" (o "Vector2.Distance"). Tuttavia, il calcolo delle radici quadrate in un ciclo con 200 proiettili brucia centinaia di cicli CPU non necessari!

Confrontando la distanza quadrata con la somma del raggio quadrato, eliminiamo completamente la radice quadrata:

\(\text{DistanceSquared} \le (r_A + r_B)^2\)

Implementazione di MonoGame: Circle contro Circle

public struct Circle
{
    public Vector2 Center;
    public float Radius;

    public Circle(Vector2 center, float radius)
    {
        Center = center;
        Radius = radius;
    }

    public bool Intersects(Circle other)
    {
        float radiusSum = this.Radius + other.Radius;
        
        // MonoGame built-in Vector2.DistanceSquared
        return Vector2.DistanceSquared(this.Center, other.Center) <= (radiusSum * radiusSum);
    }
}

Ora integralo direttamente in un'entità:

public class PaintTrekFighter : Sprite
{
    public float CollisionRadius = 18f;
    public Vector2 Center => Position + new Vector2(Texture.Width * 0.5f, Texture.Height * 0.5f);

    public Circle BoundingCircle => new Circle(Center, CollisionRadius);

    public bool CollidesWith(PaintTrekFighter other)
    {
        return this.BoundingCircle.Intersects(other.BoundingCircle);
    }
}

Zero radici quadrate, immune alla rotazione degli sprite e velocissimo.


4. Livello 3: Forme miste – Cerchio vs. Scatola (MathHelper.Clamp)

Cosa succede quando un'astronave circolare in Paint Trek attraversa uno stretto labirinto di barriere di difesa rettangolari o quando un proiettile rotondo colpisce un blocco quadrato in Blocked: Pixel Panzer?

Abbiamo bisogno della collisione Cerchio vs. Rettangolo.

L'algoritmo di serraggio

La strategia consiste nel trovare il punto sul rettangolo più vicino al centro del cerchio e quindi verificare se la distanza da quel punto più vicino al centro è inferiore al raggio del cerchio.

MathHelper.Clamp di MonoGame rende tutto questo banale:

public static class Collision2D
{
    public static bool CircleIntersectsRectangle(Circle circle, Rectangle rect)
    {
        // Find the closest point on the rectangle to the circle center
        float closestX = MathHelper.Clamp(circle.Center.X, rect.Left, rect.Right);
        float closestY = MathHelper.Clamp(circle.Center.Y, rect.Top, rect.Bottom);

        // Vector from closest point to circle center
        float distanceX = circle.Center.X - closestX;
        float distanceY = circle.Center.Y - closestY;

        // Check squared distance against squared radius
        float distanceSquared = (distanceX * distanceX) + (distanceY * distanceY);
        return distanceSquared <= (circle.Radius * circle.Radius);
    }
}

Ora puoi testare gli scudi dei giocatori contro mattoncini rettangolari con zero allocazioni e alta precisione!


5. Livello 4: Rilevamento continuo delle collisioni (CCD) e raggi spazzati

Hai mai sparato con un colpo di cecchino iperveloce o con un laser a rotaia nel tuo gioco, solo per vedere il proiettile passare magicamente attraverso una sottile nave nemica senza infliggere danni?

Questo bug è noto come tunneling.

Poiché i giochi discreti si aggiornano in fasi temporali (\(\Delta t = 16,6\text{ms}\) a 60 FPS), un oggetto che si muove a 1.800 pixel al secondo viaggia 30 pixel in un singolo fotogramma. Se lo scafo nemico è spesso solo 15 pixel, il proiettile era davanti al nemico nel Frame 1 e completamente dietro al nemico nel Frame 2.

Frame 1:  [ Bullet ]  --->       | Enemy Wall |
Frame 2:                          | Enemy Wall |       --->  [ Bullet ]
                               (NO HIT DETECTED!)

La soluzione: segmento con spazzata e riquadro (metodo lastra)

Invece di testare un singolo punto, testiamo l'intero segmento di linea che collega la posizione del proiettile sul Frame 1 (previousPosition) al Frame 2 (currentPosition).

Ecco il metodo di intersezione delle lastre di raycasting di produzione dal nostro titolo associato SpiralWar:

public static class ContinuousCollision
{
    public static bool IntersectsSweptRay(Vector2 rayStart, Vector2 rayEnd, Rectangle box, out Vector2 hitPoint)
    {
        hitPoint = Vector2.Zero;

        Vector2 direction = rayEnd - rayStart;
        float tMin = 0f;
        float tMax = 1f;

        // Clip against X slabs
        if (MathF.Abs(direction.X) > 1e-6f)
        {
            float invX = 1f / direction.X;
            float t1 = (box.Left - rayStart.X) * invX;
            float t2 = (box.Right - rayStart.X) * invX;
            if (t1 > t2) (t1, t2) = (t2, t1);

            tMin = MathF.Max(tMin, t1);
            tMax = MathF.Min(tMax, t2);
            if (tMin > tMax) return false;
        }
        else if (rayStart.X < box.Left || rayStart.X > box.Right)
        {
            return false;
        }

        // Clip against Y slabs
        if (MathF.Abs(direction.Y) > 1e-6f)
        {
            float invY = 1f / direction.Y;
            float t1 = (box.Top - rayStart.Y) * invY;
            float t2 = (box.Bottom - rayStart.Y) * invY;
            if (t1 > t2) (t1, t2) = (t2, t1);

            tMin = MathF.Max(tMin, t1);
            tMax = MathF.Min(tMax, t2);
            if (tMin > tMax) return false;
        }
        else if (rayStart.Y < box.Top || rayStart.Y > box.Bottom)
        {
            return false;
        }

        hitPoint = rayStart + direction * tMin;
        return true;
    }
}

In Blocked: Pixel Panzer, l'abilità continua Laser Beam del giocatore utilizza esattamente questo raycast per tagliare file di blocchi discendenti senza perdere una singola collisione.


6. Livello 5: rilevamento delle collisioni pixel-perfect di livello produttivo

Ora raggiungiamo il livello massimo di precisione 2D: Collisione Pixel-Perfect.

In uno sparatutto in stile retrò o in un combattimento aereo tra astronavi, le forme irregolari (barili di carri armati, ali, cabine di pilotaggio) sono circondate da pixel trasparenti nella trama dello sprite. Quando un missile nemico colpisce quello spazio trasparente, i giocatori se ne accorgono immediatamente.

La collisione pixel-perfect controlla i canali alfa (trasparenza) effettivi delle texture sovrapposte. Se due pixel non trasparenti si sovrappongono alla stessa coordinata mondiale, si è verificato un vero colpo fisico.

L'errore fatale: GetData dentro Update()

Molti tutorial istruiscono i principianti a farlo:

// DO NOT DO THIS!
Color[] dataA = new Color[textureA.Width * textureA.Height];
textureA.GetData(dataA); // STALLS GPU, CREATES MASSIVE GC LAG!

La chiamata a Texture2D.GetData() durante il runtime forza la CPU ad attendere che la pipeline della GPU venga scaricata, riportando la memoria della texture sul bus. Facendo questo per dozzine di sprite, ogni fotogramma aumenterà il tuo gioco da 60 FPS a 5 FPS e attiverà immediatamente gli ANR Android!

La soluzione testata in battaglia da Blocked: Pixel Panzer

In Blocked: Pixel Panzer, il nostro Sprite.cs implementa una routine IntersectsPixel ultra ottimizzata costruita attorno a 5 regole cruciali:

  1. Cache statica degli array di colori: GetData viene chiamato esattamente una volta quando la texture viene caricata e archiviato in un Dictionary<Texture2D, Color[]>.
  2. AABB Early Exit Guard: Se Bounds.Intersects(other.Bounds) è falso, eseguiamo il salvataggio immediato. Il 99% dei controlli viene eliminato prima di ispezionare un singolo pixel.
  3. Supporto per Texture Atlas e SourceRectangle: Gestisce gli sprite inseriti negli atlanti di texture utilizzando gli offset del rettangolo di origine.
  4. Finestra di sovrapposizione calcolata: Eseguiamo il loop solo sull'esatto rettangolo di intersezione tra entrambi gli sprite ("Math.Max(a.Top, b.Top)`, ecc.).
  5. Cortocircuito alfa: Se il pixel dello Sprite A è trasparente (A <= 20), lo Sprite B viene completamente saltato.
  6. Property Hoisting: L'accesso alle proprietà virtuali ("Bounds") in cicli nidificati crea migliaia di copie di strutture. Li memorizziamo nelle variabili dello stack locale prima di entrare nel ciclo.

Ecco il codice completo e pronto per la produzione:

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class AdvancedSprite : Sprite
{
    private static readonly Dictionary<Texture2D, Color[]> _textureDataCache = new();

    public Rectangle? SourceRectangle { get; set; }

    public static Color[] GetCachedTextureData(Texture2D texture)
    {
        if (!_textureDataCache.TryGetValue(texture, out var data))
        {
            data = new Color[texture.Width * texture.Height];
            texture.GetData(data);
            _textureDataCache[texture] = data;
        }
        return data;
    }

    public bool IntersectsPixel(AdvancedSprite other)
    {
        // 1. Hoist bounds into local stack variables
        Rectangle a = this.Bounds;
        Rectangle b = other.Bounds;

        // 2. Fast AABB Broad-Phase check
        if (!a.Intersects(b)) return false;

        // Fallback to rectangle check if textures are missing
        if (this.Texture == null || other.Texture == null) return true;

        // 3. Retrieve pre-cached color arrays (Zero GPU latency)
        Color[] dataA = GetCachedTextureData(this.Texture);
        Color[] dataB = GetCachedTextureData(other.Texture);

        int aTexW = this.Texture.Width;
        int bTexW = other.Texture.Width;

        // Atlas / SourceRectangle offset mapping
        bool aHasSrc = this.SourceRectangle.HasValue;
        Rectangle aSrc = aHasSrc ? this.SourceRectangle.Value : Rectangle.Empty;
        int aOriginX = aHasSrc ? aSrc.X : 0;
        int aOriginY = aHasSrc ? aSrc.Y : 0;
        int aSpanX   = aHasSrc ? aSrc.Width  : this.Texture.Width;
        int aSpanY   = aHasSrc ? aSrc.Height : this.Texture.Height;

        bool bHasSrc = other.SourceRectangle.HasValue;
        Rectangle bSrc = bHasSrc ? other.SourceRectangle.Value : Rectangle.Empty;
        int bOriginX = bHasSrc ? bSrc.X : 0;
        int bOriginY = bHasSrc ? bSrc.Y : 0;
        int bSpanX   = bHasSrc ? bSrc.Width  : other.Texture.Width;
        int bSpanY   = bHasSrc ? bSrc.Height : other.Texture.Height;

        // 4. Calculate the overlapping sub-rectangle
        int top    = Math.Max(a.Top, b.Top);
        int bottom = Math.Min(a.Bottom, b.Bottom);
        int left   = Math.Max(a.Left, b.Left);
        int right  = Math.Min(a.Right, b.Right);

        // 5. Scan only overlapping pixels
        for (int y = top; y < bottom; y++)
        {
            // Row offsets hoisted outside the inner X loop!
            int colorA_Y = aOriginY + (int)((y - a.Y) / (float)a.Height * aSpanY);
            int colorB_Y = bOriginY + (int)((y - b.Y) / (float)b.Height * bSpanY);
            int rowA = colorA_Y * aTexW;
            int rowB = colorB_Y * bTexW;

            for (int x = left; x < right; x++)
            {
                int colorA_X = aOriginX + (int)((x - a.X) / (float)a.Width * aSpanX);

                // Early Bailout: If Pixel A is transparent, skip B entirely!
                if (dataA[colorA_X + rowA].A <= 20) continue;

                int colorB_X = bOriginX + (int)((x - b.X) / (float)b.Width * bSpanX);

                // If Pixel B is also opaque, we have confirmed contact!
                if (dataB[colorB_X + rowB].A > 20)
                {
                    return true;
                }
            }
        }

        return false;
    }
}

Questa routine viene eseguita in meno di 0,1 millisecondi sui moderni sistemi hardware e desktop Android.


7. Livello 6: griglia spaziale a fase ampia e GC mobile ad allocazione zero

Immagina che il tuo gioco abbia 100 blocchi, 60 proiettili e 20 nemici. Se testi ogni entità rispetto a ogni altra entità utilizzando cicli "for" nidificati, esegui:

\(\frac{180 \times 179}{2} = 16.110 \text{ controlli per fotogramma!}\)

A 60 FPS, ovvero quasi 1.000.000 di controlli di collisione al secondo.

Per ridimensionare il tuo gioco, devi implementare il Partizionamento spaziale a fase ampia.

La griglia hash spaziale uniforme

Dividiamo lo schermo in una griglia 2D di celle uniformi (in Blocked: Pixel Panzer, ogni cella è \(150 \times 150\) pixel). Un'entità verifica solo le collisioni con altre entità che risiedono nelle stesse celle della griglia.

Il problema della memoria mobile: GC Thrashing

Se il tuo SpatialGrid crea new List<Point>() o new List<Block>() ogni fotogramma, allocherai megabyte di spazzatura ogni minuto sull'heap gestito.

Sul runtime Mono di Android, ciò attiva frequenti Garbage Collection Gen-0, causando: "runtime mono: conflitto di blocco nativo (mono_class_is_subclass_)".

Il gioco balbetta e Google Play segnala il tuo gioco con avvisi ANR!

La soluzione: buffer antigraffio riutilizzabili

Ecco la Zero-Allocation Spatial Hash Grid da CollisionManager.cs in Blocked: Pixel Panzer:

public class SpatialGridManager
{
    private const int CELL_SIZE = 150;
    private readonly Dictionary<Point, List<Sprite>> _grid = new();

    // Zero-GC: Reusable scratch buffers allocated ONCE at startup
    private readonly List<Point> _scratchCells = new();
    private readonly HashSet<Sprite> _scratchCheckedEntities = new();

    public void BuildGrid(List<Sprite> entities)
    {
        // Clear lists without re-allocating new List objects
        foreach (var list in _grid.Values)
        {
            list.Clear();
        }

        foreach (var entity in entities)
        {
            if (!entity.IsActive) continue;

            GetOccupiedCells(entity.Bounds, _scratchCells);
            foreach (var cell in _scratchCells)
            {
                if (!_grid.TryGetValue(cell, out var list))
                {
                    list = new List<Sprite>();
                    _grid[cell] = list;
                }
                list.Add(entity);
            }
        }
    }

    private void GetOccupiedCells(Rectangle bounds, List<Point> outCells)
    {
        outCells.Clear();

        int startX = bounds.Left / CELL_SIZE;
        int startY = bounds.Top / CELL_SIZE;
        int endX = bounds.Right / CELL_SIZE;
        int endY = bounds.Bottom / CELL_SIZE;

        for (int x = startX; x <= endX; x++)
        {
            for (int y = startY; y <= endY; y++)
            {
                outCells.Add(new Point(x, y));
            }
        }
    }

    public void CheckCollisions(Player player)
    {
        if (player.IsInvulnerable) return;

        GetOccupiedCells(player.Bounds, _scratchCells);
        _scratchCheckedEntities.Clear();

        foreach (var cell in _scratchCells)
        {
            if (_grid.TryGetValue(cell, out var entitiesInCell))
            {
                foreach (var other in entitiesInCell)
                {
                    if (other == player || _scratchCheckedEntities.Contains(other)) continue;
                    _scratchCheckedEntities.Add(other);

                    // 1. Broad-phase AABB test
                    if (player.Bounds.Intersects(other.Bounds))
                    {
                        // 2. High-precision Pixel-Perfect test
                        if (player is AdvancedSprite advPlayer && other is AdvancedSprite advOther)
                        {
                            if (advPlayer.IntersectsPixel(advOther))
                            {
                                player.TakeDamage(10);
                            }
                        }
                    }
                }
            }
        }
    }
}

Riutilizzando _scratchCells e _scratchCheckedEntities, l'intera fase di collisione viene eseguita con 0 byte di allocazione heap per frame.


8. Tabella comparativa riepilogativa

Tecnica Costo matematico Supporto rotazionale Tunneling sicuro? Miglior caso d'uso in MonoGame
AABB ("Rettangolo.Interseca") Ultraveloce (~4 confronti interi) No No Blocchi della griglia, proiettile a fase ampia, interfaccia utente
Cerchio (DistanzaQuadrata) Molto veloce (3 muls, 0 sqrt) Sì (invariante) No Navi rotonde, palle di fuoco, sfere di energia
Cerchio vs Scatola ("Morsetto") Veloce (MathHelper.Clamp locale) Sì No Giocatore circolare che attraversa muri stretti
Raggio spazzato (CCD a lastra) Moderato (Raycast parametrico) Sì Sì Proiettili da cecchino veloci, fucili a rotaia, laser
Pixel perfetto ("IntersectsPixel") Selettivo (sottoretto limitato) Sì No Contorni di sprite irregolari, hitbox discreti
Griglia spaziale (Zero-GC) Fase ampia (\(O(N^2) \a O(N)\)) N/D N/D Onde dense, inferni di proiettili, Android 60 FPS

Vetrine della produzione nel mondo reale: Arar Games

Queste architetture di collisione non sono esperimenti teorici: sono la vera base ingegneristica che alimenta i nostri titoli rilasciati in commercio:

  • Bloccato: Pixel Panzer: Il nostro gioco arcade di sopravvivenza di carri armati retrò su Google Play e Microsoft Store. Presenta il nostro sistema di collisione completo a due livelli: una griglia di hash spaziale ad allocazione zero che filtra i blocchi che cadono e un controllo del contatto perfetto al pixel per carri armati, aerei da combattimento, gusci di torrette e munizioni elementali ColorWheel.
  • Paint Trek: Il nostro frenetico sparatutto spaziale con collisione circolare rotazionale, raycasting continuo e sistemi di difesa missilistica a volume spazzato.

Conclusione e passaggi successivi

MonoGame ti dà la possibilità di progettare il rilevamento delle collisioni che si adatta perfettamente alle esigenze del tuo gioco. Eseguendo i costosi controlli Pixel-Perfect con rapidi test AABB, utilizzando le Distanze quadrate per i cerchi ed eliminando la raccolta dei rifiuti con Scratch Buffer riutilizzabili, puoi offrire prestazioni FPS 60/120 uniformi su console sia su piattaforme desktop che mobili.

Dai un'occhiata ai nostri giochi sugli app store per vedere questi sistemi di collisione in azione e inizia a implementare questi modelli nei tuoi progetti MonoGame oggi stesso!



Parole chiave e hashtag SEO

Parole chiave: Rilevamento collisioni 2D MonoGame, sviluppo giochi C#, Rectangle.Intersects MonoGame, collisione pixel perfetta C#, griglia hash spaziale MonoGame, collisione continua a raggio spazzato, ottimizzazione delle prestazioni dei giochi indie, loop di gioco con allocazione zero, ottimizzazione Android MonoGame, Arar Games, Blocked Pixel Panzer, Paint Trek.

#MonoGame #CSharp #GameDev #IndieDev #GamePhysics #DotNet #2DGameDev #MobileGameDev #PerformanceOptimization #CleanCode #BlockedPixelPanzer #PaintTrek #ArarGames #GameProgramming




Hiç yorum yok:

Yorum Gönder