27 Eylül 2026 Pazar

تشخیص برخورد دوبعدی در بازی MonoGame: از مستطیل های پایه تا سیستم های پیکسل کامل با تخصیص صفر

تشخیص برخورد دوبعدی در MonoGame: از مستطیل های پایه تا سیستم های پیکسلی با تخصیص صفر

هنگام ساخت بازی‌های دوبعدی در موتورهای بصری مانند Unity یا Godot، تشخیص برخورد اغلب مانند یک سری چک باکس بازرس به نظر می‌رسد: یک «BoxCollider2D» یا «CircleCollider2D» را می‌زنید، یک «Rigidbody» وصل می‌کنید و امیدوارید که مرحله فیزیک داخلی در دستگاه‌های تلفن همراه دچار لکنت نشود.

با این حال، در MonoGame و C#، شما در کنترل کامل هستید. هیچ سربار فیزیک پنهان، هیچ اینرسی چرخشی ناخواسته، و هیچ سنبله مرموز جمع آوری زباله (GC) که بودجه فریم شما را بدزدد وجود ندارد.

در Arar Games، زمانی که Blocked: Pixel Panzer و Paint Trek را ساختیم، حلقه‌های بازی آرکید ما برای پردازش صدها گلوله دشمن پرسرعت، شبکه‌های آجری در حال انفجار، برجک‌های چرخان تانک، پرواز جت‌های جنگنده و سپرهای ذرات با سرعت 60 تا 120 درجه فارنهایت در رایانه‌های شخصی ویندوز و دستگاه‌های Android نیاز داشت. یک موتور فیزیک همه‌منظوره مورد بحث نبود - ما به یک معماری برخورد طبقه‌بندی شده و هدفمند نیاز داشتیم.

