Виявлення двовимірних зіткнень у MonoGame: від простих прямокутників до ідеальних піксельних систем із нульовим розподілом
Під час створення 2D-ігор у візуальних движках, таких як Unity або Godot, виявлення зіткнень часто виглядає як серія прапорців інспектора: ви натискаєте на «BoxCollider2D» або «CircleCollider2D», приєднуєте «Rigidbody» і сподіваєтеся, що крок внутрішньої фізики не заїкається на мобільних пристроях.
Однак у MonoGame і C# ви повністю контролюєте. Немає жодних прихованих фізичних накладних витрат, жодної небажаної інерції обертання та жодних таємничих стрибків збирання сміття (GC), які вкрадають ваш бюджет кадру.
У Arar Games, коли ми створювали Blocked: Pixel Panzer і Paint Trek, цикли нашої аркадної гри потребували обробки сотень високошвидкісних ворожих куль, вибухових цегляних сіток, обертових башт танків, обльотів винищувачів і щитів від частинок зі швидкістю від 60 до 120 FPS на ПК з Windows і пристроях Android. Про фізичний механізм загального призначення не могло бути й мови — нам потрібна була спеціально створена багаторівнева архітектура зіткнень.
У цьому вичерпному керованому кодом посібнику ми почнемо з абсолютних основ зіткнень MonoGame («Rectangle.Intersects» зі справжніми спрайтами «Bullet» і «Enemy») і перейдемо до розширених перевірок кіл, змішаного затискання, протитунельних променів, Pixel-Perfect зіткнення і Zero-Allocation Spatial Сітки, оптимізовані для виживання мобільного GC.
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);
}
}
Тепер давайте створимо конкретні класи 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 є тест Axis-Aligned Bounding Box (AABB). Термін «вирівнювання по осі» просто означає, що краї прямокутника повністю паралельні осям \(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
Ось як ви перевіряєте зіткнення між списком активних куль і списком активних ворогів у вашому головному циклі «Оновлення» 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. Ітерація назад дозволяє безпечно видалити елемент без проблем із повторним індексуванням пам’яті.
The Arcade Secret: "Fair 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 снарядами спалює сотні непотрібних циклів ЦП!
Порівнюючи квадрат відстані із сумою квадратів радіуса, ми повністю виключаємо квадратний корінь:
\(\text{DistanceSquared} \le (r_A + r_B)^2\)
Реалізація моноігри: 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: Безперервне виявлення зіткнень (ПЗС) і скановані промені
Ви коли-небудь стріляли у своїй грі з надшвидкісного снайперського снаряда або лазерної рейкової зброї, щоб спостерігати, як куля чарівним чином проходить крізь тонке вороже судно, не завдаючи шкоди?
Ця помилка відома як тунелювання.
Оскільки дискретні ігри оновлюються з кроками в часі (\(\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!)
Рішення: розгорнутий сегмент проти коробки (метод плити)
Замість того, щоб перевіряти одну точку, ми перевіряємо весь сегмент лінії, що з’єднує позицію маркера на кадрі 1 (previousPosition) і кадрі 2 (currentPosition).
Ось метод перетину слябів 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: Виявлення зіткнень виробничого рівня з ідеальним пікселем
Тепер ми досягли найвищого рівня точності 2D: Ідеальне зіткнення пікселів.
У ретро-танковому шутері чи повітряному бою з космічним кораблем неправильні форми (стволи танків, крила, кабіни пілотів) оточені прозорими пікселями у текстурі спрайту. Коли ворожа ракета потрапляє в цей прозорий простір, гравці відразу помічають це.
Ідеальне зіткнення пікселів перевіряє фактичні альфа-канали (прозорість) текстур, що накладаються. Якщо два непрозорі пікселі перекриваються в одній світовій координаті, відбулося справжнє фізичне попадання.
Фатальна помилка: 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() під час виконання змушує ЦП чекати, поки конвеєр графічного процесора очиститься, повертаючи пам'ять текстур назад через шину. Виконання цього для десятків спрайтів у кожному кадрі збільшить швидкість вашої гри з 60 кадрів/с до 5 кадрів/с і негайно спричинить помилки ANR Android!
Перевірене в боях рішення від Blocked: Pixel Panzer
У Blocked: Pixel Panzer наш Sprite.cs реалізує ультраоптимізовану процедуру IntersectsPixel, побудовану навколо 5 важливих правил:
- Статичний кеш масиву кольорів:
GetDataвикликається рівно один раз під час завантаження текстури та зберігається вDictionary<Texture2D, Color[]>. - AABB Early Exit Guard: Якщо
Bounds.Intersects(other.Bounds)є хибним, ми негайно виручаємося. 99% перевірок усуваються до перевірки окремого пікселя. - **Підтримка атласу текстур і
SourceRectangle: ** обробляє спрайти, упаковані в атласи текстур, використовуючи зміщення вихідного прямокутника. - Обчислене вікно перекриття: Ми лише проходимо по прямокутнику перетину обох спрайтів (
Math.Max(a.Top, b.Top)тощо). - Альфа-коротке замикання: Якщо піксель спрайта A прозорий (
A <= 20), спрайт B повністю пропускається. - Підвищення властивостей: Доступ до віртуальних властивостей (
Межі) у вкладених циклах створює тисячі копій структур. Ми зберігаємо їх у локальних змінних стека перед входом у цикл.
Ось повний, готовий до виробництва код:
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 мілісекунди на сучасному апаратному забезпеченні Android і настільних системах.
7. Рівень 6: широкофазова просторова сітка та мобільний GC з нульовим розподілом
Уявіть, що у вашій грі 100 блоків, 60 куль і 20 ворогів. Якщо ви перевіряєте кожну сутність проти кожної іншої сутності за допомогою вкладених циклів for, ви виконуєте:
\(\frac{180 \times 179}{2} = 16 110 \text{ перевірок на кадр!}\)
При 60 FPS це майже 1 000 000 перевірок зіткнень щосекунди.
Щоб масштабувати свою гру, ви повинні застосувати Broad-Phase Spatial Partitioning.
Уніфікована просторова хеш-сітка
Ми ділимо екран на двовимірну сітку рівномірних комірок (у Blocked: Pixel Panzer кожна комірка становить \(150 \times 150\) пікселів). Сутність перевіряє зіткнення лише з іншими сутностями, які знаходяться в тих самих клітинках сітки.
Проблема мобільної пам'яті: GC Thrashing
Якщо ваша SpatialGrid створює новий List<Point>() або новий List<Block>() кожного кадру, ви розподіляєте мегабайти сміття щохвилини в керованій купі.
У середовищі виконання Android Mono це запускає часті Gen-0 Garbage Collections, спричиняючи:
mono runtime: суперечка рідного блокування (mono_class_is_subclass_)
Гра запинається, а Google Play позначає вашу гру попередженнями про ANR!
Рішення: багаторазові скретч-буфери
Ось просторова хеш-сітка з нульовим розподілом із CollisionManager.cs у 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);
}
}
}
}
}
}
}
}
Завдяки повторному використанню _scratchCells і _scratchCheckedEntities, весь етап зіткнення виконується з 0 байтами виділення купи на кадр.
8. Зведена порівняльна таблиця
| Техніка | Математична вартість | Ротаційна підтримка | Безпечне тунелювання? | Найкращий варіант використання в MonoGame |
|---|---|---|---|---|
AABB (Rectangle.Intersects) |
Надшвидкий (~4 цілих порівняння) | Ні | Ні | Сітка блоків, куля широкофазна, UI |
Коло (DistanceSquared) |
Дуже швидкий (3 муль, 0 sqrt) | Так (Інваріант) | Ні | Круглі кораблі, вогняні кулі, енергетичні сфери |
Коло проти прямокутника (Затискач) |
Швидко (Local MathHelper.Clamp) | Так | Ні | Круговий гравець, що пересувається по вузьких стінах |
| Swept Ray (Slab CCD) | Помірний (параметричний Raycast) | Так | Так | Швидкісні снайперські кулі, рельсотрони, лазери |
Pixel-Perfect (IntersectsPixel) |
Selective (Restricted Sub-Rect) | Так | Ні | Нерегулярні контури спрайтів, чесні хітбокси |
| Просторова сітка (Zero-GC) | Широка фаза (\(O(N^2) \to O(N)\)) | Н/З | Н/З | Щільні хвилі, пекло куль, Android 60 FPS |
Покази реального виробництва: Arar Games
Ці архітектури зіткнень не є теоретичними експериментами — вони є справжньою інженерною основою, на якій працюють наші комерційно випущені назви:
- Заблоковано: Pixel Panzer: Наша ретро-аркадна гра про виживання в танках у Google Play і Microsoft Store. Він має нашу повну дворівневу систему зіткнень: просторову хеш-сітку з нульовим розподілом, яка фільтрує падаючі блоки, і перевірку ідеального контакту для танків, винищувачів, баштових снарядів і елементарних боєприпасів ColorWheel.
- Paint Trek: наш стрімкий космічний шутер із зіткненнями за обертовим колом, безперервним трансляцією променів і системами протиракетного захисту зі змінним об’ємом.
Висновок і наступні кроки
MonoGame дає вам можливість розробити систему виявлення зіткнень, яка ідеально відповідає потребам вашої гри. Дотримуючись дорогих перевірок Pixel-Perfect за допомогою швидких тестів AABB, використовуючи Квадратні відстані для кіл і усуваючи збирання сміття за допомогою Багаторазових буферів скретчів, ви можете забезпечити плавну продуктивність 60/120 FPS на консолі як на настільних, так і на мобільних платформах.
Ознайомтеся з нашими іграми в магазинах додатків, щоб побачити ці системи зіткнень у дії, і почніть впроваджувати ці шаблони у власні проекти MonoGame вже сьогодні!
Магазин посилань і ресурсів
- Заблоковано: Pixel Panzer у Google Play: Завантажити для Android
- Заблоковано: Pixel Panzer у Microsoft Store: Завантажити для Windows
- Framework MonoGame: monogame.net
Ключові слова та хештеги для 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