27 Eylül 2026 Pazar

MonoGame의 2D 충돌 감지: 기본 직사각형에서 제로 할당 픽셀 완벽 시스템까지




MonoGame의 2D 충돌 감지: 기본 직사각형에서 제로 할당 픽셀 완벽 시스템까지

Unity 또는 Godot와 같은 시각적 엔진에서 2D 게임을 구축할 때 충돌 감지는 종종 일련의 검사기 확인란처럼 느껴집니다. 'BoxCollider2D' 또는 'CircleCollider2D'를 선택하고 'Rigidbody'를 연결한 다음 내부 물리 단계가 모바일 장치에서 끊기지 않기를 바랍니다.

그러나 MonoGame 및 **C#**에서는 완전히 제어할 수 있습니다. 숨겨진 물리 오버헤드, 원치 않는 회전 관성, 프레임 예산을 훔치는 신비한 GC(가비지 수집) 스파이크가 없습니다.

Arar Games에서 Blocked: Pixel Panzer 및 Paint Trek을 제작했을 때 아케이드 게임 루프는 Windows PC와 Android 기기 모두에서 수백 개의 고속 적 총알, 폭발하는 벽돌 격자, 회전하는 탱크 포탑, 전투기의 저공비행 및 입자 보호막을 60~120FPS로 처리해야 했습니다. 범용 물리 엔진은 불가능했습니다. 우리는 특별히 제작된 계층형 충돌 아키텍처가 필요했습니다.

이 포괄적인 코드 기반 가이드에서는 MonoGame 충돌(실제 'Bullet' 및 'Enemy' 스프라이트가 포함된 'Rectangle.Intersects')의 절대 기본부터 시작하여 고급 원형 검사, 혼합 클램핑, 터널링 방지 레이캐스트, 프로덕션 등급 픽셀 완벽한 충돌 및 모바일 GC 생존에 최적화된 제로 할당 공간 그리드까지 구축합니다.


1. 기초: 간단한 MonoGame 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: 가장 간단한 충돌 – Rectangle.Intersects(AABB)

MonoGame의 가장 기본적인 2D 충돌 검사는 AABB(Axis-Aligned Bounding Box) 테스트입니다. "축 정렬"이라는 용어는 단순히 직사각형의 가장자리가 화면의 \(X\) 및 \(Y\) 축(회전 없음)과 완전히 평행함을 의미합니다.

MonoGame은 'Rectangle.Intersects(Rectangle value)'라는 빠른 내장 메서드를 제공합니다.

Rectangle.Intersects가 내부적으로 작동하는 방식

표면 아래에서 MonoGame은 네 가지 정수 비교를 실행합니다.

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

네 가지 조건이 모두 충족되면 직사각형이 겹칩니다. 하나의 조건이라도 실패하면 빈 축이 이를 분리하며 충돌이 불가능합니다.

실제 게임플레이 코드: Game1.Update의 Bullet 대 Enemy

다음은 기본 MonoGame '업데이트' 루프 내에서 활성 총알 목록과 활성 적 목록 간의 충돌을 확인하는 방법입니다.

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

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

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

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

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

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

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

        base.Update(gameTime);
    }
}

성능 팁: 역순으로 반복합니다(for (int i = list.Count - 1; i >= 0; i--)). foreach를 사용하고 _bullets.Remove(bullet)를 호출하려고 하면 C#에서는 InvalidOperationException: Collection was Modified가 발생합니다. 뒤로 반복하면 메모리 재인덱싱 문제 없이 안전하게 요소를 제거할 수 있습니다.

아케이드 비밀: Inflate를 통한 "공정한 히트박스"

Blocked: Pixel Panzer와 같은 복고풍 게임에서는 스프라이트 텍스처에 투명한 여백이나 안테나 스파이크가 포함되는 경우가 많습니다. 총알이 텍스처의 빈 투명 모서리에 닿아 플레이어의 탱크가 폭발하는 경우 플레이어는 속았다는 느낌을 받게 됩니다.

충돌이 반응적이고 공평하게 느껴지도록 하기 위해 게임은 Rectangle.Inflate를 사용하여 스프라이트 내부에 더 작은 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이 이상적인 솔루션입니다.

제곱근 함정

두 원은 중심 사이의 거리가 반지름의 합보다 작거나 같을 때 충돌합니다.

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

코드에서는 유클리드 거리를 계산할 때 Math.Sqrt(또는 Vector2.Distance)를 사용합니다. 그러나 200개의 발사체가 포함된 루프에서 제곱근을 계산하면 수백 개의 불필요한 CPU 사이클이 소모됩니다!

제곱 거리와 제곱 반경 합계를 비교하여 제곱근을 완전히 제거합니다.

\(\text{거리제곱} \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);
    }
}

제곱근이 0이고 스프라이트 회전에 면역이며 번개처럼 빠릅니다.


4. 레벨 3: 혼합 모양 – 원 대 상자(MathHelper.Clamp)

