Mobile Game Dev etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster
Mobile Game Dev etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster

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 FPS както на компютър с 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;
}

Ако и четирите условия са изпълнени, правоъгълниците се припокриват. Ако дори едно условие е неуспешно, празна ос ги разделя и не е възможен сблъсък.

Истински код за игра: Bullet срещу Enemy в Game1.Update

Ето как проверявате сблъсъци между списък с активни куршуми и списък с активни врагове във вашия основен цикъл за актуализация на MonoGame:

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

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

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

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

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

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

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

        base.Update(gameTime);
    }
}

Съвет за производителност: Забележете, че итерираме назад (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).

Ето производствения метод за пресичане на плочи за raycasting от нашето придружаващо заглавие 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 точност: Pixel-Perfect Collision.

В ретро стрелба с танкове или ръкопашен бой с космически кораби, неправилни форми (цилиндри на танкове, крила, пилотски кабини) са заобиколени от прозрачни пиксели в текстурата на спрайта. Когато вражеска ракета удари това прозрачно пространство, играчите забелязват веднага.

Идеалният сблъсък на пиксели проверява действителните алфа (прозрачност) канали на припокриващите се текстури. Ако два непрозрачни пиксела се припокриват в една и съща световна координата, е настъпило истинско физическо попадение.

Фаталната грешка: 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. Static Color Array Cache: GetData се извиква точно веднъж, когато текстурата се зарежда, и се съхранява в Dictionary<Texture2D, Color[]>.
  2. AABB Early Exit Guard: Ако Bounds.Intersects(other.Bounds) е невярно, спасяваме незабавно. 99% от проверките се елиминират преди проверка на един пиксел.
  3. Текстурен атлас и поддръжка на SourceRectangle: Обработва спрайтове, опаковани в атласи на текстури, като използва отместванията на правоъгълника на източника.
  4. Прозорец с изчислено припокриване: Преминаваме само през точния пресечен правоъгълник между двата спрайта (Math.Max(a.Top, b.Top) и т.н.).
  5. Alpha Short-Circuiting: Ако пикселът на Sprite A е прозрачен (A <= 20), Sprite 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 врага. Ако тествате всеки обект срещу всеки друг обект с помощта на вложени цикли „за“, вие изпълнявате:

\(\frac{180 \times 179}{2} = 16 110 \text{ проверки на кадър!}\)

При 60 FPS, това е почти 1 000 000 проверки за сблъсък всяка секунда.

За да мащабирате играта си, трябва да внедрите широкофазово пространствено разделяне.

Единната пространствена хеш мрежа

Разделяме екрана на 2D мрежа от еднакви клетки (в Blocked: Pixel Panzer всяка клетка е \(150 \times 150\) пиксела). Един обект тества сблъсъци само срещу други обекти, които се намират в същите клетки на мрежата.

Проблемът с мобилната памет: GC Thrashing

Ако вашият SpatialGrid създава new List<Point>() или new List<Block>() всеки кадър, вие разпределяте мегабайти боклук всяка минута в управляваната купчина.

В Mono runtime на Android това задейства често Gen-0 Garbage Collections, причинявайки: моно изпълнение: Конкурс за собствено заключване (mono_class_is_subclass_)

Играта заеква и Google Play маркира играта ви с предупреждения за ANR!

Решението: Буфери за многократна употреба

Ето Пространствената хеш мрежа с нулево разпределение от CollisionManager.cs в Блокиран: Pixel Panzer:

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

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

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

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

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

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

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

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

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

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

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

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

Чрез повторно използване на _scratchCells и _scratchCheckedEntities, цялата стъпка на сблъсък се изпълнява с 0 байта разпределение на купчина на кадър.


8. Обобщена таблица за сравнение

