25 Eylül 2026 Cuma

MonoGame’de 2D Çarpışma Tespiti: Basit Dikdörtgenlerden Sıfır-GC Piksel-Hassasiyetine




MonoGame’de 2D Çarpışma Tespiti: Basit Dikdörtgenlerden Sıfır-GC Piksel-Hassasiyetine

Unity veya Godot gibi görsel oyun motorlarında 2D oyun geliştirirken çarpışma tespiti (collision detection) genellikle arayüzdeki birkaç onay kutusundan ibarettir: Nesneye bir BoxCollider2D veya CircleCollider2D eklersiniz, bir Rigidbody iliştirirsiniz ve arka plandaki fizik motorunun mobil cihazlarda takılma yapmamasını umarsınız.

Oysa MonoGame ve C# dünyasında kontrolün tamamı sizin elinizdedir. Gizli bir fizik motoru yükü, istenmeyen dönme sürtünmesi ve kare bütçenizi çalan gizemli çöp toplayıcı (Garbage Collection - GC) takılmalarıyla uğraşmazsınız.

Arar Games olarak Blocked: Pixel Panzer ve Paint Trek oyunlarımızı geliştirirken, arcade oyun döngümüzün hem Windows PC'de hem de Android mobil cihazlarda saniyede 60 ila 120 FPS hızında yüzlerce yüksek hızlı düşman mermisini, patlayan tuğla ızgaralarını, dönen tank taretlerini, savaş uçaklarını ve koruyucu enerji kalkanlarını işlemesi gerekiyordu. Genel amaçlı ağır fizik kütüphaneleri bu hız için hantal kalıyordu; bu yüzden amaca özel katmanlı bir çarpışma mimarisi inşa ettik.

Bu rehberde, lafı uzatmadan doğrudan çalışan kodlarla ilerleyeceğiz: En basit MonoGame çarpışmasından (Rectangle.Intersects ile somut Bullet ve Enemy sprite nesneleri) başlayacak; daire kontrolleri, karma şekil kırpmaları, mermilerin duvardan geçmesini önleyen raycast çözümleri, üretim seviyesinde Pixel-Perfect (Piksel-Hassas) çarpışma ve Android ANR kilitlenmelerini bitiren Sıfır-Tahsisli Uzamsal Izgara (Spatial Hash Grid) mimarisine kadar adım adım kodlayacağız.


1. Temel Yapı: Temiz Bir MonoGame Sprite Hiyerarşisi

Çarpışma tespiti yapabilmek için önce ekranda hareket eden temiz varlıklara ihtiyacımız var. MonoGame'de bir nesne temel olarak bir konuma (Vector2), bir dokuya (Texture2D) ve bir sınırlayıcı dikdörtgene (Rectangle) sahiptir.

İşte oyunlarımızda temel aldığımız taban sınıfı:

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

Şimdi bu tabandan türeyen somut Player, Enemy ve Bullet sınıflarımızı oluşturalım:

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 temel sınıflarımız hazır olduğuna göre, aralarındaki çarpışmaları en basitten en karmaşığa doğru test etmeye başlayalım.


2. Kademe 1: En Basit Çarpışma – Rectangle.Intersects (AABB)

MonoGame’de en temel ve en hızlı 2D çarpışma testi Axis-Aligned Bounding Box (AABB) yöntemidir. "Eksenle hizalı" ifadesi, dikdörtgenin kenarlarının ekranın Kartezyen \(X\) ve \(Y\) eksenlerine her zaman paralel olduğu (hiçbir rotasyon içermediği) anlamına gelir.

MonoGame bunun için son derece hızlı bir yerleşik metoda sahiptir: Rectangle.Intersects(Rectangle value).

Rectangle.Intersects Arka Planda Nasıl Çalışır?

MonoGame kaynak kodunda bu metot tam olarak dört tamsayı karşılaştırması yapar:

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

Dört koşul da sağlanıyorsa iki dikdörtgen kesişiyordur. Tek bir koşul bile bozulursa dikdörtgenler arasında boş bir eksen vardır ve çarpışma imkansızdır.

Gerçek Oyun Döngüsü: Game1.Update İçinde Mermi ve Düşman Kontrolü

