2D fizika etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster
2D fizika etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster

27 Eylül 2026 Pazar

MonoGame-da 2D to'qnashuvni aniqlash: asosiy to'rtburchaklardan nol taqsimlangan pikselli mukammal tizimlargacha

MonoGame-da 2D to'qnashuvni aniqlash: asosiy to'rtburchaklardan nol taqsimlangan pikselli mukammal tizimlargacha

Unity yoki Godot kabi vizual dvigatellarda 2D oʻyinlarni yaratishda toʻqnashuvni aniqlash koʻpincha inspektorning katakchalari qatoriga oʻxshab ketadi: siz “BoxCollider2D” yoki “CircleCollider2D” ni bosing, “Rigidbody”ni biriktirasiz va mobil qurilmalarda ichki fizika qadami qoqilmasligiga umid qilasiz.

MonoGame va C#-da esa siz to'liq nazorat qilasiz. Hech qanday yashirin fizika yuki, istalmagan aylanish inertsiyasi va sizning ramka byudjetingizni o'g'irlaydigan sirli axlat yig'ish (GC) yo'q.

Arar Games da, Blocked: Pixel Panzer va Paint Trek-ni yaratganimizda, bizning arkada o‘yin halqalarimiz yuzlab yuqori tezlikdagi dushman o‘qlarini, portlovchi g‘isht to‘rlarini, aylanuvchi tank minoralarini, qiruvchi samolyotlarning parvozlarini va zarracha qalqonlarini Windows va Android qurilmalarida 60 dan 120 PS gacha tezlikda qayta ishlash uchun kerak edi. Umumiy maqsadli fizika dvigateli haqida gap bo'lishi mumkin emas edi - bizga maqsadli qurilgan, bosqichli to'qnashuv arxitekturasi kerak edi.

Ushbu keng qamrovli, kodga asoslangan qo'llanmada biz MonoGame to'qnashuvining mutlaq asoslaridan boshlaymiz ("To'rtburchak. Kesishlar" haqiqiy "O'q" va "Dushman" spritlari bilan) va ilg'or aylana tekshiruvlari, aralash qisish, tunnelga qarshi raykaslar, ishlab chiqarish darajasidagi **Pixelsion-Zero-Pixelsion-Zo'lik va Alli-Pixel-Pixel-Pixel-Pixel-Pixel-Pixel-Pixel-Pixel-Pixel-Pixel-Pixel-Pixel-Zo'ga qarshi ilg'or tekshiruvlarni o'tkazamiz. Grids mobil GC omon qolish uchun optimallashtirilgan.


1. Jamg'arma: oddiy MonoGame "Sprite" ierarxiyasi

To'qnashuvlarni aniqlashdan oldin bizga toza o'yin ob'ektlari kerak. MonoGame-da ob'ekt asosan pozitsiyaga, teksturaga va chegaralovchi to'rtburchakka ega.

Mana bizning o'yinlarimizda ishlatiladigan asosiy ob'ekt arxitekturasi:

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

Keling, aniq “Player”, “Dushman” va “Bullet” sinflarini yarataylik:

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

Ushbu ob'ektlar mavjud bo'lganda, keling, eng oddiy usuldan boshlab ular orasidagi to'qnashuvlarni qanday aniqlashni o'rganamiz.


2. 1-daraja: Eng oddiy toʻqnashuv – “Toʻrtburchaklar kesishadi” (AABB)

MonoGame-dagi eng asosiy ikki o'lchovli to'qnashuv tekshiruvi bu Axis-Aligned Bounding Box (AABB) testidir. "O'qga tekislangan" atamasi oddiygina to'rtburchakning qirralari ekranning \(X\) va \(Y\) o'qlariga to'liq parallel ekanligini bildiradi (aylanish yo'q).

MonoGame tezkor, o'rnatilgan usulni taqdim etadi: To'rtburchak. Kesishadi (To'rtburchak qiymati).

To'rtburchak. Kesishadi kaput ostida qanday ishlaydi

Sirt ostida MonoGame to'rtta butun son taqqoslashni amalga oshiradi:

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

Agar to'rtta shart bajarilsa, to'rtburchaklar bir-biriga yopishadi. Agar bitta shart bajarilmasa, bo'sh o'q ularni ajratib turadi va hech qanday to'qnashuv mumkin emas.

Haqiqiy oʻyin kodi: “Game1.Update”dagi “Bullet” va “Dushman” oʻrtasidagi

Asosiy MonoGame Update tsiklingizdagi faol o'qlar ro'yxati va faol dushmanlar ro'yxati o'rtasidagi to'qnashuvni qanday tekshirishingiz mumkin:

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

Ishlash bo'yicha maslahat: E'tibor bering, biz orqaga qarab takrorlaymiz (for (int i = list.Count - 1; i >= 0; i--))! Agar siz "foreach" dan foydalansangiz va "_bullets.Remove(bullet)" ga qo'ng'iroq qilmoqchi bo'lsangiz, C# "InvalidOperationException: Collection o'zgartirildi" ni chiqaradi. Orqaga takrorlash xotirani qayta indekslash muammosisiz elementni xavfsiz olib tashlash imkonini beradi.

Arkada siri: "Inflate" orqali "Fair Hitboxes"

Blocked: Pixel Panzer kabi retro o‘yinlarda sprite teksturalari ko‘pincha shaffof chegaralar yoki antenna tirqishlarini o‘z ichiga oladi. Agar o'yinchining tanki o'q uning teksturasining bo'sh shaffof burchagiga tegishi sababli portlasa, o'yinchi o'zini aldangandek his qiladi.

Toʻqnashuvni sezgir va adolatli qilish uchun oʻyinlar “Rectangle.Inflate” yordamida sprite ichidagi kichikroq Hitbox dan foydalanadi:

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. 2-daraja: Aylanadan aylanaga to‘qnashuv (aylanish immuniteti)

To'rtburchaklar statik bloklar va panjara plitalari uchun juda yaxshi ishlaydi, lekin spritlar aylanganda ular muvaffaqiyatsizlikka uchraydi. Kvadrat bo'lmagan kosmik kema Paint Trek da aylansa, aylanuvchi burchaklarni o'rab olish uchun o'q bo'ylab tekislangan chegara qutisi kengayishi kerak, bu esa bo'sh havoda "fantom to'qnashuvlari" ga olib keladi.

Dumaloq asteroidlar, maqsadli energiya sharlari va aylanuvchi kosmik kemalar uchun Bounding Circles ideal yechimdir.

Kvadrat ildiz tuzog'i

Ikki doira markazlari orasidagi masofa radiuslari yig‘indisidan kichik yoki teng bo‘lganda to‘qnashadi:

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

Kodda Evklid masofasini hisoblashda "Math.Sqrt" (yoki "Vektor2.Distance") ishlatiladi. Biroq, kvadrat ildizlarni 200 ta snaryadli halqada hisoblash yuzlab keraksiz CPU tsikllarini yoqib yuboradi!

kvadrat masofani kvadrat radius yig‘indisi bilan solishtirib, biz kvadrat ildizni butunlay yo‘q qilamiz:

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

MonoGame Amalga oshirish: "Circle" va "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);
    }
}

Endi buni to'g'ridan-to'g'ri ob'ektga integratsiya qiling:

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

Nol kvadrat ildizlar, sprite aylanishiga qarshi immunitet va chaqmoq tezligida.


4. 3-daraja: Aralash shakllar – doira va quti (MathHelper.Clamp)

Paint Trek da dumaloq kosmik kema to‘rtburchaklar shaklidagi mudofaa to‘siqlaridan iborat qattiq labirintdan o‘tib ketsa yoki Bloklangan: Pixel Panzer da yumaloq o‘q kvadrat blokga tegsa nima bo‘ladi?

Bizga Circle vs. Rectangle toʻqnashuvi kerak.

Siqish algoritmi

Strategiya to'rtburchakda aylananing markaziga eng yaqin nuqtani topib, so'ngra eng yaqin nuqtadan markazgacha bo'lgan masofa doira radiusidan kichikroq yoki yo'qligini tekshirishdan iborat.

MonoGame 'MathHelper.Clamp' buni ahamiyatsiz qiladi:

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

Endi siz o'yinchi qalqonlarini to'rtburchaklar g'ishtlarga qarshi nol taqsimot va yuqori aniqlik bilan sinab ko'rishingiz mumkin!


5. 4-daraja: Uzluksiz toʻqnashuvni aniqlash (CCD) va oʻchirilgan nurlar

Siz o'z o'yiningizda o'qning sehrli tarzda yupqa dushman kemasidan to'g'ridan-to'g'ri o'tib ketishini tomosha qilish uchun gipertezlikdagi snayper yoki relsli to'pponchani o'qqa tutganmisiz?

Bu xato tunnellash nomi bilan tanilgan.

