27 Eylül 2026 Pazar

Wykrywanie kolizji 2D w grze MonoGame: od podstawowych prostokątów po systemy z zerową alokacją i doskonałą jakością pikseli




Wykrywanie kolizji 2D w grze MonoGame: od podstawowych prostokątów po systemy z zerową alokacją i doskonałą jakością pikseli

Podczas tworzenia gier 2D w silnikach wizualnych, takich jak Unity lub Godot, wykrywanie kolizji często przypomina serię pól wyboru inspektora: uderzasz w BoxCollider2D lub CircleCollider2D, podłączasz Rigidbody i masz nadzieję, że wewnętrzny krok fizyki nie zacina się na urządzeniach mobilnych.

Jednak w MonoGame i C# masz całkowitą kontrolę. Nie ma żadnych ukrytych kosztów związanych z fizyką, niepożądanej bezwładności obrotowej ani tajemniczych skoków zbierania śmieci (GC), które kradną budżet na ramę.

Kiedy w Arar Games budowaliśmy Blocked: Pixel Panzer i Paint Trek, nasze pętle gier zręcznościowych wymagały przetwarzania setek szybkich pocisków wroga, eksplodujących ceglanych siatek, obracających się wieżyczek czołgów, przelotów myśliwców i tarcz cząsteczkowych przy 60 do 120 FPS na komputerach PC i urządzeniach z systemem Android. Nie było mowy o silniku fizyki ogólnego przeznaczenia – potrzebowaliśmy specjalnie zaprojektowanej, wielopoziomowej architektury kolizyjnej.

W tym obszernym, opartym na kodzie przewodniku zaczniemy od absolutnych podstaw kolizji w grze MonoGame („Rectangle.Intersects” z prawdziwymi ikonkami „Bullet” i „Enemy”) i przejdziemy do zaawansowanych kontroli okręgów, mieszanego zaciskania, promieni zapobiegających tunelowaniu, kolizji idealnej dla pikseli i siatek przestrzennych o zerowej alokacji zoptymalizowanych pod kątem przetrwania mobilnego GC.


1. Podstawa: prosta hierarchia „Sprite” w grze pojedynczej

Przed wykryciem kolizji potrzebujemy czystych jednostek gry. W MonoGame jednostka zasadniczo posiada pozycję, teksturę i prostokąt ograniczający.

Oto podstawowa architektura jednostek używana w naszych grach:

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

Stwórzmy teraz konkretne klasy „Gracz”, „Wróg” i „Pocisk”:

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

Mając już te elementy, przyjrzyjmy się, jak wykryć kolizje między nimi, zaczynając od najprostszej metody.


2. Poziom 1: Najprostsza kolizja – „Rectangle.Intersects” (AABB)

Najbardziej podstawową metodą kontroli kolizji 2D w grze MonoGame jest test wyrównanej ramki ograniczającej (AABB). Termin „wyrównany do osi” oznacza po prostu, że krawędzie prostokąta są całkowicie równoległe do osi \(X\) i \(Y\) ekranu (bez obrotu).

MonoGame udostępnia szybką, wbudowaną metodę: Rectangle.Intersects(Wartość prostokąta).

Jak działa „Rectangle.Intersects” pod maską

Pod powierzchnią MonoGame wykonuje cztery porównania liczb całkowitych:

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

Jeśli wszystkie cztery warunki są spełnione, prostokąty nakładają się na siebie. Jeśli choć jeden warunek nie zostanie spełniony, oddziela je pusta oś i nie jest możliwa żadna kolizja.

Prawdziwy kod rozgrywki: „Pocisk” kontra „Wróg” w „Grze 1. Aktualizacja”

Oto jak sprawdzić kolizje pomiędzy listą aktywnych kul i listą aktywnych wrogów w głównej pętli „Aktualizacji” gry MonoGame:

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

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

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

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

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

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

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

        base.Update(gameTime);
    }
}

Wskazówka dotycząca wydajności: Zauważ, że wykonujemy iterację wstecz („for (int i = list.Count - 1; i >= 0; i--))! Jeśli użyjesz foreachi spróbujesz wywołać_bullets.Remove(bullet), C# zgłosi InvalidOperationException: Kolekcja została zmodyfikowana`. Iteracja wstecz umożliwia bezpieczne usuwanie elementów bez problemów z ponownym indeksowaniem pamięci.