در این راهنمای جامع و مبتنی بر کد، ما از اصول اولیه برخورد MonoGame («مستطیل. تقاطع» با «گلوله» و «دشمن» جن‌های واقعی) شروع می‌کنیم و به بررسی‌های دایره‌ای پیشرفته، گیره‌های مختلط، پخش‌های پرتوی ضد تونل، درجه تولید Pixelsion-A*Afects Grids برای بقای 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 تست جعبه مرزی تراز محور (AABB) است. اصطلاح "Axis-aligned" به سادگی به این معنی است که لبه های مستطیل کاملاً موازی با محورهای \(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: مجموعه اصلاح شد» را ارسال می‌کند. تکرار به عقب امکان حذف ایمن عنصر را بدون مشکلات نمایه سازی مجدد حافظه فراهم می کند.

راز Arcade: "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 می‌چرخد، یک جعبه مرزی هم‌تراز با محور باید منبسط شود تا گوشه‌های چرخان را محصور کند و در نتیجه "برخوردهای فانتوم" در هوای خالی ناامیدکننده باشد.

برای سیارک‌های دایره‌ای، گوی‌های انرژی خانه‌دار، و فضاپیماهای در حال چرخش، دایره‌های مرزی راه‌حل ایده‌آل هستند.

تله ریشه مربع

دو دایره زمانی با هم برخورد می کنند که فاصله مراکز آنها کمتر یا مساوی با مجموع شعاع آنها باشد:

\(\text{فاصله}(C_A، C_B) \le r_A + r_B\)

در کد، محاسبه فاصله اقلیدسی از «Math.Sqrt» (یا «Vector2.Distance») استفاده می‌کند. با این حال، محاسبه ریشه های مربع در یک حلقه با 200 پرتابه، صدها چرخه CPU غیر ضروری را می سوزاند!

با مقایسه فاصله مربع در برابر ** مجموع شعاع مربع**، ریشه دوم را کاملا حذف می کنیم:

\(\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!)

راه حل: Swept Segment در مقابل Box (روش Slab)

به جای آزمایش یک نقطه، کل پاره خط را که موقعیت گلوله را در قاب 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: Pixel Panzer، مهارت پیوسته پرتو لیزر بازیکن از این پرتو پرتوی دقیق استفاده می‌کند تا بدون از دست دادن یک برخورد، ردیف‌هایی از بلوک‌های نزولی را برش دهد.


6. سطح 5: درجه تولید پیکسل- تشخیص برخورد کامل

اکنون به سطح نهایی دقت دوبعدی می رسیم: برخورد کامل پیکسل.

در یک تیراندازی یکپارچهسازی با سیستمعامل تانک یا درگیری با سفینه فضایی، اشکال نامنظم (شکه تانک، بالها، کابین خلبان کابین) توسط پیکسل های شفاف در بافت جن احاطه شده است. وقتی یک موشک دشمن به آن فضای شفاف برخورد می کند، بازیکنان بلافاصله متوجه می شوند.

برخورد کامل پیکسل کانال های آلفای واقعی (شفافیت) بافت های همپوشانی را بررسی می کند. اگر دو پیکسل غیر شفاف در یک مختصات جهانی همپوشانی داشته باشند، یک ضربه فیزیکی واقعی رخ داده است.

اشتباه مهلک: "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()» در طول زمان اجرا، CPU را مجبور می‌کند تا منتظر بمانند تا خط لوله GPU صاف شود و حافظه بافت را روی گذرگاه به عقب بکشد. انجام این کار برای ده ها اسپرایت در هر فریم، بازی شما را از 60 فریم بر ثانیه به 5 فریم در ثانیه کاهش می دهد و بلافاصله ANR های اندروید را آغاز می کند!

راه حل آزمایش شده در نبرد از مسدود شده: 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. **Texture Atlas & "SourceRectangle" پشتیبانی: ** با استفاده از جابجایی مستطیل منبع، با sprites های بسته بندی شده در اطلس های بافتی، کنترل می کند.
  4. پنجره همپوشانی محاسبه شده: ما فقط روی مستطیل تقاطع دقیق بین هر دو اسپریت حلقه می زنیم (`Math.Max(a.Top، b.Top) و غیره).
  5. آلفا اتصال کوتاه: اگر پیکسل Sprite A شفاف باشد (A <= 20`)، Sprite B کاملاً نادیده گرفته می شود.
  6. بالا بردن ویژگی: دسترسی به ویژگی های مجازی ('Bounds') در حلقه های تودرتو، هزاران کپی ساختار ایجاد می کند. قبل از ورود به حلقه آنها را در متغیرهای پشته محلی ذخیره می کنیم.

در اینجا کد کامل و آماده تولید آمده است:

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 میلی ثانیه در سخت افزار مدرن اندروید و سیستم های دسکتاپ اجرا می شود.


7. سطح 6: شبکه فضایی فاز وسیع و GC موبایل با تخصیص صفر

تصور کنید بازی شما دارای 100 بلوک، 60 گلوله و 20 دشمن است. اگر هر موجودیتی را با هر موجودیت دیگری با استفاده از حلقه‌های تودرتوی «for» آزمایش کنید، انجام می‌دهید:

\(\frac{180 \times 179}{2} = 16110 \text{ چک در هر فریم!}\)

در سرعت 60 فریم در ثانیه، تقریباً 1000000 بررسی برخورد در هر ثانیه است.

برای مقیاس بندی بازی خود، باید پارتیشن بندی فضایی فاز وسیع را اجرا کنید.

شبکه هش فضایی یکنواخت

ما صفحه را به یک شبکه دو بعدی از سلول های یکنواخت تقسیم می کنیم (در Blocked: Pixel Panzer، هر سلول 150 دلار \ برابر 150 دلار پیکسل است). یک موجودیت فقط برخوردها را با سایر موجودیت‌هایی که در سلول‌های شبکه یکسانی هستند آزمایش می‌کند.

مشکل حافظه موبایل: GC Thrashing

اگر «SpatialGrid» شما در هر فریم «List New List()» یا «New List()» ایجاد کند، هر دقیقه مگابایت زباله را روی پشته مدیریت شده اختصاص می دهید.

در زمان اجرا مونو اندروید، این کار باعث ایجاد جمع‌آوری زباله‌های مکرر Gen-0 می‌شود که باعث می‌شود: «زمان اجرای مونو: مناقشه قفل بومی (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 عدد صحیح مقایسه) نه نه بلوک های شبکه، گلوله فاز گسترده، UI
دایره ('DistanceSquared') بسیار سریع (3 muls, 0 sqrt) بله (نامغیر) نه کشتی های گرد، گوی های آتشین، گوی های انرژی
** دایره در مقابل جعبه (گیره)** سریع (محلی MathHelper.Clamp) بله نه پخش کننده دایره ای در حال حرکت در دیوارهای تنگ
Swept Ray (Slab CCD) متوسط ​​(پارامتری Raycast) بله بله گلوله های سریع تک تیرانداز، تفنگ ریلی، لیزر
Pixel-Perfect ('IntersectsPixel') انتخابی (محدود شده زیر راست) بله نه کانتورهای اسپرایت نامنظم، هیت باکس های منصفانه
شبکه فضایی (صفر-GC) فاز گسترده (\(O(N^2) \تا O(N)\)) N/A N/A امواج متراکم، جهنم های گلوله، اندروید 60 FPS

ویترین های تولید در دنیای واقعی: بازی های آرار

این معماری‌های برخورد، آزمایش‌های نظری نیستند - آنها پایه‌های مهندسی واقعی هستند که عناوین تجاری منتشر شده ما را تقویت می‌کنند:

  • مسدود شده: Pixel Panzer: بازی آرکید بقا تانک یکپارچهسازی با سیستمعامل ما در Google Play و فروشگاه مایکروسافت. این سیستم دارای سیستم برخورد دو لایه کامل ما است: شبکه هش فضایی با تخصیص صفر که بلوک های در حال سقوط را فیلتر می کند، و تماس کامل پیکسلی را برای تانک ها، جت های جنگنده، پوسته های برجک و مهمات عنصری ColorWheel بررسی می کند.
  • Paint Trek: تیرانداز فضایی سریع ما دارای برخورد دایره‌ای چرخشی، پخش مداوم پرتو، و سیستم‌های دفاع موشکی با حجم بالا.

نتیجه گیری و مراحل بعدی

MonoGame به شما قدرت طراحی تشخیص برخورد را می دهد که کاملاً با نیازهای بازی شما مطابقت دارد. با انتخاب چک‌های گران قیمت Pixel-Perfect در پشت تست‌های سریع AABB، استفاده از فاصله‌های مربعی برای دایره‌ها، و حذف جمع‌آوری زباله با بافرهای خراش قابل استفاده مجدد، می‌توانید عملکرد یکنواخت 60/120 FPS را در هر دو پلتفرم دسکتاپ و موبایل ارائه دهید.

بازی‌های ما را در فروشگاه‌های اپلیکیشن بررسی کنید تا این سیستم‌های برخورد را در عمل ببینید و همین امروز پیاده‌سازی این الگوها را در پروژه‌های MonoGame خود شروع کنید!


پیوندها و منابع فروشگاه


کلمات کلیدی و هشتگ سئو

کلیدواژه ها: تشخیص برخورد دوبعدی MonoGame، توسعه بازی C#، Rectangle.Intersects MonoGame، Pixel Collision Perfect C#، شبکه هش فضایی MonoGame، swept ray collision، بهینه سازی عملکرد بازی مستقل، تخصیص صفر، بازی Loop بهینه سازی شده اندروید، بازی های بهینه سازی شده Paint MonoG، Paxelme Pixela راهپیمایی

#MonoGame #CSharp #GameDev #IndieDev #GamePhysics #DotNet #2DGameDev #MobileGameDev #PerformanceOptimization #CleanCode #BlockedPixelPixelPanTan #برنامه نویسی بازی

Phát hiện va chạm 2D trong MonoGame: Từ hình chữ nhật cơ bản đến hệ thống hoàn hảo về điểm ảnh không phân bổ

Phát hiện va chạm 2D trong MonoGame: Từ hình chữ nhật cơ bản đến hệ thống hoàn hảo về điểm ảnh không phân bổ

Khi xây dựng trò chơi 2D bằng các công cụ trực quan như Unity hoặc Godot, việc phát hiện va chạm thường giống như một loạt hộp kiểm tra: bạn nhấn vào BoxCollider2D hoặc CircleCollider2D, đính kèm Rigidbody và hy vọng bước vật lý bên trong không bị vấp trên thiết bị di động.

Tuy nhiên, trong MonoGame và C#, bạn có toàn quyền kiểm soát. Không có chi phí vật lý ẩn, không có quán tính quay không mong muốn và không có đột biến thu gom rác (GC) bí ẩn nào ăn cắp ngân sách khung hình của bạn.

Tại Arar Games, khi chúng tôi xây dựng Blocked: Pixel Panzer và Paint Trek, vòng lặp trò chơi arcade của chúng tôi cần xử lý hàng trăm viên đạn tốc độ cao của kẻ thù, lưới gạch nổ, tháp xe tăng xoay, máy bay chiến đấu bay ngang qua và lá chắn hạt ở tốc độ 60 đến 120 FPS trên cả thiết bị Windows PC và Android. Một công cụ vật lý có mục đích chung là không thể - chúng tôi cần một kiến ​​trúc va chạm theo cấp độ, được xây dựng có mục đích.

Trong hướng dẫn toàn diện, dựa trên mã này, chúng ta sẽ bắt đầu từ những điều cơ bản tuyệt đối về va chạm MonoGame (Rectangle.Intersects với các họa tiết Bullet và Enemy thực) và xây dựng các bước kiểm tra vòng tròn nâng cao, kẹp hỗn hợp, raycast chống đào hầm, va chạm hoàn hảo theo pixel cấp sản xuất và Lưới không gian phân bổ bằng 0 được tối ưu hóa để tồn tại GC trên thiết bị di động.


1. Nền tảng: Hệ thống phân cấp Sprite MonoGame đơn giản

Trước khi phát hiện xung đột, chúng ta cần các thực thể trò chơi sạch. Trong MonoGame, một thực thể về cơ bản sở hữu một vị trí, kết cấu và hình chữ nhật bao quanh.

Đây là kiến ​​trúc thực thể cơ bản được sử dụng trong các trò chơi của chúng tôi:

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

Bây giờ, hãy tạo các lớp Player, Enemy và Bullet cụ thể:

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

Với những thực thể này, hãy khám phá cách phát hiện xung đột giữa chúng, bắt đầu từ phương pháp đơn giản nhất.


2. Cấp độ 1: Va chạm đơn giản nhất – Rectangle.Intersects (AABB)

Kiểm tra va chạm 2D cơ bản nhất trong MonoGame là thử nghiệm Hộp giới hạn căn chỉnh theo trục (AABB). Thuật ngữ "căn chỉnh theo trục" chỉ có nghĩa là các cạnh của hình chữ nhật hoàn toàn song song với trục \(X\) và \(Y\) của màn hình (không xoay).

MonoGame cung cấp một phương thức tích hợp nhanh: Rectangle.Intersects(Rectangle value).

Cách thức hoạt động của Rectangle.Intersects

Bên dưới bề mặt, MonoGame thực hiện bốn phép so sánh số nguyên:

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

Nếu tất cả bốn điều kiện được đáp ứng, các hình chữ nhật sẽ chồng lên nhau. Nếu thậm chí một điều kiện không thành công, một trục trống sẽ tách chúng ra và không thể xảy ra va chạm.

Mã trò chơi thực tế: Bullet vs. Enemy trong Game1.Update

Đây là cách bạn kiểm tra va chạm giữa danh sách các viên đạn đang hoạt động và danh sách kẻ thù đang hoạt động bên trong vòng lặp MonoGame Update chính của bạn:

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

Mẹo về hiệu suất: Lưu ý rằng chúng tôi lặp ngược lại (for (int i = list.Count - 1; i >= 0; i--))! Nếu bạn sử dụng foreach và cố gắng gọi _bullets.Remove(bullet), C# sẽ ném ra một InvalidOperationException: Collection đã được sửa đổi. Việc lặp lại cho phép loại bỏ phần tử an toàn mà không gặp vấn đề về lập chỉ mục lại bộ nhớ.

Bí mật trò chơi điện tử: "Hitboxes công bằng" thông qua Inflate

Trong các trò chơi cổ điển như Blocked: Pixel Panzer, họa tiết sprite thường bao gồm các lề trong suốt hoặc các gai ăng-ten. Nếu xe tăng của người chơi phát nổ do một viên đạn chạm vào một góc trong suốt trống rỗng trên kết cấu của nó, người chơi sẽ cảm thấy bị lừa.

Để tạo cảm giác va chạm nhanh và công bằng, trò chơi sử dụng Hitbox nhỏ hơn bên trong sprite bằng cách sử dụng 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. Cấp độ 2: Va chạm vòng tròn (Miễn dịch quay)

Hình chữ nhật hoạt động tốt cho các khối tĩnh và ô lưới, nhưng chúng không thành công khi các hình vẽ xoay. Khi một con tàu vũ trụ không vuông quay trong Paint Trek, hộp giới hạn theo trục phải mở rộng để bao quanh các góc quay, dẫn đến "va chạm ảo" khó chịu trong không khí trống rỗng.

Đối với các tiểu hành tinh hình tròn, quả cầu năng lượng dẫn đường và tàu vũ trụ quay, Vòng tròn giới hạn là giải pháp lý tưởng.

Bẫy căn bậc hai

Hai đường tròn va chạm nhau khi khoảng cách giữa tâm của chúng nhỏ hơn hoặc bằng tổng bán kính của chúng:

\(\text{Khoảng cách}(C_A, C_B) \le r_A + r_B\)

Trong mã, tính toán khoảng cách Euclide sử dụng Math.Sqrt (hoặc Vector2.Distance). Tuy nhiên, việc tính căn bậc hai trong một vòng lặp với 200 đường đạn sẽ đốt cháy hàng trăm chu kỳ CPU không cần thiết!

Bằng cách so sánh khoảng cách bình phương với tổng bán kính bình phương, chúng ta loại bỏ hoàn toàn căn bậc hai:

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

Triển khai MonoGame: Circle so với 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);
    }
}