Diskret o'yinlar vaqt bosqichlarida yangilanganligi sababli (60 FPSda \(\Delta t = 16,6\text{ms}\)), soniyada 1800 piksel tezlikda harakatlanuvchi ob'ekt bir kadrda 30 piksel sayohat qiladi. Agar dushman korpusining qalinligi atigi 15 piksel bo'lsa, o'q 1-ramkada dushman oldida, 2-ramkada esa butunlay dushman orqasida edi.

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

Yechim: Supurilgan segment va quti (plitalar usuli)

Bitta nuqtani sinab ko'rish o'rniga, biz o'qning 1-kadrdagi o'rnini (oldingiPosition) 2-framega (joriyPosition) bog'laydigan butun chiziq segmentini sinab ko'ramiz.

SpiralWar nomli hamrohimizdagi ishlab chiqarish raycasting plita kesish usuli:

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 o‘yinida o‘yinchining uzluksiz Laser Beam mahorati bitta to‘qnashuvni o‘tkazib yubormasdan pastga tushuvchi bloklar qatorini kesib o‘tish uchun aynan shu raycastdan foydalanadi.


6. 5-daraja: ishlab chiqarish darajasidagi piksel-mukammal to‘qnashuvni aniqlash

Endi biz 2D aniqlikning eng yuqori darajasiga erishdik: Pixel-Perfect Collision.

Retro tank otuvchi yoki kosmik kema it jangida tartibsiz shakllar (tank bochkalari, qanotlar, kokpit kabinalari) sprite teksturasida shaffof piksellar bilan o'ralgan. Dushman raketasi shaffof bo'shliqqa tushganda, o'yinchilar darhol buni sezadilar.

Piksel-mukammal to'qnashuv bir-biriga yopishgan teksturalarning haqiqiy alfa (shaffoflik) kanallarini tekshiradi. Agar ikkita shaffof bo'lmagan piksel bir xil dunyo koordinatasida bir-biriga to'g'ri kelsa, haqiqiy jismoniy zarba sodir bo'ldi.

Fatal xato: Update() ichidagi GetData

Ko'pgina darsliklar yangi boshlanuvchilarga buni qilishni buyuradi:

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

Ishlash vaqtida "Texture2D.GetData()" ga qo'ng'iroq qilish protsessorni GPU quvur liniyasining tozalanishini kutishga majbur qiladi va tekstura xotirasini avtobus orqali orqaga tortadi. Buni har bir kadrda oʻnlab spritlar uchun qilish oʻyiningizni 60 FPS dan 5 FPS gacha oshiradi va Android ANR’larini darhol ishga tushiradi!

Bloklangan: Pixel Panzer dan jangovar sinovdan o'tgan yechim

Bloklangan: Pixel Panzer ilovasida ‘Sprite.cs’ 5 ta muhim qoida atrofida tuzilgan ultra optimallashtirilgan “IntersectsPixel” dasturini amalga oshiradi:

  1. Statik rang massivi keshi: GetData tekstura yuklanganda aynan bir marta chaqiriladi va Dictionary<Texture2D, Color[]>da saqlanadi.
  2. AABB Early Exit Guard: Agar Bounds.Intersects(other.Bounds) noto'g'ri bo'lsa, biz darhol kafolat beramiz. Bitta pikselni tekshirishdan oldin tekshiruvlarning 99% yo'q qilinadi.
  3. Texture Atlas va SourceRectangle qoʻllab-quvvatlashi: Toʻgʻri toʻrtburchaklar ofset manbalaridan foydalangan holda tekstura atlaslariga oʻralgan spritlarni boshqaradi.
  4. Hisoblangan ustma-ust oyna: Biz faqat ikkala spritlar (Math.Max(a.Top, b.Top) va h.k.) oʻrtasidagi aniq kesishgan toʻrtburchak ustidan aylanamiz.
  5. Alfa qisqa tutashuvi: Agar Sprite A pikseli shaffof bo'lsa (A <= 20), Sprite B butunlay o'tkazib yuboriladi.
  6. Mulkni ko'tarish: Ichki ko'chadan virtual xususiyatlarga (Chegaralar) kirish minglab struktura nusxalarini yaratadi. Loopga kirishdan oldin ularni mahalliy stek o'zgaruvchilarida saqlaymiz.

Mana to'liq, ishlab chiqarishga tayyor 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 tartib zamonaviy Android apparat va ish stoli tizimlarida 0,1 millisekunddan kamroq vaqt ichida amalga oshiriladi.


7. 6-daraja: Keng fazali fazoviy tarmoq va nol taqsimlangan mobil GC

Tasavvur qiling-a, sizning o'yiningizda 100 ta blok, 60 ta o'q va 20 ta dushman bor. Agar siz har bir ob'ektni har bir boshqa ob'ektga qarshi o'rnatilgan "for" tsikllari yordamida sinab ko'rsangiz, quyidagilarni bajarasiz:

\(\frac{180 \times 179}{2} = 16,110 \text{ har bir kadr uchun tekshiruv!}\)

60 FPS da, bu har soniyada deyarli 1 000 000 to'qnashuv tekshiruvi.

O'yinni kengaytirish uchun siz Keng fazali fazoviy qismlarga ajratish-ni amalga oshirishingiz kerak.

Yagona fazoviy xesh panjara

Biz ekranni bir xil hujayralardan iborat 2D panjaraga ajratamiz (Bloklangan: Pixel Panzer ichida, har bir hujayra 150$\150$ pikselga teng). Tashkilot faqat bir xil katakchalarda joylashgan boshqa ob'ektlarga qarshi to'qnashuvlarni sinovdan o'tkazadi.

Mobil xotira muammosi: GC Thrashing

Agar SpatialGrid har bir freymda yangi Royxat()yokiyangi Royxat<Blok>() yaratsa, siz boshqariladigan to`plamda har daqiqada megabayt axlatni ajratasiz.

Android-ning Mono ish vaqtida, bu tez-tez Gen-0 axlat yig'ishlarini ishga tushiradi, bu esa quyidagilarga sabab bo'ladi: mono ish vaqti: mahalliy blokirovka bahsi (mono_class_is_subclass_)

O'yin qoqilib ketadi va Google Play o'yiningizni ANR ogohlantirishlari bilan belgilaydi!

Yechim: Qayta foydalanish mumkin bo'lgan skretch buferlari

Mana Bloklangan: Pixel Panzer ichidagi CollisionManager.cs dan nol taqsimlangan fazoviy xesh panjara:

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" va "_scratchCheckedEntities" dan qayta foydalanish orqali to'qnashuvning to'liq bosqichi har bir kadr uchun 0 bayt yig'ish taqsimoti bilan ishlaydi.


8. Xulosa solishtirish jadvali

Texnika Matematik narx Rotatsion yordam Tunnel xavfsizmi? MonoGame-da eng yaxshi foydalanish holati
AABB (Tortburchak. Kesishadi`) Ultra-tezkor (~4 ta butun son taqqoslash) Yo'q Yo'q Grid bloklari, o'q keng fazali, UI
Doira (DistanceSquared) Juda tez (3 muls, 0 sqrt) Ha (Invariant) Yo'q Dumaloq kemalar, olov sharlari, energiya sharlari
Circle vs Box (Clamp) Tez (Mahalliy MathHelper.Clamp) Ha Yo'q Qattiq devorlarda aylanma o'yinchi
Soflangan nur (Slab CCD) O'rtacha (Parametrik Raycast) Ha Ha Tezkor snayper o'qlari, relsli qurollar, lazerlar
Pixel-Perfect (IntersectsPixel) Tanlangan (cheklangan sub-rect) Ha Yo'q Noqonuniy sprite konturlari, adolatli xitbokslar
Fazoviy tarmoq (nol-GC) Keng fazali (\(O(N^2) \to O(N)\)) Yo'q Yo'q Zich to'lqinlar, o'q jahannamlari, Android 60 FPS

Haqiqiy dunyo ishlab chiqarish ko'rgazmalari: Arar o'yinlari

Ushbu to'qnashuv arxitekturalari nazariy eksperimentlar emas - ular bizning tijorat maqsadlarida chiqarilgan nomlarimizni quvvatlaydigan haqiqiy muhandislik asosidir:

  • Bloklangan: Pixel Panzer: Google Play va Microsoft Store do‘konidagi retro tank omon qolish arkada o‘yinimiz. U bizning to'liq ikki bosqichli to'qnashuv tizimimizni o'z ichiga oladi: tushib qolgan bloklarni filtrlaydigan nol taqsimlangan fazoviy xesh panjara va tanklar, qiruvchi samolyotlar, minora snaryadlari va ColorWheel elementar o'q-dorilari uchun piksel-mukammal kontaktni tekshirish.
  • Paint Trek: Aylanma aylana to‘qnashuvi, uzluksiz raykasting va keng ko‘lamli raketaga qarshi mudofaa tizimlarini o‘z ichiga olgan tez sur’atli kosmik otishmamiz.