Sekret salonu gier: „Dobre trafienia” poprzez „Nadmuchanie”.

W grach retro, takich jak Blocked: Pixel Panzer, tekstury ikonek często zawierają przezroczyste marginesy lub kolce antenowe. Jeśli czołg gracza eksploduje, ponieważ kula dotknęła pustego przezroczystego rogu jego tekstury, gracz poczuje się oszukany.

Aby kolizja wydawała się responsywna i sprawiedliwa, gry używają mniejszego Hitboxu wewnątrz ikonki, używając opcji 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. Poziom 2: Zderzenie koło-okrąg (odporność na obrót)

Prostokąty świetnie sprawdzają się w przypadku bloków statycznych i płytek siatki, ale zawodzą, gdy duszki się obracają. Kiedy statek kosmiczny o kształcie innym niż kwadratowy obraca się w Paint Trek, obwiednia wyrównana do osi musi się rozszerzyć, aby objąć obracające się rogi, co skutkuje frustrującymi „widmowymi zderzeniami” w pustym powietrzu.

W przypadku okrągłych asteroid, samonaprowadzających się kul energii i wirujących statków kosmicznych idealnym rozwiązaniem są Okręgi ograniczające.

Pułapka na pierwiastek kwadratowy

Dwa okręgi zderzają się, gdy odległość między ich środkami jest mniejsza lub równa sumie ich promieni:

\(\text{Odległość}(C_A, C_B) \le r_A + r_B\)

W kodzie do obliczania odległości euklidesowej używa się parametru „Math.Sqrt” (lub „Vector2.Distance”). Jednak obliczanie pierwiastków kwadratowych w pętli z 200 pociskami powoduje spalanie setek niepotrzebnych cykli procesora!

Porównując kwadrat odległości z kwadratową sumą promieni, całkowicie eliminujemy pierwiastek kwadratowy:

\(\text{OdległośćKwadrat} \le (r_A + r_B)^2\)

Implementacja gry pojedynczej: „Koło” kontra „Koło”.

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

Teraz zintegruj to bezpośrednio z jednostką:

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

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

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

Zero pierwiastków kwadratowych, odporność na rotację duszków i błyskawiczna szybkość.


4. Poziom 3: Kształty mieszane – okrąg kontra pudełko („MathHelper.Clamp”)

Co się stanie, gdy okrągły statek kosmiczny w Paint Trek przebije się przez ciasny labirynt prostokątnych barier obronnych lub gdy okrągła kula trafi w kwadratowy blok w Blocked: Pixel Panzer?

Potrzebujemy kolizji Koło kontra prostokąt.

Algorytm zaciskania

Strategia polega na znalezieniu punktu na prostokącie, który jest najbliżej środka okręgu, a następnie sprawdzeniu, czy odległość od tego najbliższego punktu do środka jest mniejsza niż promień okręgu.

MathHelper.Clamp firmy MonoGame sprawia, że ​​jest to trywialne:

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

Możesz teraz testować tarcze graczy w porównaniu z prostokątnymi cegłami z zerowym przydziałem i dużą dokładnością!


5. Poziom 4: Ciągłe wykrywanie kolizji (CCD) i promienie odchylone

Czy kiedykolwiek w grze wystrzeliłeś superszybki pocisk snajperski lub laser z karabinu szynowego, tylko po to, by zobaczyć, jak kula magicznie przechodzi prosto przez cienki wrogi statek, nie zadając uszkodzeń?

Ten błąd jest znany jako tunelowanie.

Ponieważ oddzielne gry aktualizują się w odstępach czasowych (\(\Delta t = 16,6\text{ms}\) przy 60 FPS), obiekt poruszający się z szybkością 1800 pikseli na sekundę przemieszcza się 30 pikseli w pojedynczej klatce. Jeśli kadłub wroga ma tylko 15 pikseli grubości, kula znajdowała się przed wrogiem w klatce 1 i całkowicie za wrogiem w klatce 2.

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

Rozwiązanie: Segment ukosowany a pudełko (metoda płyty)

Zamiast testować pojedynczy punkt, testujemy cały segment linii łączący pozycję pocisku w klatce 1 („poprzednia pozycja”) z klatką 2 („bieżąca pozycja”).

Oto produkcyjna metoda przecięcia płyt metodą raycastingu z naszego towarzyszącego tytułu 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;
    }
}

