27 Eylül 2026 Pazar

Ανίχνευση σύγκρουσης 2D στο MonoGame: Από βασικά ορθογώνια σε συστήματα μηδενικής κατανομής pixel-Perfect




Δισδιάστατη ανίχνευση σύγκρουσης στο MonoGame: Από βασικά ορθογώνια σε συστήματα μηδενικής κατανομής pixel-Perfect

Κατά τη δημιουργία παιχνιδιών 2D σε οπτικές μηχανές όπως το Unity ή το Godot, η ανίχνευση σύγκρουσης μοιάζει συχνά με μια σειρά πλαισίων ελέγχου επιθεωρητή: χτυπάτε ένα "BoxCollider2D" ή "CircleCollider2D", επισυνάπτετε ένα "Rigidbody" και ελπίζετε ότι το εσωτερικό βήμα της φυσικής δεν τραυλίζει σε κινητές συσκευές.

Στο MonoGame και C#, ωστόσο, έχετε τον απόλυτο έλεγχο. Δεν υπάρχουν κρυφές επιβαρύνσεις φυσικής, ανεπιθύμητη περιστροφική αδράνεια και μυστηριώδεις αιχμές συλλογής σκουπιδιών (GC) που κλέβουν τον προϋπολογισμό του καρέ σας.

Στην Arar Games, όταν κατασκευάσαμε τα Blocked: Pixel Panzer και Paint Trek, οι βρόχοι παιχνιδιών arcade χρειάζονταν για την επεξεργασία εκατοντάδων εχθρικών σφαιρών υψηλής ταχύτητας, εκρηκτικών πλέγματος από τούβλα, περιστρεφόμενων πυργίσκων δεξαμενών, αεροσκαφών μαχητικού αεροσκάφους και ασπίδων σωματιδίων σε 60 έως 120 FPS σε συσκευές PC και Android. Μια μηχανή φυσικής γενικής χρήσης ήταν εκτός θέματος - χρειαζόμασταν μια ειδικά σχεδιασμένη, κλιμακωτή αρχιτεκτονική σύγκρουσης.

Σε αυτόν τον περιεκτικό οδηγό κώδικα, θα ξεκινήσουμε από τα βασικά στοιχεία της σύγκρουσης MonoGame ("Rectangle.Intersects" με πραγματικά "Bullet" και "Enemy" sprites) και θα αναπτύξουμε προηγμένους κύκλους ελέγχους, μικτό σύσφιξη, εκπομπές ακτίνων κατά της σήραγγας, ποιότητας παραγωγής Pixelsion-A*Perfect. Πλέγματα βελτιστοποιημένα για επιβίωση GC για κινητά.


1. The Foundation: A Simple MonoGame Sprite Hierarchy

Πριν ανιχνεύσουμε συγκρούσεις, χρειαζόμαστε καθαρές οντότητες παιχνιδιού. Στο MonoGame, μια οντότητα κατέχει βασικά μια θέση, μια υφή και ένα οριοθετημένο ορθογώνιο.

Ακολουθεί η βασική αρχιτεκτονική οντοτήτων που χρησιμοποιείται στα παιχνίδια μας:

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

Τώρα ας δημιουργήσουμε συγκεκριμένες κατηγορίες «Player», «Enemy» και «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;
    }
}

Με αυτές τις οντότητες στη θέση τους, ας εξερευνήσουμε πώς να ανιχνεύσουμε συγκρούσεις μεταξύ τους, ξεκινώντας από την απλούστερη μέθοδο.


2. Επίπεδο 1: Η απλούστερη σύγκρουση – «Ορθογώνιο. Τέμνονται» (AABB)

Ο πιο θεμελιώδης έλεγχος 2D σύγκρουσης στο MonoGame είναι η δοκιμή Axis-Aligned Bounding Box (AABB). Ο όρος "ευθυγραμμισμένος άξονας" σημαίνει απλώς ότι οι άκρες του ορθογωνίου είναι εντελώς παράλληλες με τους άξονες \(X\) και \(Y\) της οθόνης (χωρίς περιστροφή).

