27 Eylül 2026 Pazar

2D ግጭትን በMonoGame ውስጥ መለየት፡ ከመሰረታዊ አራት ማዕዘናት ወደ ዜሮ ድልድል ፒክስል-ፍጹም ስርዓቶች




2D ግጭትን በMonoGame ውስጥ መለየት፡ ከመሰረታዊ አራት ማዕዘናት ወደ ዜሮ ድልድል ፒክስል-ፍጹም ስርዓቶች

እንደ ዩኒቲ ወይም ጎዶት ባሉ ምስላዊ ሞተሮች ውስጥ 2D ጨዋታዎችን ሲገነቡ የግጭት ፈልጎ ማግኘት ብዙ ጊዜ እንደ ተከታታይ የተቆጣጣሪ አመልካች ሳጥኖች ይሰማዎታል፡ በ BoxCollider2D ወይምCircleCollider2D ላይ በጥፊ ይመታሉ፣ Rigidbody ያያይዙ እና የውስጣዊው የፊዚክስ እርምጃ በሞባይል መሳሪያዎች ላይ እንደማይንተባተብ ተስፋ እናደርጋለን።

በ*MonoGame** እና C# ውስጥ ግን ሙሉ በሙሉ ተቆጣጥረሃል። ምንም የተደበቀ የፊዚክስ በላይ የለም፣ ምንም ያልተፈለገ ተዘዋዋሪ inertia የለም፣ እና ምንም ሚስጥራዊ የቆሻሻ አሰባሰብ (ጂሲ) የፍሬም በጀት የሚሰርቁ።

በ አራር ጨዋታዎች፣ ** የታገዱ: Pixel Panzer *** እና ** Paint Trek** ስንገነባ በመቶዎች የሚቆጠሩ ባለከፍተኛ ፍጥነት የጠላት ጥይቶችን፣ የሚፈነዳ የጡብ ፍርግርግን፣ የሚሽከረከሩ ታንኮችን፣ ተዋጊ ጄት ዝንብዎችን፣ እና ቅንጣት ጋሻዎችን ከ60 እስከ 120 FPS በሁለቱም የዊንዶውስ ፒሲ እና አንድሮይድ መሳሪያዎች ለማስኬድ ያስፈልጉ ነበር። አጠቃላይ ዓላማ ያለው የፊዚክስ ሞተር ከጥያቄ ውጭ ነበር - ዓላማ-የተገነባ፣ ደረጃ ያለው የግጭት አርክቴክቸር ያስፈልገናል።

በዚህ ሁሉን አቀፍ፣ በኮድ-ተኮር መመሪያ ውስጥ፣ ከMonoGame ግጭት ፍፁም መሰረታዊ ነገሮች እንጀምራለን ('አራት ማእዘን። ከእውነተኛ 'ጥይት' እና 'ጠላት'' sprites ጋር) እና የላቀ የክበብ ፍተሻዎችን እንገነባለን፣ የተቀላቀለ ክላምፕስ፣ ፀረ-መቃኛ ጨረሮች፣ የምርት-ደረጃ ** ፒክስል-ፍፃሜ፣ ግሪኮል* ለሞባይል 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);
    }
}

አሁን የኮንክሪት 'ተጫዋች'፣ 'ጠላት' እና 'ቡሌት' ክፍሎችን እንፍጠር፡

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 ውስጥ በጣም መሠረታዊው የ2ዲ ግጭት ፍተሻ በአክሲስ-የሰለጠነ ማሰሪያ ሳጥን (AABB) ሙከራ ነው። "ዘንግ-የተሰለፈ" የሚለው ቃል በቀላሉ የአራት ማዕዘኑ ጠርዞች ከማያ ገጹ _MAT_BLOCK_0 እና _\(Y\) መጥረቢያዎች ጋር ሙሉ በሙሉ ትይዩ ናቸው ማለት ነው።

MonoGame ፈጣን እና አብሮገነብ ዘዴን ይሰጣል፡ አራት ማዕዘን። ያገናኛል(አራት ማዕዘን እሴት)።