Bây giờ tích hợp điều này trực tiếp vào một thực thể:

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

Không có căn bậc hai, không bị xoay sprite và nhanh như chớp.


4. Cấp độ 3: Hình dạng hỗn hợp – Hình tròn so với Hình hộp (MathHelper.Clamp)

Điều gì sẽ xảy ra khi một con tàu vũ trụ hình tròn trong Paint Trek di chuyển qua một mê cung chật hẹp gồm các hàng rào phòng thủ hình chữ nhật hoặc khi một viên đạn tròn bắn trúng một khối vuông trong Blocked: Pixel Panzer?

Chúng ta cần va chạm Hình tròn và Hình chữ nhật.

Thuật toán kẹp

Chiến lược là tìm điểm trên hình chữ nhật gần tâm hình tròn nhất, sau đó kiểm tra xem khoảng cách từ điểm gần nhất đó đến tâm có nhỏ hơn bán kính hình tròn hay không.

MathHelper.Clamp của MonoGame khiến điều này trở nên tầm thường:

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

Bây giờ bạn có thể kiểm tra lá chắn của người chơi chống lại các viên gạch hình chữ nhật với mức phân bổ bằng 0 và độ chính xác cao!


5. Cấp độ 4: Phát hiện va chạm liên tục (CCD) & Tia quét

Bạn đã bao giờ bắn một phát đạn bắn tỉa siêu tốc hoặc một khẩu súng laser trong trò chơi của mình chỉ để xem viên đạn xuyên thẳng qua một con tàu mỏng của kẻ thù một cách kỳ diệu mà không gây sát thương chưa?

Lỗi này được gọi là đào hầm.

Vì các trò chơi riêng biệt cập nhật theo các bước thời gian (\(\Delta t = 16,6\text{ms}\) ở tốc độ 60 FPS), một vật thể chuyển động với tốc độ 1.800 pixel mỗi giây sẽ di chuyển 30 pixel trong một khung hình. Nếu thân tàu địch chỉ dày 15 pixel thì viên đạn nằm ở phía trước kẻ địch trên Khung 1 và hoàn toàn ở phía sau kẻ thù trên Khung 2.

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

Giải pháp: Phân đoạn quét so với Hộp (Phương pháp phiến)

Thay vì kiểm tra một điểm duy nhất, chúng tôi kiểm tra toàn bộ đoạn đường nối vị trí của dấu đầu dòng trên Khung 1 (previousPosition) đến Khung 2 (currentPosition).

Đây là phương pháp sản xuất raycasting Slab-intersection từ tiêu đề đồng hành của chúng tôi 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;
    }
}

Trong Blocked: Pixel Panzer, Kỹ năng Tia Laser liên tục của người chơi sử dụng phương pháp truyền tia chính xác này để cắt xuyên qua các hàng khối giảm dần mà không bỏ sót một va chạm nào.


6. Cấp độ 5: Phát hiện va chạm pixel hoàn hảo cấp độ sản xuất

Giờ đây, chúng tôi đã đạt đến mức độ chính xác 2D cao nhất: Va chạm hoàn hảo đến từng pixel.

Trong game bắn súng xe tăng cổ điển hoặc trận không chiến tàu vũ trụ, các hình dạng bất thường (thùng xe tăng, cánh, buồng lái buồng lái) được bao quanh bởi các pixel trong suốt trong kết cấu sprite. Khi tên lửa của địch bắn trúng khoảng không trong suốt đó, người chơi sẽ nhận ra ngay lập tức.

Va chạm hoàn hảo về điểm ảnh sẽ kiểm tra các kênh alpha (độ trong suốt) thực tế của các họa tiết chồng chéo. Nếu hai pixel không trong suốt chồng lên nhau ở cùng một tọa độ thế giới, thì một cú đánh vật lý thực sự đã xảy ra.

Sai lầm tai hại: GetData bên trong Update()

Nhiều hướng dẫn hướng dẫn người mới bắt đầu làm điều này:

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

Việc gọi Texture2D.GetData() trong thời gian chạy buộc CPU phải đợi đường dẫn GPU xử lý, kéo bộ nhớ kết cấu trở lại bus. Thực hiện việc này cho hàng chục hình ảnh trong mỗi khung hình sẽ giúp trò chơi của bạn tăng tốc từ 60 FPS lên 5 FPS và kích hoạt ANR của Android ngay lập tức!

Giải pháp đã được thử nghiệm trong trận chiến từ Bị chặn: Pixel Panzer

Trong Đã chặn: Pixel Panzer, Sprite.cs của chúng tôi triển khai quy trình IntersectsPixel cực kỳ tối ưu hóa được xây dựng dựa trên 5 quy tắc quan trọng:

  1. Bộ nhớ đệm mảng màu tĩnh: GetData được gọi chính xác một lần khi kết cấu tải và được lưu trữ trong Dictionary<Texture2D, Color[]>.
  2. AABB Bảo vệ thoát sớm: Nếu Bounds.Intersects(other.Bounds) là sai, chúng tôi sẽ bảo lãnh ngay lập tức. 99% lượt kiểm tra được loại bỏ trước khi kiểm tra từng pixel.
  3. Hỗ trợ Bản đồ kết cấu & SourceRectangle: Xử lý các họa tiết được đóng gói thành các tập bản đồ kết cấu bằng cách sử dụng các offset hình chữ nhật nguồn.
  4. Cửa sổ chồng chéo được tính toán: Chúng tôi chỉ lặp qua hình chữ nhật giao nhau chính xác giữa cả hai họa tiết (Math.Max(a.Top, b.Top), v.v.).
  5. Ngắn mạch Alpha: Nếu pixel của Sprite A trong suốt (A <= 20), Sprite B hoàn toàn bị bỏ qua.
  6. Kéo thuộc tính: Truy cập các thuộc tính ảo (Giới hạn) trong các vòng lặp lồng nhau sẽ tạo ra hàng nghìn bản sao cấu trúc. Chúng tôi lưu trữ chúng trong các biến ngăn xếp cục bộ trước khi vào vòng lặp.

Đây là mã hoàn chỉnh, sẵn sàng sản xuất:

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

Quy trình này thực hiện trong vòng chưa đầy 0,1 mili giây trên hệ thống máy tính để bàn và phần cứng Android hiện đại.


7. Cấp độ 6: Lưới không gian pha rộng & GC di động phân bổ không

Hãy tưởng tượng trò chơi của bạn có 100 khối, 60 viên đạn và 20 kẻ thù. Nếu bạn kiểm tra mọi thực thể với mọi thực thể khác bằng cách sử dụng các vòng lặp for lồng nhau, thì bạn thực hiện:

\(\frac{180 \times 179}{2} = 16.110 \text{ kiểm tra trên mỗi khung!}\)

Ở tốc độ 60 FPS, tức là gần 1.000.000 lượt kiểm tra va chạm mỗi giây.

Để mở rộng quy mô trò chơi của mình, bạn phải triển khai Phân vùng không gian theo pha rộng.

Lưới băm không gian thống nhất

Chúng tôi chia màn hình thành một lưới 2D gồm các ô đồng nhất (trong Blocked: Pixel Panzer, mỗi ô có giá trị \(150 \times 150\) pixel). Một thực thể chỉ kiểm tra xung đột với các thực thể khác nằm trong cùng một ô lưới.