Το MonoGame παρέχει μια γρήγορη, ενσωματωμένη μέθοδο: "Rectangle.Intersects(Rectangle value)".

Πώς λειτουργεί το «Rectangle.Intersects» κάτω από την κουκούλα

Κάτω από την επιφάνεια, το MonoGame εκτελεί τέσσερις συγκρίσεις ακεραίων:

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

Εάν πληρούνται και οι τέσσερις προϋποθέσεις, τα ορθογώνια επικαλύπτονται. Εάν έστω και μία συνθήκη αποτύχει, ένας κενός άξονας τους χωρίζει και δεν είναι δυνατή η σύγκρουση.

Πραγματικός κωδικός παιχνιδιού: "Bullet" εναντίον "Enemy" στο "Game1.Update"

Δείτε πώς ελέγχετε τις συγκρούσεις μεταξύ μιας λίστας ενεργών κουκκίδων και μιας λίστας ενεργών εχθρών μέσα στον κύριο βρόχο «Ενημέρωση» του 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);
    }
}

Συμβουλή απόδοσης: Παρατηρήστε ότι επαναλαμβάνουμε αντίστροφα («για (int i = λίστα.Μέτρηση - 1; i >= 0; i--)»)! Εάν χρησιμοποιείτε το "foreach" και προσπαθήσετε να καλέσετε "_bullets.Remove(bullet)", το C# εμφανίζει ένα "InvalidOperationException: Η συλλογή τροποποιήθηκε". Η επανάληψη προς τα πίσω επιτρέπει την ασφαλή αφαίρεση στοιχείων χωρίς προβλήματα δημιουργίας ευρετηρίου μνήμης.

Το μυστικό Arcade: "Fair Hitboxes" μέσω "Inflate".

Σε ρετρό παιχνίδια όπως το Blocked: Pixel Panzer, οι υφές sprite συχνά περιλαμβάνουν διαφανή περιθώρια ή αιχμές κεραίας. Εάν το τανκ του παίκτη εκραγεί επειδή μια σφαίρα άγγιξε μια άδεια διαφανή γωνία της υφής του, ο παίκτης θα αισθανθεί εξαπατημένος.

Για να κάνουν τη σύγκρουση να αποκρίνεται και να είναι δίκαιη, τα παιχνίδια χρησιμοποιούν ένα μικρότερο Hitbox μέσα στο sprite χρησιμοποιώντας το "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: Σύγκρουση από κύκλο σε κύκλο (Περιστροφική ανοσία)

Τα ορθογώνια λειτουργούν εξαιρετικά για στατικά μπλοκ και πλακίδια πλέγματος, αλλά αποτυγχάνουν όταν τα sprites περιστρέφονται. Όταν ένα μη τετράγωνο διαστημόπλοιο περιστρέφεται στο Paint Trek, ένα πλαίσιο οριοθέτησης ευθυγραμμισμένου με τον άξονα πρέπει να επεκταθεί για να περικλείσει τις περιστρεφόμενες γωνίες, με αποτέλεσμα απογοητευτικές "φανταστικές συγκρούσεις" στον κενό αέρα.

Για κυκλικούς αστεροειδείς, ενεργειακές σφαίρες που επιστρέφουν στο σπίτι και περιστρεφόμενα διαστημόπλοια, οι Οριακοί Κύκλοι είναι η ιδανική λύση.

Η Παγίδα της Τετραγωνικής Ρίζας

Δύο κύκλοι συγκρούονται όταν η απόσταση μεταξύ των κέντρων τους είναι μικρότερη ή ίση με το άθροισμα των ακτίνων τους:

\(\text{Απόσταση}(C_A, C_B) \le r_A + r_B\)