Paint Trek에서 원형 우주선이 직사각형 방어벽으로 이루어진 좁은 미로를 통과하거나 Blocked: Pixel Panzer에서 둥근 총알이 정사각형 블록에 부딪히면 어떻게 되나요?

원 대 직사각형 충돌이 필요합니다.

클램핑 알고리즘

전략은 원의 중심에 가장 가까운 직사각형의 점을 찾은 다음 가장 가까운 점에서 중심까지의 거리가 원의 반경보다 작은지 테스트하는 것입니다.

MonoGame의 MathHelper.Clamp는 이 작업을 간단하게 만듭니다.

public static class Collision2D
{
    public static bool CircleIntersectsRectangle(Circle circle, Rectangle rect)
    {
        // Find the closest point on the rectangle to the circle center
        float closestX = MathHelper.Clamp(circle.Center.X, rect.Left, rect.Right);
        float closestY = MathHelper.Clamp(circle.Center.Y, rect.Top, rect.Bottom);

        // Vector from closest point to circle center
        float distanceX = circle.Center.X - closestX;
        float distanceY = circle.Center.Y - closestY;

        // Check squared distance against squared radius
        float distanceSquared = (distanceX * distanceX) + (distanceY * distanceY);
        return distanceSquared <= (circle.Radius * circle.Radius);
    }
}

이제 할당이 없고 정확도가 높은 직사각형 벽돌에 대해 플레이어 방패를 테스트할 수 있습니다!


5. 레벨 4: 연속 충돌 감지(CCD) 및 스위프 광선

게임에서 초고속 저격탄이나 레일건 레이저를 발사했지만 총알이 손상을 주지 않고 얇은 적 함선을 마법처럼 관통하는 것을 본 적이 있습니까?

이 버그는 터널링으로 알려져 있습니다.

개별 게임은 시간 단계(60FPS에서 \(\Delta t = 16.6\text{ms}\))로 업데이트되므로 초당 1,800픽셀로 움직이는 개체는 **단일 프레임에서 30픽셀 이동합니다. 적 선체의 두께가 15픽셀에 불과한 경우 총알은 프레임 1에서 적 앞에 있었고 프레임 2에서는 적 뒤에 완전히 있었습니다.

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

해결 방법: 스윕 세그먼트 대 상자(슬래브 방법)

단일 지점을 테스트하는 대신 프레임 1(previousPosition)의 총알 위치를 프레임 2(currentPosition)에 연결하는 전체 선분을 테스트합니다.

다음은 동반 타이틀 SpiralWar의 프로덕션 레이캐스팅 슬래브 교차 방법입니다.

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

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

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

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

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

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

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

Blocked: Pixel Panzer에서 플레이어의 지속적인 레이저 빔 스킬은 이 정확한 레이캐스트를 사용하여 단 한 번의 충돌도 놓치지 않고 하강하는 블록의 행을 잘라냅니다.


6. 레벨 5: 프로덕션 등급의 완벽한 픽셀 충돌 감지

이제 우리는 궁극적인 2D 정확도 수준인 픽셀-완벽한 충돌에 도달했습니다.

복고풍 탱크 슈팅 게임이나 우주선 공중전에서는 불규칙한 모양(탱크 배럴, 날개, 조종석 조종석)이 스프라이트 텍스처의 투명한 픽셀로 둘러싸여 있습니다. 적의 미사일이 투명한 공간에 닿으면 플레이어는 즉시 알아차립니다.

픽셀 완벽한 충돌은 겹치는 텍스처의 실제 알파(투명도) 채널을 검사합니다. 두 개의 불투명 픽셀이 동일한 세계 좌표에서 겹치는 경우 실제 물리적 타격이 발생한 것입니다.

치명적인 실수: 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 파이프라인이 플러시될 때까지 기다리게 되어 버스를 통해 텍스처 메모리를 다시 끌어옵니다. 프레임마다 수십 개의 스프라이트에 대해 이 작업을 수행하면 게임이 60FPS에서 5FPS로 줄어들고 Android ANR이 즉시 트리거됩니다!

Blocked: Pixel Panzer의 전투 테스트를 거친 솔루션

Blocked: Pixel Panzer에서 'Sprite.cs'는 5가지 중요한 규칙을 기반으로 구축된 매우 최적화된 'IntersectsPixel' 루틴을 구현합니다.

  1. 정적 색상 배열 캐시: GetData는 텍스처가 로드될 때 정확히 한 번 호출되고 Dictionary<Texture2D, Color[]>에 저장됩니다.
  2. AABB 조기 종료 가드: Bounds.Intersects(other.Bounds)가 false인 경우 즉시 구제 조치를 취합니다. 단일 픽셀을 검사하기 전에 검사의 99%가 제거됩니다.
  3. 텍스처 아틀라스 및 SourceRectangle 지원: 소스 직사각형 오프셋을 사용하여 텍스처 아틀라스에 압축된 스프라이트를 처리합니다.
  4. 계산된 오버랩 창: 두 스프라이트 사이의 정확한 교차 직사각형(Math.Max(a.Top, b.Top) 등)만 반복합니다.
  5. 알파 단락: 스프라이트 A의 픽셀이 투명하면(A <= 20) 스프라이트 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;
    }
}