W Blocked: Pixel Panzer, ciągła umiejętność Laser Beam gracza wykorzystuje dokładnie ten raycast, aby przecinać rzędy opadających bloków, nie pomijając ani jednej kolizji.


6. Poziom 5: Doskonałe wykrywanie kolizji na poziomie produkcyjnym

Teraz osiągamy najwyższy poziom dokładności 2D: Kolizja idealna w pikselach.

W retro strzelance czołgowej lub walce powietrznej na statku kosmicznym nieregularne kształty (beczki czołgów, skrzydła, kokpity kokpitu) są otoczone przezroczystymi pikselami w teksturze sprite'a. Kiedy wrogi pocisk uderza w tę przezroczystą przestrzeń, gracze natychmiast to zauważają.

Doskonała kolizja pikseli sprawdza rzeczywiste kanały alfa (przezroczystość) nakładających się tekstur. Jeśli dwa nieprzezroczyste piksele nakładają się na tę samą współrzędną świata, następuje prawdziwe fizyczne trafienie.

Fatalny błąd: „GetData” wewnątrz „Update()”.

Wiele samouczków instruuje początkujących, jak to zrobić:

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

Wywołanie Texture2D.GetData() w czasie wykonywania zmusza procesor do oczekiwania na opróżnienie potoku GPU, co powoduje przeciągnięcie pamięci tekstur z powrotem przez magistralę. Wykonanie tej czynności dla dziesiątek duszków w każdej klatce spowoduje spowolnienie gry z 60 klatek na sekundę do 5 klatek na sekundę i natychmiastowe wywołanie błędów ANR systemu Android!

Sprawdzone w boju rozwiązanie od Zablokowano: Pixel Panzer

