27 Eylül 2026 Pazar

2D-törmäystentunnistus MonoGamessa: perussuorakulmioista nolla-allokaatioon pikselitäydellisiin järjestelmiin




2D-törmäystentunnistus MonoGamessa: perussuorakulmioista nolla-allokaatioon pikselitäydellisiin järjestelmiin

Kun rakennetaan 2D-pelejä visuaalisissa moottoreissa, kuten Unity tai Godot, törmäysten havaitseminen tuntuu usein sarjalta tarkastajien valintaruutuja: lyötät "BoxCollider2D" tai "CircleCollider2D", kiinnität "Rigidbody" ja toivot, että sisäisen fysiikan vaihe ei pätki mobiililaitteissa.

MonoGamessa ja C#:ssa olet kuitenkin täysin hallinnassa. Yläpuolella ei ole piilotettua fysiikkaa, ei-toivottua pyörimishitautta eikä salaperäisiä roskakeräyspiikkejä, jotka varastavat kehysbudjettisi.

Arar Gamesissa, kun rakensimme Blocked: Pixel Panzerin ja Paint Trekin, arcade-pelisilmukamme tarvitsivat satojen nopeiden vihollisen luotien, räjähtävien tiiliverkkojen, pyörivien tankkitornien, hävittäjälentokoneiden ohituksia ja hiukkassuojaimia Windows- ja 60 FPS-laitteissa. Yleiskäyttöinen fysiikkamoottori ei tullut kysymykseen – tarvitsimme tarkoitukseen rakennetun, porrastetun törmäysarkkitehtuurin.

Tässä kattavassa koodiohjatussa oppaassa aloitamme MonoGame-törmäyksen ehdottomista perusasioista ("Rectangle.Intersects" oikeilla "Bullet"- ja "Enemy" spriteillä) ja jatkamme edistyneitä ympyrätarkistuksia, sekakiinnitystä, tunnelointia estäviä sädelähetyksiä, tuotantotason Pixel-AllocationZero-Perfectation- ja -pisteiden yhteensovittamista. optimoitu mobiili GC:n selviytymiseen.


1. Säätiö: Yksinkertainen MonoGame "Sprite" -hierarkia

Ennen törmäysten havaitsemista tarvitsemme puhtaita pelikokonaisuuksia. MonoGamessa entiteetillä on pohjimmiltaan sijainti, pintakuvio ja rajaava suorakulmio.

Tässä on peleissämme käytetty peruskokonaisuusarkkitehtuuri:

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

Luodaan nyt konkreettiset "Player"-, "Enemy"- ja "Bulet"-luokat:

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

Kun nämä entiteetit ovat paikoillaan, tutkitaan kuinka havaita niiden väliset törmäykset yksinkertaisimmasta menetelmästä alkaen.


2. Taso 1: Yksinkertaisin törmäys – "Rectangle.Intersects" (AABB)

MonoGamen perustavanlaatuisin 2D-törmäystarkistus on Axis-Aligned Bounding Box (AABB) -testi. Termi "akselin suuntainen" tarkoittaa yksinkertaisesti, että suorakulmion reunat ovat täysin yhdensuuntaiset näytön \(X\)- ja \(Y\)-akseleiden kanssa (ei kiertoa).

MonoGame tarjoaa nopean, sisäänrakennetun menetelmän: "Rectangle.Intersects(Rectangle value)".

Miten "Rectangle.Intersects" toimii konepellin alla

MonoGame suorittaa pinnan alla neljä kokonaislukuvertailua:

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

Jos kaikki neljä ehtoa täyttyvät, suorakulmiot menevät päällekkäin. Jos yksikin ehto epäonnistuu, tyhjä akseli erottaa ne, eikä törmäys ole mahdollinen.

Todellinen pelikoodi: "Bullet" vs. "Enemy" pelissä Game1.Update

Näin voit tarkistaa aktiivisten luotien luettelon ja aktiivisten vihollisten luettelon väliset törmäykset MonoGame-päivityksen pääsilmukassa:

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

Suorituskykyvinkki: Huomaa, että iteroitamme taaksepäin (for (int i = list.Count - 1; i >= 0; i--))! Jos käytät "foreach" ja yrität kutsua "_bullets.Remove(bullet)", C# antaa "InvalidOperationException: Collection was modified" -ilmoituksen. Taaksepäin iterointi mahdollistaa elementtien turvallisen poistamisen ilman muistin uudelleenindeksointiongelmia.

The Arcade Secret: "Fair Hitboxes" 'Inflaten' kautta