이 루틴은 최신 Android 하드웨어 및 데스크톱 시스템에서 0.1밀리초 이내에 실행됩니다.


7. 레벨 6: 광역 공간 그리드 및 제로 할당 모바일 GC

게임에 블록 100개, 총알 60개, 적이 20명 있다고 상상해 보세요. 중첩된 for 루프를 사용하여 다른 모든 엔터티에 대해 모든 엔터티를 테스트하는 경우 다음을 수행합니다.

\(\frac{180 \times 179}{2} = 16,110 \text{ 프레임당 확인!}\)

60FPS에서는 거의 1초마다 1,000,000번의 충돌 확인이 이루어집니다.

게임을 확장하려면 광역 위상 공간 분할을 구현해야 합니다.

균일한 공간 해시 그리드

화면을 균일한 셀의 2D 그리드로 나눕니다(Blocked: Pixel Panzer에서 각 셀은 \(150 \times 150\) 픽셀입니다). 엔터티는 동일한 그리드 셀에 있는 다른 엔터티와의 충돌만 테스트합니다.

모바일 메모리 문제: GC 스래싱

SpatialGrid가 프레임마다 new List<Point>() 또는 new List<Block>()을 생성하는 경우 관리되는 힙에 매분 메가바이트의 가비지를 할당하게 됩니다.

Android의 Mono 런타임에서는 Gen-0 가비지 수집이 자주 발생하여 다음이 발생합니다. 모노 런타임: 기본 잠금 경합(mono_class_is_subclass_)

게임이 끊기고 Google Play에서 게임에 ANR 경고를 표시합니다!

해결책: 재사용 가능한 스크래치 버퍼

다음은 Blocked: Pixel Panzer의 CollisionManager.cs에 있는 Zero-Allocation 공간 해시 그리드입니다.

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 (Rectangle.Intersects) 초고속(~4개의 정수 비교) 아니요 아니요 그리드 블록, 총알 넓은 단계, UI
원(DistanceSquared) 매우 빠름(3뮬, 0sqrt) 예(불변) 아니요 둥근 배, 불덩어리, 에너지 오브
원 대 상자(Clamp) 빠른(로컬 MathHelper.Clamp) 예 아니요 좁은 벽을 탐색하는 원형 플레이어
스웹트 레이(슬래브 CCD) 보통(파라메트릭 레이캐스트) 예 예 빠른 저격 총알, 레일건, 레이저
완벽한 픽셀(IntersectsPixel) 선택적(제한된 하위 사각형) 예 아니요 불규칙한 스프라이트 윤곽, 공정한 히트박스
공간 그리드(Zero-GC) 광역상(\(O(N^2) \to O(N)\)) 해당 없음 해당 없음 빽빽한 파도, 총알 지옥, Android 60FPS

실제 제작 쇼케이스: Arar Games

이러한 충돌 아키텍처는 이론적인 실험이 아닙니다. 상업적으로 출시된 타이틀을 지원하는 실제 엔지니어링 기반입니다.

  • 차단됨: Pixel Panzer: Google Play 및 Microsoft Store에서 제공되는 복고풍 탱크 생존 아케이드 게임입니다. 이는 제로 할당 공간 해시 그리드 필터링 낙하 블록, 탱크, 전투기, 터렛 포탄 및 ColorWheel 원소 탄약에 대한 완벽한 픽셀 접촉 검사 등 완벽한 2계층 충돌 시스템을 갖추고 있습니다.
  • Paint Trek: 회전 원 충돌, 연속 레이캐스팅, 스윕 볼륨 미사일 방어 시스템을 갖춘 빠른 ​​속도의 우주 슈팅 게임입니다.

결론 및 다음 단계

MonoGame은 게임 요구 사항에 완벽하게 맞는 충돌 감지를 설계할 수 있는 기능을 제공합니다. 빠른 AABB 테스트 뒤에 값비싼 픽셀 완벽 검사를 적용하고, 원에 제곱 거리를 활용하고, 재사용 가능한 스크래치 버퍼로 가비지 수집을 제거함으로써 데스크톱과 모바일 플랫폼 모두에서 콘솔처럼 부드러운 60/120FPS 성능을 제공할 수 있습니다.

앱 스토어에서 우리 게임을 확인하여 이러한 충돌 시스템이 작동하는 모습을 확인하고 지금 바로 귀하의 MonoGame 프로젝트에 이러한 패턴을 구현해 보세요!


매장 링크 및 리소스


SEO 키워드 및 해시태그

키워드: MonoGame 2D 충돌 감지, C# 게임 개발, Rectangle.Intersects MonoGame, 픽셀 완벽 충돌 C#, 공간 해시 그리드 MonoGame, 스위프 광선 연속 충돌, 인디 게임 성능 최적화, 제로 할당 게임 루프, Android MonoGame 최적화, Arar Games, Blocked Pixel Panzer, Paint Trek.

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




Hiç yorum yok:

Yorum Gönder