如何基于旋转值动态偏移子弹的Bounding Circle碰撞检测边界
子弹碰撞圆动态偏移实现问题
我正在开发子弹 projectile 逻辑,为子弹配套了圆形碰撞边界(Bounding Circle),希望将该碰撞圆固定在子弹的头部位置,确保子弹撞击物体时可以正常触发碰撞。下方展示了子弹实体以及代表碰撞边界的蓝色圆圈:
右下角的示例是我想要实现的效果,但需要适配子弹的任意旋转角度,请问有什么方案可以实现碰撞圆的动态偏移?
相关代码实现
HitBox.java(对应蓝色碰撞圆实现)
package com.icyrelic.game.object; import com.icyrelic.game.object.entity.Entity; import com.jogamp.opengl.math.VectorUtil; import lombok.Getter; import lombok.Setter; public class HitBox { @Getter private GameObject object; @Getter @Setter private float width = 0, height = 0, xOffset = 0f, yOffset = 0f, radius = 0; public HitBox(GameObject object, float width, float height) { this.object = object; this.width = width; this.height = height; } public HitBox(GameObject entity, float radius) { this.object = object; this.radius = radius; } public boolean intersects(HitBox b) { boolean collisionX = getMinX() <= b.getMaxX() && getMaxX() >= b.getMinX(); boolean collisionY = getMinY() <= b.getMaxY() && getMaxY() >= b.getMinY(); return collisionX && collisionY; } public boolean willCollide(HitBox b, float x, float y) { //Circle vs Circle if(radius != 0 && b.getRadius() != 0) return circleCollide(x, y, b.getRadius(), b.getXOffset(), b.getYOffset()); //Circle vs AABB if((radius != 0 && b.getRadius() == 0) || (radius == 0 && b.getRadius() != 0)) return circleAABBCollusion(b, x, y); //AABB vs AABB boolean collisionX = getMinX() <= x+b.getWidth()/2 && getMaxX() >= x-b.getWidth()/2; boolean collisionY = getMinY() <= y+b.getHeight()/2 && getMaxY() >= y-b.getHeight()/2; return collisionX && collisionY; } private boolean circleCollide(float x2, float y2, float radius2, float xOffset2, float yOffset2) { double xDif = (object.getLocation().getX() + xOffset) - (x2 + xOffset2); double yDif = (object.getLocation().getY() + yOffset) - (y2 + yOffset2); double distanceSquared = xDif * xDif + yDif * yDif; return distanceSquared < (radius + radius2) * (radius + radius2); } private boolean circleAABBCollusion(HitBox b, float x, float y) { HitBox circle = radius == 0 ? b : this; HitBox aabb = radius == 0 ? this : b; float[] circleCenter = radius == 0 ? new float[] {x, y} : new float[] {object.getLocation().getX(), object.getLocation().getY()}; float[] aabbCenter = radius == 0 ? new float[] {object.getLocation().getX(), object.getLocation().getY()} : new float[] {x, y}; float closestX = circleCenter[0] + circle.getXOffset(); float closestY = circleCenter[1] + circle.getYOffset(); if(closestX < aabb.getMinX(aabbCenter[0])) closestX = aabb.getMinX(aabbCenter[0]); else if (closestX > aabb.getMaxX(aabbCenter[0])) closestX = aabb.getMaxX(aabbCenter[0]); if(closestY < aabb.getMinY(aabbCenter[1])) closestY = aabb.getMinY(aabbCenter[1]); else if (closestY > aabb.getMaxY(aabbCenter[1])) closestY = aabb.getMaxY(aabbCenter[1]); float dist = VectorUtil.distSquareVec3(new float[]{circleCenter[0]+circle.getXOffset(), circleCenter[1]+circle.getYOffset(), 1}, new float[]{closestX, closestY, 1}); return dist <= circle.getRadius() * circle.getRadius(); } private float getMinX() { return object.getLocation().getX()+xOffset - width/2; } private float getMaxX() { return object.getLocation().getX()+xOffset + width/2; } private float getMinY() { return object.getLocation().getY()+yOffset - height/2; } private float getMaxY() { return object.getLocation().getY()+yOffset + height/2;} private float getMinX(float x) { return x+xOffset - width/2; } private float getMaxX(float x) { return x+xOffset + width/2; } private float getMinY(float y) { return y+yOffset - height/2; } private float getMaxY(float y) { return y+yOffset + height/2;} public void setSize(float width, float height) { this.width = width; this.height = height; } public void setOffset(float x, float y) { this.xOffset = x; this.yOffset = y; } }
Bullet.java
package com.icyrelic.game.object.projectile.type; import com.icyrelic.game.Graphics.Animation; import com.icyrelic.game.Graphics.ImageResource; import com.icyrelic.game.object.projectile.Projectile; public class Bullet extends Projectile { public Bullet(float x, float y, float width, float height, float rotation, float rotationOffset) { super(x,y,width, height, rotation, rotationOffset); hitBox.setRadius(0.03f); hitBox.setYOffset(-0.12f); location.setRotationOffset(-0f); animations = new Animation[1]; ImageResource[] a = new ImageResource[1]; a[0] = new ImageResource("/Projectile/Bullet.png"); animations[0] = new Animation(a, 1); } @Override public void update() { //location.setRotation(0); } }
Projectile.java
package com.icyrelic.game.object.projectile; import com.icyrelic.game.Graphics.Graphics; import com.icyrelic.game.object.GameObject; import com.icyrelic.game.object.HitBox; import lombok.Getter; @Getter public abstract class Projectile extends GameObject { protected float speed = 1.0f; protected HitBox hitBox; public Projectile(float x, float y, float width, float height, float rotation, float rotationOffset) { super(x, y, width, height, rotation, rotationOffset); this.hitBox = new HitBox(this, width, height); } @Override public void render() { super.render(); Graphics.setColor(0,0, 1, 1); if(hitBox.getRadius() == 0) { Graphics.drawRect(location.getX() + hitBox.getXOffset(), location.getY() + hitBox.getYOffset(), hitBox.getWidth(), hitBox.getHeight(), false); } else { Graphics.drawHollowCircle(location.getX() + hitBox.getXOffset(), location.getY() + hitBox.getYOffset(), hitBox.getRadius()); } Graphics.setColor(1,0, 0, 1); //Graphics.drawRect(location.getX(), location.getY(), width/2, height/2, false); Graphics.setColor(1,1, 1, 1); Graphics.setColor(1,1, 1, 1); } }
GameObject.java
package com.icyrelic.game.object; import com.icyrelic.game.Graphics.Animation; import com.icyrelic.game.Graphics.Graphics; import com.icyrelic.game.world.Location; import lombok.Getter; @Getter public abstract class GameObject { protected Location location; protected float width = 1, height = 1; protected float xImageOffset = 0, yImageOffset = 0; protected Animation[] animations; protected int currentAnimation = 0; public GameObject(float x, float y, float width, float height, float rotation, float rotationOffset) { this.location = new Location(x, y, rotation, rotationOffset); this.width = width; this.height = height; } public void render () { animations[currentAnimation].play(); Graphics.setRotation(location.getRotation() + location.getRotationOffset()); Graphics.drawImage(animations[currentAnimation].getImage(), location.getX(), location.getY(), xImageOffset, yImageOffset, width, height); Graphics.setRotation(0); } public abstract void update (); }
已尝试的实现效果
我尝试套用Alberto提供的思路调整代码后得到了如下结果,效果接近正确方向但仍未完全符合要求:
hitBox.setXOffset(((float) Math.sin(location.getRotationRadians()) )); hitBox.setYOffset(((float) Math.cos(location.getRotationRadians()) ));

解决方案
1. 修复HitBox构造函数错误
当前HitBox的圆形碰撞构造函数存在赋值错误,会触发空指针异常,先修正:
// 原错误代码 public HitBox(GameObject entity, float radius) { this.object = object; this.radius = radius; } // 修正后 public HitBox(GameObject entity, float radius) { this.object = entity; this.radius = radius; }
2. 实现旋转偏移动态计算
你需要对子弹静止时的头部原始偏移量做2D旋转变换,而不是直接使用单位角度的sin/cos值,具体修改如下:
在Bullet.java中定义原始偏移量,并且在update方法中实时计算旋转后的偏移:
public class Bullet extends Projectile { // 子弹静止时头部的碰撞圆原始偏移量,和你之前固定设置的偏移一致 private static final float ORIGIN_OFFSET_X = 0f; private static final float ORIGIN_OFFSET_Y = -0.12f; public Bullet(float x, float y, float width, float height, float rotation, float rotationOffset) { super(x,y,width, height, rotation, rotationOffset); hitBox.setRadius(0.03f); location.setRotationOffset(-0f); animations = new Animation[1]; ImageResource[] a = new ImageResource[1]; a[0] = new ImageResource("/Projectile/Bullet.png"); animations[0] = new Animation(a, 1); } @Override public void update() { float radians = location.getRotationRadians(); // 2D 点旋转公式:(x,y)绕原点旋转θ弧度后得到 (x*cosθ - y*sinθ, x*sinθ + y*cosθ) float rotatedX = (float) (ORIGIN_OFFSET_X * Math.cos(radians) - ORIGIN_OFFSET_Y * Math.sin(radians)); float rotatedY = (float) (ORIGIN_OFFSET_X * Math.sin(radians) + ORIGIN_OFFSET_Y * Math.cos(radians)); // 更新碰撞圆偏移 hitBox.setOffset(rotatedX, rotatedY); } }
3. 方向适配说明
如果测试后发现碰撞圆偏移方向不符合预期(比如偏移到子弹尾部、侧边),可以根据你的游戏坐标系调整公式符号:
- 如果Y轴向上为正,旋转方向为顺时针,可尝试将
radians改为-radians - 也可以微调公式中sin项的正负号,直到碰撞圆稳定跟随子弹头部旋转即可。
内容的提问来源于stack exchange,提问作者IcyRelic
相关产品推荐
相关产品推荐

