Detekcija sudara etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster
Detekcija sudara etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster

27 Eylül 2026 Pazar

2D otkrivanje sudara u MonoGameu: od osnovnih pravokutnika do sustava savršenih piksela bez dodjele nule

2D otkrivanje sudara u MonoGameu: od osnovnih pravokutnika do sustava savršenih piksela bez dodjele nule

Kada gradite 2D igre u vizualnim motorima kao što su Unity ili Godot, otkrivanje sudara često izgleda kao niz potvrdnih okvira inspektora: stavite BoxCollider2D ili CircleCollider2D, pričvrstite Rigidbody i nadate se da korak unutarnje fizike neće zastajkivati na mobilnim uređajima.

U MonoGame i C#, međutim, vi imate potpunu kontrolu. Nema skrivenih fizičkih troškova, nema neželjene rotacijske inercije i nema tajanstvenih skokova skupljanja smeća (GC) koji vam kradu budžet za okvir.

U Arar Games, kada smo izgradili Blocked: Pixel Panzer i Paint Trek, naše petlje arkadnih igara trebale su obraditi stotine brzih neprijateljskih metaka, eksplodirajućih mreža od cigala, rotirajućih tenkovskih kupola, preleta borbenih aviona i štitova od čestica pri 60 do 120 FPS na Windows PC-u i Android uređajima. Fizički motor opće namjene nije dolazio u obzir - trebala nam je namjenski izgrađena višeslojna arhitektura sudara.

U ovom sveobuhvatnom vodiču vođenom kodom, krenut ćemo od apsolutnih osnova MonoGame kolizije ('Rectangle.Intersects' sa stvarnim Bullet i Enemy spriteovima) i prijeći na napredne provjere krugova, miješano stezanje, anti-tuneliranje raycasta, Pixel-Perfect sudara proizvodne kvalitete i Zero-Allocation Spatial Rešetke optimizirane za preživljavanje mobilnog GC-a.


1. Temelj: jednostavna hijerarhija MonoGame Sprite

Prije otkrivanja sudara potrebni su nam čisti entiteti igre. U MonoGameu, entitet u osnovi posjeduje položaj, teksturu i granični pravokutnik.

Ovo je osnovna arhitektura entiteta koja se koristi u našim igrama:

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

Kreirajmo sada konkretne klase Igrač, Neprijatelj i Metak:

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

S tim entitetima na mjestu, istražimo kako otkriti kolizije između njih, počevši od najjednostavnije metode.


2. Razina 1: Najjednostavniji sudar – Rectangle.Intersects (AABB)

Najosnovnija 2D provjera sudara u MonoGameu je Axis-Aligned Bounding Box (AABB) test. Izraz "poravnano s osi" jednostavno znači da su rubovi pravokutnika potpuno paralelni s \(X\) i \(Y\) osi zaslona (bez rotacije).

MonoGame pruža brzu, ugrađenu metodu: Rectangle.Intersects(Rectangle value).

Kako Rectangle.Intersects radi ispod poklopca

Ispod površine, MonoGame izvršava četiri cjelobrojne usporedbe:

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

Ako su ispunjena sva četiri uvjeta, pravokutnici se preklapaju. Ako čak i jedan uvjet ne ispuni, prazna os ih razdvaja i nije moguć sudar.

Pravi kod igre: Bullet protiv Enemy u Game1.Update

Evo kako provjeravate kolizije između popisa aktivnih metaka i popisa aktivnih neprijatelja unutar vaše glavne MonoGame Update petlje:

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

Savjet za performanse: Primijetite da ponavljamo unatrag (for (int i = list.Count - 1; i >= 0; i--))! Ako koristite foreach i pokušate pozvati _bullets.Remove(bullet), C# izbacuje InvalidOperationException: Collection was modified. Ponavljanje unatrag omogućuje sigurno uklanjanje elementa bez problema s ponovnim indeksiranjem memorije.

The Arcade Secret: "Fair Hitboxes" putem Inflate