Xulosa va keyingi qadamlar

MonoGame sizga o'yiningiz ehtiyojlariga to'liq mos keladigan to'qnashuvni aniqlashni loyihalash quvvatini beradi. Qimmatbaho Pixel-Perfect tekshiruvlarini tezkor AABB sinovlaridan o‘tkazish, doiralar uchun Kvadrat masofalar-dan foydalanish va Qayta foydalanish mumkin bo‘lgan skretch buferlari bilan axlat yig‘ishni yo‘q qilish orqali siz ish stoli va mobil platformalarda konsolning 60/120 FPS ishlashini ta’minlay olasiz.

Ushbu to'qnashuv tizimlarini ishlayotganini ko'rish uchun ilovalar do'konlaridagi o'yinlarimizni ko'rib chiqing va bugun o'zingizning MonoGame loyihalaringizda ushbu naqshlarni qo'llashni boshlang!


Do'kon havolalari va manbalari


SEO kalit so'zlari va hashtaglari

Kalit so‘zlar: MonoGame 2D to‘qnashuvni aniqlash, C# o‘yinini ishlab chiqish, Rectangle.Intersects MonoGame, piksel mukammal to‘qnashuvi C#, fazoviy xesh-grid MonoGame, to‘xtovsiz uzatilgan to‘qnashuv, indie o‘yin samaradorligini optimallashtirish, nol taqsimlangan o‘yin tsikli, Android MonoG’ni optimallashtirish, o‘yinni optimallashtirish. Paint Trek.

#MonoGame #CSharp #GameDev #IndieDev #GamePhysics #DotNet #2DGameDev #MobileGameDev #PerformanceOptimization ``#Clean#GameDev #PaintTrek #ArarGames #GameProgramming`

2D zaznavanje trkov v igri MonoGame: od osnovnih pravokotnikov do sistemov z ničelno dodelitvijo slikovnih pik

2D zaznavanje trkov v igri MonoGame: od osnovnih pravokotnikov do sistemov z ničelno dodelitvijo slikovnih pik

Pri izdelavi 2D iger v vizualnih motorjih, kot sta Unity ali Godot, se zaznavanje trka pogosto zdi kot vrsta inšpektorskih potrditvenih polj: udarite po BoxCollider2D ali CircleCollider2D, pritrdite Rigidbody in upate, da korak notranje fizike ne bo zatikal na mobilnih napravah.

V MonoGame in C# pa imate popoln nadzor. Ni nobenih skritih fizikalnih stroškov, nobene neželene rotacijske vztrajnosti in nobenih skrivnostnih konic zbiranja smeti (GC), ki bi ukradli vaš proračun za okvir.

Pri Arar Games, ko smo zgradili Blocked: Pixel Panzer in Paint Trek, so naše zanke arkadnih iger morale obdelati na stotine hitrih sovražnih nabojev, eksplozivnih opečnih mrež, vrtečih se tankovskih kupol, preletov bojnih letal in ščitov proti delcem pri 60 do 120 FPS tako v računalnikih z operacijskim sistemom Windows kot v napravah Android. Splošni fizikalni motor ni prišel v poštev – potrebovali smo namensko izdelano večstopenjsko arhitekturo trkov.

V tem obsežnem vodniku, ki temelji na kodi, bomo začeli z absolutnimi osnovami trka MonoGame ('Rectangle.Intersects' z resničnimi spriti 'Bullet' in 'Enemy') in nadaljevali z naprednimi preverjanji krogov, mešanim vpenjanjem, žarkovnimi oddajami proti tuneliranju, Pixel-Perfect collision in Zero-Allocation Spatial proizvodne stopnje. Mreže** optimizirane za mobilno preživetje GC.


1. Osnova: preprosta hierarhija Sprite MonoGame

Preden zaznamo trke, potrebujemo čiste entitete igre. V igri MonoGame ima entiteta položaj, teksturo in mejni pravokotnik.

Tukaj je osnovna arhitektura entitete, ki se uporablja v naših igrah:

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

Zdaj pa ustvarimo konkretne razrede Player, Enemy in 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;
    }
}

Ko so te entitete na mestu, raziščimo, kako zaznati trke med njimi, začenši z najpreprostejšo metodo.


2. 1. stopnja: Najpreprostejši trk – Pravokotnik.Preseka (AABB)

Najbolj temeljno preverjanje dvodimenzionalnih kolizij v igri MonoGame je test Axis-Aligned Bounding Box (AABB). Izraz "poravnano z osjo" preprosto pomeni, da so robovi pravokotnika popolnoma vzporedni z osema \(X\) in \(Y\) zaslona (brez vrtenja).

MonoGame ponuja hitro, vgrajeno metodo: Rectangle.Intersects(Rectangle value).

Kako Rectangle.Intersects deluje pod pokrovom

Pod površjem MonoGame izvede štiri primerjave celih števil:

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

Če so izpolnjeni vsi štirje pogoji, se pravokotnika prekrivata. Če vsaj en pogoj ni izpolnjen, ju loči prazna os in kolizija ni možna.

Resnična koda igranja: Bullet proti Enemy v Game1.Update

Tukaj je opisano, kako preverite kolizije med seznamom aktivnih nabojev in seznamom aktivnih sovražnikov v vaši glavni zanki MonoGame Update:

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

Nasvet za učinkovitost: Upoštevajte, da ponavljamo nazaj (for (int i = list.Count - 1; i >= 0; i--))! Če uporabite foreach in poskusite poklicati _bullets.Remove(bullet), C# vrže InvalidOperationException: Collection was modified. Ponavljanje nazaj omogoča varno odstranjevanje elementov brez težav s ponovnim indeksiranjem pomnilnika.

The Arcade Secret: "Fair Hitboxes" prek Inflate

V retro igrah, kot je Blocked: Pixel Panzer, teksture sprite pogosto vključujejo prozorne robove ali antenske konice. Če igralčev rezervoar eksplodira, ker se je krogla dotaknila praznega prozornega vogala njegove teksture, se bo igralec počutil prevaranega.

Da bi bilo trčenje odzivno in pošteno, igre uporabljajo manjši Hitbox znotraj sprite z uporabo 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. 2. stopnja: Trčenje kroga v krog (rotacijska odpornost)

Pravokotniki delujejo odlično za statične bloke in mrežne ploščice, vendar ne uspejo, ko se spriti vrtijo. Ko se nekvadratna vesoljska ladja vrti v Paint Trek, se mora z osjo poravnano omejevalno polje razširiti, da zajame vrteče se vogale, kar povzroči frustrirajoče "fantomske trke" v praznem zraku.

Za krožne asteroide, usmerjene energijske krogle in vrteča se vesoljska plovila so Omejevalni krogi idealna rešitev.

Past kvadratnega korena

Dva kroga trčita, ko je razdalja med njunima središčema manjša ali enaka vsoti njunih polmerov:

\(\besedilo{Razdalja}(C_A, C_B) \le r_A + r_B\)

V kodi se za izračun evklidske razdalje uporablja Math.Sqrt (ali Vector2.Distance). Vendar pa računanje kvadratnih korenov v zanki z 200 izstrelki porabi na stotine nepotrebnih ciklov procesorja!

Če primerjamo kvadrat razdalje s kvadratom vsote radijev, popolnoma odstranimo kvadratni koren:

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

Implementacija MonoGame: Circle proti 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);
    }
}

Zdaj integrirajte to neposredno v entiteto:

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

Nič kvadratnih korenin, imun na vrtenje sprite in bliskovito hiter.


4. Raven 3: Mešane oblike – krog proti škatli (MathHelper.Clamp)

Kaj se zgodi, ko okrogla vesoljska ladja v Paint Trek pluje skozi tesen labirint pravokotnih obrambnih ovir ali ko okrogla krogla zadene kvadratni blok v Blocked: Pixel Panzer?

Potrebujemo trk Krog proti pravokotniku.

Algoritem vpenjanja

Strategija je najti točko na pravokotniku, ki je najbližje središču kroga, in nato preizkusiti, ali je razdalja od te najbližje točke do središča manjša od polmera kroga.

MonoGameov MathHelper.Clamp naredi to trivialno:

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

Zdaj lahko preizkusite ščite igralcev pred pravokotnimi kockami z ničelnimi dodelitvami in visoko natančnostjo!


5. Stopnja 4: Neprekinjeno zaznavanje trčenja (CCD) in prestreljeni žarki

Ste že kdaj v svoji igri izstrelili visokohitrostni ostrostrelni naboj ali laserski railgun, samo da bi opazovali, kako je krogla čarobno prešla naravnost skozi tanko sovražnikovo ladjo, ne da bi povzročila škodo?

