27 Eylül 2026 Pazar

Выяўленне 2D-сутыкненняў у MonoGame: ад простых прамавугольнікаў да сістэм з ідэальным пікселем без размеркавання




Выяўленне 2D-сутыкненняў у MonoGame: ад простых прастакутнікаў да сістэм з ідэальным пікселем з нулявым размеркаваннем

Пры стварэнні 2D-гульняў у такіх візуальных рухавічках, як Unity або Godot, выяўленне сутыкненняў часта выглядае як серыя сцяжкоў інспектара: вы націскаеце на BoxCollider2D або CircleCollider2D, далучаеце Rigidbody і спадзяецеся, што ўнутраны крок фізікі не заікаецца на мабільных прыладах.

Аднак у MonoGame і C# вы цалкам кантралюеце. Няма ніякіх схаваных накладных выдаткаў на фізіку, непажаданай інэрцыі кручэння і таямнічых скокаў збору смецця (GC), якія крадуць ваш бюджэт кадраў.

У Arar Games, калі мы стваралі Blocked: Pixel Panzer і Paint Trek, нашы цыклы аркаднай гульні павінны былі апрацоўваць сотні высакахуткасных варожых куль, выбухаючыя цагляныя сеткі, якія верцяцца вежы танкаў, пралёты знішчальнікаў і шчыты ад часціц з хуткасцю ад 60 да 120 кадраў у секунду як на ПК з Windows, так і на прыладах Android. Пра фізічны механізм агульнага прызначэння не магло быць і гаворкі — нам патрэбна была спецыяльна створаная шматузроўневая архітэктура сутыкненняў.

У гэтым поўным кіраўніцтве, якое кіруецца кодам, мы пачнем з абсалютных асноў сутыкнення MonoGame ("Rectangle.Intersects" з рэальнымі спрайтамі "Bullet" і "Enemy") і пяройдзем да прасунутых праверак круга, змешанага заціску, анты-тунэлявання прамянёў, Pixel-Perfect сутыкнення і Zero-Allocation Spatial Сеткі аптымізаваныя для выжывання мабільнага GC.


1. Аснова: простая іерархія монагульні Sprite

Перш чым выяўляць сутыкненні, нам патрэбны чыстыя гульнявыя сутнасці. У 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;
}

Калі ўсе чатыры ўмовы выкананы, прамавугольнікі перакрываюцца. Калі хаця б адна ўмова не выконваецца, іх раздзяляе пустая вось, і сутыкненне немагчыма.

Код рэальнага геймплэя: Куля супраць Ворага ў 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);
    }
}

Парада па прадукцыйнасці: Звярніце ўвагу, што мы выконваем ітэрацыі ў зваротным кірунку (for (int i = list.Count - 1; i >= 0; i--))! Калі вы выкарыстоўваеце foreach і спрабуеце выклікаць _bullets.Remove(bullet), C# выдае InvalidOperationException: Collection was modified. Ітэрацыя назад дазваляе бяспечна выдаляць элементы без праблем з паўторнай індэксацыяй памяці.

The Arcade Secret: "Fair Hitboxes" праз Inflate

У такіх рэтра-гульнях, як Blocked: Pixel Panzer, тэкстуры спрайтаў часта ўключаюць празрыстыя палі або шыпы антэн. Калі танк гульца выбухне з-за таго, што куля закранула пусты празрысты кут яго тэкстуры, гулец адчуе сябе падманутым.

Каб зрабіць сутыкненне адчувальным і справядлівым, у гульнях выкарыстоўваецца меншы Hitbox унутры спрайта з дапамогай 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: Сутыкненне ад круга да круга (Імунітэт да кручэння)

Прастакутнікі выдатна працуюць для статычных блокаў і плітак сеткі, але яны не працуюць, калі спрайты круцяцца. Калі неквадратны касмічны карабель круціцца ў Paint Trek, абмежавальная рамка, выраўнаваная па восі, павінна пашырацца, каб ахапіць верцяцца куты, што прыводзіць да расчаравальных «фантомных сутыкненняў» у пустым паветры.

Для круглых астэроідаў, энергетычных шароў з саманавядзеннем і касмічных караблёў, якія верцяцца, Абмежавальныя кругі з'яўляюцца ідэальным рашэннем.

Пастка квадратнага кораня

Дзве акружнасці сутыкаюцца, калі адлегласць паміж іх цэнтрамі меншая або роўная суме іх радыусаў:

\(\тэкст{Адлегласць}(C_A, C_B) \le r_A + r_B\)

У кодзе для вылічэння эўклідавай адлегласці выкарыстоўваецца Math.Sqrt (або Vector2.Distance). Аднак вылічэнне квадратных каранёў у цыкле з 200 снарадамі спальвае сотні непатрэбных цыклаў працэсара!

Параўноўваючы квадрат адлегласці з квадратам сумы радыуса, мы цалкам выключаем квадратны корань:

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

Рэалізацыя MonoGame: Circle супраць Circle

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