Техника Математически разходи Ротационна поддръжка Безопасно ли е тунелирането? Най-добър случай на използване в MonoGame
AABB (Правоъгълник.Пресича се) Ултра-бърз (~4 сравнения на цели числа) Не Не Мрежови блокове, широка фаза на куршума, потребителски интерфейс
Кръг (DistanceSquared) Много бързо (3 muls, 0 sqrt) Да (непроменлива) Не Кръгли кораби, огнени топки, енергийни кълба
Кръг срещу кутия (Скоба) Бързо (Local MathHelper.Clamp) Да Не Кръгъл играч, навигиращ през тесни стени
Swept Ray (Slab CCD) Умерен (Параметричен Raycast) Да Да Бързи снайперски куршуми, релсови оръдия, лазери
Pixel-Perfect (IntersectsPixel) Селективен (ограничен под-рект) Да Не Неправилни контури на спрайтове, справедливи хитбоксове
Пространствена мрежа (Zero-GC) Широка фаза (\(O(N^2) \до O(N)\)) N/A N/A Плътни вълни, ад от куршуми, 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

2D բախման հայտնաբերում MonoGame-ում. հիմնական ուղղանկյուններից մինչև զրոյական տեղաբաշխման Pixel-Perfect համակարգեր

2D բախման հայտնաբերում MonoGame-ում. հիմնական ուղղանկյուններից մինչև զրոյական տեղաբաշխման Pixel-Perfect համակարգեր

Երբ կառուցում եք 2D խաղեր վիզուալ շարժիչներով, ինչպիսիք են Unity-ը կամ Godot-ը, բախումների հայտնաբերումը հաճախ թվում է ստուգիչի վանդակների մի շարք. դուք ապտակում եք «BoxCollider2D»-ին կամ «CircleCollider2D»-ին, կցում եք «Rigidbody» և հուսով եք, որ ներքին ֆիզիկայի քայլը չի կակազում շարժական սարքերում:

MonoGame-ում և C#-ում, այնուամենայնիվ, դուք լիովին վերահսկում եք: Չկա ոչ մի թաքնված ֆիզիկա, ոչ մի անցանկալի պտտվող իներցիա և ոչ մի առեղծվածային աղբահանության (GC) աճեր, որոնք գողանում են ձեր շրջանակի բյուջեն:

Arar Games-ում, երբ մենք կառուցեցինք Blocked: Pixel Panzer և Paint Trek-ը, մեր արկադային խաղերի հանգույցներն անհրաժեշտ էին հարյուրավոր արագընթաց թշնամու փամփուշտներ մշակելու համար, պայթող աղյուսե ցանցեր, պտտվող տանկի պտուտահաստոցներ, կործանիչ ինքնաթիռների թռիչքներ և մասնիկների վահաններ 60-ից մինչև 120 F PS արագությամբ և՛ Windows PC-ում, և՛ Android սարքերում: Ընդհանուր նշանակության ֆիզիկայի շարժիչի մասին խոսք անգամ լինել չէր կարող. մեզ անհրաժեշտ էր նպատակային, աստիճանավոր բախման ճարտարապետություն:

Այս համապարփակ, կոդերի վրա հիմնված ուղեցույցում մենք կսկսենք MonoGame-ի բախման բացարձակ հիմունքներից («Rectangle. Intersects»՝ իրական «Bullet» և «Enemy» sprites) և կկառուցենք առաջադեմ շրջանային ստուգումներ, խառը սեղմում, հակաթունելային ճառագայթային հեռարձակումներ, արտադրական աստիճանի Pixellision-A, coc... Ցանցեր օպտիմիզացված բջջային 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)

MonoGame-ում 2D բախման ամենահիմնական ստուգումը Առանցքով հավասարեցված սահմանային տուփն է (AABB): «Առանցքով հավասարեցված» տերմինը պարզապես նշանակում է, որ ուղղանկյունի եզրերը լիովին զուգահեռ են էկրանի \(X\) և \(Y\) առանցքներին (առանց պտույտի):

MonoGame-ն ապահովում է արագ, ներկառուցված մեթոդ՝ «Rectangle.Intersects(Rectangle value)»:

Ինչպես է «Rectangle.Intersects»-ը աշխատում գլխարկի տակ

Մակերեւույթի տակ MonoGame-ն իրականացնում է չորս ամբողջ թվերի համեմատություն.

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

Եթե ​​բոլոր չորս պայմանները բավարարված են, ապա ուղղանկյունները համընկնում են: Եթե ​​նույնիսկ մեկ պայման ձախողվի, դատարկ առանցքը նրանց բաժանում է, և բախում հնարավոր չէ:

Իրական խաղի կոդը՝ «Bullet» ընդդեմ «Enemy» «Game1.Update»-ում