Στον κώδικα, ο υπολογισμός της Ευκλείδειας απόστασης χρησιμοποιεί «Math.Sqrt» (ή «Vector2.Distance»). Ωστόσο, ο υπολογισμός των τετραγωνικών ριζών σε έναν βρόχο με 200 βλήματα καίει εκατοντάδες περιττούς κύκλους CPU!

Συγκρίνοντας την τετραγωνική απόσταση με το άθροισμα της τετραγωνικής ακτίνας, εξαλείφουμε εντελώς την τετραγωνική ρίζα:

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

Υλοποίηση MonoGame: «Κύκλος» έναντι «Κύκλος».

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

Τώρα ενσωματώστε το απευθείας σε μια οντότητα:

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

Μηδενικές τετραγωνικές ρίζες, ανοσία στην περιστροφή sprite και αστραπιαία.


4. Επίπεδο 3: Μικτά σχήματα – Κύκλος εναντίον Πλαισίου («MathHelper.Clamp»)

Τι συμβαίνει όταν ένα κυκλικό διαστημόπλοιο στο Paint Trek πλοηγείται μέσα από έναν σφιχτό λαβύρινθο από ορθογώνια αμυντικά εμπόδια ή όταν μια στρογγυλή σφαίρα χτυπά ένα τετράγωνο τετράγωνο στο Blocked: Pixel Panzer;

Χρειαζόμαστε σύγκρουση Κύκλος εναντίον Ορθογώνιου.

Ο αλγόριθμος σύσφιξης

Η στρατηγική είναι να βρείτε το σημείο στο ορθογώνιο που είναι πιο κοντά στο κέντρο του κύκλου και στη συνέχεια να ελέγξετε εάν η απόσταση από αυτό το πλησιέστερο σημείο στο κέντρο είναι μικρότερη από την ακτίνα του κύκλου.

Το "MathHelper.Clamp" της MonoGame κάνει αυτό το ασήμαντο:

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

Τώρα μπορείτε να δοκιμάσετε ασπίδες παικτών έναντι ορθογώνιων τούβλων με μηδενικές κατανομές και υψηλή ακρίβεια!


5. Επίπεδο 4: Ανίχνευση συνεχούς σύγκρουσης (CCD) & ακτίνες σάρωσης

Έχετε πυροβολήσει ποτέ έναν γύρο ελεύθερου σκοπευτή υπερταχύτητας ή ένα λέιζερ railgun στο παιχνίδι σας, μόνο για να παρακολουθήσετε τη σφαίρα να περνάει μαγικά κατευθείαν μέσα από ένα λεπτό εχθρικό πλοίο χωρίς να προκαλέσει ζημιά;

Αυτό το σφάλμα είναι γνωστό ως tunneling.

Επειδή τα διακριτά παιχνίδια ενημερώνονται με χρονικά βήματα (\(\Delta t = 16,6\text{ms}\) στα 60 FPS), ένα αντικείμενο που κινείται με ταχύτητα 1.800 pixel ανά δευτερόλεπτο ταξιδεύει 30 pixel σε ένα μόνο πλαίσιο. Εάν η γάστρα του εχθρού έχει πάχος μόνο 15 pixel, η σφαίρα ήταν μπροστά από τον εχθρό στο πλαίσιο 1 και εντελώς πίσω από τον εχθρό στο πλαίσιο 2.

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

Η λύση: Swept Segment vs. Box (Μέθοδος πλάκας)

Αντί να δοκιμάσουμε ένα μόνο σημείο, δοκιμάζουμε ολόκληρο το τμήμα γραμμής που συνδέει τη θέση της κουκκίδας στο πλαίσιο 1 («προηγούμενη θέση») με το πλαίσιο 2 («τρέχουσα θέση»).

Εδώ είναι η μέθοδος παραγωγής ακτινοβολίας πλάκας-τομής από τον συνοδευτικό μας τίτλο 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, η συνεχής ικανότητα Δέσμης λέιζερ του παίκτη χρησιμοποιεί αυτήν την ακριβή εκπομπή ακτίνων για να τεμαχίσει σειρές κατερχόμενων μπλοκ χωρίς να χάσει ούτε μια σύγκρουση.