Vấn đề về bộ nhớ di động: GC bị đập

Nếu SpatialGrid của bạn tạo Danh sách mới<Point>() hoặc Danh sách mới<Block>() mỗi khung hình, thì bạn phân bổ megabyte rác mỗi phút trên vùng nhớ được quản lý.

Trên thời gian chạy Mono của Android, điều này kích hoạt Bộ sưu tập rác Gen-0 thường xuyên, gây ra: thời gian chạy đơn âm: Tranh chấp khóa gốc (mono_class_is_subclass_)

Trò chơi bị giật và Google Play gắn cờ trò chơi của bạn kèm theo cảnh báo ANR!

Giải pháp: Bộ đệm chống xước có thể tái sử dụng

Đây là Lưới băm không gian phân bổ bằng 0 từ CollisionManager.cs trong Đã chặn: 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);
                            }
                        }
                    }
                }
            }
        }
    }
}

Bằng cách sử dụng lại _scratchCells và _scratchCheckedEntities, toàn bộ bước xung đột sẽ chạy với 0 byte phân bổ vùng nhớ heap cho mỗi khung.


##8. Bảng so sánh tóm tắt

Kỹ thuật Chi phí toán học Hỗ trợ luân phiên Đào hầm an toàn? Trường hợp sử dụng tốt nhất trong MonoGame
AABB (Hình chữ nhật.Giao điểm) Cực nhanh (~4 so sánh số nguyên) Không Không Khối lưới, pha rộng, giao diện người dùng
Vòng tròn (Khoảng cách bình phương) Rất nhanh (3 mul, 0 sqrt) Có (Bất biến) Không Tàu tròn, quả cầu lửa, quả cầu năng lượng
Vòng tròn vs Hộp (Kẹp) Nhanh (MathHelper.Clamp cục bộ) Có Không Máy nghe nhạc tròn điều hướng các bức tường chật hẹp
Tia quét (CCD phiến) Trung bình (Raycast tham số) Có Có Đạn bắn tỉa nhanh, súng trường, tia laser
Pixel-Perfect (IntersectsPixel) Chọn lọc (Phụ lục bị hạn chế) Có Không Đường viền sprite không đều, hitbox công bằng
Lưới không gian (Zero-GC) Pha rộng (\(O(N^2) \to O(N)\)) Không áp dụng Không áp dụng Sóng dày đặc, địa ngục đạn, Android 60 FPS

Buổi giới thiệu sản xuất trong thế giới thực: Arar Games

Những kiến trúc va chạm này không phải là những thử nghiệm lý thuyết—chúng là nền tảng kỹ thuật thực sự hỗ trợ các tựa game được phát hành thương mại của chúng tôi:

  • Đã chặn: Pixel Panzer: Trò chơi arcade sinh tồn xe tăng cổ điển của chúng tôi trên Google Play và Microsoft Store. Nó có hệ thống va chạm hai tầng hoàn chỉnh của chúng tôi: lưới băm không gian phân bổ bằng 0 lọc các khối rơi và kiểm tra tiếp xúc hoàn hảo đến từng pixel đối với xe tăng, máy bay chiến đấu, đạn pháo tháp và đạn nguyên tố ColorWheel.
  • Paint Trek: Game bắn súng không gian nhịp độ nhanh của chúng tôi có hệ thống va chạm vòng tròn quay, truyền tia liên tục và hệ thống phòng thủ tên lửa quét theo khối lượng.

Kết luận & Các bước tiếp theo

MonoGame cung cấp cho bạn khả năng thiết kế tính năng phát hiện va chạm hoàn toàn phù hợp với nhu cầu trò chơi của bạn. Bằng cách kiểm tra Pixel-Perfect đắt tiền đằng sau các thử nghiệm AABB nhanh, sử dụng Khoảng cách bình phương cho vòng tròn và loại bỏ việc thu gom rác bằng Bộ đệm trầy xước có thể tái sử dụng, bạn có thể mang lại hiệu suất 60/120 FPS mượt mà như bảng điều khiển trên cả nền tảng máy tính để bàn và thiết bị di động.

Hãy xem các trò chơi của chúng tôi trên các cửa hàng ứng dụng để xem hoạt động của các hệ thống va chạm này và bắt đầu triển khai các mô hình này trong các dự án MonoGame của riêng bạn ngay hôm nay!


Lưu trữ liên kết & tài nguyên


Từ khóa SEO & Hashtags

Từ khóa: Phát hiện va chạm MonoGame 2D, phát triển trò chơi C#, Rectangle.Intersects MonoGame, va chạm pixel hoàn hảo C#, lưới băm không gian MonoGame, va chạm tia quét liên tục, tối ưu hóa hiệu suất trò chơi indie, vòng lặp trò chơi phân bổ bằng 0, tối ưu hóa Android MonoGame, Trò chơi Arar, Pixel Panzer bị chặn, Paint Trek.

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

MonoGame-da 2D to'qnashuvni aniqlash: asosiy to'rtburchaklardan nol taqsimlangan pikselli mukammal tizimlargacha

MonoGame-da 2D to'qnashuvni aniqlash: asosiy to'rtburchaklardan nol taqsimlangan pikselli mukammal tizimlargacha

Unity yoki Godot kabi vizual dvigatellarda 2D oʻyinlarni yaratishda toʻqnashuvni aniqlash koʻpincha inspektorning katakchalari qatoriga oʻxshab ketadi: siz “BoxCollider2D” yoki “CircleCollider2D” ni bosing, “Rigidbody”ni biriktirasiz va mobil qurilmalarda ichki fizika qadami qoqilmasligiga umid qilasiz.

MonoGame va C#-da esa siz to'liq nazorat qilasiz. Hech qanday yashirin fizika yuki, istalmagan aylanish inertsiyasi va sizning ramka byudjetingizni o'g'irlaydigan sirli axlat yig'ish (GC) yo'q.

Arar Games da, Blocked: Pixel Panzer va Paint Trek-ni yaratganimizda, bizning arkada o‘yin halqalarimiz yuzlab yuqori tezlikdagi dushman o‘qlarini, portlovchi g‘isht to‘rlarini, aylanuvchi tank minoralarini, qiruvchi samolyotlarning parvozlarini va zarracha qalqonlarini Windows va Android qurilmalarida 60 dan 120 PS gacha tezlikda qayta ishlash uchun kerak edi. Umumiy maqsadli fizika dvigateli haqida gap bo'lishi mumkin emas edi - bizga maqsadli qurilgan, bosqichli to'qnashuv arxitekturasi kerak edi.

Ushbu keng qamrovli, kodga asoslangan qo'llanmada biz MonoGame to'qnashuvining mutlaq asoslaridan boshlaymiz ("To'rtburchak. Kesishlar" haqiqiy "O'q" va "Dushman" spritlari bilan) va ilg'or aylana tekshiruvlari, aralash qisish, tunnelga qarshi raykaslar, ishlab chiqarish darajasidagi **Pixelsion-Zero-Pixelsion-Zo'lik va Alli-Pixel-Pixel-Pixel-Pixel-Pixel-Pixel-Pixel-Pixel-Pixel-Pixel-Pixel-Pixel-Zo'ga qarshi ilg'or tekshiruvlarni o'tkazamiz. Grids mobil GC omon qolish uchun optimallashtirilgan.


1. Jamg'arma: oddiy MonoGame "Sprite" ierarxiyasi

To'qnashuvlarni aniqlashdan oldin bizga toza o'yin ob'ektlari kerak. MonoGame-da ob'ekt asosan pozitsiyaga, teksturaga va chegaralovchi to'rtburchakka ega.

Mana bizning o'yinlarimizda ishlatiladigan asosiy ob'ekt arxitekturasi:

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

Keling, aniq “Player”, “Dushman” va “Bullet” sinflarini yarataylik:

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

Ushbu ob'ektlar mavjud bo'lganda, keling, eng oddiy usuldan boshlab ular orasidagi to'qnashuvlarni qanday aniqlashni o'rganamiz.


2. 1-daraja: Eng oddiy toʻqnashuv – “Toʻrtburchaklar kesishadi” (AABB)

MonoGame-dagi eng asosiy ikki o'lchovli to'qnashuv tekshiruvi bu Axis-Aligned Bounding Box (AABB) testidir. "O'qga tekislangan" atamasi oddiygina to'rtburchakning qirralari ekranning \(X\) va \(Y\) o'qlariga to'liq parallel ekanligini bildiradi (aylanish yo'q).