Ահա, թե ինչպես եք ստուգում բախումները ակտիվ փամփուշտների և ակտիվ թշնամիների ցանկի միջև ձեր հիմնական MonoGame «Update» հանգույցի ներսում.

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. Հավաքածուն փոփոխվել է»: Հետադարձ շարժումը թույլ է տալիս տարրերի անվտանգ հեռացում առանց հիշողության վերաինդեքսավորման խնդիրների:

Արկադային գաղտնիքը. «Fair Hitboxes»՝ «Inflate»-ի միջոցով

Ռետրո խաղերում, ինչպիսին է Blocked: Pixel Panzer-ն է, sprite հյուսվածքները հաճախ ներառում են թափանցիկ լուսանցքներ կամ ալեհավաքի ցցիկներ: Եթե ​​խաղացողի տանկը պայթի, քանի որ գնդակը դիպել է նրա հյուսվածքի դատարկ թափանցիկ անկյունին, խաղացողը իրեն խաբված կզգա:

Որպեսզի բախումն արձագանքող և արդար լինի, խաղերն օգտագործում են ավելի փոքր 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-ում, առանցքի վրա հավասարեցված սահմանափակող տուփը պետք է ընդարձակվի՝ պարփակելով պտտվող անկյունները, ինչը հանգեցնում է դատարկ օդում զայրացնող «ֆանտոմային բախումների»:

Շրջանաձև աստերոիդների, շարժվող էներգիայի ուղեծրերի և պտտվող տիեզերանավերի համար Սահմանափակ շրջանակները իդեալական լուծում են:

Քառակուսի արմատների թակարդը

Երկու շրջաններ բախվում են, երբ նրանց կենտրոնների միջև հեռավորությունը փոքր է կամ հավասար է նրանց շառավիղների գումարին.