U retro igrama kao što je Blocked: Pixel Panzer teksture spritea često uključuju prozirne margine ili antenske šiljke. Ako igračev spremnik eksplodira jer je metak dotaknuo prazan prozirni kut njegove teksture, igrač će se osjećati prevarenim.

Kako bi sudar bio brz i pošten, igre koriste manji Hitbox unutar spritea koristeći 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. Razina 2: Sudar od kruga do kruga (otpornost na rotaciju)

Pravokutnici su izvrsni za statične blokove i rešetkaste pločice, ali ne uspijevaju kada se spriteovi okreću. Kada se nekvadratni svemirski brod rotira u Paint Treku, granični okvir poravnat s osi mora se proširiti kako bi obuhvatio kutove koji se okreću, što rezultira frustrirajućim "fantomskim sudarima" u praznom zraku.

Za kružne asteroide, energetske kugle za samonavođenje i rotirajuće svemirske letjelice, Ograničujući krugovi idealno su rješenje.

Zamka kvadratnog korijena

Dvije se kružnice sudaraju kada je udaljenost između njihovih središta manja ili jednaka zbroju njihovih polumjera:

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

U kodu, izračunavanje euklidske udaljenosti koristi Math.Sqrt (ili Vector2.Distance). Međutim, izračunavanje kvadratnih korijena u petlji s 200 projektila troši stotine nepotrebnih CPU ciklusa!

Uspoređujući kvadrat udaljenosti s kvadratom zbroja radijusa, potpuno eliminiramo kvadratni korijen:

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

Implementacija MonoGame: Krug protiv Kruga

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

Sada integrirajte ovo izravno u entitet:

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

Bez kvadratnih korijena, otporan na rotaciju spritea i brz kao munja.


4. Razina 3: Mješoviti oblici – Krug protiv kutije (MathHelper.Clamp)

Što se događa kada kružni svemirski brod u Paint Treku prolazi kroz uski labirint pravokutnih obrambenih barijera ili kada okrugli metak pogodi kvadratni blok u Blocked: Pixel Panzer?

Treba nam Krug protiv pravokutnika sudar.

Algoritam stezanja

Strategija je pronaći točku na pravokutniku koja je najbliža središtu kruga, a zatim testirati je li udaljenost od te najbliže točke do središta manja od polumjera kruga.

MonoGameov MathHelper.Clamp čini ovo trivijalnim:

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

Sada možete testirati štitove igrača protiv pravokutnih cigli s nula dodjela i visokom preciznošću!


5. Razina 4: Kontinuirana detekcija sudara (CCD) & Swept Rays

Jeste li ikada u svojoj igrici ispalili snajperski metak visoke brzine ili railgun laser, samo da biste gledali kako metak magično prolazi ravno kroz tanki neprijateljski brod bez nanošenja štete?

Ovaj bug je poznat kao tuneliranje.

Budući da se diskretne igre ažuriraju u vremenskim koracima (\(\Delta t = 16,6\text{ms}\) pri 60 FPS), objekt koji se kreće brzinom od 1800 piksela u sekundi putuje 30 piksela u jednom okviru. Ako je trup neprijatelja debeo samo 15 piksela, metak je bio ispred neprijatelja na kadru 1, a potpuno iza neprijatelja u kadru 2.

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

Rješenje: precrtani segment u odnosu na okvir (metoda ploča)

Umjesto testiranja jedne točke, testiramo cijeli segment linije koji povezuje poziciju grafičke oznake na okviru 1 (previousPosition) s okvirom 2 (currentPosition).

Ovo je metoda presjeka ploča raycastinga iz našeg popratnog naslova 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;
    }
}

U Blocked: Pixel Panzer, igračeva kontinuirana vještina laserske zrake koristi točan raycast za rezanje nizova padajućih blokova bez propuštanja ijednog sudara.


6. Razina 5: Pixel-Perfect Collision Detection proizvodne razine