6. Επίπεδο 5: Παραγωγή-Βαθμός Pixel-Τέλεια ανίχνευση σύγκρουσης

Τώρα φτάνουμε στο απόλυτο επίπεδο ακρίβειας 2D: Pixel-Perfect Collision.

Σε ένα ρετρό σκοπευτή τανκ ή σκυλομαχία διαστημόπλοιου, τα ακανόνιστα σχήματα (βαρέλια δεξαμενής, φτερά, πιλοτήρια πιλοτηρίου) περιβάλλονται από διαφανή pixel στην υφή sprite. Όταν ένας εχθρικός πύραυλος χτυπήσει αυτόν τον διαφανή χώρο, οι παίκτες το αντιλαμβάνονται αμέσως.

Η σύγκρουση τέλεια για pixel επιθεωρεί τα πραγματικά κανάλια άλφα (διαφάνεια) των επικαλυπτόμενων υφών. Εάν δύο αδιαφανή pixel επικαλύπτονται στην ίδια παγκόσμια συντεταγμένη, έχει συμβεί ένα πραγματικό φυσικό χτύπημα.

Το μοιραίο λάθος: «GetData» μέσα στο «Update()».

Πολλά μαθήματα καθοδηγούν τους αρχάριους να το κάνουν αυτό:

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

Η κλήση του «Texture2D.GetData()» κατά τη διάρκεια του χρόνου εκτέλεσης αναγκάζει την CPU να περιμένει να ξεπλυθεί η γραμμή GPU, τραβώντας τη μνήμη texture πίσω από το δίαυλο. Κάνοντας αυτό για δεκάδες sprites, κάθε καρέ, θα ανεβάσει το παιχνίδι σας από 60 FPS σε 5 FPS και θα ενεργοποιήσει αμέσως ANR Android!

The Battle-Tested Solution από το Blocked: Pixel Panzer

Στο Blocked: Pixel Panzer, το "Sprite.cs" μας εφαρμόζει μια εξαιρετικά βελτιστοποιημένη ρουτίνα "IntersectsPixel" που βασίζεται σε 5 κρίσιμους κανόνες:

  1. Cache Static Color Array: Το "GetData" καλείται ακριβώς μία φορά όταν φορτώνεται η υφή και αποθηκεύεται σε ένα "Dictionary<Texture2D, Color[]>".
  2. AABB Early Exit Guard: Εάν το "Bounds.Intersects(other.Bounds)" είναι ψευδές, διασώζουμε αμέσως. Το 99% των ελέγχων εξαλείφονται πριν από την επιθεώρηση ενός μόνο pixel.
  3. Texture Atlas & SourceRectangle Υποστήριξη: Χειρίζεται sprites συσκευασμένα σε άτλαντες υφής χρησιμοποιώντας μετατοπίσεις ορθογωνίου πηγής.
  4. Υπολογισμένο παράθυρο επικάλυψης: Κάνουμε βρόχο μόνο πάνω από το ακριβές ορθογώνιο τομής μεταξύ των δύο sprites (Math.Max(a.Top, b.Top), κ.λπ.).
  5. Βραχυκύκλωμα Alpha: Εάν το εικονοστοιχείο του Sprite A είναι διαφανές (A <= 20), το Sprite B παραλείπεται εντελώς.
  6. Ανύψωση ιδιοτήτων: Η πρόσβαση σε εικονικές ιδιότητες ('Bounds') σε ένθετους βρόχους δημιουργεί χιλιάδες αντίγραφα δομής. Τα αποθηκεύουμε σε τοπικές μεταβλητές στοίβας πριν μπούμε στον βρόχο.

Εδώ είναι ο πλήρης, έτοιμος για παραγωγή κώδικα:

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

