27 Eylül 2026 Pazar

2D ütközésészlelés a MonoGame játékban: az alapvető téglalapoktól a nulla kiosztású pixel-tökéletes rendszerekig




2D ütközésészlelés a MonoGame játékban: az alap téglalapoktól a nulla kiosztású pixel tökéletes rendszerekig

Amikor 2D-s játékokat készítenek olyan vizuális motorokban, mint a Unity vagy a Godot, az ütközésészlelés gyakran úgy tűnik, mintha egy sor ellenőrző jelölőnégyzet lenne: rácsapsz egy "BoxCollider2D" vagy "CircleCollider2D"-re, csatolsz egy "Rigidbody"-t, és reméled, hogy a belső fizika lépései nem akadoznak a mobileszközökön.

A MonoGame és C# játékban azonban teljes mértékben te irányítod. Nincs rejtett fizika, nincs nem kívánt forgási tehetetlenség, és nincsenek rejtélyes szemétgyűjtési (GC) tüskék, amelyek ellopnák a keret költségvetését.

Az Arar Games-en, amikor megépítettük a Blocked: Pixel Panzert és a Paint Treket, arcade játékhurkjainknak több száz nagysebességű ellenséges lövedék, felrobbanó téglarács, forgó harckocsi tornyok, vadászrepülőgépek átrepülése és részecskepajzsok feldolgozásához volt szükség Windows 20PS-en és 60 FPS-en egyaránt. Az általános célú fizikai motor szóba sem jöhetett – szükségünk volt egy erre a célra épített, többszintű ütközési architektúrára.

Ebben az átfogó, kódvezérelt útmutatóban a MonoGame ütközés abszolút alapjaiból indulunk ki ("Rectangle.Intersects" valódi "Bullet" és "Enemy" sprite-ekkel), és továbbhaladunk a fejlett körellenőrzésekig, vegyes rögzítéshez, alagút-gátló sugárküldéshez, éles szintű Pixel-Perfectation-összeállításhoz**, és kiosztáshoz**. mobil GC túlélésre optimalizálva.


1. Az alapítvány: Egy egyszerű MonoGame Sprite hierarchia

Az ütközések észlelése előtt tiszta játék entitásokra van szükségünk. A MonoGame-ben egy entitás alapvetően egy pozícióval, egy textúrával és egy határoló téglalappal rendelkezik.

Íme a játékainkban használt alap entitás architektú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);
    }
}

Most hozzunk létre konkrét „Player”, „Enemy” és „Bullet” osztályokat:

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

Ha ezek az entitások a helyükön vannak, akkor a legegyszerűbb módszertől kezdve vizsgáljuk meg, hogyan lehet észlelni a köztük lévő ütközéseket.


2. 1. szint: A legegyszerűbb ütközés – „Téglalap.Metszik” (AABB)

A MonoGame legalapvetőbb 2D ütközési ellenőrzése az Axis-Aligned Bounding Box (AABB) teszt. A "tengelyhez igazított" kifejezés egyszerűen azt jelenti, hogy a téglalap élei teljesen párhuzamosak a képernyő \(X\) és \(Y\) tengelyével (nincs elforgatás).

A MonoGame egy gyors, beépített módszert kínál: Rectangle.Intersects(Rectangle value).

Hogyan működik a Rectangle.Intersects a motorháztető alatt

A felület alatt a MonoGame négy egész szám összehasonlítást hajt végre:

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

Ha mind a négy feltétel teljesül, a téglalapok átfedik egymást. Ha akár egy feltétel is meghiúsul, egy üres tengely választja el őket, és nem lehetséges ütközés.

Valódi játékkód: "Bullet" vs. "Enemy" a "Game1.Update"-ben

Így ellenőrizheti az ütközéseket az aktív golyók listája és az aktív ellenségek listája között a fő MonoGame „Frissítés” hurkon belül:

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

Tipp a teljesítményhez: Figyeljük meg, hogy visszafelé iterálunk (for (int i = list.Count - 1; i >= 0; i--))! Ha a "foreach" parancsot használja, és megpróbálja meghívni a "_bullets.Remove(bullet)" parancsot, a C# egy "InvalidOperationException: Collection was modified" (InvalidOperationException: Collection was modified) üzenetet dob. A visszafelé történő iteráció lehetővé teszi az elemek biztonságos eltávolítását memória-újraindexelési problémák nélkül.