Ta hrošč je znan kot tuneliranje.

Ker se diskretne igre posodabljajo v časovnih korakih (\(\Delta t = 16,6\text{ms}\) pri 60 sličicah na sekundo), predmet, ki se premika s hitrostjo 1800 slikovnih pik na sekundo, potuje 30 slikovnih pik v enem okvirju. Če je sovražnikov trup debel samo 15 slikovnih pik, je bila krogla na 1. sličici pred sovražnikom, na 2. sličici pa povsem za sovražnikom.

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

Rešitev: prečrtani segment proti škatli (metoda plošče)

Namesto testiranja ene same točke testiramo celoten odsek črte, ki povezuje položaj krogle na okvirju 1 (»previousPosition«) z okvirjem 2 (»currentPosition«).

Tukaj je produkcijska metoda raycasting plošče presečišča iz našega spremljevalnega 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;
    }
}

V igri Blocked: Pixel Panzer igralčeva neprekinjena spretnost laserskega žarka uporablja to natančno oddajanje žarkov za rezanje skozi vrstice padajočih blokov, ne da bi zgrešil niti en sam trk.


6. Stopnja 5: zaznavanje kolizij s popolno slikovno piko proizvodnega razreda

Zdaj smo dosegli najvišjo raven 2D natančnosti: Pixel-Perfect Collision.

V retro streljačini s tanki ali zračnem boju z vesoljsko ladjo so nepravilne oblike (cevi tankov, krila, pilotske kabine) obdane s prozornimi slikovnimi pikami v teksturi sprite. Ko sovražnikova raketa zadene ta prozorni prostor, igralci takoj opazijo.

Popolno trčenje pikslov pregleda dejanske alfa (prosojnost) kanale prekrivajočih se tekstur. Če se dve neprozorni slikovni piki prekrivata na isti svetovni koordinati, je prišlo do pravega fizičnega zadetka.

Usodna napaka: GetData znotraj Update()

Številne vadnice navajajo začetnike, da to storijo:

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

Klic Texture2D.GetData() med izvajanjem prisili CPE, da počaka, da se cevovod GPE izprazni, pri čemer potegne teksturni pomnilnik nazaj preko vodila. Če to storite za desetine sprite vsak okvir, boste svojo igro znižali s 60 FPS na 5 FPS in takoj sprožili ANR-je za Android!

V bitkah preizkušena rešitev iz Blokirano: Pixel Panzer

V Blocked: Pixel Panzer naš Sprite.cs implementira izjemno optimizirano rutino IntersectsPixel, zgrajeno okoli 5 ključnih pravil:

  1. Statični barvni predpomnilnik: GetData se pokliče točno enkrat, ko se tekstura naloži, in shrani v `Dictionary<Texture2D, Color[]>``.
  2. AABB Early Exit Guard: Če je Bounds.Intersects(other.Bounds) napačen, se takoj rešimo. 99 % pregledov je izločenih pred pregledom posamezne slikovne pike.
  3. Podpora za atlas tekstur in SourceRectangle: obravnava sprite, zapakirane v atlase tekstur z odmiki izvornega pravokotnika.
  4. Izračunano okno prekrivanja: Zankamo samo čez natančen pravokotnik presečišča med obema spriteom ('Math.Max(a.Top, b.Top)' itd.).
  5. Alpha Short-Circuiting: Če je piksel Sprite A prozoren (A <= 20), je Sprite B popolnoma preskočen.
  6. Property Hoisting: Dostop do virtualnih lastnosti ('Meje') v ugnezdenih zankah ustvari na tisoče kopij strukture. Pred vstopom v zanko jih shranimo v spremenljivke lokalnega sklada.

Tukaj je popolna koda, pripravljena za proizvodnjo:

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

Ta rutina se v sodobni strojni opremi Android in namiznih sistemih izvede v manj kot 0,1 milisekunde.


7. Raven 6: širokofazna prostorska mreža in mobilni GC z ničelno dodelitvijo

Predstavljajte si, da ima vaša igra 100 blokov, 60 nabojev in 20 sovražnikov. Če preizkusite vsako entiteto glede na vsako drugo entiteto z uporabo ugnezdenih zank za, izvedete:

\(\frac{180 \times 179}{2} = 16.110 \text{ preverjanj na okvir!}\)

Pri 60 FPS je to skoraj 1.000.000 preverjanj trkov vsako sekundo.

Če želite razširiti svojo igro, morate implementirati Broad-Phase Spatial Partitioning.

Enotna prostorska zgoščena mreža

Zaslon razdelimo na 2D mrežo enotnih celic (v Blocked: Pixel Panzer je vsaka celica \(150 \krat 150\) slikovnih pik). Entiteta preizkuša samo trke z drugimi entitetami, ki se nahajajo v istih celicah mreže.

Težava mobilnega pomnilnika: GC Thrashing

Če vaš SpatialGrid ustvari new List<Point>() ali new List<Block>() vsak okvir, dodelite megabajte smeti vsako minuto upravljanemu kupu.

V Androidovem izvajalnem okolju Mono to sproži pogosto zbiranje smeti Gen-0, kar povzroči: mono runtime: Native lock spor (mono_class_is_subclass_)

Igra jeclja in Google Play vašo igro označi z opozorili o ANR!

Rešitev: medpomnilniki za večkratno uporabo

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

S ponovno uporabo _scratchCells in _scratchCheckedEntities teče celoten korak trka z 0 bajti dodelitve kopice na okvir.


8. Povzetek primerjalne tabele

Tehnika Matematični stroški Rotacijska podpora Tuneliranje varno? Najboljši primer uporabe v MonoGame
AABB (Pravokotnik. Presekanja) Izjemno hiter (~4 celih primerjav) Ne Ne Mrežni bloki, krogla široke faze, UI
Krog (Kvadrat razdalje) Zelo hitro (3 muls, 0 sqrt) Da (Nespremenljivo) Ne Okrogle ladje, ognjene krogle, energijske krogle
Krog proti škatli (Sponka) Hitro (lokalni MathHelper.Clamp) Da Ne Krožni igralec, ki krmari po tesnih stenah
Swept Ray (Slab CCD) Zmerno (Parametric Raycast) Da Da Hitre ostrostrelske krogle, tračnice, laserji
Pixel-Perfect ('IntersectsPixel`) Selektivno (omejen podnaslov) Da Ne Nepravilne konture sprite, pošteni hit boxes
Prostorska mreža (Zero-GC) Široka faza (\(O(N^2) \do O(N)\)) ni na voljo ni na voljo Gosti valovi, peklenski pekli, Android 60 FPS

Prikazi produkcije v resničnem svetu: igre Arar

Te kolizijske arhitekture niso teoretični eksperimenti – so pravi inženirski temelj, ki poganja naše komercialno izdane naslove:

  • Blokirano: Pixel Panzer: Naša retro arkadna igra za preživetje v tankih v trgovinah Google Play in Microsoft Store. Vsebuje naš celoten dvonivojski sistem za trčenje: prostorsko zgoščevalno mrežo z ničelno dodelitvijo, ki filtrira padajoče bloke, in popolno preverjanje kontakta za tanke, bojna letala, kupole in elementarno strelivo ColorWheel.
  • Paint Trek: Naša hitra vesoljska strelska igra s trčenjem v rotacijskem krogu, neprekinjenim oddajanjem žarkov in protiraketnimi obrambnimi sistemi s pokritim obsegom.

Zaključek in naslednji koraki

MonoGame vam daje moč za oblikovanje zaznavanja trkov, ki popolnoma ustreza potrebam vaše igre. Z uporabo dragih Pixel-Perfect preverjanj za hitrimi AABB testi, uporabo Kvadratnih razdalj za kroge in odpravo zbiranja smeti z Reusable Scratch Buffers, lahko zagotovite gladko delovanje konzole 60/120 FPS na namiznih in mobilnih platformah.

Oglejte si naše igre v trgovinah z aplikacijami, če si želite ogledati te sisteme trkov v akciji, in začnite izvajati te vzorce v svojih projektih MonoGame še danes!


Shranjujte povezave in vire


SEO ključne besede in hashtagi

Ključne besede: MonoGame 2D zaznavanje trkov, razvoj iger C#, Rectangle.Intersects MonoGame, pixel perfect collision C#, prostorska zgoščena mreža MonoGame, neprekinjeno trčenje swept ray, optimizacija zmogljivosti neodvisnih iger, zanka igre z ničelno dodelitvijo, optimizacija Android MonoGame, Arar Games, Blocked Pixel Panzer, Paint Trek.

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

2D susidūrimų aptikimas „MonoGame“: nuo pagrindinių stačiakampių iki nulinio paskirstymo taškų tobulų sistemų

2D susidūrimų aptikimas MonoGame: nuo pagrindinių stačiakampių iki nulinio paskirstymo taškų tobulų sistemų