Aktif mermiler ve düşmanlar arasındaki çarpışmayı ana Update döngüsünde şu şekilde kontrol ederiz:

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: Listeleri geriye doğru (for (int i = list.Count - 1; i >= 0; i--)) taradığımıza dikkat edin! foreach döngüsü içinde _bullets.Remove(bullet) çağırmaya çalışırsanız C# derhal InvalidOperationException: Collection was modified hatası fırlatır. Sondan başa doğru döngü kurmak, liste elemanlarını bellek indeksi kayması olmadan güvenle silmenizi sağlar.

Arcade Sırrı: Inflate ile "Adil Hitbox" Tasarımı

Blocked: Pixel Panzer gibi retro arcade oyunlarında sprite dokularının kenarlarında genellikle boş saydam pikseller veya anten çıkıntıları bulunur. Eğer oyuncunun tankı merminin o boş saydam piksele değmesiyle patlarsa oyuncu haksızlığa uğradığını hisseder.

Çarpışmayı oyuncuya karşı adil ve pürüzsüz hissettirmek için Rectangle.Inflate kullanarak hitbox'ı sprite dokusundan birkaç piksel içeri çekeriz:

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. Kademe 2: Daire-Daire Çarpışması (Dönme Bağışıklığı)

Dikdörtgenler sabit bloklar için harikadır; ancak sprite'lar dönmeye başladığında sınırlayıcı dikdörtgen köşeleri kapsamak için devasa boyutlara genişler. Bu da boş havada mermi çarpmış gibi algılanan "hayalet temaslara" (phantom collisions) neden olur.

Paint Trek'teki dönen uzay gemileri, dairesel asteroitler ve küresel enerji kalkanları için Sınırlayıcı Daireler (Bounding Circles) kusursuz çözümdür.

Karekök Tuzağı

İki daire, merkezleri arasındaki mesafe yarıçaplarının toplamından küçük veya eşitse çarpışır:

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

Kod yazarken iki nokta arasındaki mesafeyi bulmak için Math.Sqrt ya da Vector2.Distance kullanılır. Ancak ekranda 200 mermi varken her karede yüzlerce kez karekök hesaplamak işlemciyi fuzuli yere yorar!

Eşitsizliğin her iki tarafının karesini alarak karekök alma maliyetinden tamamen kurtuluruz:

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

MonoGame Uygulaması: Daire ve Daire Kesişimi

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

Bunu bir oyun nesnesine entegre etmek son derece kolaydır:

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 karekök işlemi, sprite rotasyonundan tamamen bağımsız ve inanılmaz derecede hızlı.


4. Kademe 3: Karma Şekiller – Daire ve Dikdörtgen (MathHelper.Clamp)

Paint Trek'te dairesel bir uzay gemisi dar koridorlardaki dikdörtgen savunma duvarları arasından geçerken veya Blocked: Pixel Panzer'de yuvarlak bir bomba kare bir bloğa çarptığında ne yapacağız?

Bize Daire vs. Dikdörtgen çarpışması gerekir.

Kırpma (Clamping) Algoritması

Temel mantık; dikdörtgenin yüzeyinde dairenin merkezine en yakın olan noktayı bulmak ve o nokta ile daire merkezi arasındaki karesel mesafenin daire yarıçapının karesinden küçük olup olmadığını kontrol etmektir.

MonoGame'in yerleşik MathHelper.Clamp metodu bunu iki satıra indirir:

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

Böylece tek bir bellek tahsisi yapmadan karmaşık geometriyi çözmüş olursunuz.


5. Kademe 4: Sürekli Çarpışma Tespiti (CCD) ve Işın Kırpma (Raycasting)

Oyununuzda saniyede binlerce piksel hızla uçan bir keskin nişancı mermisi ya da lazer attığınızda, merminin ince bir düşmanın içinden hiçbir hasar vermeden geçip gittiğini gördünüz mü?

Bu hataya oyun geliştirmede Tünelleme (Tunneling) denir.

Oyunlar zaman adımlarıyla güncellendiği için (\(16.6\text{ms}\)), saniyede 1.800 piksel hızla giden bir mermi tek bir karede 30 piksel zıplar. Önündeki düşman gövdesi 15 piksel kalınlığındaysa; mermi 1. Karede düşmanın önündedir, 2. Karede ise arkasına geçmiştir. Ayrık kontroller sırasında düşmanla hiçbir karede kesişmemiştir!

1. Kare:  [ Mermi ]  --->       | Duvar / Düşman |
2. Kare:                        | Duvar / Düşman |       --->  [ Mermi ]
                               (ÇARPIŞMA KAÇIRILDI!)