Sada smo dosegli vrhunsku razinu 2D točnosti: Pixel-Perfect Collision.

U retro tenkovskoj pucačini ili zračnoj borbi svemirskih brodova, nepravilni oblici (cijev tenka, krila, kokpit kokpita) okruženi su prozirnim pikselima u teksturi spritea. Kada neprijateljski projektil pogodi taj prozirni prostor, igrači to odmah primjećuju.

Pixel-perfect collision provjerava stvarne alfa (prozirne) kanale tekstura koje se preklapaju. Ako se dva netransparentna piksela preklapaju na istoj svjetskoj koordinati, dogodio se pravi fizički pogodak.

Fatalna pogreška: GetData unutar Update()

Mnogi vodiči upućuju početnike da to učine:

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

Pozivanje Texture2D.GetData() tijekom vremena izvođenja prisiljava CPU da čeka da se GPU cjevovod ispere, povlačeći memoriju teksture natrag preko sabirnice. Ako to učinite za desetke spriteova u svakom okviru, vaša igra će se smanjiti sa 60 FPS na 5 FPS i odmah pokrenuti Android ANR-ove!

Rješenje testirano u bitkama iz Blokirano: Pixel Panzer

U Blocked: Pixel Panzer, naš Sprite.cs implementira ultra-optimiziranu IntersectsPixel rutinu izgrađenu oko 5 ključnih pravila:

  1. Static Color Array Cache: GetData se poziva točno jednom kada se tekstura učitava i pohranjuje u Rječnik<Texture2D, Color[]>.
  2. AABB Early Exit Guard: Ako je Bounds.Intersects(other.Bounds) netočan, odmah izlazimo. 99% provjera eliminira se prije provjere jednog piksela.
  3. Texture Atlas & SourceRectangle podrška: Rukuje spriteovima upakiranima u atlase tekstura koristeći pomake izvornog pravokutnika.
  4. Prozor izračunatog preklapanja: Prelazimo samo preko točnog pravokutnika presjeka između oba spritea (Math.Max(a.Top, b.Top), itd.).
  5. Alfa kratki spoj: Ako je piksel Sprite A proziran (A <= 20), Sprite B je potpuno preskočen.
  6. Podizanje svojstava: Pristup virtualnim svojstvima (Granice) u ugniježđenim petljama stvara tisuće kopija strukture. Spremamo ih u lokalne varijable stoga prije ulaska u petlju.

Ovdje je potpuni kod spreman za proizvodnju:

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

Ova se rutina izvršava za manje od 0,1 milisekunde na modernom Android hardveru i stolnim sustavima.


7. Razina 6: prostorna mreža široke faze i mobilni GC s nultom dodjelom

