27 Eylül 2026 Pazar

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




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

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

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

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

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


1. Pagrindas: paprasta MonoGame „Sprite“ hierarchija

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

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

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class Sprite
{
    public Vector2 Position;
    public Texture2D Texture;
    public Color Tint = Color.White;
    public bool IsActive = true;

    // The raw Axis-Aligned Bounding Box (AABB)
    public virtual Rectangle Bounds => new Rectangle(
        (int)Position.X,
        (int)Position.Y,
        Texture != null ? Texture.Width : 0,
        Texture != null ? Texture.Height : 0
    );

    public virtual void Draw(SpriteBatch spriteBatch)
    {
        if (!IsActive || Texture == null) return;
        spriteBatch.Draw(Texture, Position, Tint);
    }
}

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

public class Bullet : Sprite
{
    public Vector2 Velocity;
    public int Damage = 25;

    public void Update(GameTime gameTime)
    {
        Position += Velocity * (float)gameTime.ElapsedGameTime.TotalSeconds;
    }
}

public class Enemy : Sprite
{
    public int Health = 100;

    public void TakeDamage(int damage)
    {
        Health -= damage;
        if (Health <= 0)
        {
            IsActive = false;
        }
    }
}

public class Player : Sprite
{
    public int Health = 100;
    public bool IsInvulnerable = false;

    public void TakeDamage(int damage)
    {
        if (IsInvulnerable) return;
        Health -= damage;
    }
}

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


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

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

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

Kaip "Stačiakampis. Susikerta" veikia po gaubtu

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

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

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

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

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

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

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

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

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

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

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

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

        base.Update(gameTime);
    }
}

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

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

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

Kad susidūrimas būtų jautrus ir teisingas, žaidimuose naudojamas mažesnis Hitbox, esantis sprite viduje, naudojant „Rectangle.Inflate“:

public class EnemyTank : Enemy
{
    // Shrink the bounding box by 6 pixels on all sides for fair collision
    public override Rectangle Bounds
    {
        get
        {
            Rectangle raw = base.Bounds;
            raw.Inflate(-6, -6); // Reduces width and height by 12px
            return raw;
        }
    }
}

3. 2 lygis: susidūrimas tarp ratų (sukimosi atsparumas)

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

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

Kvadratinės šaknies spąstai

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

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

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

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

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

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

public struct Circle
{
    public Vector2 Center;
    public float Radius;

    public Circle(Vector2 center, float radius)
    {
        Center = center;
        Radius = radius;
    }

    public bool Intersects(Circle other)
    {
        float radiusSum = this.Radius + other.Radius;
        
        // MonoGame built-in Vector2.DistanceSquared
        return Vector2.DistanceSquared(this.Center, other.Center) <= (radiusSum * radiusSum);
    }
}

Dabar integruokite tai tiesiogiai į objektą:

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

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

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

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


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

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

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

Suspaudimo algoritmas

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

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

public static class Collision2D
{
    public static bool CircleIntersectsRectangle(Circle circle, Rectangle rect)
    {
        // Find the closest point on the rectangle to the circle center
        float closestX = MathHelper.Clamp(circle.Center.X, rect.Left, rect.Right);
        float closestY = MathHelper.Clamp(circle.Center.Y, rect.Top, rect.Bottom);

        // Vector from closest point to circle center
        float distanceX = circle.Center.X - closestX;
        float distanceY = circle.Center.Y - closestY;

        // Check squared distance against squared radius
        float distanceSquared = (distanceX * distanceX) + (distanceY * distanceY);
        return distanceSquared <= (circle.Radius * circle.Radius);
    }
}

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


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

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

Ši klaida žinoma kaip tuneliavimas.

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

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

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

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

Štai gamybos spinduliavimo plokščių susikirtimo metodas iš mūsų pavadinimo SpiralWar:

public static class ContinuousCollision
{
    public static bool IntersectsSweptRay(Vector2 rayStart, Vector2 rayEnd, Rectangle box, out Vector2 hitPoint)
    {
        hitPoint = Vector2.Zero;

        Vector2 direction = rayEnd - rayStart;
        float tMin = 0f;
        float tMax = 1f;

        // Clip against X slabs
        if (MathF.Abs(direction.X) > 1e-6f)
        {
            float invX = 1f / direction.X;
            float t1 = (box.Left - rayStart.X) * invX;
            float t2 = (box.Right - rayStart.X) * invX;
            if (t1 > t2) (t1, t2) = (t2, t1);

            tMin = MathF.Max(tMin, t1);
            tMax = MathF.Min(tMax, t2);
            if (tMin > tMax) return false;
        }
        else if (rayStart.X < box.Left || rayStart.X > box.Right)
        {
            return false;
        }

        // Clip against Y slabs
        if (MathF.Abs(direction.Y) > 1e-6f)
        {
            float invY = 1f / direction.Y;
            float t1 = (box.Top - rayStart.Y) * invY;
            float t2 = (box.Bottom - rayStart.Y) * invY;
            if (t1 > t2) (t1, t2) = (t2, t1);

            tMin = MathF.Max(tMin, t1);
            tMax = MathF.Min(tMax, t2);
            if (tMin > tMax) return false;
        }
        else if (rayStart.Y < box.Top || rayStart.Y > box.Bottom)
        {
            return false;
        }

        hitPoint = rayStart + direction * tMin;
        return true;
    }
}

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


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

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

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

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

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

Daugelyje vadovėlių pradedantiesiems nurodoma tai padaryti:

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

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

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

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

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

Štai visas, paruoštas gamybai kodas:

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class AdvancedSprite : Sprite
{
    private static readonly Dictionary<Texture2D, Color[]> _textureDataCache = new();

    public Rectangle? SourceRectangle { get; set; }

    public static Color[] GetCachedTextureData(Texture2D texture)
    {
        if (!_textureDataCache.TryGetValue(texture, out var data))
        {
            data = new Color[texture.Width * texture.Height];
            texture.GetData(data);
            _textureDataCache[texture] = data;
        }
        return data;
    }

    public bool IntersectsPixel(AdvancedSprite other)
    {
        // 1. Hoist bounds into local stack variables
        Rectangle a = this.Bounds;
        Rectangle b = other.Bounds;

        // 2. Fast AABB Broad-Phase check
        if (!a.Intersects(b)) return false;

        // Fallback to rectangle check if textures are missing
        if (this.Texture == null || other.Texture == null) return true;

        // 3. Retrieve pre-cached color arrays (Zero GPU latency)
        Color[] dataA = GetCachedTextureData(this.Texture);
        Color[] dataB = GetCachedTextureData(other.Texture);

        int aTexW = this.Texture.Width;
        int bTexW = other.Texture.Width;

        // Atlas / SourceRectangle offset mapping
        bool aHasSrc = this.SourceRectangle.HasValue;
        Rectangle aSrc = aHasSrc ? this.SourceRectangle.Value : Rectangle.Empty;
        int aOriginX = aHasSrc ? aSrc.X : 0;
        int aOriginY = aHasSrc ? aSrc.Y : 0;
        int aSpanX   = aHasSrc ? aSrc.Width  : this.Texture.Width;
        int aSpanY   = aHasSrc ? aSrc.Height : this.Texture.Height;

        bool bHasSrc = other.SourceRectangle.HasValue;
        Rectangle bSrc = bHasSrc ? other.SourceRectangle.Value : Rectangle.Empty;
        int bOriginX = bHasSrc ? bSrc.X : 0;
        int bOriginY = bHasSrc ? bSrc.Y : 0;
        int bSpanX   = bHasSrc ? bSrc.Width  : other.Texture.Width;
        int bSpanY   = bHasSrc ? bSrc.Height : other.Texture.Height;

        // 4. Calculate the overlapping sub-rectangle
        int top    = Math.Max(a.Top, b.Top);
        int bottom = Math.Min(a.Bottom, b.Bottom);
        int left   = Math.Max(a.Left, b.Left);
        int right  = Math.Min(a.Right, b.Right);

        // 5. Scan only overlapping pixels
        for (int y = top; y < bottom; y++)
        {
            // Row offsets hoisted outside the inner X loop!
            int colorA_Y = aOriginY + (int)((y - a.Y) / (float)a.Height * aSpanY);
            int colorB_Y = bOriginY + (int)((y - b.Y) / (float)b.Height * bSpanY);
            int rowA = colorA_Y * aTexW;
            int rowB = colorB_Y * bTexW;

            for (int x = left; x < right; x++)
            {
                int colorA_X = aOriginX + (int)((x - a.X) / (float)a.Width * aSpanX);

                // Early Bailout: If Pixel A is transparent, skip B entirely!
                if (dataA[colorA_X + rowA].A <= 20) continue;

                int colorB_X = bOriginX + (int)((x - b.X) / (float)b.Width * bSpanX);

                // If Pixel B is also opaque, we have confirmed contact!
                if (dataB[colorB_X + rowB].A > 20)
                {
                    return true;
                }
            }
        }

        return false;
    }
}

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


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

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

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

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

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