Нуль квадратных каранёў, неўспрымальны да кручэння спрайтаў і вокамгненны.


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) і качаюцца прамяні

Ці стралялі вы калі-небудзь са звышхуткаснага снайперскага снарада або з лазернай рэйкі ў сваёй гульні толькі для таго, каб назіраць, як куля магічным чынам праходзіць праз тонкі варожы карабель, не наносячы пашкоджанняў?

Гэтая памылка вядомая як тунэляванне.

Паколькі дыскрэтныя гульні абнаўляюцца з крокамі ў часе (\(\Delta t = 16,6\text{ms}\) пры 60 FPS), аб'ект, які рухаецца з хуткасцю 1800 пікселяў у секунду, рухаецца 30 пікселяў у адным кадры. Калі таўшчыня корпуса праціўніка складае ўсяго 15 пікселяў, на кадры 1 куля знаходзілася наперадзе праціўніка, а на кадры 2 - цалкам ззаду.

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

Рашэнне: разгорнуты сегмент супраць каробкі (метад пліты)

Замест таго, каб правяраць адну кропку, мы правяраем увесь адрэзак лініі, які злучае пазіцыю маркера ў кадры 1 (previousPosition) і кадры 2 (currentPosition).

Вось метад перакрыжавання пліт прамянёвай кастынгу з нашай спадарожнай назвы 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: Ідэальнае піксельнае выяўленне сутыкненняў вытворчага класа

Цяпер мы дасягнулі найвышэйшага ўзроўню дакладнасці 2D: Ідэальнае сутыкненне пікселяў.

У рэтра танкавым шутэры або паветраным баі касмічных караблёў няправільныя формы (ствалы танкаў, крылы, кабіны) акружаны празрыстымі пікселямі ў тэкстуры спрайтаў. Калі варожая ракета трапляе ў гэтую празрыстую прастору, гульцы адразу гэта заўважаюць.

Ідэальнае сутыкненне пікселяў правярае фактычныя альфа-каналы (празрыстасць) перакрываючыхся тэкстур. Калі два непразрыстыя пікселі перакрываюцца ў адной сусветнай каардынаце, адбылося сапраўднае фізічнае трапленне.

Фатальная памылка: 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() падчас выканання прымушае працэсар чакаць ачысткі канвеера графічнага працэсара, вяртаючы тэкстурную памяць па шыне. Робячы гэта для дзясяткаў спрайтаў у кожным кадры, ваша гульня будзе зніжацца з 60 FPS да 5 FPS і неадкладна запускаць ANR Android!

Выпрабаванае ў баях рашэнне ад Заблакіравана: Pixel Panzer

У Blocked: Pixel Panzer наш Sprite.cs рэалізуе звышаптымізаваную працэдуру IntersectsPixel, пабудаваную вакол 5 найважнейшых правіл:

  1. Кэш масіва статычных колераў: GetData выклікаецца роўна адзін раз пры загрузцы тэкстуры і захоўваецца ў Dictionary<Texture2D, Color[]>.
  2. AABB Early Exit Guard: Калі Bounds.Intersects(other.Bounds) несапраўднае, мы неадкладна выручаемся. 99% праверак адмяняюцца перад праверкай аднаго пікселя.
  3. Падтрымка тэкстурнага атласа і SourceRectangle: Апрацоўвае спрайты, спакаваныя ў тэкстурныя атласы з выкарыстаннем зрушэнняў зыходнага прамавугольніка.
  4. Разлічанае акно перакрыцця: мы перабіраем толькі дакладны прамавугольнік перасячэння паміж абодвума спрайтамі (Math.Max(a.Top, b.Top) і г.д.).
  5. Альфа-кароткае замыканне: Калі піксель спрайта A празрысты (A <= 20), спрайт B цалкам прапускаецца.
  6. Пад'ём уласцівасцей: Доступ да віртуальных уласцівасцей (Межы) ва ўкладзеных цыклах стварае тысячы копій структур. Мы захоўваем іх у лакальных зменных стэка перад уваходам у цыкл.

Вось поўны, гатовы да вытворчасці код:

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: шырокафазавая прасторавая сетка і мабільны GC з нулявым размеркаваннем

Уявіце, што ў вашай гульні 100 блокаў, 60 куль і 20 ворагаў. Калі вы правяраеце кожны аб'ект супраць любога іншага аб'екта з дапамогай укладзеных цыклаў for, вы выконваеце:

\(\frac{180 \times 179}{2} = 16 110 \text{ чэкаў на кадр!}\)

Пры 60 кадрах у секунду гэта складае амаль 1 000 000 праверак на сутыкненне кожную секунду.

Каб маштабаваць сваю гульню, вы павінны рэалізаваць Шырокафазавае прасторавае раздзяленне.

Аднастайная прасторавая хэш-сетка