MonoGame tezkor, o'rnatilgan usulni taqdim etadi: To'rtburchak. Kesishadi (To'rtburchak qiymati).

To'rtburchak. Kesishadi kaput ostida qanday ishlaydi

Sirt ostida MonoGame to'rtta butun son taqqoslashni amalga oshiradi:

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

Agar to'rtta shart bajarilsa, to'rtburchaklar bir-biriga yopishadi. Agar bitta shart bajarilmasa, bo'sh o'q ularni ajratib turadi va hech qanday to'qnashuv mumkin emas.

Haqiqiy oʻyin kodi: “Game1.Update”dagi “Bullet” va “Dushman” oʻrtasidagi

Asosiy MonoGame Update tsiklingizdagi faol o'qlar ro'yxati va faol dushmanlar ro'yxati o'rtasidagi to'qnashuvni qanday tekshirishingiz mumkin:

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

Ishlash bo'yicha maslahat: E'tibor bering, biz orqaga qarab takrorlaymiz (for (int i = list.Count - 1; i >= 0; i--))! Agar siz "foreach" dan foydalansangiz va "_bullets.Remove(bullet)" ga qo'ng'iroq qilmoqchi bo'lsangiz, C# "InvalidOperationException: Collection o'zgartirildi" ni chiqaradi. Orqaga takrorlash xotirani qayta indekslash muammosisiz elementni xavfsiz olib tashlash imkonini beradi.

Arkada siri: "Inflate" orqali "Fair Hitboxes"

Blocked: Pixel Panzer kabi retro o‘yinlarda sprite teksturalari ko‘pincha shaffof chegaralar yoki antenna tirqishlarini o‘z ichiga oladi. Agar o'yinchining tanki o'q uning teksturasining bo'sh shaffof burchagiga tegishi sababli portlasa, o'yinchi o'zini aldangandek his qiladi.

Toʻqnashuvni sezgir va adolatli qilish uchun oʻyinlar “Rectangle.Inflate” yordamida sprite ichidagi kichikroq Hitbox dan foydalanadi:

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-daraja: Aylanadan aylanaga to‘qnashuv (aylanish immuniteti)

To'rtburchaklar statik bloklar va panjara plitalari uchun juda yaxshi ishlaydi, lekin spritlar aylanganda ular muvaffaqiyatsizlikka uchraydi. Kvadrat bo'lmagan kosmik kema Paint Trek da aylansa, aylanuvchi burchaklarni o'rab olish uchun o'q bo'ylab tekislangan chegara qutisi kengayishi kerak, bu esa bo'sh havoda "fantom to'qnashuvlari" ga olib keladi.

Dumaloq asteroidlar, maqsadli energiya sharlari va aylanuvchi kosmik kemalar uchun Bounding Circles ideal yechimdir.

Kvadrat ildiz tuzog'i

Ikki doira markazlari orasidagi masofa radiuslari yig‘indisidan kichik yoki teng bo‘lganda to‘qnashadi:

\(\text{Distance}(C_A, C_B) \le r_A + r_B\)

Kodda Evklid masofasini hisoblashda "Math.Sqrt" (yoki "Vektor2.Distance") ishlatiladi. Biroq, kvadrat ildizlarni 200 ta snaryadli halqada hisoblash yuzlab keraksiz CPU tsikllarini yoqib yuboradi!

kvadrat masofani kvadrat radius yig‘indisi bilan solishtirib, biz kvadrat ildizni butunlay yo‘q qilamiz:

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

MonoGame Amalga oshirish: "Circle" va "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);
    }
}

Endi buni to'g'ridan-to'g'ri ob'ektga integratsiya qiling:

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

Nol kvadrat ildizlar, sprite aylanishiga qarshi immunitet va chaqmoq tezligida.


4. 3-daraja: Aralash shakllar – doira va quti (MathHelper.Clamp)

Paint Trek da dumaloq kosmik kema to‘rtburchaklar shaklidagi mudofaa to‘siqlaridan iborat qattiq labirintdan o‘tib ketsa yoki Bloklangan: Pixel Panzer da yumaloq o‘q kvadrat blokga tegsa nima bo‘ladi?

Bizga Circle vs. Rectangle toʻqnashuvi kerak.

Siqish algoritmi

Strategiya to'rtburchakda aylananing markaziga eng yaqin nuqtani topib, so'ngra eng yaqin nuqtadan markazgacha bo'lgan masofa doira radiusidan kichikroq yoki yo'qligini tekshirishdan iborat.

MonoGame 'MathHelper.Clamp' buni ahamiyatsiz qiladi:

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

Endi siz o'yinchi qalqonlarini to'rtburchaklar g'ishtlarga qarshi nol taqsimot va yuqori aniqlik bilan sinab ko'rishingiz mumkin!


5. 4-daraja: Uzluksiz toʻqnashuvni aniqlash (CCD) va oʻchirilgan nurlar

Siz o'z o'yiningizda o'qning sehrli tarzda yupqa dushman kemasidan to'g'ridan-to'g'ri o'tib ketishini tomosha qilish uchun gipertezlikdagi snayper yoki relsli to'pponchani o'qqa tutganmisiz?

Bu xato tunnellash nomi bilan tanilgan.

Diskret o'yinlar vaqt bosqichlarida yangilanganligi sababli (60 FPSda \(\Delta t = 16,6\text{ms}\)), soniyada 1800 piksel tezlikda harakatlanuvchi ob'ekt bir kadrda 30 piksel sayohat qiladi. Agar dushman korpusining qalinligi atigi 15 piksel bo'lsa, o'q 1-ramkada dushman oldida, 2-ramkada esa butunlay dushman orqasida edi.

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

Yechim: Supurilgan segment va quti (plitalar usuli)

Bitta nuqtani sinab ko'rish o'rniga, biz o'qning 1-kadrdagi o'rnini (oldingiPosition) 2-framega (joriyPosition) bog'laydigan butun chiziq segmentini sinab ko'ramiz.

SpiralWar nomli hamrohimizdagi ishlab chiqarish raycasting plita kesish usuli:

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 o‘yinida o‘yinchining uzluksiz Laser Beam mahorati bitta to‘qnashuvni o‘tkazib yubormasdan pastga tushuvchi bloklar qatorini kesib o‘tish uchun aynan shu raycastdan foydalanadi.


6. 5-daraja: ishlab chiqarish darajasidagi piksel-mukammal to‘qnashuvni aniqlash

Endi biz 2D aniqlikning eng yuqori darajasiga erishdik: Pixel-Perfect Collision.

Retro tank otuvchi yoki kosmik kema it jangida tartibsiz shakllar (tank bochkalari, qanotlar, kokpit kabinalari) sprite teksturasida shaffof piksellar bilan o'ralgan. Dushman raketasi shaffof bo'shliqqa tushganda, o'yinchilar darhol buni sezadilar.

Piksel-mukammal to'qnashuv bir-biriga yopishgan teksturalarning haqiqiy alfa (shaffoflik) kanallarini tekshiradi. Agar ikkita shaffof bo'lmagan piksel bir xil dunyo koordinatasida bir-biriga to'g'ri kelsa, haqiqiy jismoniy zarba sodir bo'ldi.

Fatal xato: Update() ichidagi GetData

Ko'pgina darsliklar yangi boshlanuvchilarga buni qilishni buyuradi:

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

Ishlash vaqtida "Texture2D.GetData()" ga qo'ng'iroq qilish protsessorni GPU quvur liniyasining tozalanishini kutishga majbur qiladi va tekstura xotirasini avtobus orqali orqaga tortadi. Buni har bir kadrda oʻnlab spritlar uchun qilish oʻyiningizni 60 FPS dan 5 FPS gacha oshiradi va Android ANR’larini darhol ishga tushiradi!

Bloklangan: Pixel Panzer dan jangovar sinovdan o'tgan yechim

