PNG图片干扰碰撞检测逻辑,未接触即触发游戏结束求助
小行星游戏PNG图片导致碰撞检测误触发的修复方案
问题背景
开发的小行星游戏中,使用PNG格式的小行星图片时,飞船与小行星仍有一段距离就触发游戏结束。排查后确定问题与PNG图片相关,当前碰撞检测代码及小行星类实现如下:
碰撞检测代码
for (int i = 0; i < asteroidCount; i++) { asteroids[i].move(); if (asteroids[i].getY() <= 0 || asteroids[i].getY() + asteroids[i].getAsteroidHeight() >= getHeight()) { asteroids[i].setYDir(-asteroids[i].getYDir()); } if (asteroids[i].getX() <= 0 || asteroids[i].getX() + asteroids[i].getAsteroidWidth() >= getWidth()) { asteroids[i].setXDir(-asteroids[i].getXDir()); } //Collision code for when the asteroid hits the ship if (ship.getX() < asteroids[i].getX() + asteroids[i].getAsteroidWidth() && ship.getX() + ship.getshipWidth() > asteroids[i].getX() && ship.getY() < asteroids[i].getY() + asteroids[i].getAsteroidHeight() &&ship.getY() + ship.getshipHeight() > asteroids[i].getY()) { //Game over, stop the timer t.stop(); JOptionPane.showMessageDialog(this, "Game Over! Asteroid hit the ship."); System.exit(0); } } repaint();
Asteroids类实现
import javax.swing.*; import java.awt.*; public class Asteroids { //Declare fields private int xPos, yPos, asteroidHeight, asteroidWidth; private ImageIcon imgAsteroid; private int xDir, yDir; private Color c; private boolean useImage; //Create a 3rd constructor that creates a ball using an image public Asteroids(int x, int y, ImageIcon img) { xPos = x; yPos = y; imgAsteroid = img; asteroidWidth = img.getIconWidth(); asteroidHeight = img.getIconHeight(); useImage = true; xDir = 2; yDir = 2; } //Accessor methods to return the values of the fields public int getX() { return xPos; } public int getY() { return yPos; } public int getAsteroidWidth() { return asteroidWidth; } public int getAsteroidHeight() { return asteroidHeight; } public Image getImage() { return imgAsteroid.getImage(); } //Mutator methods to change the values of fields public void setAsteroidWidth(int width) { asteroidWidth = width; } public void setAsteroidHeight(int height) { asteroidHeight = height; } public void setColor(Color col) { c = col; } public void setImageIcon(ImageIcon imgAsteroids) { imgAsteroid = imgAsteroids; } public void setX(int x) { xPos = x; } public void setY(int y) { yPos = y; } public void draw(Graphics2D g2) { if (useImage == false) { g2.setColor(c); g2.fillOval(xPos, yPos, asteroidWidth, asteroidHeight); } else { g2.drawImage(getImage(), xPos, yPos, null); } } //Overload the move method so the parameters determine how many pixels to move public void move() { xPos += xDir; yPos += yDir; } public int getXDir() { return xDir; } public void setXDir(int xDir) { this.xDir = xDir; } public int getYDir() { return yDir; } public void setYDir(int yDir) { this.yDir = yDir; } }
问题根源
当前碰撞检测使用的是图片的完整矩形边界,但PNG图片通常包含大量透明区域(比如小行星是不规则形状,图片画布为矩形,周围全是透明像素)。碰撞检测会把这些透明区域也判定为小行星的碰撞范围,导致飞船还未接触到实际的小行星图形就触发碰撞。
修复方案
方案1:手动调整碰撞边界(快速简单)
不需要修改碰撞检测逻辑,而是根据小行星的实际可视区域,手动设置更小的碰撞宽高,并调整位置偏移让碰撞中心与图片中心对齐:
public Asteroids(int x, int y, ImageIcon img) { xPos = x; yPos = y; imgAsteroid = img; // 保留图片原始宽高用于绘制 int originalWidth = img.getIconWidth(); int originalHeight = img.getIconHeight(); // 设置实际碰撞的宽高(比如取图片宽高的70%,根据实际图形调整) asteroidWidth = (int)(originalWidth * 0.7); asteroidHeight = (int)(originalHeight * 0.7); // 调整坐标,让碰撞矩形与图片中心对齐 xPos += (originalWidth - asteroidWidth) / 2; yPos += (originalHeight - asteroidHeight) / 2; useImage = true; xDir = 2; yDir = 2; }
如果不想修改绘制用的坐标,可以给Asteroids类单独添加collisionX、collisionY、collisionWidth、collisionHeight属性,碰撞检测时使用这些属性,绘制时仍用原始的xPos、yPos和图片宽高。
方案2:像素级碰撞检测(精确)
通过检测两个物体的非透明像素是否重叠来触发碰撞,完全忽略透明区域:
- 给Asteroids类添加获取碰撞掩码的方法:
private boolean[][] collisionMask; // 在构造函数中初始化掩码 public Asteroids(int x, int y, ImageIcon img) { // ... 原有代码 ... collisionMask = generateCollisionMask(img.getImage()); } private boolean[][] generateCollisionMask(Image img) { int width = img.getWidth(null); int height = img.getHeight(null); boolean[][] mask = new boolean[width][height]; PixelGrabber pg = new PixelGrabber(img, 0, 0, width, height, true); try { pg.grabPixels(); int[] pixels = (int[]) pg.getPixels(); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { // 判断像素是否非透明(alpha通道不为0) mask[x][y] = ((pixels[y * width + x] >> 24) & 0xFF) != 0; } } } catch (InterruptedException e) { e.printStackTrace(); } return mask; } // 提供获取掩码的方法 public boolean[][] getCollisionMask() { return collisionMask; }
- 修改碰撞检测逻辑:
for (int i = 0; i < asteroidCount; i++) { asteroids[i].move(); // ... 原有边界反弹逻辑 ... // 计算两个物体的重叠区域 int overlapXStart = Math.max(ship.getX(), asteroids[i].getX()); int overlapXEnd = Math.min(ship.getX() + ship.getshipWidth(), asteroids[i].getX() + asteroids[i].getAsteroidWidth()); int overlapYStart = Math.max(ship.getY(), asteroids[i].getY()); int overlapYEnd = Math.min(ship.getY() + ship.getshipHeight(), asteroids[i].getY() + asteroids[i].getAsteroidHeight()); // 无重叠区域则跳过 if (overlapXStart >= overlapXEnd || overlapYStart >= overlapYEnd) { continue; } boolean[][] asteroidMask = asteroids[i].getCollisionMask(); boolean[][] shipMask = ship.getCollisionMask(); // 假设Ship类也实现了相同的掩码逻辑 boolean isCollided = false; // 遍历重叠区域的每个像素 for (int x = overlapXStart; x < overlapXEnd; x++) { for (int y = overlapYStart; y < overlapYEnd; y++) { // 转换为物体自身的局部坐标 int asteroidLocalX = x - asteroids[i].getX(); int asteroidLocalY = y - asteroids[i].getY(); int shipLocalX = x - ship.getX(); int shipLocalY = y - ship.getY(); // 检查坐标是否在掩码范围内,且两个像素都非透明 if (asteroidLocalX >= 0 && asteroidLocalX < asteroidMask.length && asteroidLocalY >= 0 && asteroidLocalY < asteroidMask[0].length && shipLocalX >= 0 && shipLocalX < shipMask.length && shipLocalY >= 0 && shipLocalY < shipMask[0].length && asteroidMask[asteroidLocalX][asteroidLocalY] && shipMask[shipLocalX][shipLocalY]) { isCollided = true; break; } } if (isCollided) break; } if (isCollided) { t.stop(); JOptionPane.showMessageDialog(this, "Game Over! Asteroid hit the ship."); System.exit(0); } } repaint();
注意:像素级检测性能略低于矩形检测,若游戏中有大量小行星,建议提前缓存碰撞掩码,避免每次检测都重新生成。
内容的提问来源于stack exchange,提问作者user22660358
相关产品推荐
相关产品推荐