Kuriant 2D žaidimus vaizdiniuose varikliuose, tokiuose kaip „Unity“ ar „Godot“, susidūrimo aptikimas dažnai atrodo kaip tikrintojo žymimieji langeliai: uždedate „BoxCollider2D“ arba „CircleCollider2D“, pridedate „Rigidbody“ ir tikitės, kad vidinis fizikos veiksmas netrukdys mobiliuosiuose įrenginiuose.

Tačiau MonoGame ir C# žaidimuose jūs visiškai kontroliuojate. Nėra paslėptos fizikos virš galvos, jokios nepageidaujamos sukimosi inercijos ir jokių paslaptingų šiukšlių surinkimo (GC) šuolių, vagiančių jūsų kadrų biudžetą.

Arar Games, kai sukūrėme Blocked: Pixel Panzer ir Paint Trek, mūsų arkadinių žaidimų kilpų reikėjo apdoroti šimtus greitaeigių priešo kulkų, sprogstančių plytų tinklelių, besisukančių tankų bokštelių, naikintuvų apvažiavimų ir dalelių skydų 1 PC ir 60 FPS Android įrenginiuose. Bendrosios paskirties fizinis variklis nebuvo svarstomas – mums reikėjo specialiai sukurtos, pakopinės susidūrimo architektūros.

Šiame išsamiame, kodu pagrįstame vadove pradėsime nuo absoliučių „MonoGame“ susidūrimo („Stačiakampis. Susikerta“ su tikromis „Bullet“ ir „Enemy“ spritais) pagrindų ir tęsime pažangius apskritimo patikrinimus, mišrius suspaudimus, prieš tunelius nukreiptus spindulius, gamybinio lygio Pixel-Perfectation,Pixel-PerfectationZiero ir perfectation**. optimizuotas mobiliajam GC išgyvenimui.


1. Pagrindas: paprasta MonoGame „Sprite“ hierarchija

Prieš aptikdami susidūrimus, mums reikia švarių žaidimų objektų. „MonoGame“ esybė iš esmės turi poziciją, tekstūrą ir ribojantį stačiakampį.

Štai pagrindinė objekto architektūra, naudojama mūsų žaidimuose:

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

Dabar sukurkime konkrečias „Player“, „Enemy“ ir „Bullet“ klases:

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

Turėdami šiuos objektus, panagrinėkime, kaip aptikti jų susidūrimus, pradedant nuo paprasčiausio metodo.


2. 1 lygis: paprasčiausias susidūrimas – „Stačiakampis. Susikerta“ (AABB)

Svarbiausias 2D susidūrimo patikrinimas MonoGame yra Axis-Aligned Bounding Box (AABB) testas. Sąvoka „išlygiuota pagal ašį“ paprasčiausiai reiškia, kad stačiakampio kraštai yra visiškai lygiagrečios ekrano ašims \(X\) ir \(Y\) (be sukimosi).

„MonoGame“ siūlo greitą, integruotą metodą: „Stačiakampis. Susikerta (stačiakampio reikšmė)“.

Kaip "Stačiakampis. Susikerta" veikia po gaubtu

Po paviršiumi MonoGame atlieka keturis sveikųjų skaičių palyginimus:

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

Jei tenkinamos visos keturios sąlygos, stačiakampiai sutampa. Jei nors viena sąlyga nepavyksta, tuščia ašis jas atskiria ir joks susidūrimas neįmanomas.

Tikras žaidimo kodas: „Bullet“ prieš „Enemy“ žaidime „Game1.Update“

Štai kaip galite patikrinti susidūrimus tarp aktyvių kulkų sąrašo ir aktyvių priešų sąrašo pagrindinėje MonoGame „Update“ kilpoje:

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

Našumo patarimas: Atkreipkite dėmesį, kad kartojame atgal („for (int i = list.Count - 1; i >= 0; i--)“)! Jei naudojate „foreach“ ir bandote iškviesti „_bullets.Remove(bullet)“, C# pateikia pranešimą „InvalidOperationException: Collection was modified“. Kartojimas atgal leidžia saugiai pašalinti elementus be atminties pakartotinio indeksavimo problemų.

Arkados paslaptis: „Fair Hitboxes“ per „Inflate“.

Retro žaidimuose, pvz., Blocked: Pixel Panzer, „Sprite“ tekstūros dažnai apima skaidrias paraštes arba antenos smaigalius. Jei žaidėjo bakas sprogs, nes kulka palietė tuščią skaidrų jo tekstūros kampą, žaidėjas jausis apgautas.

Kad susidūrimas būtų jautrus ir teisingas, žaidimuose naudojamas mažesnis Hitbox, esantis sprite viduje, naudojant „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. 2 lygis: susidūrimas tarp ratų (sukimosi atsparumas)

Stačiakampiai puikiai tinka statiniams blokams ir tinklelio plytelėms, tačiau jie sugenda, kai sukasi sprites. Kai nekvadratinis erdvėlaivis sukasi Paint Trek, ašies išlygiuotas ribotuvas turi išsiplėsti, kad apimtų besisukančius kampus, todėl tuščiame ore įvyksta varginantys „fantominiai susidūrimai“.

Apvaliems asteroidams, besisukantiems energijos rutuliams ir besisukantiems erdvėlaiviams Bounding Circles yra idealus sprendimas.

Kvadratinės šaknies spąstai

Du apskritimai susiduria, kai atstumas tarp jų centrų yra mažesnis arba lygus jų spindulių sumai:

\(\tekstas{Atstumas}(C_A, C_B) \le r_A + r_B\)

Kode apskaičiuojant Euklido atstumą naudojamas „Math.Sqrt“ (arba „Vector2.Distance“). Tačiau kvadratinių šaknų skaičiavimas kilpoje su 200 sviedinių sudegina šimtus nereikalingų procesoriaus ciklų!

Palyginus kvadratinį atstumą su kvadrato spindulio suma, visiškai pašaliname kvadratinę šaknį:

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

MonoGame įgyvendinimas: „Apskritimas“ prieš „Apskritimą“.

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

Dabar integruokite tai tiesiogiai į objektą:

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

Nulis kvadratinių šaknų, atsparus sprite sukimuisi ir žaibiškas.


4. 3 lygis: mišrios formos – apskritimas prieš langelį ("MathHelper.Clamp")

Kas nutinka, kai Paint Trek apskritas erdvėlaivis plaukia per siaurą stačiakampių gynybinių barjerų labirintą arba kai apvali kulka pataiko į kvadratinį bloką Užblokuota: Pixel Panzer?

Mums reikia susidūrimo Apskritimas prieš stačiakampį.

Suspaudimo algoritmas

Strategija yra rasti tašką stačiakampyje, kuris yra arčiausiai apskritimo centro, ir tada patikrinti, ar atstumas nuo artimiausio taško iki centro yra mažesnis už apskritimo spindulį.

„MonoGame“ „MathHelper.Clamp“ daro tai trivialu:

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

Dabar galite išbandyti žaidėjų skydus nuo stačiakampių plytų su nuliu paskirstymu ir dideliu tikslumu!


5. 4 lygis: nenutrūkstamas susidūrimo aptikimas (CCD) ir skleidžiami spinduliai

Ar kada nors savo žaidime iššovėte didelio greičio snaiperio šovinį ar bėgimo pistoleto lazerį, kad pamatytumėte, kaip kulka stebuklingai prasiskverbia tiesiai per ploną priešo laivą ir nepadarė žalos?

Ši klaida žinoma kaip tuneliavimas.

Kadangi atskiri žaidimai atnaujinami laike (\(\Delta t = 16,6\text{ms}\) esant 60 FPS), objektas, judantis 1800 pikselių per sekundę greičiu, nukeliauja 30 pikselių viename kadre. Jei priešo korpusas yra tik 15 pikselių storio, kulka buvo priešais priešą 1 kadre, o visiškai už priešo 2 kadre.

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

Sprendimas: nuvalytas segmentas prieš dėžę (plokščių metodas)

Užuot išbandę vieną tašką, išbandome visą linijos atkarpą, jungiančią kulkos padėtį 1 kadre („ankstesnė padėtis“) su 2 kadru („currentPosition“).

Štai gamybos spinduliavimo plokščių susikirtimo metodas iš mūsų pavadinimo 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;
    }
}

Blocked: Pixel Panzer žaidėjo nuolatinis Lazerio spindulio įgūdis naudoja šį tikslią spinduliuotę, kad perpjautų besileidžiančių blokų eiles nepraleisdamas nė vieno susidūrimo.


6. 5 lygis: gamybos lygio pikselių tobulas susidūrimo aptikimas

Dabar pasiekiame aukščiausią 2D tikslumo lygį: Pixel-Perfect Collision.