Bloklangan: Pixel Panzer ilovasida ‘Sprite.cs’ 5 ta muhim qoida atrofida tuzilgan ultra optimallashtirilgan “IntersectsPixel” dasturini amalga oshiradi:

  1. Statik rang massivi keshi: GetData tekstura yuklanganda aynan bir marta chaqiriladi va Dictionary<Texture2D, Color[]>da saqlanadi.
  2. AABB Early Exit Guard: Agar Bounds.Intersects(other.Bounds) noto'g'ri bo'lsa, biz darhol kafolat beramiz. Bitta pikselni tekshirishdan oldin tekshiruvlarning 99% yo'q qilinadi.
  3. Texture Atlas va SourceRectangle qoʻllab-quvvatlashi: Toʻgʻri toʻrtburchaklar ofset manbalaridan foydalangan holda tekstura atlaslariga oʻralgan spritlarni boshqaradi.
  4. Hisoblangan ustma-ust oyna: Biz faqat ikkala spritlar (Math.Max(a.Top, b.Top) va h.k.) oʻrtasidagi aniq kesishgan toʻrtburchak ustidan aylanamiz.
  5. Alfa qisqa tutashuvi: Agar Sprite A pikseli shaffof bo'lsa (A <= 20), Sprite B butunlay o'tkazib yuboriladi.
  6. Mulkni ko'tarish: Ichki ko'chadan virtual xususiyatlarga (Chegaralar) kirish minglab struktura nusxalarini yaratadi. Loopga kirishdan oldin ularni mahalliy stek o'zgaruvchilarida saqlaymiz.

Mana to'liq, ishlab chiqarishga tayyor kod:

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

Bu tartib zamonaviy Android apparat va ish stoli tizimlarida 0,1 millisekunddan kamroq vaqt ichida amalga oshiriladi.


7. 6-daraja: Keng fazali fazoviy tarmoq va nol taqsimlangan mobil GC

Tasavvur qiling-a, sizning o'yiningizda 100 ta blok, 60 ta o'q va 20 ta dushman bor. Agar siz har bir ob'ektni har bir boshqa ob'ektga qarshi o'rnatilgan "for" tsikllari yordamida sinab ko'rsangiz, quyidagilarni bajarasiz:

\(\frac{180 \times 179}{2} = 16,110 \text{ har bir kadr uchun tekshiruv!}\)

60 FPS da, bu har soniyada deyarli 1 000 000 to'qnashuv tekshiruvi.

O'yinni kengaytirish uchun siz Keng fazali fazoviy qismlarga ajratish-ni amalga oshirishingiz kerak.

Yagona fazoviy xesh panjara

Biz ekranni bir xil hujayralardan iborat 2D panjaraga ajratamiz (Bloklangan: Pixel Panzer ichida, har bir hujayra 150$\150$ pikselga teng). Tashkilot faqat bir xil katakchalarda joylashgan boshqa ob'ektlarga qarshi to'qnashuvlarni sinovdan o'tkazadi.

Mobil xotira muammosi: GC Thrashing

Agar SpatialGrid har bir freymda yangi Royxat()yokiyangi Royxat<Blok>() yaratsa, siz boshqariladigan to`plamda har daqiqada megabayt axlatni ajratasiz.

Android-ning Mono ish vaqtida, bu tez-tez Gen-0 axlat yig'ishlarini ishga tushiradi, bu esa quyidagilarga sabab bo'ladi: mono ish vaqti: mahalliy blokirovka bahsi (mono_class_is_subclass_)

O'yin qoqilib ketadi va Google Play o'yiningizni ANR ogohlantirishlari bilan belgilaydi!

Yechim: Qayta foydalanish mumkin bo'lgan skretch buferlari

Mana Bloklangan: Pixel Panzer ichidagi CollisionManager.cs dan nol taqsimlangan fazoviy xesh panjara:

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" va "_scratchCheckedEntities" dan qayta foydalanish orqali to'qnashuvning to'liq bosqichi har bir kadr uchun 0 bayt yig'ish taqsimoti bilan ishlaydi.


8. Xulosa solishtirish jadvali

Texnika Matematik narx Rotatsion yordam Tunnel xavfsizmi? MonoGame-da eng yaxshi foydalanish holati
AABB (Tortburchak. Kesishadi`) Ultra-tezkor (~4 ta butun son taqqoslash) Yo'q Yo'q Grid bloklari, o'q keng fazali, UI
Doira (DistanceSquared) Juda tez (3 muls, 0 sqrt) Ha (Invariant) Yo'q Dumaloq kemalar, olov sharlari, energiya sharlari
Circle vs Box (Clamp) Tez (Mahalliy MathHelper.Clamp) Ha Yo'q Qattiq devorlarda aylanma o'yinchi
Soflangan nur (Slab CCD) O'rtacha (Parametrik Raycast) Ha Ha Tezkor snayper o'qlari, relsli qurollar, lazerlar
Pixel-Perfect (IntersectsPixel) Tanlangan (cheklangan sub-rect) Ha Yo'q Noqonuniy sprite konturlari, adolatli xitbokslar
Fazoviy tarmoq (nol-GC) Keng fazali (\(O(N^2) \to O(N)\)) Yo'q Yo'q Zich to'lqinlar, o'q jahannamlari, Android 60 FPS

Haqiqiy dunyo ishlab chiqarish ko'rgazmalari: Arar o'yinlari

Ushbu to'qnashuv arxitekturalari nazariy eksperimentlar emas - ular bizning tijorat maqsadlarida chiqarilgan nomlarimizni quvvatlaydigan haqiqiy muhandislik asosidir:

  • Bloklangan: Pixel Panzer: Google Play va Microsoft Store do‘konidagi retro tank omon qolish arkada o‘yinimiz. U bizning to'liq ikki bosqichli to'qnashuv tizimimizni o'z ichiga oladi: tushib qolgan bloklarni filtrlaydigan nol taqsimlangan fazoviy xesh panjara va tanklar, qiruvchi samolyotlar, minora snaryadlari va ColorWheel elementar o'q-dorilari uchun piksel-mukammal kontaktni tekshirish.
  • Paint Trek: Aylanma aylana to‘qnashuvi, uzluksiz raykasting va keng ko‘lamli raketaga qarshi mudofaa tizimlarini o‘z ichiga olgan tez sur’atli kosmik otishmamiz.

Xulosa va keyingi qadamlar

MonoGame sizga o'yiningiz ehtiyojlariga to'liq mos keladigan to'qnashuvni aniqlashni loyihalash quvvatini beradi. Qimmatbaho Pixel-Perfect tekshiruvlarini tezkor AABB sinovlaridan o‘tkazish, doiralar uchun Kvadrat masofalar-dan foydalanish va Qayta foydalanish mumkin bo‘lgan skretch buferlari bilan axlat yig‘ishni yo‘q qilish orqali siz ish stoli va mobil platformalarda konsolning 60/120 FPS ishlashini ta’minlay olasiz.

Ushbu to'qnashuv tizimlarini ishlayotganini ko'rish uchun ilovalar do'konlaridagi o'yinlarimizni ko'rib chiqing va bugun o'zingizning MonoGame loyihalaringizda ushbu naqshlarni qo'llashni boshlang!


Do'kon havolalari va manbalari


SEO kalit so'zlari va hashtaglari

Kalit so‘zlar: MonoGame 2D to‘qnashuvni aniqlash, C# o‘yinini ishlab chiqish, Rectangle.Intersects MonoGame, piksel mukammal to‘qnashuvi C#, fazoviy xesh-grid MonoGame, to‘xtovsiz uzatilgan to‘qnashuv, indie o‘yin samaradorligini optimallashtirish, nol taqsimlangan o‘yin tsikli, Android MonoG’ni optimallashtirish, o‘yinni optimallashtirish. Paint Trek.

#MonoGame #CSharp #GameDev #IndieDev #GamePhysics #DotNet #2DGameDev #MobileGameDev #PerformanceOptimization ``#Clean#GameDev #PaintTrek #ArarGames #GameProgramming`

مونو گیم میں 2D تصادم کا پتہ لگانا: بنیادی مستطیل سے زیرو الاوکیشن پکسل پرفیکٹ سسٹم تک

مونو گیم میں # 2D تصادم کا پتہ لگانا: بنیادی مستطیل سے زیرو الاوکیشن پکسل پرفیکٹ سسٹم تک

Unity یا Godot جیسے بصری انجنوں میں 2D گیمز بناتے وقت، تصادم کا پتہ لگانا اکثر انسپکٹر چیک باکسز کی ایک سیریز کی طرح محسوس ہوتا ہے: آپ BoxCollider2D یا CircleCollider2D پر تھپڑ مارتے ہیں، ایک Rigidbody جوڑتے ہیں، اور امید کرتے ہیں کہ اندرونی طبیعیات کا مرحلہ موبائل آلات پر نہیں ہچکچاتا۔