The Arcade Secret: "Fair Hitboxes" a "Felfújás" segítségével

Az olyan retro játékokban, mint a Blocked: Pixel Panzer, a sprite textúrák gyakran tartalmaznak átlátszó margókat vagy antennatüskéket. Ha a játékos tankja felrobban, mert egy golyó hozzáért a textúrájának egy üres átlátszó sarkához, a játékos becsapva érzi magát.

Annak érdekében, hogy az ütközés érzékeny és méltányos legyen, a játékok egy kisebb Hitboxot használnak a sprite belsejében a "Rectangle.Inflate" funkcióval:

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. szint: Kör-kör ütközés (Forgástűrés)

A téglalapok kiválóan használhatók statikus blokkokhoz és rácscsempékhez, de nem működnek, amikor a sprite-ok forognak. Amikor egy nem négyzet alakú űrhajó forog a Paint Trek-ben, egy tengelyhez igazított határolókeretnek ki kell tágulnia, hogy körülzárja a forgó sarkokat, ami frusztráló "fantomütközéseket" eredményez az üres levegőben.

A körkörös aszteroidák, az egymásra irányító energiagömbök és a forgó űrhajók esetében a Határkörök az ideális megoldás.

A négyzetgyökér csapda

Két kör ütközik, ha a középpontjuk távolsága kisebb vagy egyenlő, mint sugaruk összege:

\(\text{Távolság}(C_A, C_B) \le r_A + r_B\)

A kódban az euklideszi távolság kiszámítása a "Math.Sqrt" (vagy "Vector2.Distance") függvényt használja. Azonban a négyzetgyökök kiszámítása egy hurokban 200 lövedékkel több száz felesleges CPU ciklust éget el!

A négyzetes távolság és a négyzetes sugárösszeg összehasonlításával teljesen kiküszöböljük a négyzetgyököt:

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

MonoGame megvalósítás: "Kör" vs. "Kör".

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

Most integrálja ezt közvetlenül egy entitásba:

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

Nulla négyzetgyök, immunis a sprite forgására, és villámgyors.


4. 3. szint: Vegyes alakzatok – Kör kontra doboz (MathHelper.Clamp)

Mi történik, ha a Paint Trek kör alakú űrhajója áthalad a téglalap alakú védelmi akadályok szűk labirintusán, vagy ha egy kerek golyó eltalálja a négyzet alakú blokkot a Blocked: Pixel Panzer-ban?

Szükségünk van a Kör vs. Téglalap ütközésre.

A rögzítési algoritmus

A stratégia az, hogy megkeressük a téglalapon azt a pontot, amely a legközelebb van a kör középpontjához, majd megvizsgáljuk, hogy a legközelebbi pont és a középpont távolsága kisebb-e, mint a kör sugara.

A MonoGame MathHelper.Clamp-ja ezt triviálissá teszi:

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

Mostantól tesztelheti a játékospajzsokat téglalap alakú téglákkal szemben nulla kiosztással és nagy pontossággal!


5. 4. szint: Folyamatos ütközésészlelés (CCD) és söpört sugarak

Lőttél már valaha egy nagysebességű mesterlövész kört vagy egy sínpisztolylézert a játékban, csak azért, hogy megnézd, ahogy a golyó varázslatosan áthalad egy vékony ellenséges hajón anélkül, hogy sebzést okozna?

Ezt a hibát tunneling néven ismerik.

Mivel a diszkrét játékok időbeli lépésekben frissülnek (\(\Delta t = 16,6\text{ms}\) 60 FPS-nél), egy 1800 képpont/másodperc sebességgel mozgó objektum 30 képpontot tesz meg egyetlen képkockán belül. Ha az ellenséges hajótest csak 15 pixel vastag, akkor a golyó az 1. képkockán az ellenség előtt volt, a 2. képkockán pedig teljesen az ellenség mögött.

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

A megoldás: Seprett szegmens kontra doboz (födém módszer)

Egyetlen pont tesztelése helyett teszteljük a teljes vonalszakaszt, amely összeköti a golyó pozícióját az 1. képkockán ("previousPosition") a 2. képkockával ("currentPosition").

Íme a gyártási raycasting lemez-metszés módszer a SpiralWar kísérőcímünkből:

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