Zamislite da vaša igra ima 100 blokova, 60 metaka i 20 neprijatelja. Ako svaki entitet testirate u odnosu na svaki drugi entitet pomoću ugniježđenih petlji `za', izvodite:

\(\frac{180 \times 179}{2} = 16,110 \text{ provjera po okviru!}\)

Pri 60 FPS, to je gotovo 1.000.000 provjera sudara svake sekunde.

Da biste skalirali svoju igru, morate implementirati Broad-Phase Spatial Partitioning.

Jedinstvena prostorna hash mreža

Zaslon dijelimo na 2D mrežu jednakih ćelija (u Blokirano: Pixel Panzer, svaka ćelija ima 150$ \puta 150$ piksela). Entitet samo testira kolizije s drugim entitetima koji se nalaze u istim ćelijama mreže.

Problem mobilne memorije: GC Thrashing

Ako vaš SpatialGrid stvara novi List<Point>() ili novi List<Block>() svaki okvir, vi svake minute dodjeljujete megabajte smeća na upravljanoj hrpi.

U Androidovom Mono runtimeu, ovo pokreće česte Gen-0 Garbage Collections, uzrokujući: mono runtime: nadmetanje nad izvornim zaključavanjem (mono_class_is_subclass_)

Igra zastajkuje i Google Play označava vašu igru upozorenjima o ANR-u!

Rješenje: Scratch pufferi za višekratnu upotrebu

Ovdje je Zero-Allocation Spatial Hash Grid iz CollisionManager.cs u 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);
                            }
                        }
                    }
                }
            }
        }
    }
}

Ponovnom upotrebom _scratchCells i _scratchCheckedEntities, cijeli korak sudara izvodi se s 0 bajtova raspodjele hrpe po okviru.


8. Sažeta tablica usporedbe

Tehnika Matematički trošak Rotacijska podrška Je li tuneliranje sigurno? Najbolja upotreba u MonoGame
AABB (Pravokutnik.Sjecišta) Ultrabrzo (~4 usporedbe cijelih brojeva) Ne Ne Mrežni blokovi, široka faza metka, UI
Krug (Udaljenost na kvadrat) Vrlo brzo (3 muls, 0 sqrt) Da (nepromjenjivo) Ne Okrugli brodovi, vatrene kugle, energetske kugle
Krug protiv kutije (Stega) Brzo (Local MathHelper.Clamp) Da Ne Kružni igrač koji se kreće kroz uske zidove
Swept Ray (Slab CCD) Umjereno (Parametric Raycast) Da Da Brzi snajperski meci, railguns, laseri
Pixel-Perfect (IntersectsPixel) Selektivno (ograničeno podpravo) Da Ne Nepravilne konture spritea, pošteni hitboxovi
Prostorna mreža (Zero-GC) Široka faza (\(O(N^2) \do O(N)\)) N/A N/A Gusti valovi, pakao od metaka, Android 60 FPS

Prikazi produkcije u stvarnom svijetu: Arar igre

Ove kolizijske arhitekture nisu teoretski eksperimenti - one su pravi inženjerski temelj koji pokreće naše komercijalno objavljene naslove:

  • Blokirano: Pixel Panzer: Naša retro arkadna igra za preživljavanje tenkova na Google Playu i Microsoft Storeu. Sadrži naš potpuni dvoslojni sustav sudara: prostornu raspršenu rešetku s nultom raspodjelom koja filtrira padajuće blokove i savršenu provjeru kontakta za tenkove, borbene zrakoplove, kupole i elementarno streljivo ColorWheel.
  • Paint Trek: Naša brza svemirska pucačina koja sadrži sudar u rotacijskom krugu, kontinuirano emitiranje zraka i obrambene sustave od raketnih projektila s precrtanim volumenom.

Zaključak i sljedeći koraci

MonoGame vam daje moć dizajniranja detekcije sudara koja savršeno odgovara potrebama vaše igre. Usklađivanjem skupih Pixel-Perfect provjera iza brzih AABB testova, korištenjem Kvadratne udaljenosti za krugove i uklanjanjem skupljanja smeća s Reusable Scratch Buffers, možete isporučiti glatku izvedbu od 60/120 FPS konzole na stolnim i mobilnim platformama.

Provjerite naše igre u trgovinama aplikacija kako biste vidjeli ove sustave sudara na djelu i počnite implementirati ove obrasce u svoje MonoGame projekte već danas!


Veze i resursi trgovine


SEO ključne riječi i hashtagovi

Ključne riječi: MonoGame 2D otkrivanje sudara, C# razvoj igre, Rectangle.Intersects MonoGame, pixel perfect collision C#, prostorna hash grid MonoGame, swept ray kontinuirana kolizija, optimizacija performansi indie igre, zero allocation game loop, Android MonoGame optimizacija, Arar Games, Blocked Pixel Panzer, Paint Trek.

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

2D detekcija sudara u MonoGameu: od osnovnih pravokutnika do savršenih sistema piksela s nultom dodjelom

2D detekcija sudara u MonoGameu: od osnovnih pravokutnika do savršenih sistema piksela s nultom dodjelom

Kada pravite 2D igre u vizuelnim mašinama kao što su Unity ili Godot, detekcija sudara se često čini kao niz kontrolnih polja za potvrdu: udarite na BoxCollider2D ili CircleCollider2D, pričvrstite Rigidbody i nadate se da interni korak fizike neće zastoj na mobilnim uređajima.

U MonoGame i C#, međutim, imate potpunu kontrolu. Nema skrivene fizike, nema neželjene rotacione inercije i nema misteriozne skokove sakupljanja smeća (GC) koji kradu vaš budžet okvira.

U Arar Games, kada smo napravili Blocked: Pixel Panzer i Paint Trek, naše petlje arkadnih igara bile su potrebne za obradu stotina neprijateljskih metaka velike brzine, eksplodirajuće mreže od cigli, rotirajućih tenkova, preleta borbenih aviona i štitova od čestica na Windows 12 i Android uređajima na 62 FPS do 60 FPS. Fizički motor opšte namene nije dolazio u obzir – bila nam je potrebna namenska, višeslojna arhitektura sudara.

U ovom sveobuhvatnom vodiču vođenom kodom, počet ćemo od apsolutnih osnova sudara MonoGame-a („Pravougaonik. Presijeca se“ sa stvarnim Metak i Neprijateljskim spritovima) i nadograditi do naprednih provjera krugova, mješovitog stezanja, anti-tuneliranja raycasts-a, produkcijske razine P-erfect-a, Allo** Prostorne mreže** optimizirane za opstanak mobilnog GC-a.


1. Temelj: Jednostavna MonoGame Sprite hijerarhija

Prije otkrivanja sudara, potrebni su nam čisti entiteti igre. U MonoGame-u, entitet u osnovi posjeduje poziciju, teksturu i granični pravougaonik.

Evo osnovne arhitekture entiteta koja se koristi u našim igrama:

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

Sada kreirajmo konkretne klase Player, Enemy i 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;
    }
}

Sa ovim entitetima na mjestu, hajde da istražimo kako otkriti kolizije između njih, počevši od najjednostavnije metode.


2. Nivo 1: Najjednostavniji sudar – Rectangle.Intersets (AABB)

Najosnovnija 2D provjera kolizije u MonoGameu je Axis-Aligned Bounding Box (AABB) test. Termin "poravnan po osi" jednostavno znači da su ivice pravougaonika potpuno paralelne sa osama \(X\) i \(Y\) ekrana (bez rotacije).

MonoGame provides a fast, built-in method: Rectangle.Intersects(Rectangle value).

Kako Rectangle.Intersets radi ispod haube

Underneath the surface, MonoGame executes four integer comparisons:

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

If all four conditions are met, the rectangles overlap. If even one condition fails, an empty axis separates them, and no collision is possible.

Real Gameplay Code: Bullet vs. Enemy in Game1.Update

Here is how you check collisions between a list of active bullets and a list of active enemies inside your main MonoGame Update loop:

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

Performance Tip: Notice we iterate backwards (for (int i = list.Count - 1; i >= 0; i--))! If you use foreach and try to call _bullets.Remove(bullet), C# throws an InvalidOperationException: Collection was modified. Iterating backwards allows safe element removal without memory re-indexing issues.

The Arcade Secret: "Fair Hitboxes" via Inflate

In retro games like Blocked: Pixel Panzer, sprite textures often include transparent margins or antenna spikes. If the player's tank explodes because a bullet touched an empty transparent corner of its texture, the player will feel cheated.

To make collision feel responsive and fair, games use a smaller Hitbox inside the sprite using 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. Level 2: Circle-to-Circle Collision (Rotational Immunity)

Rectangles work great for static blocks and grid tiles, but they fail when sprites rotate. When a non-square spaceship rotates in Paint Trek, an axis-aligned bounding box must expand to enclose the spinning corners, resulting in frustrating "phantom collisions" in empty air.

For circular asteroids, homing energy orbs, and rotating spacecraft, Bounding Circles are the ideal solution.

The Square Root Trap

Two circles collide when the distance between their centers is less than or equal to the sum of their radii:

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

In code, calculating Euclidean distance uses Math.Sqrt (or Vector2.Distance). However, calculating square roots in a loop with 200 projectiles burns hundreds of unnecessary CPU cycles!

By comparing the squared distance against the squared radius sum, we completely eliminate the square root:

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

MonoGame Implementation: Circle vs. 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);
    }
}

Now integrate this directly into an entity:

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 square roots, immune to sprite rotation, and lightning-fast.


4. Level 3: Mixed Shapes – Circle vs. Box (MathHelper.Clamp)

What happens when a circular spaceship in Paint Trek navigates through a tight maze of rectangular defense barriers, or when a round bullet hits a square block in Blocked: Pixel Panzer?

We need Circle vs. Rectangle collision.

The Clamping Algorithm

The strategy is to find the point on the rectangle that is closest to the circle's center, and then test whether the distance from that closest point to the center is less than the circle's radius.

MonoGame's MathHelper.Clamp makes this trivial:

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

You can now test player shields against rectangular bricks with zero allocations and high accuracy!


5. Level 4: Continuous Collision Detection (CCD) & Swept Rays

Have you ever fired a hyper-velocity sniper round or a railgun laser in your game, only to watch the bullet magically pass straight through a thin enemy ship without dealing damage?

This bug is known as tunneling.

Because discrete games update in time steps (\(\Delta t = 16.6\text{ms}\) at 60 FPS), an object moving at 1,800 pixels per second travels 30 pixels in a single frame. If the enemy hull is only 15 pixels thick, the bullet was in front of the enemy on Frame 1, and completely behind the enemy on Frame 2.

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

The Solution: Swept Segment vs. Box (Slab Method)

Instead of testing a single point, we test the entire line segment connecting the bullet's position on Frame 1 (previousPosition) to Frame 2 (currentPosition).

Here is the production raycasting slab-intersection method from our companion title 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, the player's continuous Laser Beam skill uses this exact raycast to slice through rows of descending blocks without missing a single collision.


6. Level 5: Production-Grade Pixel-Perfect Collision Detection

Now we reach the ultimate level of 2D accuracy: Pixel-Perfect Collision.

In a retro tank shooter or spaceship dogfight, irregular shapes (tank barrels, wings, cockpit cockpits) are surrounded by transparent pixels in the sprite texture. When an enemy missile hits that transparent space, players notice immediately.

Pixel-perfect collision inspects the actual alpha (transparency) channels of the overlapping textures. If two non-transparent pixels overlap at the same world coordinate, a true physical hit has occurred.

The Fatal Mistake: GetData inside Update()

Many tutorials instruct beginners to do this:

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

Calling Texture2D.GetData() during runtime forces the CPU to wait for the GPU pipeline to flush, pulling texture memory back over the bus. Doing this for dozens of sprites every frame will tank your game from 60 FPS to 5 FPS and trigger Android ANRs immediately!

The Battle-Tested Solution from Blocked: Pixel Panzer

In Blocked: Pixel Panzer, our Sprite.cs implements an ultra-optimized IntersectsPixel routine built around 5 crucial rules:

  1. Static Color Array Cache: GetData is called exactly once when the texture loads, and stored in a Dictionary<Texture2D, Color[]>.
  2. AABB Early Exit Guard: If Bounds.Intersects(other.Bounds) is false, we bail out immediately. 99% of checks are eliminated before inspecting a single pixel.
  3. Texture Atlas & SourceRectangle Support: Handles sprites packed into texture atlases using source rectangle offsets.
  4. Calculated Overlap Window: We only loop over the exact intersection rectangle between both sprites (Math.Max(a.Top, b.Top), etc.).
  5. Alpha Short-Circuiting: If Sprite A's pixel is transparent (A <= 20), Sprite B is completely skipped.
  6. Property Hoisting: Accessing virtual properties (Bounds) in nested loops creates thousands of struct copies. We store them in local stack variables before entering the loop.

Here is the complete, production-ready code:

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

This routine executes in under 0.1 milliseconds on modern Android hardware and desktop systems.


7. Level 6: Broad-Phase Spatial Grid & Zero-Allocation Mobile GC

Imagine your game has 100 blocks, 60 bullets, and 20 enemies. If you test every entity against every other entity using nested for loops, you perform:

\(\frac{180 \times 179}{2} = 16,110 \text{ checks per frame!}\)

At 60 FPS, that is nearly 1,000,000 collision checks every second.

To scale your game, you must implement Broad-Phase Spatial Partitioning.

The Uniform Spatial Hash Grid

We divide the screen into a 2D grid of uniform cells (in Blocked: Pixel Panzer, each cell is \(150 \times 150\) pixels). An entity only tests collisions against other entities that reside in the same grid cells.

The Mobile Memory Problem: GC Thrashing

If your SpatialGrid creates new List<Point>() or new List<Block>() every frame, you allocate megabytes of garbage every minute on the managed heap.

On Android's Mono runtime, this triggers frequent Gen-0 Garbage Collections, causing: mono runtime: Native lock contention (mono_class_is_subclass_)

The game stutters and Google Play flags your game with ANR warnings!

The Solution: Reusable Scratch Buffers

Here is the Zero-Allocation Spatial Hash Grid from 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);
                            }
                        }
                    }
                }
            }
        }
    }
}

By reusing _scratchCells and _scratchCheckedEntities, the entire collision step runs with 0 bytes of heap allocation per frame.


8. Summary Comparison Table

Technique Mathematical Cost Rotational Support Tunneling Safe? Best Use Case in MonoGame
AABB (Rectangle.Intersects) Ultra-Fast (~4 integer comparisons) No No Grid blocks, bullet broad-phase, UI
Circle (DistanceSquared) Very Fast (3 muls, 0 sqrt) Yes (Invariant) No Round ships, fireballs, energy orbs
Circle vs Box (Clamp) Fast (Local MathHelper.Clamp) Yes No Circular player navigating tight walls
Swept Ray (Slab CCD) Moderate (Parametric Raycast) Yes Yes Fast sniper bullets, railguns, lasers
Pixel-Perfect (IntersectsPixel) Selective (Restricted Sub-rect) Yes No Irregular sprite contours, fair hitboxes
Spatial Grid (Zero-GC) Broad-phase (\(O(N^2) \to O(N)\)) N/A N/A Dense waves, bullet hells, Android 60 FPS

Real-World Production Showcases: Arar Games

These collision architectures are not theoretical experiments—they are the real engineering foundation powering our commercially released titles:

  • Blocked: Pixel Panzer: Our retro tank survival arcade game on Google Play and Microsoft Store. It features our complete two-tier collision system: a zero-allocation spatial hash grid filtering falling blocks, and pixel-perfect contact checking for tanks, fighter jets, turret shells, and ColorWheel elemental ammunition.
  • Paint Trek: Our fast-paced space shooter featuring rotational circle collision, continuous raycasting, and swept-volume missile defense systems.

Conclusion & Next Steps

MonoGame gives you the power to design collision detection that perfectly matches the needs of your game. By gating expensive Pixel-Perfect checks behind fast AABB tests, utilizing Squared Distances for circles, and eliminating garbage collection with Reusable Scratch Buffers, you can deliver console-smooth 60/120 FPS performance on both desktop and mobile platforms.

Check out our games on the app stores to see these collision systems in action, and start implementing these patterns in your own MonoGame projects today!



SEO Keywords & Hashtags

Keywords: MonoGame 2D collision detection, C# game development, Rectangle.Intersects MonoGame, pixel perfect collision C#, spatial hash grid MonoGame, swept ray continuous collision, indie game performance optimization, zero allocation game loop, Android MonoGame optimization, Arar Games, Blocked Pixel Panzer, Paint Trek.

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