Retropeleissä, kuten Blocked: Pixel Panzer, sprite-tekstuurit sisältävät usein läpinäkyviä marginaaleja tai antennipiikkijä. Jos pelaajan tankki räjähtää, koska luoti kosketti tyhjää läpinäkyvää kulmaansa, pelaaja tuntee itsensä huijatuksi.

Jotta törmäys tuntuisi reagoivalta ja oikeudenmukaiselta, pelit käyttävät pienempää Hitboxia spriten sisällä käyttämällä "Rectangle.Inflate" -toimintoa:

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. Taso 2: Ympyrästä ympyrään törmäys (kiertohäiriönsieto)

Suorakulmiot toimivat erinomaisesti staattisissa lohkoissa ja ruudukkolaatoissa, mutta ne epäonnistuvat spritien pyöriessä. When a non-square spaceship rotates in Paint Trek, an axis-aligned bounding box must expand to enclose the spinning corners, resulting in frustrating "phantom collisions" in empty air.

Bounding Circles on ihanteellinen ratkaisu pyöreälle asteroidille, suuntautuville energiapalloille ja pyöriville avaruusaluksille.

Neliöjuuren ansa

Kaksi ympyrää törmäävät toisiinsa, kun niiden keskipisteiden välinen etäisyys on pienempi tai yhtä suuri kuin niiden säteiden summa:

\(\teksti{etäisyys}(C_A, C_B) \le r_A + r_B\)

Koodissa euklidisen etäisyyden laskeminen käyttää arvoa "Math.Sqrt" (tai "Vektori2.Etäisyys"). Kuitenkin neliöjuurien laskeminen silmukassa 200 ammuksen kanssa polttaa satoja tarpeettomia prosessorijaksoja!

Vertaamalla neliöetäisyyttä säteen neliön summaan eliminoimme neliöjuuren kokonaan:

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

MonoGame-toteutus: "Ympyrä" vs. "Ympyrä".

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

Integroi tämä nyt suoraan kokonaisuuteen:

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

Nolla neliöjuurta, immuuni spriten pyörimiselle ja salamannopea.


4. Taso 3: Sekalaiset muodot – ympyrä vs. laatikko (MathHelper.Clamp)

Mitä tapahtuu, kun Paint Trekin pyöreä avaruusalus navigoi suorakaiteen muotoisten puolustusesteiden tiukan labyrintin läpi tai kun pyöreä luoti osuu neliömäiseen lohkoon Blocked: Pixel Panzerissa?

Tarvitsemme Ympyrä vs. suorakulmio -törmäyksen.

Kiinnitysalgoritmi

Tavoitteena on löytää suorakulmion piste, joka on lähinnä ympyrän keskustaa, ja sitten testata, onko etäisyys lähimmästä pisteestä keskipisteeseen pienempi kuin ympyrän säde.

MonoGamen "MathHelper.Clamp" tekee tästä triviaalin:

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

Voit nyt testata pelaajakilpiä suorakaiteen muotoisia tiiliä vastaan ​​nollavarauksilla ja suurella tarkkuudella!


5. Taso 4: Jatkuva törmäyksen tunnistus (CCD) ja pyyhkäisysäteily

Oletko koskaan ampunut pelissäsi hypernopeatar-ampujalaukkua tai kiskoase laseria vain nähdäksesi luodin kulkevan taianomaisesti suoraan ohuen vihollisaluksen läpi aiheuttamatta vahinkoa?

Tämä vika tunnetaan nimellä tunnelointi.

Koska erilliset pelit päivittyvät aikaaskelin (\(\Delta t = 16,6\text{ms}\) 60 FPS:llä), 1 800 pikseliä sekunnissa liikkuva kohde kulkee 30 pikseliä yhdessä kehyksessä. Jos vihollisen runko on vain 15 pikseliä paksu, luoti oli vihollisen edessä kehyksessä 1 ja kokonaan vihollisen takana kehyksessä 2.

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

Ratkaisu: Pyyhkäisy segmentti vs. laatikko (laattamenetelmä)

Yhden pisteen testaamisen sijaan testaamme koko viivaosuuden, joka yhdistää luodin sijainnin kehyksessä 1 ("previousPosition") kehykseen 2 ("currentPosition").

Tässä on tuotantosädevalulaatan leikkausmenetelmä kumppaninimestämme 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 -pelissä pelaajan jatkuva Laser Beam -taito käyttää tätä tarkkaa sädelähetystä laskevien lohkojen rivien läpi leikkaamiseen ilman yhtäkään törmäystä.