Αυτή η ρουτίνα εκτελείται σε λιγότερο από 0,1 χιλιοστά του δευτερολέπτου σε σύγχρονο υλικό Android και επιτραπέζια συστήματα.


7. Επίπεδο 6: Χωρικό πλέγμα ευρείας φάσης & μηδενική κατανομή Mobile GC

Φανταστείτε ότι το παιχνίδι σας έχει 100 μπλοκ, 60 σφαίρες και 20 εχθρούς. Εάν δοκιμάσετε κάθε οντότητα έναντι κάθε άλλης οντότητας χρησιμοποιώντας ένθετους βρόχους «for», εκτελείτε:

\(\frac{180 \times 179}{2} = 16.110 \text{ έλεγχοι ανά καρέ!}\)

Στα 60 FPS, δηλαδή σχεδόν 1.000.000 έλεγχοι σύγκρουσης κάθε δευτερόλεπτο.

Για να κλιμακώσετε το παιχνίδι σας, πρέπει να εφαρμόσετε Ευρεία Φάση Χωρικής Διαμέρισης.

Το ομοιόμορφο χωρικό πλέγμα κατακερματισμού

Χωρίζουμε την οθόνη σε ένα πλέγμα 2D ομοιόμορφων κελιών (στο Blocked: Pixel Panzer, κάθε κελί είναι $150 \ επί 150 $ pixel). Μια οντότητα ελέγχει μόνο συγκρούσεις με άλλες οντότητες που βρίσκονται στα ίδια κελιά πλέγματος.

Το πρόβλημα της μνήμης του κινητού: GC Thrashing

Εάν το "SpatialGrid" σας δημιουργεί "new List()" ή "new List()" κάθε καρέ, εκχωρείτε megabyte σκουπιδιών κάθε λεπτό στον διαχειριζόμενο σωρό.

Στο χρόνο εκτέλεσης Mono του Android, αυτό ενεργοποιεί συχνές συλλογές σκουπιδιών Gen-0, προκαλώντας: μονόχρονος χρόνος εκτέλεσης: Ισχυρή διαμάχη κλειδώματος (mono_class_is_subclass_)

Το παιχνίδι τραυλίζει και το Google Play επισημαίνει το παιχνίδι σας με προειδοποιήσεις ANR!

Η λύση: Επαναχρησιμοποιήσιμα Scratch Buffer

Ακολουθεί το Χωρικό πλέγμα κατακερματισμού μηδενικής κατανομής από το CollisionManager.cs στο Αποκλεισμένο: 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);
                            }
                        }
                    }
                }
            }
        }
    }
}

Με την επαναχρησιμοποίηση των «_scratchCells» και «_scratchCheckedEntities», ολόκληρο το βήμα σύγκρουσης εκτελείται με 0 byte κατανομής σωρού ανά καρέ.


8. Συνοπτικός Συγκριτικός Πίνακας

Τεχνική Μαθηματικό Κόστος Περιστροφική Υποστήριξη Ασφαλής διάνοιξη σήραγγας; Καλύτερη περίπτωση χρήσης στο MonoGame
AABB ("Ορθογώνιο. Τέμνεται") Εξαιρετικά γρήγορο (~4 συγκρίσεις ακεραίων) Όχι Όχι Μπλοκ πλέγματος, κουκκίδα ευρείας φάσης, διεπαφή χρήστη
Κύκλος (DistanceSquared) Πολύ γρήγορα (3 muls, 0 sqrt) Ναι (Αμετάβλητο) Όχι Στρογγυλά πλοία, βολίδες, ενεργειακές σφαίρες
Circle vs Box (Clamp) Fast (Local MathHelper.Clamp) Ναι Όχι Κυκλικός παίκτης που περιηγείται σε στενούς τοίχους
Swept Ray (Πλάκα CCD) Μέτρια (Παραμετρική Raycast) Ναι Ναι Γρήγορες σφαίρες ελεύθερου σκοπευτή, όπλα, λέιζερ
Pixel-Perfect (IntersectsPixel) Επιλεκτική (Περιορισμένη Υπο-ορθή) Ναι Όχι Ακανόνιστα περιγράμματα sprite, ανοιχτόχρωμα hitbox
Χωρικό Πλέγμα (Μηδέν-GC) Ευρεία φάση (\(O(N^2) \έως O(N)\)) N/A N/A Πυκνά κύματα, κόλαση, Android 60 FPS