A Blocked: Pixel Panzer játékban a játékos folyamatos Lézersugár-készsége pontosan ezt a raycast-ot használja a leszálló blokkok sorain való átmetszéshez anélkül, hogy egyetlen ütközést is kihagyna.


6. 5. szint: gyártási fokozatú pixel-tökéletes ütközésészlelés

Most elértük a 2D pontosság legmagasabb szintjét: Pixel-Perfect Collision.

A retró tanklövőben vagy űrhajó kutyaharcban a szabálytalan formákat (tankcsövek, szárnyak, pilótafülke pilótafülke) átlátszó pixelek veszik körül a sprite textúrában. Amikor egy ellenséges rakéta eléri ezt az átlátszó helyet, a játékosok azonnal észreveszik.

A pixel-tökéletes ütközés megvizsgálja az átfedő textúrák tényleges alfa (átlátszósági) csatornáit. Ha két nem átlátszó képpont ugyanazon a világkoordinátán fedi egymást, akkor valódi fizikai találat történt.

A végzetes hiba: "GetData" az "Update()"-ben

Számos oktatóanyag erre utasítja a kezdőket:

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

A "Texture2D.GetData()" futás közbeni meghívása arra kényszeríti a CPU-t, hogy megvárja, amíg a GPU-folyamat kiürül, és visszahúzza a textúra memóriáját a buszon. Ha ezt több tucatnyi sprite-nál csinálod, minden képkocka 60 FPS-ről 5 FPS-re gyorsítja a játékodat, és azonnal aktiválja az Android ANR-eket!

A harcban tesztelt megoldás a Blocked: Pixel Panzertől

A Blocked: Pixel Panzer-ben a Sprite.cs egy ultraoptimalizált IntersectsPixel rutint valósít meg, amely 5 kulcsfontosságú szabály köré épül fel:

  1. Statikus színtömb gyorsítótár: A „GetData” pontosan egyszer kerül meghívásra a textúra betöltésekor, és a „Szótárban<Texture2D, Color[]>” tárolódik.
  2. AABB Early Exit Guard: Ha a Bounds.Intersects(other.Bounds) hamis, azonnal kimentjük. Az ellenőrzések 99%-a megszűnik egyetlen képpont vizsgálata előtt.
  3. Texture Atlas & SourceRectangle támogatás: Kezeli a textúra atlaszokba csomagolt sprite-eket a forrástéglalap-eltolások segítségével.
  4. Kiszámított átfedési ablak: Csak a két sprite közötti pontos metszésszögű téglalap fölé hurkolunk (Math.Max(a.Top, b.Top) stb.).
  5. Alfa rövidzárlat: Ha a Sprite A képpontja átlátszó (A <= 20), a Sprite B teljesen kimarad.
  6. Tulajdonság kiemelés: A beágyazott hurkokban lévő virtuális tulajdonságok (határok) elérése több ezer struktúra-másolatot hoz létre. Ezeket lokális veremváltozókban tároljuk, mielőtt belépnénk a ciklusba.

Íme a teljes, gyártásra kész kód:

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

Ez a rutin 0,1 ezredmásodperc alatt fut le modern Android hardvereken és asztali rendszereken.


7. 6. szint: Széles fázisú térháló és nulla kiosztású mobil GC

Képzeld el, hogy a játékodban 100 blokk, 60 golyó és 20 ellenség található. Ha minden entitást minden más entitáshoz képest tesztel beágyazott "for" ciklusok segítségével, a következőket kell végrehajtania:

\(\frac{180 \times 179}{2} = 16 110 \text{ csekk keretenként!}\)

60 FPS-nél ez közel 1 000 000 ütközési ellenőrzés másodpercenként.

A játék méretezéséhez meg kell valósítania a Széles fázisú térbeli particionálást.

Az egységes térbeli hash-rács

A képernyőt egységes cellákból álló 2D rácsra osztjuk (a Blocked: Pixel Panzer-ben minden cella 150 \(\x 150\) pixel). Egy entitás csak az ütközéseket teszteli más entitásokkal szemben, amelyek ugyanabban a rácscellában találhatók.

A mobil memória probléma: GC Thrashing

Ha a SpatialGrid minden keretben létrehoz egy új List<Point>()' vagy new List()`-ot, akkor percenként megabájt szemetet foglal le a kezelt kupacban.