6. Taso 5: Tuotantotason pikselin täydellinen törmäystunnistus

Nyt saavutamme 2D-tarkkuuden äärimmäisen tason: Pixel-Perfect Collision.

Retro-panssariampuja- tai avaruusaluksen koirataistelussa epäsäännölliset muodot (tankin tynnyrit, siivet, ohjaamon ohjaamot) ympäröivät läpinäkyviä pikseleitä sprite-tekstuurissa. Kun vihollisen ohjus osuu tähän läpinäkyvään tilaan, pelaajat huomaavat sen välittömästi.

Pikselitäydellinen törmäys tarkastaa päällekkäisten pintakuvioiden todelliset alfa-kanavat (läpinäkyvyys). Jos kaksi läpinäkymätöntä pikseliä menevät päällekkäin samassa maailmankoordinaatissa, on tapahtunut todellinen fyysinen osuma.

Kohtalokas virhe: "GetData" "Update()" -kohdassa

Monet opetusohjelmat neuvovat aloittelijoita tekemään tämän:

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

"Texture2D.GetData()":n kutsuminen ajon aikana pakottaa suorittimen odottamaan, että GPU-liukuhihna tyhjenee, mikä vetää tekstuurimuistin takaisin väylän yli. Kun teet tämän kymmenille spriteille jokaisessa ruudussa, pelisi tankkaa 60 FPS:stä 5 FPS:ään ja laukaisee Androidin ANR:t välittömästi!

Taistelutestattu ratkaisu Blocked: Pixel Panzerilta

Blocked: Pixel Panzerissa Sprite.cs toteuttaa erittäin optimoidun IntersectsPixel-rutiinin, joka on rakennettu viiden tärkeän säännön ympärille:

  1. Staattisen väritaulukon välimuisti: "GetData" kutsutaan tasan kerran, kun pintakuviointi latautuu, ja se tallennetaan sanakirjaan<Texture2D, Color[]>".
  2. AABB Early Exit Guard: Jos Bounds.Intersects(other.Bounds) on väärä, pelastamme välittömästi. 99 % tarkistuksista poistetaan ennen yhden pikselin tarkastamista.
  3. Tekstuurikartasto ja 'SourceRectangle' -tuki: Käsittelee tekstuurikartasteihin pakattuja spritejä käyttämällä lähdesuorakulmion siirtymiä.
  4. Laskettu limitysikkuna: Teemme silmukan vain tarkan molempien spritien välisen leikkaussuorakulmion yli (Math.Max(a.Top, b.Top) jne.).
  5. Alfa-oikosulku: Jos Sprite A:n pikseli on läpinäkyvä (A <= 20), Sprite B ohitetaan kokonaan.
  6. Property Hoisting: Virtuaalisten ominaisuuksien (Bounds) käyttäminen sisäkkäisissä silmukoissa luo tuhansia rakennekopioita. Tallennamme ne paikallisiin pinomuuttujiin ennen silmukkaan siirtymistä.

Tässä on täydellinen, tuotantovalmis koodi:

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

Tämä rutiini suoritetaan alle 0,1 millisekunnissa nykyaikaisissa Android-laitteistoissa ja työpöytäjärjestelmissä.


7. Taso 6: Laajavaiheinen tilaverkko ja nollavaraus mobiili GC

Kuvittele, että pelissäsi on 100 lohkoa, 60 luotia ja 20 vihollista. Jos testaat jokaista entiteettiä kaikkia muita entiteettejä vastaan käyttämällä sisäkkäisiä for-silmukoita, suoritat:

\(\frac{180 \times 179}{2} = 16 110 \text{ shekkiä kehystä kohden!}\)

60 FPS:llä eli lähes 1 000 000 törmäystarkistusta sekunnissa.

Skaalataksesi peliäsi sinun on otettava käyttöön Laajavaiheinen tilaosio.

Uniform Spatial Hash Grid

Jaamme näytön yhtenäisten solujen 2D-ruudukoksi (Blocked: Pixel Panzerissa jokainen solu on 150 \(\ kertaa 150\) pikseliä). Entiteetti testaa vain törmäyksiä muihin samoissa ruudukon soluissa oleviin entiteeteihin.

Mobiilimuistiongelma: GC Thrashing

Jos "SpatialGrid" luo "uuden List()" tai "new List()" joka kehyksen, varaat megatavuja roskaa joka minuutti hallittuun kasaan.