Εκθέσεις παραγωγής πραγματικού κόσμου: Arar Games

Αυτές οι αρχιτεκτονικές σύγκρουσης δεν είναι θεωρητικά πειράματα - είναι το πραγματικό θεμέλιο μηχανικής που τροφοδοτεί τους τίτλους μας που κυκλοφορούν στο εμπόριο:

  • Αποκλεισμένο: Pixel Panzer: Το ρετρό παιχνίδι μας arcade επιβίωσης δεξαμενών στο Google Play και το Microsoft Store. Διαθέτει το πλήρες σύστημα σύγκρουσης δύο επιπέδων: ένα χωρικό πλέγμα κατακερματισμού μηδενικής κατανομής που φιλτράρει πέφτουν μπλοκ και τέλειο έλεγχο επαφής με pixel για άρματα μάχης, μαχητικά αεροσκάφη, οβίδες πυργίσκων και στοιχειώδη πυρομαχικά ColorWheel.
  • Paint Trek: Ο γρήγορος διαστημικός σκοπευτής μας διαθέτει περιστροφική κυκλική σύγκρουση, συνεχή εκπομπή ακτίνων και αντιπυραυλικά αμυντικά συστήματα σαρωμένου όγκου.

Συμπέρασμα & Επόμενα Βήματα

Το MonoGame σάς δίνει τη δύναμη να σχεδιάσετε ανίχνευση σύγκρουσης που ταιριάζει απόλυτα με τις ανάγκες του παιχνιδιού σας. Πραγματοποιώντας ακριβούς ελέγχους Pixel-Perfect πίσω από γρήγορες δοκιμές AABB, χρησιμοποιώντας Τετραγωνικές αποστάσεις για κύκλους και εξαλείφοντας τη συλλογή σκουπιδιών με Επαναχρησιμοποιήσιμα Scratch Buffer, μπορείτε να προσφέρετε ομαλή απόδοση 60/120 FPS για επιτραπέζιους υπολογιστές και φορητές πλατφόρμες.

Ρίξτε μια ματιά στα παιχνίδια μας στα καταστήματα εφαρμογών για να δείτε αυτά τα συστήματα σύγκρουσης σε δράση και ξεκινήστε να εφαρμόζετε αυτά τα μοτίβα στα δικά σας έργα MonoGame σήμερα!


Σύνδεσμοι και πόροι καταστήματος


Λέξεις-κλειδιά & Hashtags SEO

Λέξεις-κλειδιά: Ανίχνευση σύγκρουσης MonoGame 2D, ανάπτυξη παιχνιδιών C#, Rectangle.Intersects MonoGame, pixel τέλεια σύγκρουση C#, χωρικό πλέγμα κατακερματισμού MonoGame, συνεχής σύγκρουση ακτίνων σάρωσης, βελτιστοποίηση απόδοσης ανεξάρτητου παιχνιδιού, μηδενική κατανομή παιχνιδιών βρόχος, Android MonoG βελτιστοποιημένος βρόχος παιχνιδιών, Παιχνίδια Android MonoG Paxelme Οδοιπορικό.

#MonoGame #CSharp #GameDev #IndieDev #GamePhysics #DotNet #2DGameDev #MobileGameDev #PerformanceOptimization #CleanCode #BlockedPixelPinar «#Προγραμματισμός παιχνιδιού».




Hiç yorum yok:

Yorum Gönder