MonoGame اور C# میں، تاہم، آپ مکمل کنٹرول میں ہیں۔ کوئی پوشیدہ طبیعیات اوور ہیڈ، کوئی ناپسندیدہ گردشی جڑتا نہیں ہے، اور کوئی پراسرار کچرا جمع کرنے (GC) اسپائکس آپ کے فریم بجٹ کو چوری نہیں کرتا ہے۔

Arar گیمز میں، جب ہم نے Blocked: Pixel Panzer اور Paint Trek بنایا، تو ہمارے آرکیڈ گیم لوپس کو سیکڑوں تیز رفتار دشمن کی گولیوں، اینٹوں کے پھٹنے والے گرڈز، گھومنے والے ٹینک برج، فائٹر جیٹ فلائی بائیز، اور پارٹیکل شیلڈز کی ضرورت ہوتی ہے۔ ایک عمومی مقصدی فزکس انجن سوال سے باہر تھا — ہمیں ایک مقصد سے بنایا ہوا، ٹائرڈ ٹکراؤ فن تعمیر کی ضرورت تھی۔

اس جامع، کوڈ سے چلنے والی گائیڈ میں، ہم MonoGame کے تصادم کی مکمل بنیادی باتوں سے شروع کریں گے (Rectangle. Intersects with realBullet اور Enemy sprites) اور اعلی درجے کے دائرے کی جانچ، مخلوط کلیمپنگ، اینٹی ٹنلنگ رے کاسٹ، پروڈکشن گریڈ P-lixel** اور P-Lixer** زیرو ایلوکیشن اسپیشل گرڈز موبائل جی سی کی بقا کے لیے موزوں ہیں۔


1. فاؤنڈیشن: ایک سادہ مونو گیم سپرائٹ درجہ بندی

تصادم کا پتہ لگانے سے پہلے، ہمیں کلین گیم اداروں کی ضرورت ہے۔ 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);
    }
}

اب آئیے کنکریٹ 'پلیئر'، 'دشمن' اور 'بلٹ' کلاسز بنائیں:

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: سادہ ترین تصادم - `مستطیل۔

MonoGame میں سب سے بنیادی 2D تصادم کی جانچ Axis-Aligned Bounding Box (AABB) ٹیسٹ ہے۔ اصطلاح "محور سے منسلک" کا سیدھا مطلب ہے کہ مستطیل کے کنارے مکمل طور پر اسکرین کے \(X\) اور \(Y\) محور کے متوازی ہیں (کوئی گردش نہیں)۔

MonoGame ایک تیز، بلٹ ان طریقہ فراہم کرتا ہے: Rectangle.Intersects(Rectangle value)۔

کس طرح ہڈ کے نیچے 'مستطیل' انٹرسیکٹس کام کرتا ہے۔

سطح کے نیچے، MonoGame چار عددی موازنہ انجام دیتا ہے:

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

اگر چاروں شرائط پوری ہو جائیں تو مستطیل اوورلیپ ہو جاتا ہے۔ اگر ایک شرط بھی ناکام ہو جاتی ہے تو، ایک خالی محور انہیں الگ کر دیتا ہے، اور کوئی تصادم ممکن نہیں ہے۔

اصلی گیم پلے کوڈ: گیم1. اپ ڈیٹ میں گولی بمقابلہ دشمن

یہ ہے کہ آپ فعال گولیوں کی فہرست اور اپنے مرکزی 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);
    }
}

کارکردگی کا مشورہ: نوٹس کریں کہ ہم پیچھے کی طرف دہراتے ہیں ( (int i = list.Count - 1; i >= 0; i--))! اگر آپ foreach استعمال کرتے ہیں اور _bullets.Remove(bullet) کو کال کرنے کی کوشش کرتے ہیں تو C# ایک InvalidOperationException: Collection was modified پھینک دیتا ہے۔ پیچھے کی طرف اعادہ کرنا میموری کو دوبارہ اشاریہ سازی کے مسائل کے بغیر محفوظ عنصر کو ہٹانے کی اجازت دیتا ہے۔

آرکیڈ سیکریٹ: 'فیئر ہٹ باکسز' بذریعہ 'انفلیٹ'

ریٹرو گیمز جیسے Blocked: Pixel Panzer میں، اسپرائٹ ٹیکسچرز میں اکثر شفاف مارجن یا اینٹینا اسپائکس شامل ہوتے ہیں۔ اگر کھلاڑی کا ٹینک پھٹ جاتا ہے کیونکہ گولی اس کی ساخت کے خالی شفاف کونے کو چھوتی ہے، تو کھلاڑی خود کو دھوکہ دہی کا احساس کرے گا۔

تصادم کو ذمہ دار اور منصفانہ محسوس کرنے کے لیے، گیمز 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: دائرے سے دائرے کا تصادم (گھمنے والی قوت مدافعت)

مستطیل جامد بلاکس اور گرڈ ٹائلوں کے لیے بہت اچھا کام کرتا ہے، لیکن جب اسپرائٹس گھومتے ہیں تو وہ ناکام ہوجاتے ہیں۔ جب ایک غیر مربع جہاز پینٹ ٹریک میں گھومتا ہے، ایک محور سے منسلک باؤنڈنگ باکس کو گھومنے والے کونوں کو گھیرنے کے لیے پھیلانا چاہیے، جس کے نتیجے میں خالی ہوا میں مایوس کن "پریتی تصادم" ہوتا ہے۔

سرکلر ایسٹرائڈز، توانائی کے مداروں کو گھومنے اور گھومنے والے خلائی جہاز کے لیے، باؤنڈنگ سرکلز مثالی حل ہیں۔

اسکوائر روٹ ٹریپ

دو دائرے اس وقت آپس میں ٹکراتے ہیں جب ان کے مراکز کے درمیان فاصلہ ان کے ریڈیائی کے مجموعے سے کم یا برابر ہو:

\(\text{Distance}(C_A, C_B) \le r_A + r_B\)

کوڈ میں، Euclidean فاصلے کا حساب لگانے میں Math.Sqrt (یا Vector2.Distance) استعمال ہوتا ہے۔ تاہم، 200 پروجیکٹائل کے ساتھ ایک لوپ میں مربع جڑوں کا حساب لگانا سینکڑوں غیر ضروری CPU سائیکلوں کو جلا دیتا ہے!

مربع فاصلہ کا مربع رداس جمع سے موازنہ کرکے، ہم مربع جڑ کو مکمل طور پر ختم کرتے ہیں:

\(\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)

کیا ہوتا ہے جب پینٹ ٹریک میں ایک سرکلر اسپیس شپ مستطیل دفاعی رکاوٹوں کی ایک تنگ بھولبلییا سے گزرتا ہے، یا جب گول گولی بلاکڈ: پکسل پینزر میں مربع بلاک سے ٹکراتی ہے؟

ہمیں سرکل بمقابلہ مستطیل ٹکراؤ کی ضرورت ہے۔

کلیمپنگ الگورتھم

حکمت عملی یہ ہے کہ مستطیل پر اس نقطہ کو تلاش کریں جو دائرے کے مرکز کے قریب ترین ہے، اور پھر جانچیں کہ آیا اس قریب ترین نقطہ سے مرکز کا فاصلہ دائرے کے رداس سے کم ہے۔

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 پر)، ایک شے 1,800 پکسلز فی سیکنڈ کی رفتار سے حرکت کرتی ہے **ایک فریم میں 30 پکسلز۔ اگر دشمن کا ہل صرف 15 پکسل موٹا ہے، تو گولی فریم 1 پر دشمن کے سامنے تھی، اور فریم 2 پر دشمن کے بالکل پیچھے تھی۔

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

حل: سویپٹ سیگمنٹ بمقابلہ باکس (سلیب کا طریقہ)

ایک پوائنٹ کو جانچنے کے بجائے، ہم فریم 1 (سابقہ پوزیشن) پر بلٹ کی پوزیشن کو فریم 2 (موجودہ پوزیشن) سے جوڑنے والے پورے لائن سیگمنٹ کی جانچ کرتے ہیں۔

ہمارے ساتھی ٹائٹل 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 میں، کھلاڑی کی مسلسل Laser Beam Skill اس عین مطابق raycast کو ایک بھی تصادم کھوئے بغیر اترتے ہوئے بلاکس کی قطاروں کو کاٹنے کے لیے استعمال کرتی ہے۔


6. لیول 5: پروڈکشن-گریڈ پکسل-پرفیکٹ تصادم کا پتہ لگانا

اب ہم 2D درستگی کی حتمی سطح پر پہنچ گئے ہیں: Pixel-Perfect Collision۔

ریٹرو ٹینک شوٹر یا اسپیس شپ ڈاگ فائٹ میں، فاسد شکلیں (ٹینک بیرل، ونگز، کاک پٹ کاک پٹ) سپرائٹ ٹیکسچر میں شفاف پکسلز سے گھری ہوتی ہیں۔ جب دشمن کا میزائل اس شفاف جگہ سے ٹکراتا ہے تو کھلاڑی فوراً نوٹس لیتے ہیں۔

پکسل پرفیکٹ تصادم اوور لیپنگ ٹیکسچرز کے اصل الفا (شفافیت) چینلز کا معائنہ کرتا ہے۔ اگر دو غیر شفاف پکسلز ایک ہی عالمی کوآرڈینیٹ پر اوورلیپ ہوتے ہیں تو ایک حقیقی جسمانی ہٹ واقع ہوئی ہے۔

مہلک غلطی: Update() کے اندر GetData

بہت سے سبق ابتدائیوں کو ایسا کرنے کی ہدایت کرتے ہیں:

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

رن ٹائم کے دوران Texture2D.GetData() کال کرنا CPU کو GPU پائپ لائن کے فلش ہونے کا انتظار کرنے پر مجبور کرتا ہے، ٹیکسچر میموری کو بس پر واپس کھینچتا ہے۔ درجنوں اسپرائٹس کے لیے ایسا کرنے سے ہر فریم آپ کے گیم کو 60 FPS سے 5 FPS تک لے جائے گا اور فوری طور پر Android ANRs کو متحرک کر دے گا!

بلاکڈ: 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 ملی سیکنڈ سے کم وقت میں مکمل ہوتی ہے۔


7. لیول 6: براڈ فیز اسپیشل گرڈ اور زیرو ایلوکیشن موبائل جی سی

تصور کریں کہ آپ کے گیم میں 100 بلاکس، 60 گولیاں اور 20 دشمن ہیں۔ اگر آپ nested for loops کا استعمال کرتے ہوئے ہر ہستی کو ہر دوسری ہستی کے خلاف جانچتے ہیں، تو آپ انجام دیتے ہیں:

\(\frac{180 \times 179}{2} = 16,110 \text{ چیک فی فریم!}\)

60 FPS پر، یہ تقریباً 1,000,000 تصادم کی جانچ پڑتال ہر سیکنڈ ہے۔

اپنے گیم کی پیمائش کرنے کے لیے، آپ کو براڈ فیز اسپیشل پارٹیشننگ کو لاگو کرنا ہوگا۔

یونیفارم اسپیشل ہیش گرڈ

ہم اسکرین کو یکساں سیلز کے 2D گرڈ میں تقسیم کرتے ہیں (Blocked: Pixel Panzer میں، ہر سیل \(150 \times 150\) پکسلز ہے)۔ ایک ہستی صرف دوسرے اداروں کے خلاف ٹکراؤ کی جانچ کرتی ہے جو ایک ہی گرڈ سیل میں رہتی ہیں۔

موبائل میموری کا مسئلہ: جی سی تھریشنگ

اگر آپ کا SpatialGrid ہر فریم میں نئی فہرست<پوائنٹ>() یا نئی فہرست<Block>() بناتا ہے، تو آپ منظم ڈھیر پر ہر منٹ میگا بائٹس کوڑا کرکٹ مختص کرتے ہیں۔

اینڈرائیڈ کے مونو رن ٹائم پر، یہ بار بار Gen-0 کوڑے کے مجموعے کو متحرک کرتا ہے، جس کی وجہ سے: `مونو رن ٹائم: مقامی لاک تنازعہ (مونو_کلاس_اس_سب کلاس_)'