Androidin Mono-ajotilassa tämä käynnistää usein Gen-0-roskakeräyksiä, mikä aiheuttaa: mono runtime: Alkuperäinen lukkokilpailu (mono_class_is_subclass_)

Peli pätkii ja Google Play merkitsee pelisi ANR-varoituksella!

Ratkaisu: uudelleen käytettävät raaputuspuskurit

Tässä on Zero-Allocation Spatial Hash Grid `CollisionManager.cs-sivustolta Estetty: 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);
                            }
                        }
                    }
                }
            }
        }
    }
}

Käyttämällä _scratchCells- ja _scratchCheckedEntities-tiedostoja uudelleen, koko törmäysvaihe suoritetaan 0 tavulla kehystä kohden.


8. Yhteenveto vertailutaulukko

Tekniikka Matemaattiset kustannukset Rotaatiotuki Turvallinen tunnelointi? Paras käyttökotelo MonoGamessa
AABB (Suorakamio.Leikkaukset) Erittäin nopea (~4 kokonaislukuvertailua) Ei Ei Ristikkolohkot, luotilaajavaiheinen, käyttöliittymä
Ympyrä (DistanceSquared) Erittäin nopea (3 multaa, 0 neliömetriä) Kyllä (Invariantti) Ei Pyöreät laivat, tulipallot, energiapallot
Ympyrä vs. laatikko ("kiinnike") Nopea (paikallinen MathHelper.Clamp) Kyllä Ei Pyöreä pelaaja navigoi ahtaissa seinissä
Swept Ray (Slab CCD) Keskitaso (parametrinen säteilylähetys) Kyllä Kyllä Nopeat ampujan luodit, kiskoaseet, laserit
Pixel-Perfect (IntersectsPixel) Valikoiva (rajoitettu alisuora) Kyllä Ei Epäsäännölliset sprite-muodot, reilut hitboxit
Spatial Grid (Zero-GC) Laajavaiheinen (\(O(N^2) \to O(N)\)) N/A N/A Tiheät aallot, luotihelvetit, Android 60 FPS

Tosimaailman tuotantoesityksiä: Arar Games

Nämä törmäysarkkitehtuurit eivät ole teoreettisia kokeita – ne ovat todellinen tekninen perusta, joka toimii kaupallisesti julkaistuille nimikkeillemme:

  • Estetty: Pixel Panzer: Retro tankkien selviytymispelimme Google Playssa ja Microsoft Storessa. Siinä on täydellinen kaksitasoinen törmäysjärjestelmämme: nolla-allokoitu spatiaalinen hash-verkko, joka suodattaa putoavia lohkoja, ja pikselitäydellinen kontaktien tarkistus tankeille, hävittäjille, tornin kuorille ja ColorWheel-elementaaliammuksille.
  • Paint Trek: Nopeatempoinen avaruusampujamme, jossa on pyörivä ympyrätörmäys, jatkuva sädelähetys ja suuren tilavuuden ohjuspuolustusjärjestelmät.

Johtopäätös ja seuraavat vaiheet

MonoGame antaa sinulle mahdollisuuden suunnitella törmäystunnistuksen, joka vastaa täydellisesti pelisi tarpeita. Aidattamalla kalliit Pixel-Perfect -tarkistukset nopeiden AABB-testien taakse, käyttämällä neliöetäisyyksiä ympyröille ja eliminoimalla roskien keräämisen Uudelleenkäytettävillä Scratch Buffers -puskurilla, voit tarjota konsolitasoisen 60/120 FPS:n suorituskyvyn sekä pöytäkoneilla että mobiilialustoilla.

Tutustu peleihimme sovelluskaupoissa nähdäksesi nämä törmäysjärjestelmät toiminnassa ja aloita näiden mallien toteuttaminen omissa MonoGame-projekteissasi jo tänään!


Kaupan linkit ja resurssit


SEO-avainsanat ja hashtagit

Avainsanat: MonoGame 2D -törmäysten tunnistus, C#-pelikehitys, Rectangle.Intersects MonoGame, pixel perfect collision C#, spatial hash grid MonoGame, pyyhkäisysäteilyn jatkuva törmäys, indie-pelin suorituskyvyn optimointi, nollaallokaatiopelisilmukka, Android Arlock B -optimointi, Panzer-peli, PanoGame Trek.

#MonoGame #CSharp #GameDev #IndieDev #GamePhysics #DotNet #2DGameDev #MobileGameDev #PerformanceOptimization #CleanCode #PanintTre``xel #ArarGames #Peliohjelmointi




Hiç yorum yok:

Yorum Gönder