Retro tanko šaudykloje arba erdvėlaivių kovoje netaisyklingos formos (tanko statinės, sparnai, kabinos kabinos) yra apsuptos skaidriais pikseliais sprite tekstūroje. Kai priešo raketa pataiko į tą skaidrią erdvę, žaidėjai tai iškart pastebi.

„Pixel-perfect“ susidūrimas tikrina tikrus sutampančių tekstūrų alfa (skaidrumo) kanalus. Jei du neskaidrūs pikseliai sutampa toje pačioje pasaulio koordinatėje, įvyko tikras fizinis smūgis.

Lemtinga klaida: „GetData“ lauke „Update()“.

Daugelyje vadovėlių pradedantiesiems nurodoma tai padaryti:

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

Vykdymo metu iškvietus „Texture2D.GetData()“, centrinis procesorius verčiamas laukti, kol GPU konvejeris išsikraus, o tekstūros atmintis vėl perkeliama per magistralę. Jei tai padarysite daugybei „spraite“ kiekviename kadre, jūsų žaidimas bus padidintas nuo 60 FPS iki 5 FPS ir iškart suaktyvinsite „Android“ ANR!

Mūšio patikrintas sprendimas iš Blocked: Pixel Panzer

Programoje Blocked: Pixel Panzer mūsų „Sprite.cs“ įgyvendina itin optimizuotą „IntersectsPixel“ rutiną, pagrįstą 5 esminėmis taisyklėmis:

  1. Statinė spalvų masyvo talpykla: „GetData“ iškviečiama tiksliai vieną kartą, kai įkeliama tekstūra, ir saugoma „Žodyne<Texture2D, Color[]>“.
  2. AABB ankstyvo išėjimo apsauga: Jei „Bounds.Intersects(other.Bounds)“ yra klaidinga, mes nedelsdami gelbėjame. 99 % patikrinimų pašalinami prieš apžiūrint vieną pikselį.
  3. Tekstūros atlaso ir šaltinio stačiakampio palaikymas: tvarko į tekstūrų atlasus supakuotus spritus, naudodamas šaltinio stačiakampio poslinkius.
  4. Apskaičiuotas persidengimo langas: sujungiame tik tikslią abiejų spritų sankirtos stačiakampį („Math.Max(a.Top, b.Top)“ ir kt.).
  5. Alfa trumpasis jungimas: jei „Sprite A“ pikselis yra skaidrus („A <= 20“), „Sprite B“ visiškai praleidžiamas.
  6. Ypatybės pakėlimas: prieiga prie virtualių savybių („ribos“) įdėtose kilpose sukuria tūkstančius struktūrų kopijų. Prieš įeidami į kilpą, juos saugome vietiniuose kamino kintamuosiuose.

Štai visas, paruoštas gamybai kodas:

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

Šiuolaikinėse „Android“ aparatinės įrangos ir stalinių kompiuterių sistemose ši tvarka vykdoma per mažiau nei 0,1 milisekundės.


7. 6 lygis: plačios fazės erdvinis tinklelis ir nulinio paskirstymo mobilusis GC

Įsivaizduokite, kad jūsų žaidime yra 100 blokų, 60 kulkų ir 20 priešų. Jei išbandote kiekvieną objektą, palyginti su visais kitais objektais, naudodami įdėtas „for“ kilpas, atliksite:

\(\frac{180 \times 179}{2} = 16 110 \text{ čekių viename kadre!}\)

Esant 60 FPS, tai yra beveik 1 000 000 susidūrimų patikrinimų kas sekundę.

Norėdami išplėsti savo žaidimą, turite įdiegti Plačios fazės erdvinį skaidymą.

Vienoda erdvinė maišos tinklelis

Ekraną padalijame į 2D vienodų langelių tinklelį (Blocked: Pixel Panzer kiekvienas langelis yra \(150 \x 150\) pikselių). Objektas tik išbando susidūrimus su kitais objektais, esančiais tose pačiose tinklelio ląstelėse.

Mobiliosios atminties problema: GC Thrashing

Jei jūsų „SpatialGrid“ kiekvieną kadrą sukuria „naują sąrašą<Taškas>()“ arba „naują sąrašą()“, kiekvieną minutę valdomame krūvoje paskirsite megabaitų šiukšlių.

Naudojant „Android Mono“ vykdymo laiką, tai suaktyvina dažnus „Gen-0“ šiukšlių rinkimus ir sukelia: „mono vykdymo laikas: savosios užrakto ginčas (mono_class_is_subclass_)“.

Žaidimas mikčioja, o „Google Play“ pažymi jūsų žaidimą su ANR įspėjimais!

Sprendimas: daugkartinio naudojimo įbrėžimų buferiai

Štai Nulinio paskirstymo erdvinės maišos tinklelis iš „CollisionManager.cs“, esantis Užblokuota: 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);
                            }
                        }
                    }
                }
            }
        }
    }
}

Pakartotinai naudojant „_scratchCells“ ir „_scratchCheckedEntities“, visas susidūrimo veiksmas vykdomas naudojant 0 baitų krūvos paskirstymą vienam kadrui.


8. Suvestinė palyginimo lentelė

Technika Matematinės išlaidos Rotacinė parama Saugu tuneliu? Geriausias naudojimo dėklas MonoGame
AABB ("Stačiakampis. Susikerta") Itin greitas (~4 sveikųjų skaičių palyginimai) Ne Ne Tinklelio blokai, kulkos plačiafazis, UI
Apskritimas („DistanceSquared“) Labai greitas (3 mulsai, 0 kv.m.) Taip (Nekintamas) Ne Apvalūs laivai, ugnies kamuoliai, energijos rutuliai
Apskritimas prieš dėžę ("spaustukas") Greitas (Local MathHelper.Clamp) Taip Ne Apvalus grotuvas, važiuojantis siauromis sienomis
Swept Ray (Slab CCD) Vidutinis (parametrinis Raycast) Taip Taip Greitosios snaiperių kulkos, bėgiai, lazeriai
Pixel-Perfect ("IntersectsPixel") Atrankinis (Ribotas sub-tiesus) Taip Ne Netaisyklingi sprite kontūrai, teisingi hitboxai
Erdvinis tinklelis (nulis-GC) Plačios fazės (\(O(N^2) \į O(N)\)) N/A N/A Tankios bangos, kulkų pragarai, Android 60 FPS

Realaus pasaulio produkcijos demonstracijos: „Arar“ žaidimai

Šios susidūrimo architektūros nėra teoriniai eksperimentai – jos yra tikrasis inžinerinis pagrindas, kuris maitina mūsų komerciškai išleistus pavadinimus:

  • Užblokuota: „Pixel Panzer“: Mūsų retro tankų išgyvenimo arkadinis žaidimas „Google Play“ ir „Microsoft Store“. Jame yra visa mūsų dviejų pakopų susidūrimo sistema: nulinio paskirstymo erdvinė maišos tinklelis, filtruojantis krintančius blokus, ir tobulas tankų, naikintuvų, bokštelių sviedinių ir elementinės „ColorWheel“ šaudmenų kontaktų tikrinimas.
  • Paint Trek: Mūsų sparti kosminė šaudyklė, pasižyminti sukimosi rato susidūrimu, nuolatiniu spinduliavimu ir didelio tūrio priešraketinės gynybos sistemomis.

Išvada ir tolesni žingsniai

MonoGame suteikia jums galią sukurti susidūrimų aptikimą, kuris puikiai atitiktų jūsų žaidimo poreikius. Atliekant brangias Pixel-Perfect patikras po greitų AABB testų, naudojant Kvadratinius atstumus apskritimams ir pašalinus šiukšlių rinkimą naudojant Pakartotinai naudojamus įbrėžimų buferius, galite užtikrinti sklandų konsolės 60/120 FPS našumą tiek staliniuose kompiuteriuose, tiek mobiliosiose platformose.

Peržiūrėkite mūsų žaidimus programų parduotuvėse, kad pamatytumėte, kaip veikia šios susidūrimo sistemos, ir pradėkite įgyvendinti šiuos modelius savo MonoGame projektuose jau šiandien!


Parduotuvių nuorodos ir ištekliai


SEO raktiniai žodžiai ir žymos su grotelėmis

Raktiniai žodžiai: MonoGame 2D susidūrimų aptikimas, C# žaidimų kūrimas, stačiakampis. Susikerta MonoGame, pikselių tobulas susidūrimas C#, erdvinis maišos tinklelis MonoGame, nuolatinis susidūrimas, nepriklausomo žaidimo našumo optimizavimas, nulinio paskirstymo žaidimo kilpa, Android Arlock B žaidimo optimizavimas, „Android MonoGame“ Trek.

#MonoGame #CSharp #GameDev #IndieDev #GamePhysics#DotNet#2DGameDev #MobileGameDev #PerformanceOptimization #CleanCode #PanintTreixel #PanintT`re „#ArarGames“ „#GameProgramavimas“.

