27 Eylül 2026 Pazar

MonoGame-də 2D Toqquşmanın Aşkarlanması: Əsas Düzbucaqlılardan Sıfır Ayırma Piksel Mükəmməl Sistemlərinə




MonoGame-də 2D Toqquşmanın Aşkarlanması: Əsas Düzbucaqlılardan Sıfır Ayırma Piksel Mükəmməl Sistemlərinə

Unity və ya Godot kimi vizual mühərriklərdə 2D oyunları qurarkən, toqquşmanın aşkarlanması tez-tez bir sıra müfəttiş qeyd qutuları kimi hiss olunur: siz BoxCollider2D və ya CircleCollider2D üzərinə sillə vurursunuz, Rigidbody əlavə edirsiniz və ümid edirik ki, daxili fizika addımı mobil cihazlarda kəkələməz.

MonoGame və C#-da isə tam nəzarət sizdədir. Heç bir gizli fizika yükü, arzuolunmaz fırlanma ətaləti və çərçivə büdcənizi oğurlayan sirli zibil kolleksiyası (GC) sıçrayışları yoxdur.

Arar Games-də, Bloklanmış: Pixel Panzer və Paint Trek qurarkən, arcade oyun dövrələrimiz həm Windows PC, həm də Android cihazlarında 60-120 PS-də yüzlərlə yüksək sürətli düşmən gülləsini, partlayan kərpic torlarını, fırlanan tank qüllələrini, döyüş təyyarələrinin uçuşlarını və hissəcik qalxanlarını emal etmək üçün lazım idi. Ümumi təyinatlı fizika mühərrikindən söhbət belə gedə bilməzdi - bizə məqsədyönlü, səviyyəli toqquşma arxitekturası lazım idi.

Bu hərtərəfli, kodla idarə olunan bələdçidə biz MonoGame toqquşmasının mütləq əsaslarından ("Rectangle. Intersects" real "Bullet" və "Düşmən" spritləri ilə) başlayacağıq və təkmil çevrə yoxlamaları, qarışıq sıxma, anti-tunel şüaları, istehsal dərəcəli Pixelsion-Zeroalfectioner, Scooter Pixelsion-Pixel-Pixelsion-alfectsalffekti ilə inkişaf edəcəyik. Şəbəkələr** mobil GC-nin sağ qalması üçün optimallaşdırılmışdır.


1. Vəqf: Sadə MonoGame Sprite İerarxiyası

Toqquşmaları aşkar etməzdən əvvəl bizə təmiz oyun obyektləri lazımdır. MonoGame-də obyekt əsas olaraq mövqeyə, fakturaya və məhdudlaşdırıcı düzbucaqlıya malikdir.

Oyunlarımızda istifadə olunan əsas obyekt arxitekturası budur:

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

İndi gəlin konkret “Oyunçu”, “Düşmən” və “Güllə” siniflərini yaradaq:

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

Bu obyektlər mövcud olduqda, gəlin ən sadə üsuldan başlayaraq, onlar arasında toqquşmaların necə aşkar ediləcəyini araşdıraq.


2. Səviyyə 1: Ən Sadə Toqquşma – Düzbucaqlı.Kəsişir (AABB)

MonoGame-də ən əsas 2D toqquşma yoxlanışı Axis-Aligned Bounding Box (AABB) testidir. "Oxa düzülmüş" termini sadəcə olaraq düzbucaqlının kənarlarının ekranın \(X\) və \(Y\) oxlarına tam paralel olduğunu bildirir (fırlanma yoxdur).

MonoGame sürətli, daxili metodu təmin edir: Düzbucaqlı.Kəsişir (Düzbucaqlı dəyəri).

Düzbucaqlı.Kəsişir Başlıq Altında Necə İşləyir

Səthin altında MonoGame dörd tam müqayisəni həyata keçirir:

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

Bütün dörd şərt yerinə yetirilərsə, düzbucaqlılar üst-üstə düşür. Hətta bir şərt uğursuz olarsa, onları boş ox ayırır və heç bir toqquşma mümkün deyil.

Real Gameplay Kodu: Game1.Update-də Bullet vs. Düşmən

Əsas MonoGame Yeniləmə döngənizdə aktiv güllələrin siyahısı ilə aktiv düşmənlərin siyahısı arasında toqquşmaları necə yoxlayırsınız:

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

