2D Collision Detection i MonoGame: Från grundläggande rektanglar till Zero-Allocation Pixel-Perfect Systems
När man bygger 2D-spel i visuella motorer som Unity eller Godot känns kollisionsdetektering ofta som en serie kontrollrutor: du slår på en BoxCollider2D eller CircleCollider2D, fäster en Rigidbody och hoppas att det interna fysiksteget inte stammar på mobila enheter.
I MonoGame och C# har du dock total kontroll. Det finns ingen dold fysik över huvudet, ingen oönskad rotationströghet och inga mystiska skräpinsamlingar (GC) spikar som stjäl din rambudget.
På Arar Games, när vi byggde Blocked: Pixel Panzer och Paint Trek, behövde våra arkadspelsloopar för att bearbeta hundratals höghastighets fiendekulor, exploderande tegelgaller, roterande tanktorn, stridsflygplan och partikelsköldar med 60 till 120 FPS på både Windows PC och Android-enheter. En fysikmotor för allmänt bruk var uteslutet – vi behövde en specialbyggd kollisionsarkitektur i olika nivåer.
I den här omfattande, koddrivna guiden kommer vi att utgå från de absoluta grunderna för MonoGame-kollision (Rectangle. Intersects med riktiga Bullet och Enemy sprites) och bygga upp till avancerade cirkelkontroller, mixed clamping, anti-tunneling raycasts, produktions-grade Pixel-Zero-Per, All-fect Spat Collision. Grids optimerade för mobil GC-överlevnad.
1. The Foundation: A Simple MonoGame Sprite Hierarki
Innan vi upptäcker kollisioner behöver vi rena spelenheter. I MonoGame har en entitet i grunden en position, en textur och en avgränsande rektangel.
Här är den grundläggande enhetsarkitekturen som används i våra spel:
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);
}
}
Låt oss nu skapa konkreta "Player", "Enemy" och "Bullet" klasser:
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;
}
}
Med dessa enheter på plats, låt oss utforska hur man upptäcker kollisioner mellan dem, med utgångspunkt från den enklaste metoden.
2. Nivå 1: Den enklaste kollisionen – Rectangle.Intersects (AABB)
Den mest grundläggande 2D-kollisionskontrollen i MonoGame är testet Axis-Aligned Bounding Box (AABB). Termen "axeljusterad" betyder helt enkelt att rektangelns kanter är helt parallella med skärmens \(X\)- och \(Y\)-axlar (ingen rotation).
MonoGame tillhandahåller en snabb, inbyggd metod: Rectangle.Intersects(Rectangle value).
Hur Rectangle.Crosses fungerar under huven
Under ytan utför MonoGame fyra heltalsjämförelser:
public bool Intersects(Rectangle value)
{
return value.Left < this.Right &&
this.Left < value.Right &&
value.Top < this.Bottom &&
this.Top < value.Bottom;
}
Om alla fyra villkoren är uppfyllda överlappar rektanglarna. Om ens ett tillstånd misslyckas, skiljer en tom axel dem åt, och ingen kollision är möjlig.
Riktig spelkod: Bullet vs. Enemy i Game1.Update
Så här kontrollerar du kollisioner mellan en lista med aktiva kulor och en lista med aktiva fiender i din huvudslinga för 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);
}
}
Prestandatips: Lägg märke till att vi itererar baklänges (
för (int i = list.Count - 1; i >= 0; i--))! Om du använderforeachoch försöker anropa_bullets.Remove(bullet), kastar C# ettInvalidOperationException: Collection was modified. Iteration bakåt möjliggör säker borttagning av element utan problem med återindexering av minnet.
The Arcade Secret: "Fair Hitboxes" via Inflate
I retrospel som Blocked: Pixel Panzer innehåller sprite-texturer ofta genomskinliga marginaler eller antennspik. Om spelarens tank exploderar för att en kula rörde vid ett tomt genomskinligt hörn av dess struktur, kommer spelaren att känna sig lurad.
För att få kollision att kännas lyhörd och rättvis använder spel en mindre Hitbox inuti spriten med 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. Nivå 2: Cirkel-till-cirkel-kollision (rotationsimmunitet)
Rektanglar fungerar utmärkt för statiska block och rutnät, men de misslyckas när sprites roterar. När ett icke-fyrkantigt rymdskepp roterar i Paint Trek, måste en axelinriktad begränsningsruta expandera för att omsluta de snurrande hörnen, vilket resulterar i frustrerande "fantomkollisioner" i tom luft.
För cirkulära asteroider, målsökande energiklot och roterande rymdfarkoster är Bounding Circles den idealiska lösningen.
Kvadratrotsfällan
Två cirklar kolliderar när avståndet mellan deras centra är mindre än eller lika med summan av deras radier:
\(\text{Avstånd}(C_A, C_B) \le r_A + r_B\)
I kod används "Math.Sqrt" (eller "Vector2.Distance") för att beräkna euklidiskt avstånd. Men att beräkna kvadratrötter i en loop med 200 projektiler bränner hundratals onödiga CPU-cykler!
Genom att jämföra kvadratavståndet med kvadratradiesumman, eliminerar vi helt kvadratroten:
\(\text{DistanceSquared} \le (r_A + r_B)^2\)
MonoGame-implementering: Circle vs. 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);
}
}
Integrera nu detta direkt i en enhet:
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);
}
}
Noll kvadratrötter, immun mot spriterotation och blixtsnabb.
4. Nivå 3: Blandade former – Circle vs. Box (MathHelper.Clamp)
Vad händer när ett cirkulärt rymdskepp i Paint Trek navigerar genom en tät labyrint av rektangulära försvarsbarriärer, eller när en rund kula träffar ett fyrkantigt block i Blocked: Pixel Panzer?
Vi behöver Cirkel vs. Rektangel kollision.
Klämalgoritmen
Strategin är att hitta den punkt på rektangeln som är närmast cirkelns centrum och sedan testa om avståndet från den närmaste punkten till centrum är mindre än cirkelns radie.
MonoGames MathHelper.Clamp gör detta trivialt:
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);
}
}
Du kan nu testa spelarsköldar mot rektangulära tegelstenar med noll tilldelningar och hög noggrannhet!
5. Nivå 4: Kontinuerlig kollisionsdetektion (CCD) & svepande strålar
Har du någonsin avfyrat en prickskytt med hög hastighet eller en rälsvapenlaser i ditt spel, bara för att se kulan på magiskt sätt passera rakt genom ett tunt fientligt skepp utan att orsaka skada?
Denna bugg är känd som tunneling.
Eftersom diskreta spel uppdateras i tidssteg (\(\Delta t = 16,6\text{ms}\) vid 60 FPS), färdas ett objekt som rör sig med 1 800 pixlar per sekund 30 pixlar i en enda bildruta. Om fiendens skrov bara är 15 pixlar tjockt, var kulan framför fienden på bildruta 1, och helt bakom fienden på bildruta 2.
Frame 1: [ Bullet ] ---> | Enemy Wall |
Frame 2: | Enemy Wall | ---> [ Bullet ]
(NO HIT DETECTED!)
Lösningen: Swept Segment vs. Box (Slab-metod)
Istället för att testa en enda punkt testar vi hela linjesegmentet som förbinder kulans position på Frame 1 (föregående Position) till Frame 2 (currentPosition).
Här är produktionsmetoden för raycasting slab-intersection från vår följeslagare 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;
}
}
I Blocked: Pixel Panzer använder spelarens kontinuerliga Laser Beam skicklighet denna exakta strålkastning för att skära igenom rader av fallande block utan att missa en enda kollision.
6. Nivå 5: Produktionsgrad Pixel-Perfekt kollisionsdetektion
Nu når vi den ultimata nivån av 2D-noggrannhet: Pixel-Perfect Collision.
I en retro stridsvagnsskjutare eller luftstrid med rymdskepp omges oregelbundna former (tankpipor, vingar, sittbrunnscockpits) av genomskinliga pixlar i sprite-strukturen. När en fiendemissil träffar det genomskinliga utrymmet märker spelarna omedelbart.
Pixel-perfekt kollision inspekterar de faktiska alfakanalerna (transparens) för de överlappande texturerna. Om två icke-transparenta pixlar överlappar vid samma världskoordinat har en verklig fysisk träff inträffat.
The Fatal Mistake: GetData inuti Update()
Många tutorials instruerar nybörjare att göra detta:
// DO NOT DO THIS!
Color[] dataA = new Color[textureA.Width * textureA.Height];
textureA.GetData(dataA); // STALLS GPU, CREATES MASSIVE GC LAG!
Att anropa Texture2D.GetData() under körning tvingar CPU:n att vänta på att GPU-pipelinen ska spolas, vilket drar tillbaka texturminnet över bussen. Om du gör detta för dussintals sprites kommer varje bildruta att fylla ditt spel från 60 FPS till 5 FPS och utlösa Android ANRs omedelbart!
Den stridstestade lösningen från Blocked: Pixel Panzer
I Blocked: Pixel Panzer implementerar vår Sprite.cs en ultraoptimerad IntersectsPixel-rutin byggd kring fem avgörande regler:
- Static Color Array Cache:
GetDataanropas exakt en gång när texturen laddas och lagras i enDictionary<Texture2D, Color[]>. - AABB Early Exit Guard: Om
Bounds.Intersects(other.Bounds)är falsk, räddar vi ut omedelbart. 99 % av kontrollerna elimineras innan en enda pixel inspekteras. - Texturatlas & 'SourceRectangle'-stöd: Hanterar sprites packade i texturatlaser med hjälp av källrektangelförskjutningar.
- Fönster för beräknat överlappning: Vi går bara över den exakta skärningsrektangeln mellan båda sprites (
Math.Max(a.Top, b.Top), etc.). - Alfakortslutning: Om Sprite A:s pixel är transparent (
A <= 20), hoppas Sprite B över helt. - Property Hoisting: Att komma åt virtuella egenskaper ('Bounds') i kapslade loopar skapar tusentals strukturkopior. Vi lagrar dem i lokala stackvariabler innan vi går in i loopen.
Här är den kompletta, produktionsklara koden:
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;
}
}
Denna rutin körs på under 0,1 millisekunder på modern Android-hårdvara och datorsystem.
7. Nivå 6: Broad-Phase Spatial Grid & Zero-Allocation Mobile GC
Föreställ dig att ditt spel har 100 block, 60 kulor och 20 fiender. Om du testar varje entitet mot alla andra entiteter med hjälp av kapslade "for"-loopar, utför du:
\(\frac{180 \times 179}{2} = 16 110 \text{ checkar per ram!}\)
Vid 60 FPS är det nästan 1 000 000 kollisionskontroller varje sekund.
För att skala ditt spel måste du implementera Bred-Phase Spatial Partitioning.
The Uniform Spatial Hash Grid
Vi delar upp skärmen i ett 2D-rutnät med enhetliga celler (i Blocked: Pixel Panzer kostar varje cell \(150 \x 150\) pixlar). En enhet testar bara kollisioner mot andra enheter som finns i samma rutnätsceller.
Mobilminnesproblemet: GC Thrashing
Om ditt SpatialGrid skapar new List<Point>() eller new List<Block>() varje bildruta, allokerar du megabyte av skräp varje minut på den hanterade högen.
På Androids Mono runtime utlöser detta frekventa Gen-0 Garbage Collections, vilket orsakar:
mono runtime: Native lock contention (mono_class_is_subclass_)
Spelet stammar och Google Play flaggar ditt spel med ANR-varningar!
Lösningen: Återanvändbara skrapbuffertar
Här är Zero-Allocation Spatial Hash Grid från CollisionManager.cs i 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);
}
}
}
}
}
}
}
}
Genom att återanvända _scratchCells och _scratchCheckedEntities körs hela kollisionssteget med 0 byte av heap-allokering per bildruta.
8. Sammanfattande jämförelsetabell
| Teknik | Matematisk kostnad | Rotationsstöd | Säkert att köra tunnel? | Bästa användningsfallet i MonoGame |
|---|---|---|---|---|
AABB (Rektangel. Skärs) |
Ultrasnabb (~4 heltalsjämförelser) | Nej | Nej | Grid block, bullet bredfas, UI |
Cirkel (DistanceSquared) |
Mycket snabb (3 muls, 0 sqrt) | Ja (Invariant) | Nej | Runda fartyg, eldklot, energiklot |
Circle vs Box (Clamp) |
Snabb (Local MathHelper.Clamp) | Ja | Nej | Cirkulär spelare som navigerar i täta väggar |
| Swept Ray (Slab CCD) | Måttlig (Parametrisk Raycast) | Ja | Ja | Snabba prickskyttkulor, järnvägsvapen, lasrar |
Pixel-Perfect (Skärningspixel) |
Selektiv (Restricted Sub-rect) | Ja | Nej | Oregelbundna spritekonturer, rättvisa hitboxar |
| Spatial Grid (Noll-GC) | Bredfas (\(O(N^2) \till O(N)\)) | N/A | N/A | Täta vågor, kulhelveten, Android 60 FPS |
Real-World Production Showcases: Arar Games
Dessa kollisionsarkitekturer är inte teoretiska experiment – de är den verkliga tekniska grunden som driver våra kommersiellt släppta titlar:
- Blockerad: Pixel Panzer: Vårt arkadspel för överlevnad av retrotankar på Google Play och Microsoft Store. Den har vårt kompletta kollisionssystem i två nivåer: ett rumsligt hash-rutnät med nolltilldelning som filtrerar fallande block och pixelperfekt kontaktkontroll för tankar, stridsflygplan, tornskal och ColorWheel elementär ammunition.
- Paint Trek: Vårt snabba rymdskytte med roterande cirkelkollision, kontinuerlig strålkastning och missilförsvarssystem med svepande volym.
Slutsats och nästa steg
MonoGame ger dig kraften att designa kollisionsdetektering som perfekt matchar behoven i ditt spel. Genom att lägga in dyra Pixel-Perfect-checkar bakom snabba AABB-tester, använda Squared Distances för cirklar och eliminera sophämtning med Återanvändbara Scratch Buffers, kan du leverera konsolsmidig 60/120 FPS-prestanda på både stationära och mobila plattformar.
Kolla in våra spel i appbutikerna för att se dessa kollisionssystem i aktion, och börja implementera dessa mönster i dina egna MonoGame-projekt idag!
Butikslänkar och resurser
- Blockerad: Pixel Panzer på Google Play: Ladda ner för Android
- Blockerad: Pixel Panzer på Microsoft Store: Ladda ner för Windows
- MonoGame Framework: monogame.net
SEO-sökord och Hashtags
Sökord: MonoGame 2D-kollisionsdetektering, C#-spelutveckling, Rectangle.Skärar MonoGame, pixel perfekt kollision C#, spatial hash-rutnät MonoGame, sweept ray-kontinuerlig kollision, indiespelsprestandaoptimering, nollallokeringsspelslinga, Android Pixel MonoGame-optimering, optimering av Android Pixel MonoGame.
#MonoGame #CSharp #GameDev #IndieDev #GamePhysics #DotNet #2DGameDev #MobileGameDev #PerformanceOptimization #CleanCode #BlockedPixelPanzer ##Paintrek `##GameGame``
Hiç yorum yok:
Yorum Gönder