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 #برنامه نویسی بازی




Hiç yorum yok:

Yorum Gönder