W Blocked: Pixel Panzer nasz Sprite.cs implementuje ultra zoptymalizowaną procedurę IntersectsPixel zbudowaną wokół 5 kluczowych zasad:

  1. Statyczna pamięć podręczna tablicy kolorów: GetData jest wywoływana dokładnie raz podczas ładowania tekstury i zapisywana w Dictionary<Texture2D, Color[]>.
  2. AABB Strażnik wczesnego wyjścia: Jeśli Bounds.Intersects(other.Bounds) jest fałszywe, natychmiast się ratujemy. 99% kontroli jest eliminowanych przed sprawdzeniem pojedynczego piksela.
  3. Obsługa Atlasu Tekstur i SourceRectangle: Obsługuje ikonki spakowane w atlasy tekstur przy użyciu przesunięć prostokątów źródłowych.
  4. Obliczone okno nakładania się: Zapętlamy tylko dokładny prostokąt przecięcia obu duszków („Math.Max(a.Top, b.Top)` itp.).
  5. Zwarcie alfa: Jeśli piksel Sprite'a A jest przezroczysty (A <= 20), Sprite B zostanie całkowicie pominięty.
  6. Przenoszenie właściwości: Dostęp do właściwości wirtualnych („Granice”) w zagnieżdżonych pętlach tworzy tysiące kopii struktur. Przed wejściem do pętli przechowujemy je w lokalnych zmiennych stosu.

Oto kompletny, gotowy do produkcji 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;
    }
}

Ta procedura jest wykonywana w czasie krótszym niż 0,1 milisekundy na nowoczesnym sprzęcie i komputerach z systemem Android.


7. Poziom 6: Szerokofazowa sieć przestrzenna i mobilny GC z zerową alokacją

Wyobraź sobie, że Twoja gra składa się ze 100 bloków, 60 kul i 20 wrogów. Jeśli testujesz każdy element względem każdego innego elementu za pomocą zagnieżdżonych pętli „for”, wykonujesz:

\(\frac{180 \times 179}{2} = 16 110 \text{ kontroli na klatkę!}\)

Przy 60 FPS oznacza to prawie 1 000 000 kontroli kolizji na sekundę.

Aby skalować grę, musisz wdrożyć szerokofazowy podział przestrzenny.

Jednolita przestrzenna siatka skrótów

Dzielimy ekran na siatkę 2D jednolitych komórek (w Blocked: Pixel Panzer każda komórka to 150 \(\ razy 150\) pikseli). Obiekt testuje kolizje jedynie z innymi obiektami znajdującymi się w tych samych komórkach siatki.

Problem z pamięcią mobilną: szarpanie GC

Jeśli twoja SpatialGrid tworzy nową List<Point>() lub nową List<Block>() w każdej ramce, co minutę alokujesz megabajty śmieci na zarządzanej stercie.

W środowisku wykonawczym Mono Androida powoduje to częste zbieranie śmieci Gen-0, powodując: mono środowisko wykonawcze: rywalizacja o blokadę natywną (mono_class_is_subclass_)

Gra się zacina, a Google Play oznacza ją ostrzeżeniami ANR!

Rozwiązanie: bufory zarysowania wielokrotnego użytku

Oto przestrzenna siatka mieszająca o zerowej alokacji z CollisionManager.cs w 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);
                            }
                        }
                    }
                }
            }
        }
    }
}

Ponowne użycie _scratchCells i _scratchCheckedEntities powoduje, że cały krok kolizji przebiega z 0 bajtami alokacji sterty na ramkę.


8. Podsumowanie tabeli porównawczej

Technika Koszt matematyczny Wsparcie rotacyjne Tunelowanie bezpieczne? Najlepszy przypadek użycia w grze MonoGame
AABB („Prostokąt.Przecięcia”) Ultraszybki (~4 porównania liczb całkowitych) Nie Nie Bloki siatki, kula szerokofazowa, interfejs użytkownika
Koło („Odległość do kwadratu”) Bardzo szybko (3 muls, 0 sqrt) Tak (niezmienny) Nie Okrągłe statki, kule ognia, kule energii
Koło kontra pudełko („zacisk”) Szybki (lokalny MathHelper.Clamp) Tak Nie Okrągły gracz poruszający się po ciasnych ścianach
Promień skośny (CCD płyty) Umiarkowany (Ramieniowanie parametryczne) Tak Tak Szybkie kule snajperskie, działa szynowe, lasery
Doskonały piksel („Przecina piksel”) Selektywny (podrekt ograniczony) Tak Nie Nieregularne kontury duszków, uczciwe hitboxy
Siatka przestrzenna (Zero-GC) Szerokofazowy (\(O(N^2) \do O(N)\)) Nie dotyczy Nie dotyczy Gęste fale, piekło kul, Android 60 FPS

Pokazy produkcji z prawdziwego świata: gry Arar

Te architektury kolizyjne nie są eksperymentami teoretycznymi — są prawdziwym fundamentem inżynieryjnym, na którym opierają się nasze komercyjnie wydane tytuły:

  • Zablokowano: Pixel Panzer: Nasza gra zręcznościowa w stylu retro o przetrwaniu w czołgach w Google Play i Microsoft Store. Zawiera nasz kompletny dwupoziomowy system kolizyjny: przestrzenną siatkę mieszającą o zerowej alokacji, filtrującą spadające bloki oraz doskonałe w pikselach sprawdzanie kontaktu dla czołgów, myśliwców, pocisków wieży i amunicji elementarnej ColorWheel.
  • Paint Trek: Nasza szybka strzelanka kosmiczna obejmująca zderzenia kół obrotowych, ciągłe rzucanie promieni i systemy obrony przeciwrakietowej o szerokim zasięgu.

Wnioski i kolejne kroki

MonoGame daje Ci możliwość zaprojektowania wykrywania kolizji, które idealnie pasuje do potrzeb Twojej gry. Łącząc drogie testy Pixel-Perfect z szybkimi testami AABB, wykorzystując Odległości do kwadratu dla okręgów i eliminując zbieranie śmieci za pomocą Buforów zarysowania wielokrotnego użytku, możesz zapewnić płynną na konsoli wydajność 60/120 FPS zarówno na komputerach stacjonarnych, jak i na platformach mobilnych.

Sprawdź nasze gry w sklepach z aplikacjami, aby zobaczyć te systemy kolizyjne w akcji i już dziś zacznij wdrażać te wzorce we własnych projektach MonoGame!


Linki i zasoby do sklepu


Słowa kluczowe i hashtagi SEO

Słowa kluczowe: wykrywanie kolizji 2D MonoGame, tworzenie gier w C#, Rectangle.Intersects MonoGame, pikselowa kolizja C#, przestrzenna siatka mieszająca MonoGame, ciągłe kolizje Swept Ray, optymalizacja wydajności gier niezależnych, pętla gry z zerowym przydziałem, optymalizacja Android MonoGame, gry Arar, Blocked Pixel Panzer, Paint Trek.

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




Hiç yorum yok:

Yorum Gönder