በመከለያው ስር `አራት ማእዘን እንዴት እንደሚሰራ

በገጹ ስር፣ MonoGame አራት የኢንቲጀር ንፅፅሮችን ይሰራል፡-

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

አራቱም ሁኔታዎች ከተሟሉ አራት ማዕዘኖቹ ይደራረባሉ። አንድ ሁኔታ እንኳን ካልተሳካ, ባዶ ዘንግ ይለያቸዋል, እና ምንም ግጭት ሊኖር አይችልም.

እውነተኛ የጨዋታ ኮድ፡ Bullet vs. ጠላት በጨዋታ1.አዘምን ውስጥ

በዋናው ሞኖጨዋታ አዘምን ምልልስ ውስጥ ባሉ ንቁ ጥይቶች ዝርዝር እና በነቁ ጠላቶች ዝርዝር መካከል ግጭቶችን እንዴት እንደሚፈትሹ እነሆ፡-

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ን ከተጠቀምክ እና _bullet.Remove(bullet) ለመደወል ከሞከርክ C # InvalidOperationException፡ስብስብ ተስተካክሏል ይጥላል። ወደ ኋላ መደጋገም የማህደረ ትውስታ ድጋሚ ጠቋሚ ችግሮች ሳይኖሩበት ደህንነቱ የተጠበቀ ኤለመንቶችን ማስወገድ ያስችላል።

የመጫወቻ ማዕከል ሚስጥር፡ "ፍትሃዊ Hitboxes" በInflate በኩል

እንደ ** ታግዷል፡ Pixel Panzer** ባሉ ሬትሮ ጨዋታዎች ውስጥ የስፕሪት ሸካራዎች ብዙውን ጊዜ ግልጽ የሆኑ ህዳጎችን ወይም የአንቴና ሹልቆችን ያካትታሉ። የተጫዋቹ ታንኩ የሚፈነዳው ጥይት ባዶ የሆነ የሸካራነት ጥግ ስለነካ፣ ተጫዋቹ እንደተታለልክ ይሰማዋል።

ግጭቶች ምላሽ ሰጪ እና ፍትሃዊ ስሜት እንዲሰማቸው ለማድረግ፣ጨዋታዎች ትንሽ Hitbox በስፕሪት ውስጥ `አራት ማእዘንን በመጠቀም ይጠቀማሉ።

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 *** ሲሽከረከር ዘንግ-የተሰለፈ ማሰሪያ ሳጥን የሚሽከረከሩትን ማዕዘኖች ለመዝጋት መስፋፋት አለበት፣ ይህም በባዶ አየር ውስጥ የሚያበሳጭ "የፋንተም ግጭት" ያስከትላል።

ለክብ አስትሮይድ፣ ሆሚንግ ኢነርጂ ኦርቦች እና የሚሽከረከሩ የጠፈር መንኮራኩሮች Bounding Circles ምርጥ መፍትሄ ናቸው።

የካሬ ሥር ወጥመድ

በማዕከሎቻቸው መካከል ያለው ርቀት ከራዲቸው ድምር ያነሰ ወይም እኩል ሲሆን ሁለት ክበቦች ይጋጫሉ።

___ ሂሳብ_አግድ_2____

በኮድ ውስጥ፣ የዩክሊዲያን ርቀትን በማስላት Math.Sqrt (ወይም Vector2.Distance) ይጠቀማል። ነገር ግን አራት ማዕዘን ቅርጾችን በ 200 ፐሮጀክቶች ማስላት በመቶዎች የሚቆጠሩ አላስፈላጊ የሲፒዩ ዑደቶችን ያቃጥላል!

ካሬ ርቀት ከካሬ ራዲየስ ድምር ጋር በማነፃፀር የካሬውን ስር ሙሉ በሙሉ እናስወግዳለን።

___ ሂሳብ_አግድ_3____

MonoGame ትግበራ፡ ክበብ እና ክበብ

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፡ የተቀላቀሉ ቅርጾች – Circle vs. Box (MathHelper.Clamp)

በ ** Paint Trek *** ክብ ቅርጽ ያለው የጠፈር መንኮራኩር አራት ማዕዘን ቅርጽ ባላቸው የመከላከያ መሰናክሎች ውስጥ ሲዘዋወር ወይም ክብ ጥይት በ ** ታግዷል፡ Pixel Panzer *** ካሬ ብሎክ ላይ ሲመታ ምን ይከሰታል?

ክበብ እና አራት ማዕዘን ግጭት ያስፈልገናል።

የ Clamping Algorithm