Vienoda erdvinė maišos tinklelis

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

Mobiliosios atminties problema: GC Thrashing

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

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

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

Sprendimas: daugkartinio naudojimo įbrėžimų buferiai

Štai Nulinio paskirstymo erdvinės maišos tinklelis iš „CollisionManager.cs“, esantis Užblokuota: Pixel Panzer:

public class SpatialGridManager
{
    private const int CELL_SIZE = 150;
    private readonly Dictionary<Point, List<Sprite>> _grid = new();

    // Zero-GC: Reusable scratch buffers allocated ONCE at startup
    private readonly List<Point> _scratchCells = new();
    private readonly HashSet<Sprite> _scratchCheckedEntities = new();

    public void BuildGrid(List<Sprite> entities)
    {
        // Clear lists without re-allocating new List objects
        foreach (var list in _grid.Values)
        {
            list.Clear();
        }

        foreach (var entity in entities)
        {
            if (!entity.IsActive) continue;

            GetOccupiedCells(entity.Bounds, _scratchCells);
            foreach (var cell in _scratchCells)
            {
                if (!_grid.TryGetValue(cell, out var list))
                {
                    list = new List<Sprite>();
                    _grid[cell] = list;
                }
                list.Add(entity);
            }
        }
    }

    private void GetOccupiedCells(Rectangle bounds, List<Point> outCells)
    {
        outCells.Clear();

        int startX = bounds.Left / CELL_SIZE;
        int startY = bounds.Top / CELL_SIZE;
        int endX = bounds.Right / CELL_SIZE;
        int endY = bounds.Bottom / CELL_SIZE;

        for (int x = startX; x <= endX; x++)
        {
            for (int y = startY; y <= endY; y++)
            {
                outCells.Add(new Point(x, y));
            }
        }
    }

    public void CheckCollisions(Player player)
    {
        if (player.IsInvulnerable) return;

        GetOccupiedCells(player.Bounds, _scratchCells);
        _scratchCheckedEntities.Clear();

        foreach (var cell in _scratchCells)
        {
            if (_grid.TryGetValue(cell, out var entitiesInCell))
            {
                foreach (var other in entitiesInCell)
                {
                    if (other == player || _scratchCheckedEntities.Contains(other)) continue;
                    _scratchCheckedEntities.Add(other);

                    // 1. Broad-phase AABB test
                    if (player.Bounds.Intersects(other.Bounds))
                    {
                        // 2. High-precision Pixel-Perfect test
                        if (player is AdvancedSprite advPlayer && other is AdvancedSprite advOther)
                        {
                            if (advPlayer.IntersectsPixel(advOther))
                            {
                                player.TakeDamage(10);
                            }
                        }
                    }
                }
            }
        }
    }
}

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


8. Suvestinė palyginimo lentelė

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

Realaus pasaulio produkcijos demonstracijos: „Arar“ žaidimai

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

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

Išvada ir tolesni žingsniai

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

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


Parduotuvių nuorodos ir ištekliai


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

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

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




Hiç yorum yok:

Yorum Gönder