2D sadursmju noteikšana spēlē MonoGame: no pamata taisnstūriem līdz nulles sadales pikseļu perfektām sistēmām

2D sadursmju noteikšana MonoGame: no pamata taisnstūriem līdz nulles sadales pikseļu perfektām sistēmām

Veidojot 2D spēles vizuālos dzinējos, piemēram, Unity vai Godot, sadursmes noteikšana bieži vien šķiet kā inspektora izvēles rūtiņu virkne: jūs uzsitiet uz "BoxCollider2D" vai "CircleCollider2D", pievienojat "Rigidbody" un cerat, ka iekšējās fizikas darbība mobilajās ierīcēs nestostās.

Tomēr spēlēs MonoGame un C# jūs pilnībā kontrolējat. Nav slēptas fizikas virs galvas, nav nevēlamas rotācijas inerces un nav noslēpumainu atkritumu savākšanas (GC) tapa, kas zog jūsu kadru budžetu.

Arar Games, kad veidojām Blocked: Pixel Panzer un Paint Trek, mūsu arkādes spēļu cilpas bija vajadzīgas, lai apstrādātu simtiem ātrdarbīgu ienaidnieka ložu, sprāgstošu ķieģeļu režģus, rotējošus tanku torņus, iznīcinātāju aplidojumus un daļiņu vairogus ar Windows 20 FPS ierīcēm ar ātrumu 60 FPS. Par vispārējas nozīmes fizikas dzinēju nevarētu būt runa — mums bija vajadzīga mērķtiecīga, daudzpakāpju sadursmes arhitektūra.

Šajā visaptverošajā, uz kodu vadītajā rokasgrāmatā mēs sāksim no MonoGame sadursmes ("Rectangle.Intersects") absolūtajiem pamatiem ar īstiem "Bullet" un "Enemy" spraitiem un attīstīsimies līdz uzlabotām apļa pārbaudēm, jauktām iespīlēšanas, prettunelēšanas staru pārraidēm, ražošanas līmeņa Pixel-PerfectationZero-Perfectation-collokation. optimizēts mobilajām GC izdzīvošanai.


1. Pamats: vienkārša MonoGame Sprite hierarhija

Pirms sadursmju noteikšanas mums ir vajadzīgas tīras spēles vienības. MonoGame vienībai pamatā ir pozīcija, faktūra un ierobežojošs taisnstūris.

Šeit ir mūsu spēlēs izmantotā pamata entītiju arhitektūra:

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

Tagad izveidosim konkrētas klases "Spēlētājs", "Ienaidnieks" un "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;
    }
}

Kad šīs entītijas ir izveidotas, izpētīsim, kā noteikt sadursmes starp tām, sākot ar vienkāršāko metodi.


2. 1. līmenis: visvienkāršākā sadursme — taisnstūris. krustojas (AABB)

Visbūtiskākā 2D sadursmes pārbaude MonoGame ir Axis-Aligned Bounding Box (AABB) tests. Termins "ass izlīdzināts" vienkārši nozīmē, ka taisnstūra malas ir pilnībā paralēlas ekrāna \(X\) un \(Y\) asīm (bez rotācijas).

MonoGame nodrošina ātru, iebūvētu metodi: "Rectangle.Intersects(Rectangle value)".

Kā Taisnstūris.Intersects darbojas zem pārsega

Zem virsmas MonoGame veic četrus veselu skaitļu salīdzinājumus:

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

Ja ir izpildīti visi četri nosacījumi, taisnstūri pārklājas. Ja kaut viens nosacījums neizdodas, tukša ass tos atdala, un sadursme nav iespējama.

Īsts spēles kods: "Bullet" pret "Enemy" spēlē "Game1.Update"

Lūk, kā jūs varat pārbaudīt sadursmes starp aktīvo ložu sarakstu un aktīvo ienaidnieku sarakstu jūsu galvenajā MonoGame Update cilpā:

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

Veiktspējas padoms. Ņemiet vērā, ka mēs atkārtojam atpakaļgaitas (for (int i = list.Count - 1; i >= 0; i--))! Ja izmantojat "foreach" un mēģināt izsaukt "_bullets.Remove(bullet)", C# izdod "InvalidOperationException: Collection was modified". Atkārtošana atpakaļ ļauj droši noņemt elementus bez atmiņas atkārtotas indeksēšanas problēmām.

Arkādes noslēpums: "Fair Hitboxes", izmantojot 'Inflate'

Retro spēlēs, piemēram, Blocked: Pixel Panzer, sprite tekstūras bieži ietver caurspīdīgas malas vai antenas tapas. Ja spēlētāja tvertne eksplodē, jo lode pieskārās tukšam caurspīdīgam tā faktūras stūrim, spēlētājs jutīsies apkrāpts.

Lai sadursme būtu atsaucīga un godīga, spēlēs sprite iekšpusē tiek izmantots mazāks Hitbox, izmantojot opciju "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. 2. līmenis: sadursme no apļa uz apli (rotācijas imunitāte)

Taisnstūri lieliski darbojas statiskiem blokiem un režģa flīzēm, taču tie neizdodas, kad spraiti griežas. Kad Paint Trek griežas nekvadrātveida kosmosa kuģis, uz asi izlīdzinātam ierobežojošajam lodziņam ir jāpaplašina, lai aptvertu griežamos stūrus, izraisot kaitinošas "fantoma sadursmes" tukšā gaisā.

Apļveida asteroīdiem, enerģijas lodēm un rotējošiem kosmosa kuģiem Bounding Circles ir ideāls risinājums.

Kvadrātsakņu slazds

Divi apļi saduras, ja attālums starp to centriem ir mazāks vai vienāds ar to rādiusu summu:

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

Kodā, aprēķinot Eiklīda attālumu, tiek izmantots "Math.Sqrt" (vai "Vector2.Distance"). Tomēr kvadrātsakņu aprēķināšana cilpā ar 200 šāviņiem sadedzina simtiem nevajadzīgu CPU ciklu!

Salīdzinot attālumu kvadrātā ar kvadrāta rādiusa summu, mēs pilnībā izslēdzam kvadrātsakni:

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

MonoGame ieviešana: "Aplis" pret "Aplis".

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

Tagad integrējiet to tieši entītijā:

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

Nulle kvadrātsakņu, imūna pret spraitu rotāciju un zibensātri.


4. 3. līmenis: jauktas formas — aplis pret kastīti (MathHelper.Clamp)

Kas notiek, kad apļveida kosmosa kuģis programmā Paint Trek pārvietojas šaurā taisnstūrveida aizsardzības barjeru labirintā vai kad apaļa lode trāpa kvadrātveida blokā Bloķēts: Pixel Panzer?

Mums ir vajadzīga sadursme Aplis pret taisnstūri.

Saspiešanas algoritms

Stratēģija ir atrast taisnstūra punktu, kas ir vistuvāk apļa centram, un pēc tam pārbaudīt, vai attālums no šī tuvākā punkta līdz centram ir mazāks par apļa rādiusu.

MonoGame "MathHelper.Clamp" padara šo triviālu:

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

Tagad varat pārbaudīt spēlētāju vairogus pret taisnstūrveida ķieģeļiem ar nulles sadalījumu un augstu precizitāti!


5. 4. līmenis: nepārtraukta sadursmju noteikšana (CCD) un kustīgi stari

Vai esat kādreiz savā spēlē izšāvis ar ātrgaitas snaipera kārtu vai sliežu pistoles lāzeru, lai tikai noskatītos, kā lode maģiski izskrien cauri plānam ienaidnieka kuģim, nenodarot bojājumus?

Šī kļūda ir pazīstama kā tunelēšana.

Tā kā diskrētās spēles tiek atjauninātas laika soļos (\(\Delta t = 16,6\text{ms}\) pie 60 kadri/s), objekts, kas pārvietojas ar ātrumu 1800 pikseļi sekundē, pārvietojas 30 pikseļus vienā kadrā. Ja ienaidnieka korpuss ir tikai 15 pikseļu biezs, lode atradās ienaidnieka priekšā 1. kadrā un pilnībā aiz ienaidnieka 2. kadrā.

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

Risinājums: iztīrītais segments salīdzinājumā ar kārbu (plātņu metode)

Tā vietā, lai pārbaudītu vienu punktu, mēs pārbaudām visu līnijas segmentu, kas savieno aizzīmes pozīciju 1. kadrā ("iepriekšējā pozīcija") ar 2. kadru ("pašreizējā pozīcija").

Šeit ir ražošanas raycast plātņu krustošanās metode no mūsu pavadošā nosaukuma 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;
    }
}

Spēlē Blocked: Pixel Panzer spēlētāja nepārtrauktā Lāzerstara prasme izmanto šo precīzo staru apraidi, lai pāršķirtu lejupejošu bloku rindas, nepalaižot garām nevienu sadursmi.