Az Android Mono futtatókörnyezetében ez gyakori Gen-0 szemétgyűjtést vált ki, ami: mono futásidejű: Natív zárolási verseny (mono_class_is_subclass_)

A játék akadozik, és a Google Play ANR-figyelmeztetésekkel jelzi a játékodat!

A megoldás: újrafelhasználható kaparós pufferek

Íme a Zero-Allocation Spatial Hash Grid a `CollisionManager.cs-ből a Blocked: Pixel Panzer-ban:

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

A "_scratchCells" és a "_scratchCheckedEntities" újrafelhasználásával a teljes ütközési lépés 0 bájt halomkiosztás keretenként fut le.


8. Összefoglaló összehasonlító táblázat

Technika Matematikai költség Rotációs támogatás Biztonságos alagútépítés? Legjobb használati eset a MonoGame
AABB (Téglalap.Metszik) Ultra-gyors (~4 egész szám összehasonlítása) Nem Nem Rács blokkok, bullet széles fázisú, UI
Kör (DistanceSquared) Nagyon gyors (3 muls, 0 négyzetméter) Igen (változatlan) Nem Kerek hajók, tűzgolyók, energiagömbök
Kör kontra doboz ("bilincs") Gyors (Local MathHelper.Clamp) Igen Nem A szűk falakon navigáló kör alakú játékos
Swept Ray (Slab CCD) Mérsékelt (Paraméteres Raycast) Igen Igen Gyors mesterlövész golyók, sínfegyverek, lézerek
Pixel-Perfect ("IntersectsPixel") Szelektív (Korlátozott aljogosultság) Igen Nem Szabálytalan sprite-kontúrok, tisztességes hitboxok
Térháló (Zero-GC) Széles fázisú (\(O(N^2) \to O(N)\)) N/A N/A Sűrű hullámok, golyó poklok, Android 60 FPS

Valós produkciós bemutatók: Arar Games

Ezek az ütközési architektúrák nem elméleti kísérletek – ők jelentik a valódi mérnöki alapot a kereskedelmi forgalomba kerülő címeinkhez:

  • Letiltva: Pixel Panzer: Retro tank túlélő arcade játékunk a Google Playen és a Microsoft Store-on. Tartalmazza a teljes, kétszintű ütközési rendszerünket: nulla kiosztású térbeli hash rács, amely kiszűri a leeső blokkokat, és pixel-tökéletes érintkezés-ellenőrzés tankokhoz, vadászrepülőgépekhez, toronyhéjakhoz és ColorWheel elemi lőszerekhez.
  • Paint Trek: Gyors tempójú űrlövöldözőnk rotációs körütközéssel, folyamatos sugársugárzással és nagy térfogatú rakétavédelmi rendszerekkel.

Következtetés és következő lépések

A MonoGame lehetővé teszi, hogy olyan ütközésérzékelést tervezzen, amely tökéletesen megfelel a játék igényeinek. A drága Pixel-Perfect ellenőrzések gyors AABB-tesztek mögé zárva, a Négyzetes távolságok használatával a köröknél, és kiküszöbölve a szemétgyűjtést az Újrafelhasználható Scratch Bufferekkel, konzolon egyenletes, 60/120 FPS-es teljesítményt biztosíthat asztali és mobil platformon egyaránt.

Tekintse meg játékainkat az alkalmazásboltokban, hogy megtekinthesse ezeket az ütközési rendszereket működés közben, és még ma kezdje el ezeket a mintákat implementálni saját MonoGame projektjeibe!


Üzleti linkek és források


SEO kulcsszavak és hashtagek

Kulcsszavak: MonoGame 2D ütközésészlelés, C# játékfejlesztés, Rectangle.Intersects MonoGame, pixel perfect collision C#, térbeli hash grid MonoGame, swept ray folyamatos ütközés, független játék teljesítményoptimalizálása, nulla kiosztású játékhurok, Android Arlock B játék optimalizálása, Panzer, Pixel Trek.

#MonoGame #CSharp #GameDev #IndieDev #GamePhysics #DotNet #2DGameDev #MobileGameDev #Teljesítményoptimalizálás #CleanCode #PanzertTreixel #ArarGames #Játékprogramozás`




Hiç yorum yok:

Yorum Gönder