Мы дзелім экран на 2D-сетку аднастайных вочак (у Blocked: Pixel Panzer кожная ячэйка складае \(150 \times 150\) пікселяў). Аб'ект правярае сутыкненні толькі з іншымі аб'ектамі, якія знаходзяцца ў тых жа ячэйках сеткі.

Праблема мабільнай памяці: GC Thrashing

Калі ваша SpatialGrid стварае новы List<Point>() або new List<Block>() кожны кадр, вы размяркоўваеце мегабайты смецця кожную хвіліну ў кіраванай кучы.

У асяроддзі выканання Android Mono гэта выклікае частыя зборкі смецця Gen-0, выклікаючы: mono runtime: Канфлікт уласнай блакіроўкі (mono_class_is_subclass_)

Гульня заікаецца, а Google Play пазначае вашу гульню папярэджаннямі пра ANR!

Рашэнне: шматразовыя скрэтч-буферы

Вось прасторавая хэш-сетка з нулявым размеркаваннем з CollisionManager.cs у Blocked: Pixel Panzer:

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

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

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

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

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

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

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

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

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

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

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

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

Пры паўторным выкарыстанні _scratchCells і _scratchCheckedEntities увесь этап сутыкнення выконваецца з 0 байтамі размеркавання кучы на ​​кадр.


8. Зводная параўнальная табліца

Тэхніка Матэматычны кошт Паваротная падтрымка Тунэляванне бяспечна? Лепшы варыянт выкарыстання ў MonoGame
AABB (Прамавугольнік. Перасякае) Звышхуткі (~4 цэлых параўнання) Няма Няма Блокі сеткі, шырокафазны маркер, карыстацкі інтэрфейс
Круг (DistanceSquared) Вельмі хуткі (3 мульты, 0 sqrt) Так (нязменная) Няма Круглыя ​​караблі, агністыя шары, энергетычныя шары
Круг супраць скрынкі (Заціск) Хуткі (лакальны MathHelper.Clamp) Так Няма Кругавы гулец, які рухаецца па цесных сценах
Swept Ray (Slab CCD) Умераны (параметрычны Raycast) Так Так Хуткія снайперскія кулі, рэйсавыя гарматы, лазеры
Pixel-Perfect (IntersectsPixel) Selective (Restricted Sub-Rect) Так Няма Няправільныя контуры спрайтаў, дакладныя хітбоксы
Прасторавая сетка (нуль-GC) Шырокая фаза (\(O(N^2) \да O(N)\)) Н/Д Н/Д Шчыльныя хвалі, кулявыя пекла, Android 60 FPS

Вытворчыя вітрыны ў рэальным свеце: Arar Games

Гэтыя архітэктуры сутыкненняў не з'яўляюцца тэарэтычнымі эксперыментамі - яны з'яўляюцца сапраўднай інжынернай асновай, на якой працуюць нашы камерцыйна выпушчаныя назвы:

  • Заблакіравана: Pixel Panzer: Наша рэтра аркадная гульня на выжыванне ў танках у Google Play і Microsoft Store. У яго ўваходзіць наша поўная двухузроўневая сістэма сутыкненняў: прасторавая хэш-сетка з нулявым размеркаваннем, якая фільтруе падаючыя блокі, і праверка дакладнага кантакту для танкаў, знішчальнікаў, турэльных снарадаў і элементарных боепрыпасаў ColorWheel.
  • Paint Trek: Наш імклівы касмічны шутэр з сутыкненнем па крузе, бесперапыннай трансляцыяй прамянёў і сістэмамі супрацьракетнай абароны з паваротным аб'ёмам.

Выснова і наступныя крокі

MonoGame дае вам магчымасць распрацаваць сістэму выяўлення сутыкненняў, якая цалкам адпавядае патрэбам вашай гульні. Правяраючы дарагія праверкі Pixel-Perfect за хуткімі тэстамі AABB, выкарыстоўваючы Квадратныя адлегласці для кругоў і выключаючы збор смецця з дапамогай Шматразовых буфераў драпін, вы можаце забяспечыць плыўную кансольную прадукцыйнасць 60/120 FPS як на настольных, так і на мабільных платформах.

Праверце нашы гульні ў крамах прыкладанняў, каб убачыць гэтыя сістэмы сутыкненняў у дзеянні, і пачніце ўкараняць гэтыя шаблоны ў сваіх уласных праектах MonoGame сёння!


Спасылкі і рэсурсы крамы


Ключавыя словы і хэштэгі SEO

Ключавыя словы: MonoGame 2D выяўленне сутыкненняў, распрацоўка гульняў на C#, Rectangle.Intersects MonoGame, ідэальнае сутыкненне пікселяў C#, прасторавая хэш-сетка MonoGame, бесперапыннае сутыкненне з разгорнутымі прамянямі, аптымізацыя прадукцыйнасці індзі-гульні, гульнявы цыкл з нулявым размеркаваннем, аптымізацыя Android MonoGame, Arar Games, Blocked Pixel Panzer, Paint Trek.

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




Hiç yorum yok:

Yorum Gönder