You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于旋转值动态偏移子弹的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.06 09:57:03