Çözüm: Süpürülmüş Işın ve Kutu Kesişimi (Slab Yöntemi)

Tek bir nokta yerine merminin önceki karedeki konumu ile bu karedeki konumu arasına çekilen doğru parçasını (ışın) hedef kutunun paralel düzlemleriyle kesiştiririz:

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

Blocked: Pixel Panzer oyunundaki Lazer Işını Becerisi tam olarak bu algoritmayı kullanarak yukarıdan inen blok dizisini anında keser ve tünelleme hatası yaşamaz.


6. Kademe 5: Üretim Standardında Pixel-Perfect (Piksel-Hassas) Çarpışma

Şimdi 2D hassasiyetin zirvesine geliyoruz: Pixel-Perfect Çarpışma.

Düzensiz açılara sahip bir tank namlusu veya kanatlı bir uzay gemisi düşünün. Dikdörtgenler dokunun köşelerindeki şeffaf alanları da içine alır. Gerçek bir isabet olup olmadığını anlamak için örtüşen piksellerin alfa (saydamlık) değerlerini taramamız gerekir.

Ölümcül Hata: Update() İçinde GetData Çağırmak

İnternetteki pek çok başlangıç rehberi şu feci hatayı yapar:

// KESİNLİKLE YAPMAYIN!
Color[] dataA = new Color[textureA.Width * textureA.Height];
textureA.GetData(dataA); // GPU HATTINI KİLİTLER, GC KASILMALARI YARATIR!

Oyun döngüsü içinde Texture2D.GetData() çağırmak CPU'yu GPU hattını beklemeye zorlar. Bunu her karede onlarca nesne için yapmak FPS'inizi 60'tan 5'e düşürür ve Android'de doğrudan çökmelere neden olur!

Blocked: Pixel Panzer Üretim Çözümü

