Обнаружение 2D-столкновений в MonoGame: от базовых прямоугольников до идеальных по пикселям систем с нулевым выделением памяти
При создании 2D-игр в визуальных движках, таких как Unity или Godot, обнаружение столкновений часто похоже на серию флажков инспектора: вы нажимаете BoxCollider2D или CircleCollider2D, прикрепляете Rigidbody и надеетесь, что шаг внутренней физики не заикается на мобильных устройствах.
Однако в MonoGame и C# вы полностью контролируете ситуацию. Никаких скрытых физических издержек, никакой нежелательной инерции вращения и никаких загадочных всплесков сбора мусора (GC), которые крадут бюджет вашего кадра.
В Arar Games, когда мы создавали Blocked: Pixel Panzer и Paint Trek, нашим аркадным игровым циклам требовалось обрабатывать сотни высокоскоростных вражеских пуль, взрывающиеся кирпичные решетки, вращающиеся башни танков, пролеты истребителей и противочастичные щиты со скоростью от 60 до 120 кадров в секунду на ПК с Windows и устройствах Android. О физическом движке общего назначения не могло быть и речи — нам нужна была специально созданная многоуровневая архитектура коллизий.
В этом подробном, основанном на коде руководстве мы начнем с абсолютных основ столкновения MonoGame («Rectangle.Intersects» с реальными спрайтами «Bullet» и «Enemy») и перейдем к расширенным проверкам кругов, смешанным ограничениям, антитуннельным рассылкам лучей, Pixel-Perfect Collision и Пространственным сеткам с нулевым распределением, оптимизированным для выживания мобильных 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)
Самая фундаментальная проверка 2D-столкновений в MonoGame — это тест Ограничительная рамка с выравниванием по оси (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;
}
Если все четыре условия соблюдены, прямоугольники перекрываются. Если хотя бы одно условие не выполнено, их разделяет пустая ось, и столкновение невозможно.
Реальный игровой код: Bullet против Enemy в 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: коллекция была изменена. Итерация назад позволяет безопасно удалить элемент без проблем с переиндексацией памяти.
Секрет аркад: «Справедливые хитбоксы» с помощью «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\)
Реализация 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 кадрах в секунду), объект, движущийся со скоростью 1800 пикселей в секунду, проходит 30 пикселей за один кадр. Если толщина корпуса противника всего 15 пикселей, пуля находилась перед противником на первом кадре и полностью позади противника на втором кадре.
Frame 1: [ Bullet ] ---> | Enemy Wall |
Frame 2: | Enemy Wall | ---> [ Bullet ]
(NO HIT DETECTED!)
Решение: сегмент по стреловидности вместо прямоугольника (метод плиты)
Вместо проверки одной точки мы тестируем весь сегмент линии, соединяющий положение маркера в кадре 1 («предыдущая позиция») с кадром 2 («текущая позиция»).
Вот рабочий метод пересечения плит рейкастинга из нашего сопутствующего названия 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: Pixel-Perfect Collision.
В ретро-танковых шутерах или воздушных боях космических кораблей неправильные формы (ствола танков, крылья, кабины пилотов) окружены прозрачными пикселями в текстуре спрайта. Когда вражеская ракета попадает в это прозрачное пространство, игроки сразу это замечают.
Пиксельная коллизия проверяет фактические альфа-каналы (прозрачность) перекрывающихся текстур. Если два непрозрачных пикселя перекрываются в одной и той же мировой координате, произошло настоящее физическое попадание.
Фатальная ошибка: 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 кадров в секунду и немедленно вызовет ошибки Android ANR!
Проверенное в боях решение из Blocked: Pixel Panzer
В Blocked: Pixel Panzer наш Sprite.cs реализует ультраоптимизированную процедуру IntersectsPixel, построенную на основе 5 важнейших правил:
- Кэш статического цветового массива:
GetDataвызывается ровно один раз при загрузке текстуры и сохраняется вDictionary<Texture2D, Color[]>. - Защита раннего выхода AABB: Если
Bounds.Intersects(other.Bounds)имеет значение false, мы немедленно выходим из игры. 99% проверок устраняются еще до проверки одного пикселя. - Атлас текстур и поддержка SourceRectangle: обрабатывает спрайты, упакованные в атласы текстур с использованием смещений исходного прямоугольника.
- Окно расчета перекрытия: Мы просматриваем только точный прямоугольник пересечения между обоими спрайтами («Math.Max(a.Top, b.Top)» и т. д.).
- Альфа-короткое замыкание: Если пиксель спрайта А прозрачный («A <= 20»), спрайт B полностью пропускается.
- Подъем свойств. Доступ к виртуальным свойствам («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 миллисекунды на современном оборудовании Android и настольных системах.
7. Уровень 6: Широкофазная пространственная сетка и мобильный сборщик мусора с нулевым распределением
Представьте, что в вашей игре 100 блоков, 60 пуль и 20 врагов. Если вы проверяете каждую сущность на соответствие каждой другой сущности, используя вложенные циклы for, вы выполняете:
\(\frac{180 \times 179}{2} = 16 110 \text{ проверок на кадр!}\)
При 60 кадрах в секунду это почти 1 000 000 проверок коллизий каждую секунду.
Чтобы масштабировать свою игру, вам необходимо внедрить Широкофазовое пространственное разделение.
Единая пространственная хеш-сетка
Мы делим экран на двумерную сетку из однородных ячеек (в Blocked: Pixel Panzer каждая ячейка имеет размер \(150\times 150\) пикселей). Объект тестирует столкновения только с другими объектами, которые находятся в тех же ячейках сетки.
Проблема с мобильной памятью: перебор GC
Если ваш SpatialGrid создает новый List
В среде выполнения Android Mono это приводит к частым сборкам мусора Gen-0, вызывая:
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 («Прямоугольник.Пересечения») | Сверхбыстрый (~4 целочисленных сравнения) | Нет | Нет | Блоки сетки, широкая фаза маркера, пользовательский интерфейс |
| Круг («DistanceSquared») | Очень быстро (3 мул, 0 кв.р.) | Да (инвариант) | Нет | Круглые корабли, огненные шары, энергетические сферы |
| Круг против Коробки («Зажим») | Быстро (локальный MathHelper.Clamp) | Да | Нет | Круговой игрок, перемещающийся по узким стенам |
| Свип-луч (пластичная ПЗС-матрица) | Умеренный (параметрический Raycast) | Да | Да | Быстрые снайперские пули, рельсотроны, лазеры |
| Pixel-Perfect («IntersectsPixel») | Выборочный (ограниченный субпрямой) | Да | Нет | Неправильные контуры спрайтов, справедливые хитбоксы |
| Пространственная сетка (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 кадров в секунду как на настольных, так и на мобильных платформах.
Посетите наши игры в магазинах приложений, чтобы увидеть эти системы столкновений в действии, и начните внедрять эти шаблоны в свои собственные проекты MonoGame уже сегодня!
Ссылки и ресурсы магазинов
- Заблокировано: Pixel Panzer в Google Play: Загрузить для Android
- Заблокировано: Pixel Panzer в Microsoft Store: Загрузить для Windows
- MonoGame Framework: 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