گیم سٹٹرز اور گوگل پلے ANR وارننگز کے ساتھ آپ کے گیم کو جھنڈا لگاتے ہیں!

حل: دوبارہ قابل استعمال سکریچ بفرز

یہ ہے Blocked: Pixel Panzer میں CollisionManager.cs سے زیرو ایلوکیشن اسپیشل ہیش گرڈ:

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 عددی موازنہ) نہیں نہیں گرڈ بلاکس، بلٹ براڈ فیز، UI
دائرہ (DistanceSquared) بہت تیز (3 mls, 0 sqrt) ہاں (غیر متزلزل) نہیں گول جہاز، آگ کے گولے، توانائی کے مدار
سرکل بمقابلہ باکس (کلیمپ) تیز (مقامی میتھ ہیلپر. کلیمپ) جی ہاں نہیں سرکلر پلیئر تنگ دیواروں پر تشریف لے جاتا ہے
** سویپٹ رے (سلیب سی سی ڈی)** اعتدال پسند (پیرامیٹرک رے کاسٹ) جی ہاں جی ہاں تیز رفتار سنائپر گولیاں، ریل گن، لیزر
Pixel-Perfect (IntersectsPixel) سلیکٹیو (محدود ذیلی رییکٹ) جی ہاں نہیں فاسد سپرائٹ شکلیں، منصفانہ ہٹ باکسز
مقامی گرڈ (زیرو-GC) براڈ فیز (\(O(N^2) \to O(N)\)) N/A N/A گھنی لہریں، بلٹ ہیلز، Android 60 FPS

حقیقی دنیا کی پروڈکشن شوکیسز: ارار گیمز

یہ تصادم کے فن تعمیرات نظریاتی تجربات نہیں ہیں - یہ ہمارے تجارتی طور پر جاری کردہ عنوانات کو طاقت دینے والی حقیقی انجینئرنگ بنیاد ہیں:

  • مسدود: Pixel Panzer: Google Play اور Microsoft Store پر ہمارا ریٹرو ٹینک بقا آرکیڈ گیم۔ اس میں ہمارے مکمل دو درجے کے تصادم کے نظام کی خصوصیات ہیں: ایک صفر مختص مقامی ہیش گرڈ فلٹرنگ گرنے والے بلاکس، اور ٹینکوں، لڑاکا طیاروں، برج کے گولے، اور کلر وہیل عنصری گولہ بارود کے لیے پکسل پرفیکٹ رابطہ چیکنگ۔
  • پینٹ ٹریک: ہمارا تیز رفتار خلائی شوٹر جس میں گردشی دائرے کے تصادم، مسلسل رے کاسٹنگ، اور سویپٹ والیوم میزائل دفاعی نظام شامل ہیں۔

نتیجہ اور اگلے اقدامات

MonoGame آپ کو تصادم کا پتہ لگانے کا ڈیزائن بنانے کی طاقت دیتا ہے جو آپ کے گیم کی ضروریات سے بالکل میل کھاتا ہے۔ تیز رفتار AABB ٹیسٹوں کے پیچھے مہنگے Pixel-Perfect چیک کر کے، حلقوں کے لیے مربع فاصلوں کا استعمال کرتے ہوئے، اور دوبارہ قابل استعمال سکریچ بفر کے ساتھ کوڑا کرکٹ کو ختم کرکے، آپ ڈیسک ٹاپ اور موبائل دونوں پلیٹ فارمز پر کنسول کے لیے ہموار 60/120 FPS کارکردگی فراہم کر سکتے ہیں۔

تصادم کے ان نظاموں کو عملی شکل میں دیکھنے کے لیے ایپ اسٹورز پر ہمارے گیمز دیکھیں، اور آج ہی اپنے مونو گیم پروجیکٹس میں ان نمونوں کو نافذ کرنا شروع کریں!


اسٹور لنکس اور وسائل


SEO کلیدی الفاظ اور ہیش ٹیگز

مطلوبہ الفاظ: MonoGame 2D تصادم کا پتہ لگانے، C# گیم ڈیولپمنٹ، Rectangle.Intersects MonoGame، pixel perfect collision C#، مقامی ہیش گرڈ MonoGame، سویپٹ رے مسلسل تصادم، انڈی گیم پرفارمنس آپٹیمائزیشن، زیرو ایلوکیشن گیم، اینڈرائیڈ گیم اپلاک، اینڈرائیڈ گیمز اپلاک، بی آر اوز پینزر، پینٹ ٹریک۔

#MonoGame #CSharp #GameDev #IndieDev #GamePhysics #DotNet #2DGameDev #MobileGameDev #PerformanceOptimization``#CleanCode #BlockedPixelPanzers #Ars #گیم پروگرامنگ