Performans İpucu: Geriyə doğru təkrarladığımıza diqqət yetirin (for (int i = list.Count - 1; i >= 0; i--))! Əgər foreach istifadə edib _bullets.Remove(bullet) çağırmağa cəhd etsəniz, C# InvalidOperationException: Kolleksiya dəyişdirildi atır. Geriyə doğru iterasiya yaddaşın yenidən indeksləşdirilməsi problemi olmadan elementin təhlükəsiz çıxarılmasına imkan verir.

Arcade sirri: "Şişirmək" vasitəsilə "Ədalətli Hitboxlar"

Bloklanmış: Pixel Panzer kimi retro oyunlarda sprite teksturalarına tez-tez şəffaf kənarlar və ya antenanın ucları daxildir. Bir güllə onun toxumasının boş şəffaf küncünə dəydiyi üçün oyunçunun tankı partlayırsa, oyunçu özünü aldadılmış hiss edəcək.

Toqquşmanın həssas və ədalətli olmasını təmin etmək üçün oyunlar Rectangle.Inflate istifadə edərək sprite daxilində daha kiçik Hitbox istifadə edir:

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. Səviyyə 2: Dairədən Dairəyə Toqquşma (Rotasiya İmmuniteti)

Düzbucaqlılar statik bloklar və şəbəkə plitələri üçün əla işləyir, lakin spritlər fırlananda uğursuz olur. Qeyri-kvadrat kosmik gəmi Paint Trek-də fırlandıqda, oxa düzülmüş məhdudlaşdırıcı qutu fırlanan küncləri əhatə etmək üçün genişlənməlidir, nəticədə boş havada əsəbiləşən "fantom toqquşmalar" yaranır.

Dairəvi asteroidlər, hədəf enerji kürələri və fırlanan kosmik gəmilər üçün Bounding Circles ideal həlldir.

Kvadrat Kök Tələsi

Mərkəzləri arasındakı məsafə radiuslarının cəmindən az və ya bərabər olduqda iki dairə toqquşur:

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

Kodda, Evklid məsafəsinin hesablanmasında Math.Sqrt (və ya Vector2.Distance) istifadə olunur. Bununla belə, 200 mərmi ilə bir döngədə kvadrat köklərin hesablanması yüzlərlə lazımsız CPU dövrəsini yandırır!

kvadrat məsafəni ilə kvadrat radius cəmi ilə müqayisə edərək, kvadrat kökü tamamilə aradan qaldırırıq:

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

MonoGame Tətbiqi: 'Dairə' və 'Dairə'

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

İndi bunu birbaşa quruma inteqrasiya edin:

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

Sıfır kvadrat kök, sprite fırlanmasına qarşı immunitet və ildırım sürəti.


4. Səviyyə 3: Qarışıq Formalar – Dairə və Qutu ('MathHelper.Clamp')

Paint Trek-də dairəvi kosmik gəmi düzbucaqlı müdafiə maneələrinin sıx labirintindən keçdikdə və ya dəyirmi güllə Bloklanmış: Piksel Panzer-də kvadrat bloka dəydikdə nə baş verir?

Bizə Dairə ilə Düzbucaqlı toqquşması lazımdır.

Sıxma alqoritmi

Strategiya düzbucaqlıda dairənin mərkəzinə ən yaxın nöqtəni tapmaq və sonra həmin ən yaxın nöqtədən mərkəzə olan məsafənin dairənin radiusundan az olub olmadığını yoxlamaqdır.

MonoGame-in 'MathHelper.Clamp' bunu əhəmiyyətsiz edir:

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

İndi siz oyunçu qalxanlarını düzbucaqlı kərpiclərə qarşı sıfır ayırma və yüksək dəqiqliklə sınaqdan keçirə bilərsiniz!


5. Səviyyə 4: Davamlı Toqquşmanın Aşkarlanması (CCD) və Süpürülən Şüalar

Siz heç oyununuzda hiper-sürətli snayper raundunu və ya relsli tüfəng lazerini atəşə tutmusunuz, ancaq güllənin sehirli şəkildə nazik düşmən gəmisindən zərər görmədən düz keçməsini izləmisinizmi?

Bu baq tunelinq kimi tanınır.