\(\text{Հեռավորություն}(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:

Մեզ անհրաժեշտ է Շրջանակ ընդդեմ ուղղանկյունի բախում:

Ամրագրման ալգորիթմ

Ռազմավարությունն է՝ գտնել ուղղանկյան այն կետը, որն ամենամոտ է շրջանագծի կենտրոնին, այնուհետև ստուգել՝ արդյոք այդ ամենամոտ կետից դեպի կենտրոն հեռավորությունը փոքր է շրջանագծի շառավղից:

MonoGame-ի «MathHelper.Clamp»-ը սա աննշան է դարձնում.

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-ում («նախորդԴիրք») Շրջանակ 2-ին («ընթացիկ Դիրք»):

Ահա արտադրական raycasting սալաքար-հատման մեթոդը մեր ուղեկից վերնագրից 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.


6. Մակարդակ 5. արտադրական աստիճանի պիքսել-կատարյալ բախման հայտնաբերում

Այժմ մենք հասնում ենք 2D ճշգրտության վերջնական մակարդակին՝ Pixel-Perfect Collision:

Ռետրո տանկային հրաձիգում կամ տիեզերանավերի շան մարտերում անկանոն ձևերը (տանկի տակառներ, թևեր, օդաչուների խցիկի խցիկներ) շրջապատված են թափանցիկ պիքսելներով՝ սփրայթ հյուսվածքով: Երբ թշնամու հրթիռը հարվածում է այդ թափանցիկ տարածությանը, խաղացողներն անմիջապես նկատում են:

Pixel-ի կատարյալ բախումը ստուգում է համընկնող հյուսվածքների իրական ալֆա (թափանցիկության) ալիքները: Եթե ​​երկու ոչ թափանցիկ պիքսելները համընկնում են միևնույն աշխարհի կոորդինատում, տեղի է ունեցել իրական ֆիզիկական հարված:

Ճակատագրական սխալ. «GetData»՝ «Update()»-ի ներսում

Շատ ձեռնարկներ սկսնակներին հրահանգում են դա անել.

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

Գործարկման ժամանակ «Texture2D.GetData()» կանչելը ստիպում է պրոցեսորին սպասել GPU խողովակաշարի լցվելուն՝ հետ քաշելով հյուսվածքային հիշողությունը ավտոբուսի վրայով: Դա անելը տասնյակ սփրայթների համար յուրաքանչյուր կադրի դեպքում ձեր խաղը կբարձրացնի 60 FPS-ից մինչև 5 FPS և անմիջապես կսկսի Android ANR-ներ:

The Battle-Tested Solution Blocked: Pixel Panzer-ից

**Blocked.

  1. Static Color Array Cache. «GetData»-ն կանչվում է ճիշտ մեկ անգամ, երբ հյուսվածքը բեռնվում է, և պահվում է «Dictionary<Texture2D, Color[]>»-ում:
  2. AABB Early Exit Guard. Եթե «Bounds.Intersects(other.Bounds)»-ը կեղծ է, մենք անմիջապես փրկում ենք: Ստուգումների 99%-ը վերացվում է մեկ պիքսել ստուգելուց առաջ:
  3. **Հյուսվածքային ատլաս և «SourceRectangle» աջակցություն.
  4. Հաշվարկված համընկնման պատուհան. Մենք պտտվում ենք միայն երկու սփրայթների միջև ճշգրիտ հատման ուղղանկյունի վրա (Math.Max(a.Top, b.Top) և այլն):
  5. Ալֆա կարճ միացում. Եթե Sprite A-ի պիքսելը թափանցիկ է (A <= 20), Sprite B-ն ամբողջությամբ բաց է թողնվում:
  6. Հատկությունների բարձրացում. Վիրտուալ հատկությունների («Bounds») մուտքը տեղադրված օղակներում ստեղծում է հազարավոր կառուցվածքային պատճեններ: Մենք դրանք պահում ենք լոկալ stack փոփոխականներում՝ նախքան հանգույց մտնելը:

Ահա ամբողջական, արտադրության համար պատրաստ ծածկագիրը.

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 թշնամի: Եթե դուք փորձարկում եք յուրաքանչյուր էություն յուրաքանչյուր այլ էության դեմ՝ օգտագործելով nested «for» օղակները, դուք կատարում եք.

\(\frac{180 \times 179}{2} = 16110 \text{ ստուգումներ մեկ շրջանակի համար!}\)

60 FPS արագությամբ, դա գրեթե 1,000,000 բախման ստուգում է ամեն վայրկյան:

Ձեր խաղը մեծացնելու համար դուք պետք է իրականացնեք Լայն փուլային տարածական բաժանում:

Միասնական տարածական հեշ ցանց

Մենք էկրանը բաժանում ենք միատարր բջիջների 2D ցանցի (Blocked: Pixel Panzer, յուրաքանչյուր բջիջը \(150 \ անգամ 150\) պիքսել է)։ Կազմակերպությունը ստուգում է միայն բախումները այլ օբյեկտների հետ, որոնք գտնվում են նույն ցանցի բջիջներում:

Բջջային հիշողության խնդիր. GC Thrashing

Եթե ձեր «SpatialGrid»-ը ստեղծում է «new List()» կամ «new List()» յուրաքանչյուր կադր, դուք ամեն րոպե հատկացնում եք մեգաբայթ աղբ կառավարվող կույտի վրա:

Android-ի Mono-ի գործարկման ժամանակ սա առաջացնում է Gen-0-ի հաճախակի աղբի հավաքում, ինչը հանգեցնում է. «mono runtime. Native lock conference (mono_class_is_subclass_)»:

Խաղը կակազում է, և Google Play-ը ձեր խաղը նշում է ANR նախազգուշացումներով:

Լուծում. բազմակի օգտագործման քերծվածքային բուֆերներ

Ահա Զրոյական տեղաբաշխման տարածական հեշ ցանցը «CollisionManager.cs»-ից՝ Արգելափակված՝ Pixel Panzer:

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

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

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

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

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

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

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

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

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

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

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

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

«_scratchCells»-ը և «_scratchCheckedEntities»-ը վերօգտագործելով՝ բախման ամբողջ քայլն աշխատում է 0 բայթ կույտի բաշխմամբ յուրաքանչյուր կադրի համար:


8. Ամփոփ համեմատական աղյուսակ

Տեխնիկա Մաթեմատիկական արժեքը Ռոտացիոն աջակցություն Անվտանգ է թունելների մշակումը: Լավագույն օգտագործման դեպք MonoGame-ում
AABB (Ուղղանկյուն.Հատվում է) Գերարագ (~4 ամբողջ թվերի համեմատություն) Ոչ Ոչ Ցանցային բլոկներ, bullet լայն փուլ, UI
Շրջանակ (DistanceSquared) Շատ արագ (3 muls, 0sqrt) Այո (Invariant) Ոչ Կլոր նավեր, հրե գնդակներ, էներգետիկ գնդիկներ
Շրջանակ ընդդեմ տուփի (Սեղմ) Արագ (Տեղական MathHelper.Clamp) Այո Ոչ Շրջանաձև նվագարկիչ, որը նավարկում է ամուր պատերով
Swept Ray (Slab CCD) Չափավոր (պարամետրային ճառագայթում) Այո Այո Արագ դիպուկահար փամփուշտներ, երկաթուղային հրացաններ, լազերներ
Pixel-Perfect (IntersectsPixel) Ընտրովի (Սահմանափակված ենթակետ) Այո Ոչ Անկանոն սփրայթ ուրվագծեր, արդար հիթբոքսեր
Տարածական ցանց (Zero-GC) Լայն փուլ (\(O(N^2) \մինչև O(N)\)) N/A N/A Խիտ ալիքներ, փամփուշտների դժոխքներ, Android 60 FPS

Իրական աշխարհի արտադրության ցուցափեղկեր. Arar Games

Այս բախման ճարտարապետությունները տեսական փորձեր չեն, դրանք իրական ինժեներական հիմքն են, որն ապահովում է մեր առևտրային հրապարակված վերնագրերը.

  • Արգելափակված՝ Pixel Panzer: Մեր ռետրո տանկի գոյատևման արկադային խաղը Google Play-ում և Microsoft Store-ում: Այն պարունակում է մեր ամբողջական երկաստիճան բախման համակարգը՝ զրոյական տեղաբաշխման տարածական հեշ ցանց, որը զտում է ընկնող բլոկները և պիքսելներով կատարյալ կոնտակտի ստուգում տանկերի, կործանիչների, պտուտահաստոցների պարկուճների և ColorWheel տարրական զինամթերքի համար:
  • Paint Trek: Մեր արագ տեմպերով տիեզերական հրաձիգը պարունակում է պտտվող շրջանային բախում, շարունակական ճառագայթային հեռարձակում և հակահրթիռային պաշտպանության համակարգեր:

Եզրակացություն և հաջորդ քայլեր

MonoGame-ը ձեզ հնարավորություն է տալիս նախագծել բախումների հայտնաբերում, որը լիովին համապատասխանում է ձեր խաղի կարիքներին: Արագ AABB թեստերի հետևում թանկարժեք Pixel-Perfect չեկեր հավաքելով, շրջանակների համար օգտագործելով Քառակուսի հեռավորություններ և վերացնելով աղբահանությունը Բազմիցս օգտագործելի Scratch Buffers-ով, դուք կարող եք մատուցել 60/120 FPS հարթ աշխատանք ինչպես աշխատասեղանի, այնպես էլ շարժական հարթակների վրա:

Դիտեք մեր խաղերը հավելվածների խանութներում, որպեսզի տեսնեք այս բախման համակարգերը գործողության մեջ և սկսեք կիրառել այս օրինաչափությունները ձեր սեփական MonoGame նախագծերում այսօր:


Խանութի հղումներ և ռեսուրսներ


SEO հիմնաբառեր և հեշթեգներ

** Հիմնաբառեր.** MonoGame 2D բախումների հայտնաբերում, C# խաղի մշակում, Rectangle.Intersects MonoGame, պիքսելների կատարյալ բախում C#, տարածական հեշ ցանց MonoGame, անընդմեջ ճառագայթների բախում, ինդի խաղերի կատարողականի օպտիմիզացում, զրոյական տեղաբաշխման խաղի հանգույց, Android MonoG օպտիմիզացված խաղերի հանգույց, Android MonoG-ի օպտիմալացում: Ուղևորություն.

#MonoGame #CSharp #GameDev #IndieDev #GamePhysics`` #DotNet #2DGameDev #MobileGameDev`` #PerformanceOptimization #CleanCode #BlockedPixelPixelPanTan «#ԽաղիԾրագրավորում».