Blocked: Pixel Panzer oyunumuzun Sprite.cs dosyasındaki IntersectsPixel metodu beş katı optimizasyon kuralına göre çalışır:

  1. Statik Renk Dizisi Önbelleği: GetData doku yüklendiğinde yalnızca bir kez çağrılır ve Dictionary<Texture2D, Color[]> içinde saklanır.
  2. AABB Erken Çıkış Muhafızı: Eğer Bounds.Intersects(other.Bounds) yanlışsa tek bir piksel bile taranmadan anında çıkılır (kontrollerin %99'u burada elenir).
  3. SpriteSheet / SourceRectangle Desteği: Tek bir büyük sprite atlasında yer alan alt-dokuların ofsetlerini kusursuz eşler.
  4. Sınırlandırılmış Kesişim Penceresi: Tüm doku yerine yalnızca iki dikdörtgenin kesiştiği dar pencere taranır (Math.Max(a.Top, b.Top) vb.).
  5. Alfa Erken Çıkışı (Early Bailout): Eğer Sprite A'nın pikseli şeffafsa (A <= 20), Sprite B'nin pikseli hiç taranmaz.
  6. Özellik (Property) Kaldırma: Bounds gibi struct döndüren özellikleri iç döngüde çağırmak binlerce gereksiz kopyalama yaratır; bunlar döngü başında yerel değişkenlere alınır.

İşte tam çalışan üretim kodu:

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 metot, en yoğun sahnelerde bile Android telefonlarda ve Windows cihazlarında milisaniyenin onda biri sürede kusursuz çalışır.


7. Kademe 6: Uzamsal Izgara (Spatial Grid) ve Sıfır-Tahsisli Mobil GC Mimarisi

Sahnede 100 blok, 60 mermi ve 20 düşman olduğunda iç içe for döngüleriyle her nesneyi birbiriyle karşılaştırırsanız:

\(\frac{180 \times 179}{2} = \text{Kare başına } 16.110 \text{ kontrol!}\)

60 FPS hızında bu, saniyede yaklaşık 1.000.000 çarpışma kontrolü demektir.

Bunu çözmek için ekranı sanal ızgara hücrelerine böleriz (Blocked: Pixel Panzer'de her hücre \(150\times 150\) pikseldir). Bir nesne yalnızca aynı ızgara hücresinde bulunan diğer nesnelerle test edilir.

Android ve Mobil Bellek Kâbusu: GC Thrashing

Eğer her karede new List<Point>() veya new List<Sprite>() oluşturursanız, yönetilen belleğe dakikada megabaytlarca çöp yığarsınız.

Android Mono çalışma zamanında bu durum sürekli çöp toplayıcıyı (GC) tetikler ve şu meşhur kilitlenmeye yol açar: mono runtime: Native lock contention (mono_class_is_subclass_)

Oyun takılır, kare düşer ve Google Play konsolunda ANR (Uygulama Yanıt Vermiyor) uyarıları patlar!

Çözüm: Sıfır-Tahsisli Tamponlar (Scratch Buffers)

İşte Blocked: Pixel Panzer'in CollisionManager.cs dosyasında uyguladığımız Sıfır-Tahsisli Uzamsal Izgara:

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

_scratchCells ve _scratchCheckedEntities listeleri tekrar tekrar temizlenip kullanılarak çarpışma adımı kare başına tam 0 bayt bellek tahsisiyle tamamlanır.


8. Özet Karşılaştırma Tablosu

Teknik Matematiksel Maliyet Rotasyon Desteği Tünelleme Koruması MonoGame'de En Uygun Kullanım
AABB (Rectangle.Intersects) Ultra-Hızlı (~4 tamsayı kontrolü) Yok Yok Izgara blokları, mermi erken kontrolü, UI
Daire (DistanceSquared) Çok Hızlı (3 çarpma, 0 karekök) Var (Bağışık) Yok Yuvarlak gemiler, ateş topları, kalkanlar
Daire vs Kutu (Clamp) Hızlı (Yerel MathHelper.Clamp) Var Yok Duvarlar arasında gezen yuvarlak oyuncu
Süpürülmüş Işın (CCD) Orta (Parametrik Doğru Parçası) Var Var Hızlı roketler, kesintisiz lazerler
Pixel-Perfect (IntersectsPixel) Seçici (Yalnızca Kesişen Alan) Var Yok Girintili sprite hatları, adil temaslar
Uzamsal Izgara (Sıfır-GC) Geniş Faz (\(O(N^2) \to O(N)\)) İlgisiz İlgisiz Yoğun dalgalar, Android'de 60 FPS akıcılık

Gerçek Oyunlarımızdan Referanslar: Arar Games

Bu sistemler sadece teorik anlatımlar değildir; mağazalarda yer alan yapımlarımızın kalbidir:

  • Blocked: Pixel Panzer: İki kademeli çarpışma mimarimizin en eksiksiz örneğidir. ColorWheel mühimmatı, dönen oyuncu taretleri, patlayan blok formasyonları ve lazer ışınları; sıfır-tahsisli spatial grid sayesinde Google Play ve Windows'ta 60'tan fazla dilde 60 FPS akıcılıkla çalışır.
  • Paint Trek: Rotasyonel daire-daire çarpışmaları ve süpürülmüş ışın algoritmalarıyla yüksek tempolu retro uzay savaşlarını ve mermi cehennemi kaçış dinamiklerini yönetir.

Sonuç

MonoGame, gereksiz motor yüklerinden arınmış yalın yapısıyla amaca özel yüksek performanslı çarpışma sistemleri kurmanız için mükemmel bir ortam sunar. Ağır Pixel-Perfect testlerini hızlı AABB filtrelerinin arkasına gizleyerek, Daire matematiğinde kareköklerden kaçınarak ve Sıfır-Tahsisli Uzamsal Izgaralar ile bellek temizleyicisini rahatlatarak her cihazda akıcı ve pürüzsüz oyunlar inşa edebilirsiniz.

Siz MonoGame projelerinizde hangi çarpışma zorluklarıyla karşılaştınız? Mağaza bağlantılarından oyunlarımızı inceleyebilir ve deneyimlerinizi bizimle paylaşabilirsiniz!


Mağaza Bağlantıları ve Kaynaklar


SEO Anahtar Kelimeleri ve Etiketler

Anahtar Kelimeler: MonoGame 2D çarpışma tespiti, C# oyun geliştirme, Rectangle.Intersects MonoGame, pixel perfect collision C#, spatial hash grid MonoGame, sürekli çarpışma tespiti MonoGame, bağımsız oyun performans optimizasyonu, sıfır tahsisli oyun döngüsü, Android MonoGame optimizasyonu, Arar Games, Blocked Pixel Panzer, Paint Trek.

#MonoGame #CSharp #GameDev #IndieDev #GamePhysics #DotNet #2DGameDev #MobileGameDev #PerformansOptimizasyonu #CleanCode #BlockedPixelPanzer #PaintTrek #ArarGames #OyunGeliştirme




Hiç yorum yok:

Yorum Gönder