Java 中兩張影像之間的碰撞偵測
1.概述
碰撞檢測是遊戲開發、電腦圖形和模擬軟體的重要組成部分。在 Java 中,開發人員可以使用各種方法來偵測影像(或精靈)碰撞,這取決於應用程式的複雜性和效能需求。
在本教程中,我們將探討兩個影像之間的碰撞偵測的工作原理,涵蓋基本邊界形狀以及更高級的基於像素的方法。
2. Java 的 Swing 和 AWT 函式庫
我們將使用Swing 作為 GUI 鷹架,例如JFrame
和JPanel
,並使用 AWT 進行渲染和幾何圖形。我們還需要考慮到這並沒有針對高效能渲染進行最佳化。一些缺點是缺乏循環系統、複雜場景中的影像渲染速度較慢以及所有物理和碰撞的手動實現。
有多種方法可以設定循環系統,例如Timer
或Thread
。對於本文,我們將使用Thread;
因此,讓我們建立一個擴展JPanel
並實作Runnable
類別:
public class Game extends JPanel implements Runnable
這將允許我們繪製元件並為我們的執行Thread
實作運行方法:
gameThread = new Thread(this);
gameThread.start();
public void run() {
while (!collided) {
repaint();
try {
Thread.sleep(16);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
Thread.sleep(16)
應該可以帶給我們每秒大約 60 幀的速度。
我們還需要一個類別來儲存遊戲物件數據,以便我們可以輕鬆地操作圖像:
public class GameObject
讓我們加入一個建構函數和一些方法來幫助我們:
public GameObject(int x, int y, BufferedImage image) {
this.x = x;
this.y = y;
this.image = image;
this.width = image.getWidth();
this.height = image.getHeight();
}
public void move(int dx, int dy) {
x += dx;
y += dy;
}
public void draw(Graphics g) {
g.drawImage(image, x, y, null);
}
我們將在達斯維達和盧克天行者之間創造一場史詩般的決鬥,因此讓我們加載兩個緩衝圖像並創建我們的遊戲對象來使用:
BufferedImage vaderImage = ImageIO.read(new File("src/main/resources/images/vader.png"));
BufferedImage lukeImage = ImageIO.read(new File("src/main/resources/images/luke.png"));
vader = new GameObject(170, 370, vaderImage);
luke = new GameObject(1600, 370, lukeImage);
我們需要做的最後一件事是實作paintComponent()
方法來繪製圖像,並在發生碰撞時顯示一些文字:
protected void paintComponent(Graphics g) {
super.paintComponent(g);
vader.draw(g);
luke.draw(g);
if (collided) {
g.setColor(Color.RED);
g.setFont(new Font("SansSerif", Font.BOLD, 50));
g.drawString("COLLISION!", getWidth() / 2 - 100, getHeight() / 2);
}
}
3. 邊界形狀碰撞偵測
載入影像並設定Game
類別後,讓我們探索邊界框作為偵測碰撞的第一種方法。
3.1.邊界框
邊界框偵測是最簡單、最快的技術。它檢查兩幅影像的矩形區域(通常由x
、 y
、 width
和height
定義)是否重疊。 Java 的 AWT 包中的Rectangle
類別使這種方法變得簡單。讓我們利用GameObject
類別並新增另一個方法:
public Rectangle getRectangleBounds() {
return new Rectangle(x, y, width, height);
}
我們現在可以根據圖像的位置和大小建立矩形,並檢查它們是否相交。在我們的run()
方法中,我們需要添加一些東西來使影像相互移動,以及檢查它們何時發生碰撞:
vader.move(2, 0);
luke.move(-2, 0);
if (vader.getRectangleBounds().intersects(luke.getRectangleBounds())) {
collided = true;
}
一旦我們運行遊戲,邊界框碰撞看起來如下:
3.2.圓的面積
我們可以透過改變所使用的形狀來提高碰撞檢測的準確性。使用Ellipse2D
可能更有意義。
讓我們在GameObject
類別中加入另一個方法來幫助我們。它根據圖像的位置和大小返回一個Ellipse2D
:
public Ellipse2D getEllipseBounds() {
return new Ellipse2D.Double(x, y, width, height);
}
因為intersects
僅適用於矩形,所以我們無法直接檢查兩個橢圓之間的碰撞。因此,我們必須使用Area
類別並更新我們的方法來傳回一個區域而不是一個橢圓:
public Area getEllipseAreaBounds() {
Ellipse2D.Double coll = new Ellipse2D.Double(x, y, width, height);
return new Area(coll);
}
方法準備好後,讓我們為我們的角色建立兩個新區域:
Area areaVader = vader.getEllipseAreaBounds();
Area areaLuke = luke.getEllipseAreaBounds();
最後,我們將對第一個區域使用intersect
並將第二個區域作為參數傳遞。這將改變原始Area
對象,修改areaVader
以使其僅表示它與areaLuke
之間的重疊部分。
如果我們有一個交集, areaVader
現在包含重疊區域。如果沒有交集, areaVader
就會變成空。
因此,讓我們檢查它是否為空。如果它不為空,則表示發生了碰撞:
areaVader.intersect(areaLuke);
if (!areaVader.isEmpty()) {
collided = true;
}
我們可以看到,當我們運行遊戲並且橢圓相交時,檢測到碰撞,並出現訊息。此外,我們的圖像現在比以前更接近這種形狀,因此變得更加準確:
3.3.圓距離檢查
當使用Area
時,建立一個包含超過 1,000 個物件的高效能遊戲可能會變得很繁重,在這種情況下,我們可以使用圓距離檢查來代替。這種計算比真正的交叉測試更快,但依賴一些特定的假設。
具體來說,我們需要記住,這只適用於寬度和高度相等的橢圓——真正的圓形:
double dx = circleVader.getCenterX() - circleLuke.getCenterX();
double dy = circleVader.getCenterY() - circleLuke.getCenterY();
double distance = Math.sqrt(dx * dx + dy * dy);
double radiusVader = circleVader.getWidth() / 2.0;
double radiusLuke = circleLuke.getWidth() / 2.0;
boolean collided = distance < radiusVader + radiusLuke;
如果兩個圓心之間的距離小於它們的半徑總和,我們可以假設它們發生碰撞:
這種方法在表示影像碰撞方面可能並不完美,但在性能方面要好得多。
3.4.多邊形碰撞
現在,讓我們使用自訂形狀來設定碰撞檢測。在這種情況下,我們需要使用具有多個座標的多邊形。
首先,讓我們建立兩個要使用的 x 和 y 點的數組,以及另外兩個表示影像 x 和 y 值的偏移量的陣列:
int[] xPoints = new int[4];
int[] yPoints = new int[4];
int[] xOffsets = {100, 200, 100, 0};
int[] yOffsets = {0, 170, 340, 170};
偏移量設定為代表我們的影像的菱形。
接下來,我們將建立一個方法來幫助我們解決多邊形邊界以及要使用的兩個Polygon
物件:
public Polygon getPolygonBounds(int imgX, int imgY) {
for (int i = 0; i < xOffsets.length; i++) {
xPoints[i] = imgX + xOffsets[i];
yPoints[i] = imgY + yOffsets[i];
}
return new Polygon(xPoints, yPoints, xOffsets.length);
}
Polygon polyVader = getPolygonBounds(vader.x, vader.y);
Polygon polyLuke = getPolygonBounds(luke.x, luke.y);
最後,讓我們為多邊形引入兩個Area
物件並檢查它們是否相交:
Area areaVader = new Area(polyVader);
Area areaLuke = new Area(polyLuke);
areaVader.intersect(areaLuke);
if (!areaVader.isEmpty()) {
collided = true;
}
因此,當兩個形狀相交時就會發生碰撞:
透過將形狀轉換為Area
,我們甚至可以比較不同的形狀。因此,我們也可以輕鬆檢查橢圓和多邊形之間的碰撞。
4. 像素完美碰撞
當視覺準確性至關重要時,邊界框就顯得不足了。這就是像素完美碰撞偵測的用武之地。此方法在像素層級檢查重疊區域,偵測兩幅影像的非透明像素是否相交。
每個像素表示為一個 32 位元整數,分為四個 8 位元分量:紅色、綠色、藍色和控制透明度的 alpha。
讓我們先借助Math
庫來建立一個偵測碰撞的方法:
public boolean isPixelCollision(BufferedImage img1, int x1, int y1, BufferedImage img2, int x2, int y2) {
int top = Math.max(y1, y2);
int bottom = Math.min(y1 + img1.getHeight(), y2 + img2.getHeight());
int left = Math.max(x1, x2);
int right = Math.min(x1 + img1.getWidth(), x2 + img2.getWidth());
if (right <= left || bottom <= top) return false;
for (int y = top; y < bottom; y++) {
for (int x = left; x < right; x++) {
int img1Pixel = img1.getRGB(x - x1, y - y1);
int img2Pixel = img2.getRGB(x - x2, y - y2);
if (((img1Pixel >> 24) & 0xff) != 0 && ((img2Pixel >> 24) & 0xff) != 0) {
return true;
}
}
}
return false;
}
首先,我們計算兩個圖像在螢幕上繪製並可能重疊的矩形。
其次,我們檢查兩個矩形是否重疊,以避免不必要的逐像素檢查。
第三,我們開始循環遍歷螢幕空間中兩個影像重疊的區域,並取得影像內相對於左上角的像素,從全域螢幕空間轉換為影像局部像素空間。
最後,我們將像素的位數移動 24 位元來隔離 alpha 通道,然後將其提取出來。如果 alpha 值不為零,則該像素被視為不透明。如果兩個像素在相同的螢幕位置都是不透明的,則傳回 true。
以下是盧克擊敗達斯維達的方式,他用光劍直接擊中了達斯維達的頭部:
5. 結論
在本文中,我們研究了原生 Java Swing 和 AWT 方法,並介紹了處理碰撞偵測的方法,從簡單形狀到多邊形,再到像素完美碰撞。
重要的是要認識到不同的碰撞檢測方法,例如邊界框、圓圈或像素完美檢查,直接影響準確性和計算成本。因此,我們必須考慮這些因素,並在準確性和速度之間做出權衡,以最好地滿足我們的應用程式的需求。
像往常一樣,可以在GitHub上找到程式碼。