6. 5. līmenis: ražošanas pakāpes pikseļu perfekta sadursmes noteikšana

Tagad mēs sasniedzam augstāko 2D precizitātes līmeni: Pixel-Perfect Collision.

Retro tanku šāvēja vai kosmosa kuģa kaujā neregulāras formas (tanku mucas, spārni, kabīnes kabīnes) ieskauj caurspīdīgi pikseļi sprite tekstūrā. Kad ienaidnieka raķete sasniedz šo caurspīdīgo vietu, spēlētāji to nekavējoties pamana.

Pikseļu perfekta sadursme pārbauda faktiskos alfa (caurspīdīguma) kanālus, kas pārklājas. Ja divi necaurspīdīgi pikseļi pārklājas vienā pasaules koordinātā, ir noticis īsts fizisks trāpījums.

Liktenīgā kļūda: 'GetData' iekšā 'Update()'

Daudzās pamācībās iesācējiem tas ir norādīts:

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

Izsaukšana "Texture2D.GetData()" izpildlaikā liek centrālajam procesoram gaidīt, līdz GPU konveijers tiks izskalots, pārvelkot tekstūras atmiņu atpakaļ pa kopni. To darot desmitiem spraitu katrā kadrā, jūsu spēle tiks palielināta no 60 kadri/s līdz 5 kadriem/s un nekavējoties tiks aktivizēti Android ANR!

Kaujās pārbaudīts risinājums no Blocked: Pixel Panzer

Programmā Bloķēts: Pixel Panzer mūsu Sprite.cs ievieš īpaši optimizētu IntersectsPixel rutīnu, kuras pamatā ir 5 svarīgi noteikumi.

1. Statiskā krāsu masīva kešatmiņa: 'GetData' tiek izsaukta tieši vienreiz, kad tekstūra tiek ielādēta, un tiek saglabāta vārdnīcā<Texture2D, Color[]>. 2. AABB Early Exit Guard: Ja vērtība "Bounds.Intersects(other.Bounds)" ir nepatiesa, mēs nekavējoties glābjam. 99% pārbaužu tiek novērstas pirms viena pikseļa pārbaudes. 3. Tekstūras atlanta un avota taisnstūra atbalsts: apstrādā tekstūras atlantos iepakotas sprites, izmantojot avota taisnstūra nobīdes. 4. Aprēķinātais pārklāšanās logs: mēs veicam cilpu tikai pa precīzu krustojuma taisnstūri starp abiem spraitiem (Math.Max(a.Top, b.Top) utt.). 5. Alfa īsslēgums: ja Sprite A pikseļi ir caurspīdīgi (A <= 20), Sprite B tiek pilnībā izlaists. 6. Property Hoisting: Piekļūstot virtuālajiem rekvizītiem (“Robežām”) ligzdotās cilpās, tiek izveidoti tūkstošiem struktūru kopiju. Pirms ievadīšanas cilpā mēs tos uzglabājam lokālos steka mainīgajos.

Šeit ir pilns, ražošanai gatavs kods:

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

Šī rutīna tiek izpildīta mazāk nekā 0,1 milisekundē modernās Android aparatūras un galddatoru sistēmās.


7. 6. līmenis: plašas fāzes telpiskais režģis un nulles piešķīruma mobilais GC

Iedomājieties, ka jūsu spēlē ir 100 bloki, 60 lodes un 20 ienaidnieki. Ja pārbaudāt katru entītiju pret visām citām entītijām, izmantojot ligzdotās “for” cilpas, jūs veicat:

\(\frac{180 \times 179}{2} = 16 110 \text{ čeki vienā kadrā!}\)

Pie 60 FPS, tas ir gandrīz 1 000 000 sadursmju pārbaudes katru sekundi.

Lai mērogotu savu spēli, jums ir jāievieš Plašas fāzes telpiskā sadalīšana.

Vienotais telpiskais jaucējrežģis

Mēs sadalām ekrānu vienādu šūnu 2D režģī (Blocked: Pixel Panzer katra šūna ir 150 \( reizes 150 \) pikseļi). Entītija pārbauda tikai sadursmes ar citām entītijām, kas atrodas tajās pašās režģa šūnās.

Mobilās atmiņas problēma: GC Thrashing

Ja jūsu "SpatialGrid" katrā kadrā izveido "jaunu sarakstu()" vai "jaunu sarakstu ()", jūs katru minūti piešķirat atkritumu megabaitus pārvaldītajā kaudzē.

Android Mono izpildlaikā tas aktivizē biežas Gen-0 atkritumu savākšanas, izraisot: "mono izpildlaiks: vietējās bloķēšanas strīds (mono_class_is_subclass_)".

Spēle stostās, un Google Play atzīmē jūsu spēli ar ANR brīdinājumiem!

Risinājums: atkārtoti lietojami skrāpējumu buferi

Šeit ir Zero-Allocation Spatial Hash Grid no `CollisionManager.cs' sadaļā Bloķēts: 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);
                            }
                        }
                    }
                }
            }
        }
    }
}

Atkārtoti izmantojot “_scratchCells” un “_scratchCheckedEntities”, viss sadursmes posms tiek izpildīts ar 0 baitu kaudzes piešķiršanu katrā kadrā.


8. Kopsavilkuma salīdzināšanas tabula

Tehnika Matemātiskās izmaksas Rotācijas atbalsts Vai tunelēšana ir droša? Labākais lietošanas gadījums MonoGame
AABB (Taisnstūris.Krustojas) Īpaši ātrs (~4 veselu skaitļu salīdzinājumi) Nē Nē Režģa bloki, ložu platfāze, UI
Aplis (DistanceSquared) Ļoti ātrs (3 muls, 0 kvadrātmetri) Jā (nemainīgs) Nē Apaļi kuģi, ugunsbumbas, enerģijas lodes
Aplis pret kastīti ("Skava") Ātri (Local MathHelper.Clamp) Jā Nē Apļveida atskaņotājs pārvietojas šaurās sienās
Swept Ray (Slab CCD) Mērens (Parametriskā Raycast) Jā Jā Ātrās snaipera lodes, sliežu pistoles, lāzeri
Pixel-Perfect ("IntersectsPixel") Selektīvs (ierobežots apakšrekts) Jā Nē Neregulāras sprite kontūras, godīgi hitboxes
Spatial Grid (Zero-GC) Platfāze (\(O(N^2) \līdz O(N)\)) N/A N/A Blīvi viļņi, ložu elles, Android 60 FPS

Reālās pasaules ražošanas vitrīnas: Arar spēles

Šīs sadursmes arhitektūras nav teorētiski eksperimenti — tās ir reāls inženiertehniskais pamats, kas nodrošina mūsu komerciāli izdoto nosaukumu darbību:

  • Bloķēts: Pixel Panzer: Mūsu retro tanku izdzīvošanas arkādes spēle pakalpojumā Google Play un Microsoft Store. Tajā ir mūsu pilnā divu līmeņu sadursmes sistēma: nulles sadalījuma telpiskais jaucējrežģis, kas filtrē krītošos blokus, un pikseļu perfekta kontaktu pārbaude tankiem, iznīcinātājiem, torņu čaulām un ColorWheel elementārajai munīcijai.
  • Paint Trek: Mūsu ātrdarbīgā kosmosa šāvēja ar rotācijas apļa sadursmi, nepārtrauktu staru apraidi un liela apjoma pretraķešu aizsardzības sistēmām.

Secinājums un nākamie soļi

MonoGame sniedz jums iespēju izstrādāt sadursmju noteikšanu, kas lieliski atbilst jūsu spēles vajadzībām. Veicot dārgas Pixel-Perfect pārbaudes, izmantojot ātrus AABB testus, izmantojot Kvadrātveida attālumus apļiem un novēršot atkritumu savākšanu ar Atkārtoti lietojamiem Scratch Buffers, varat nodrošināt konsolei vienmērīgu 60/120 FPS veiktspēju gan galddatoros, gan mobilajās platformās.

Apskatiet mūsu spēles lietotņu veikalos, lai redzētu šīs sadursmju sistēmas darbībā, un sāciet ieviest šos modeļus savos MonoGame projektos jau šodien!


Veikalu saites un resursi


SEO atslēgvārdi un atsauces

Atslēgvārdi: MonoGame 2D sadursmju noteikšana, C# spēļu izstrāde, taisnstūris. Krustojās MonoGame, perfekta pikseļu sadursme C#, telpiskā jaukšanas režģis MonoGame, nepārtraukta sadursme, neatkarīga spēles veiktspējas optimizācija, nulles piešķiršanas spēles cilpa, Android Arlock B spēles optimizācija, Panzer Pix Game. Trek.

#MonoGame #CSharp #Spēļu izstrāde #ArarGames #SpēļuProgrammēšana`