Diskret oyunlar zaman addımları ilə yeniləndiyinə görə (\(\Delta t = 16.6\text{ms}\) 60 FPS-də) saniyədə 1800 piksel sürətlə hərəkət edən obyekt bir kadrda 30 piksel səyahət edir. Əgər düşmən gövdəsinin qalınlığı cəmi 15 pikseldirsə, güllə 1-ci kadrda düşmənin qarşısında, 2-ci kadrda isə tamamilə düşmənin arxasında idi.

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

Həll yolu: Süpürülən Seqment Qutuya qarşı (Slab Metodu)

Tək nöqtəni yoxlamaq əvəzinə, 1-ci Çərçivədə (əvvəlkiMövqe) 2-ci Kadrda (cariMövqe) güllənin mövqeyini birləşdirən bütün xətt seqmentini sınaqdan keçiririk.

Budur, SpiralWar başlığımızdan istehsal raycasting plitələr kəsişmə üsulu:

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

Bloklanmış: Piksel Panzer oyununda oyunçunun davamlı Lazer Şüa bacarığı bir toqquşmanı əldən vermədən enən blokların cərgələrini kəsmək üçün bu dəqiq raycastdan istifadə edir.


6. Səviyyə 5: İstehsal dərəcəli Piksel-Mükəmməl Toqquşmanın Aşkarlanması

İndi biz 2D dəqiqliyin son səviyyəsinə çatırıq: Piksel-Mükəmməl Toqquşma.

Retro tank atıcısı və ya kosmik gəmi it dalaşında nizamsız formalar (tank çəlləkləri, qanadlar, kokpit kokpitləri) sprite teksturasında şəffaf piksellərlə əhatə olunur. Düşmən raketi o şəffaf məkana dəyəndə, oyunçular dərhal fərq edirlər.

Piksel-mükəmməl toqquşma üst-üstə düşən fakturaların faktiki alfa (şəffaflıq) kanallarını yoxlayır. İki qeyri-şəffaf piksel eyni dünya koordinatında üst-üstə düşürsə, əsl fiziki vuruş baş verdi.

Ölümcül səhv: Update() daxilində GetData

Bir çox dərslik yeni başlayanlara bunu etməyi tapşırır:

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

İş vaxtı zamanı Texture2D.GetData() çağırılması CPU-nu GPU boru kəmərinin yuyulmasını gözləməyə məcbur edir, faktura yaddaşını avtobusun üzərindən geri çəkir. Bunu hər kadrda onlarla sprit üçün etmək oyununuzu 60 FPS-dən 5 FPS-ə qədər yığacaq və dərhal Android ANR-lərini işə salacaq!

Bloklanmış: Pixel Panzer-dən Döyüş Yolu ilə Sınaq Həll

Bloklanmış: Pixel Panzer-də 'Sprite.cs'imiz 5 mühüm qayda ətrafında qurulmuş ultra optimallaşdırılmış 'IntersectsPixel' rutinini həyata keçirir:

  1. Statik Rəng Massivi Keşi: GetData faktura yükləndikdə bir dəfə çağırılır və Lüğət<Texture2D, Rəng[]>-də saxlanılır.
  2. AABB Erkən Çıxış Mühafizəsi: Əgər Sərhədlər.Kəsişir(digər.Sərhədlər) yanlışdırsa, biz dərhal zəmanət veririk. Bir pikseli yoxlamadan əvvəl yoxlamaların 99%-i aradan qaldırılır.
  3. Texture Atlas & SourceDectangle dəstəyi: Mənbə düzbucaqlı ofsetlərindən istifadə edərək faktura atlaslarına yığılmış spritləri idarə edir.
  4. Hesablanmış Üst-üstə düşmə Pəncərəsi: Biz yalnız hər iki sprit (Math.Max(a.Top, b.Top) və s.) arasındakı dəqiq kəsişmə düzbucaqlı üzərində döngə edirik.
  5. Alfa Qısa Qapanma: Əgər Sprite A-nın pikseli şəffafdırsa (A <= 20), Sprite B tamamilə atlanır.
  6. Əmlakın qaldırılması: İç içə çevrilmiş döngələrdə virtual xassələrə ('Sərhədlər') daxil olmaq minlərlə struktur nüsxəsini yaradır. Döngəyə daxil olmamışdan əvvəl onları yerli stek dəyişənlərində saxlayırıq.

Budur tam, istehsala hazır kod:

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

Bu proqram müasir Android aparat və masaüstü sistemlərində 0,1 millisaniyədə yerinə yetirilir.


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




Hiç yorum yok:

Yorum Gönder