ስልቱ ነጥቡን ወደ ክበቡ መሃል ቅርብ በሆነው አራት ማዕዘኑ ላይ መፈለግ እና ከዚያ በጣም ቅርብ ከሆነው ወደ መሃል ያለው ርቀት ከክብ ራዲየስ ያነሰ መሆኑን ያረጋግጡ።

የሞኖ ጌም 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፡ የማያቋርጥ የግጭት ማወቂያ (ሲሲዲ) እና የተጠረገ ጨረሮች

በጨዋታዎ ውስጥ ከፍተኛ ፍጥነት ያለው አነጣጥሮ ተኳሽ ዙር ወይም የባቡር ሽጉጥ ሌዘር ተኩሶ ታውቃለህ፣ ጥይቱ ጉዳት ሳያደርስ በቀጭን የጠላት መርከብ ውስጥ በቀጥታ ሲያልፍ ለማየት ብቻ?

ይህ ስህተት ** tunneling** በመባል ይታወቃል።

ልዩ የሆኑ ጨዋታዎች በጊዜ ደረጃዎች ስለሚዘምኑ (\(\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 (currentPosition`) የሚያገናኘውን ሙሉውን የመስመር ክፍል እንፈትሻለን።

ከባልደረባችን ርዕስ SpiralWar የማምረቻ ሬይካስቲንግ ጠፍጣፋ-መገንጠያ ዘዴ እዚህ አለ፡-

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

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

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

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

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

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

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

በ ** ታግዷል፡ ፒክስል ፓንዘር *** የተጫዋቹ ቀጣይነት ያለው የሌዘር ጨረር ክህሎት ይህን ትክክለኛ ሬይካስት በመጠቀም የሚወርዱ ብሎኮችን አንድም ግጭት ሳያመልጥ ለመቁረጥ ይጠቅማል።


6. ደረጃ 5፡ የምርት-ደረጃ ፒክስል-ፍጹም የግጭት ማወቂያ

አሁን የመጨረሻው የ 2D ትክክለኛነት ደረጃ ላይ ደርሰናል፡ ** ፒክስል-ፍጹም ግጭት ***።

በሬትሮ ታንክ ተኳሽ ወይም የጠፈር መርከብ የውሻ ፍልሚያ፣ መደበኛ ያልሆኑ ቅርጾች (ታንክ በርሜሎች፣ ክንፎች፣ ኮክፒት ኮክፒትስ) በስፕሪት ሸካራነት ውስጥ ባሉ ግልጽ ፒክሰሎች የተከበቡ ናቸው። የጠላት ሚሳኤል ያንን ግልፅ ቦታ ሲመታ ተጫዋቾች ወዲያውኑ ያስተውላሉ።

ፒክስል-ፍፁም ግጭት የተደራረቡ ሸካራዎች ትክክለኛ የአልፋ (ግልጽነት) ሰርጦችን ይፈትሻል። ሁለት ግልጽ ያልሆኑ ፒክሰሎች በተመሳሳይ የአለም መጋጠሚያ ላይ ከተደራረቡ እውነተኛ አካላዊ ምት ተከስቷል።

ገዳይ ስህተት፡ GetData በአዘምን() ውስጥ

ብዙ መማሪያዎች ለጀማሪዎች ይህንን እንዲያደርጉ ያስተምራሉ-

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

Calling Texture2D.GetData() during runtime forces the CPU to wait for the GPU pipeline to flush, pulling texture memory back over the bus. Doing this for dozens of sprites every frame will tank your game from 60 FPS to 5 FPS and trigger Android ANRs immediately!

The Battle-Tested Solution from Blocked: Pixel Panzer

In Blocked: Pixel Panzer, our Sprite.cs implements an ultra-optimized IntersectsPixel routine built around 5 crucial rules:

  1. Static Color Array Cache: GetData is called exactly once when the texture loads, and stored in a Dictionary<Texture2D, Color[]>.
  2. AABB Early Exit Guard: If Bounds.Intersects(other.Bounds) is false, we bail out immediately. 99% of checks are eliminated before inspecting a single pixel.
  3. Texture Atlas & SourceRectangle Support: Handles sprites packed into texture atlases using source rectangle offsets.
  4. Calculated Overlap Window: We only loop over the exact intersection rectangle between both sprites (Math.Max(a.Top, b.Top), etc.).
  5. Alpha Short-Circuiting: If Sprite A's pixel is transparent (A <= 20), Sprite B is completely skipped.
  6. Property Hoisting: Accessing virtual properties (Bounds) in nested loops creates thousands of struct copies. We store them in local stack variables before entering the loop.

Here is the complete, production-ready code:

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

This routine executes in under 0.1 milliseconds on modern Android hardware and desktop systems.


7. Level 6: Broad-Phase Spatial Grid & Zero-Allocation Mobile GC

Imagine your game has 100 blocks, 60 bullets, and 20 enemies. If you test every entity against every other entity using nested for loops, you perform:

\(\frac{180 \times 179}{2} = 16,110 \text{ checks per frame!}\)

At 60 FPS, that is nearly 1,000,000 collision checks every second.

To scale your game, you must implement Broad-Phase Spatial Partitioning.

The Uniform Spatial Hash Grid

We divide the screen into a 2D grid of uniform cells (in Blocked: Pixel Panzer, each cell is \(150 \times 150\) pixels). An entity only tests collisions against other entities that reside in the same grid cells.

The Mobile Memory Problem: GC Thrashing

If your SpatialGrid creates new List<Point>() or new List<Block>() every frame, you allocate megabytes of garbage every minute on the managed heap.

On Android's Mono runtime, this triggers frequent Gen-0 Garbage Collections, causing: mono runtime: Native lock contention (mono_class_is_subclass_)

The game stutters and Google Play flags your game with ANR warnings!

The Solution: Reusable Scratch Buffers

Here is the Zero-Allocation Spatial Hash Grid from CollisionManager.cs in Blocked: Pixel Panzer:

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

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

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

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

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

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

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

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

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

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

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

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

By reusing _scratchCells and _scratchCheckedEntities, the entire collision step runs with 0 bytes of heap allocation per frame.


8. Summary Comparison Table

Technique Mathematical Cost Rotational Support Tunneling Safe? Best Use Case in MonoGame
AABB (Rectangle.Intersects) Ultra-Fast (~4 integer comparisons) No No Grid blocks, bullet broad-phase, UI
Circle (DistanceSquared) Very Fast (3 muls, 0 sqrt) Yes (Invariant) No Round ships, fireballs, energy orbs
Circle vs Box (Clamp) Fast (Local MathHelper.Clamp) Yes No Circular player navigating tight walls
Swept Ray (Slab CCD) Moderate (Parametric Raycast) Yes Yes Fast sniper bullets, railguns, lasers
Pixel-Perfect (IntersectsPixel) Selective (Restricted Sub-rect) Yes No Irregular sprite contours, fair hitboxes
Spatial Grid (Zero-GC) Broad-phase (\(O(N^2) \to O(N)\)) N/A N/A Dense waves, bullet hells, Android 60 FPS

Real-World Production Showcases: Arar Games

These collision architectures are not theoretical experiments—they are the real engineering foundation powering our commercially released titles:

  • Blocked: Pixel Panzer: Our retro tank survival arcade game on Google Play and Microsoft Store. It features our complete two-tier collision system: a zero-allocation spatial hash grid filtering falling blocks, and pixel-perfect contact checking for tanks, fighter jets, turret shells, and ColorWheel elemental ammunition.
  • Paint Trek: Our fast-paced space shooter featuring rotational circle collision, continuous raycasting, and swept-volume missile defense systems.

Conclusion & Next Steps

MonoGame gives you the power to design collision detection that perfectly matches the needs of your game. By gating expensive Pixel-Perfect checks behind fast AABB tests, utilizing Squared Distances for circles, and eliminating garbage collection with Reusable Scratch Buffers, you can deliver console-smooth 60/120 FPS performance on both desktop and mobile platforms.

Check out our games on the app stores to see these collision systems in action, and start implementing these patterns in your own MonoGame projects today!



SEO Keywords & Hashtags

Keywords: MonoGame 2D collision detection, C# game development, Rectangle.Intersects MonoGame, pixel perfect collision C#, spatial hash grid MonoGame, swept ray continuous collision, indie game performance optimization, zero allocation game loop, Android MonoGame optimization, Arar Games, Blocked Pixel Panzer, Paint Trek.

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




Hiç yorum yok:

Yorum Gönder