การตรวจจับการชนกันของ 2D ใน MonoGame: จากสี่เหลี่ยมพื้นฐานไปจนถึงระบบที่สมบูรณ์แบบของพิกเซลที่มีการจัดสรรเป็นศูนย์
เมื่อสร้างเกม 2 มิติด้วยเอ็นจิ้นภาพ เช่น Unity หรือ Godot การตรวจจับการชนมักจะรู้สึกเหมือนเป็นชุดของช่องตรวจสอบ: คุณตบ BoxCollider2D หรือ CircleCollider2D แนบ Rigidbody และหวังว่าขั้นตอนทางฟิสิกส์ภายในจะไม่กระตุกบนอุปกรณ์มือถือ
อย่างไรก็ตาม ใน MonoGame และ C# คุณเป็นผู้ควบคุมทั้งหมด ไม่มีค่าใช้จ่ายด้านฟิสิกส์ที่ซ่อนอยู่ ไม่มีความเฉื่อยในการหมุนที่ไม่ต้องการ และไม่มีการเก็บขยะลึกลับ (GC) ที่เพิ่มขึ้นอย่างรวดเร็วเพื่อขโมยงบประมาณเฟรมของคุณ
ที่ Arar Games เมื่อเราสร้าง Blocked: Pixel Panzer และ Paint Trek เกมอาร์เคดของเราจำเป็นต้องประมวลผลกระสุนศัตรูความเร็วสูงหลายร้อยนัด ตะแกรงอิฐที่ระเบิด ป้อมรถถังที่หมุนได้ เครื่องบินขับไล่ไอพ่นบินผ่าน และเกราะป้องกันอนุภาคที่ 60 ถึง 120 FPS ทั้งบนพีซี Windows และอุปกรณ์ Android เครื่องยนต์ฟิสิกส์อเนกประสงค์ไม่เป็นปัญหา เราต้องการสถาปัตยกรรมการชนกันแบบเป็นชั้นที่สร้างขึ้นตามจุดประสงค์
ในคู่มือที่ครอบคลุมและขับเคลื่อนด้วยโค้ดนี้ เราจะเริ่มต้นจากพื้นฐานที่แท้จริงของการชนกันของ MonoGame (สี่เหลี่ยมผืนผ้า ตัดกัน กับสไปรต์ กระสุน และ ศัตรู จริง) และสร้างการตรวจสอบวงกลมขั้นสูง การหนีบแบบผสม เรย์แคสต์ต่อต้านอุโมงค์ การชนกันของพิกเซลที่สมบูรณ์แบบ ระดับการผลิต และ กริดเชิงพื้นที่ที่มีการจัดสรรเป็นศูนย์ ปรับให้เหมาะสมเพื่อความอยู่รอดของ 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: การชนที่ง่ายที่สุด – สี่เหลี่ยมผืนผ้าตัดกัน (AABB)
การตรวจสอบการชนกันแบบ 2 มิติขั้นพื้นฐานที่สุดใน MonoGame คือการทดสอบ Axis-Aligned Bounding Box (AABB) คำว่า "จัดแนวแกน" หมายความง่ายๆ ว่าขอบของสี่เหลี่ยมผืนผ้าขนานกับแกน \(X\) และ \(Y\) ของหน้าจอ (ไม่มีการหมุน)
MonoGame มีวิธีการในตัวที่รวดเร็ว: Rectangle.Intersects(Rectangle value)
วิธีการทำงานของ สี่เหลี่ยมตัดกัน ภายใต้ประทุน
ภายใต้พื้นผิว 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 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);
}
}
เคล็ดลับด้านประสิทธิภาพ: โปรดสังเกตว่าเราวนซ้ำไปข้างหลัง (
for (int i = list.Count - 1; i >= 0; i--))! หากคุณใช้foreachและพยายามเรียก_bullets.Remove(bullet)C# จะส่งInvalidOperationException: Collection was modifiedการทำซ้ำแบบย้อนกลับช่วยให้สามารถลบองค์ประกอบได้อย่างปลอดภัยโดยไม่มีปัญหาในการจัดทำดัชนีหน่วยความจำใหม่
ความลับของอาร์เคด: "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 กล่องขอบเขตที่จัดแนวแกนจะต้องขยายเพื่อปิดมุมที่หมุนอยู่ ส่งผลให้เกิด "การชนกันของภาพลวงตา" ที่น่าหงุดหงิดในอากาศว่างเปล่า
สำหรับดาวเคราะห์น้อยทรงกลม ลูกกลมพลังงานกลับบ้าน และยานอวกาศที่หมุนได้ Bounding Circles ถือเป็นทางออกที่ดี
กับดักรากที่สอง
วงกลมสองวงชนกันเมื่อระยะห่างระหว่างจุดศูนย์กลางน้อยกว่าหรือเท่ากับผลรวมของรัศมี:
\(\ข้อความ{ระยะทาง}(C_A, C_B) \le r_A + r_B\)
ในโค้ด การคำนวณระยะทางแบบยุคลิดจะใช้ Math.Sqrt (หรือ Vector2.Distance) อย่างไรก็ตาม การคำนวณรากที่สองในลูปด้วยกระสุน 200 นัด จะเผาผลาญรอบ CPU ที่ไม่จำเป็นนับร้อย!
เมื่อเปรียบเทียบ ระยะทางกำลังสอง กับ ผลรวมรัศมีกำลังสอง เราจะกำจัดรากที่สองได้อย่างสมบูรณ์:
\(\ข้อความ{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 FPS) วัตถุที่เคลื่อนที่ด้วยความเร็ว 1,800 พิกเซลต่อวินาทีจึงเดินทาง 30 พิกเซลในเฟรมเดียว หากตัวถังศัตรูมีความหนาเพียง 15 พิกเซล กระสุนจะอยู่ด้านหน้าศัตรูในเฟรมที่ 1 และอยู่ด้านหลังศัตรูโดยสิ้นเชิงในเฟรม 2
Frame 1: [ Bullet ] ---> | Enemy Wall |
Frame 2: | Enemy Wall | ---> [ Bullet ]
(NO HIT DETECTED!)
วิธีแก้ปัญหา: Swept Segment vs. Box (วิธี Slab)
แทนที่จะทดสอบจุดเดียว เราจะทดสอบส่วนของเส้นทั้งหมดที่เชื่อมต่อตำแหน่งของสัญลักษณ์แสดงหัวข้อย่อยในเฟรม 1 (previousPosition) กับเฟรม 2 (currentPosition)
ต่อไปนี้เป็นวิธีการผลิต raycasting slab-intersection จากชื่อร่วมของเรา 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 ทักษะ Laser Beam ต่อเนื่องของผู้เล่นจะใช้การฉายรังสีที่แม่นยำนี้เพื่อฟันผ่านแถวของบล็อกจากมากไปน้อยโดยไม่พลาดการชนกันแม้แต่ครั้งเดียว
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() ในระหว่างรันไทม์บังคับให้ CPU รอให้ไปป์ไลน์ GPU ล้าง และดึงหน่วยความจำพื้นผิวกลับมาบนบัส การทำเช่นนี้กับสไปรท์หลายสิบตัวทุกเฟรมจะทำให้เกมของคุณแทงค์จาก 60 FPS เป็น 5 FPS และทริกเกอร์ 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% ของการตรวจสอบจะหมดไปก่อนที่จะตรวจสอบพิกเซลเดียว - รองรับ Atlas พื้นผิวและ
SourceRectangle: จัดการสไปรท์ที่อัดแน่นอยู่ใน Atlas พื้นผิวโดยใช้การชดเชยสี่เหลี่ยมของแหล่งที่มา - หน้าต่างที่ทับซ้อนกันจากการคำนวณ: เราจะวนซ้ำเฉพาะสี่เหลี่ยมผืนผ้าที่ตัดกันระหว่างสไปรต์ทั้งสองเท่านั้น (
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 ครั้งทุกๆ วินาที
หากต้องการปรับขนาดเกม คุณต้องใช้ การแบ่งพาร์ติชันเชิงพื้นที่แบบกว้าง
ตารางแฮชเชิงพื้นที่ที่สม่ำเสมอ
เราแบ่งหน้าจอออกเป็นตาราง 2 มิติของเซลล์ที่เหมือนกัน (ใน ถูกบล็อก: Pixel Panzer แต่ละเซลล์มีราคา 150 ดอลลาร์ \คูณ 150 ดอลลาร์พิกเซล) เอนทิตีจะทดสอบการชนกับเอนทิตีอื่น ๆ ที่อยู่ในเซลล์กริดเดียวกันเท่านั้น
ปัญหาหน่วยความจำมือถือ: GC Thrashing
หาก SpatialGrid ของคุณสร้าง new List<Point>() หรือ new List<Block>() ทุกเฟรม คุณจะจัดสรรขยะเมกะไบต์ทุกๆ นาทีบนฮีปที่ได้รับการจัดการ
บนรันไทม์แบบโมโนของ Android สิ่งนี้จะทริกเกอร์คอลเลกชันขยะ Gen-0 บ่อยครั้ง ทำให้เกิด:
รันไทม์โมโน: การโต้แย้งแบบเนทิฟล็อค (mono_class_is_subclass_)
เกมกระตุกและ Google Play ตั้งค่าสถานะเกมของคุณด้วยคำเตือน ANR!
วิธีแก้ปัญหา: บัฟเฟอร์รอยขีดข่วนแบบใช้ซ้ำได้
นี่คือ Zero-Allocation Spatial Hash Grid จาก 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) | ไม่ | ไม่ | บล็อกกริด, หัวข้อย่อยแบบกว้าง, UI |
วงกลม (DistanceSquared) |
เร็วมาก (3 หน่วย, 0 sqrt) | ใช่ (ไม่เปลี่ยนแปลง) | ไม่ | เรือทรงกลม ลูกไฟ ลูกกลมพลังงาน |
วงกลมกับกล่อง (แคลมป์) |
รวดเร็ว (MathHelper.Clamp ในเครื่อง) | ใช่ | ไม่ | ผู้เล่นวงกลมนำทางกำแพงแน่น |
| Swept Ray (Slab CCD) | ปานกลาง (Parametric Raycast) | ใช่ | ใช่ | กระสุนสไนเปอร์เร็ว, ปืนเรลกัน, เลเซอร์ |
พิกเซลสมบูรณ์แบบ (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 ที่รวดเร็ว โดยใช้ Squared Distances สำหรับวงกลม และกำจัดการรวบรวมขยะด้วย Reusable Scratch Buffers คุณสามารถส่งมอบประสิทธิภาพ 60/120 FPS ที่ราบรื่นของคอนโซลทั้งบนแพลตฟอร์มเดสก์ท็อปและมือถือ
ลองเข้าไปดูเกมของเราใน App Store เพื่อดูการทำงานของระบบการชนกัน และเริ่มปรับใช้รูปแบบเหล่านี้ในโปรเจ็กต์ MonoGame ของคุณเองวันนี้!
ลิงค์ร้านค้าและทรัพยากร
- ถูกบล็อก: Pixel Panzer บน Google Play: ดาวน์โหลดสำหรับ Android
- ถูกบล็อก: Pixel Panzer บน Microsoft Store: ดาวน์โหลดสำหรับ Windows
- กรอบงาน MonoGame: monogame.net
คำหลักและแฮชแท็ก SEO
คำสำคัญ: การตรวจจับการชนกันของ MonoGame 2D, การพัฒนาเกม C#, สี่เหลี่ยมผืนผ้า ตัดกัน MonoGame, การชนกันของพิกเซลที่สมบูรณ์แบบ C#, ตารางแฮชเชิงพื้นที่ MonoGame, การชนกันของรังสีแบบกวาดต่อเนื่อง, การเพิ่มประสิทธิภาพเกมอินดี้, ลูปเกมการจัดสรรเป็นศูนย์, การเพิ่มประสิทธิภาพ Android MonoGame, เกม Arar, Pixel Panzer ที่ถูกบล็อก, Paint Trek
#MonoGame #CSharp #GameDev #IndieDev #GamePhysics #DotNet #2DGameDev #MobileGameDev #เพิ่มประสิทธิภาพการทำงาน #CleanCode #BlockedPixelPanzer #PaintTrek #ArarGames #GameProgramming
Hiç yorum yok:
Yorum Gönder