Java实现2D游戏敌对单位射击预测算法优化需求问询
嘿,这个2D游戏里的提前量射击问题我太熟了!你之前用的近似方法确实会有误差,尤其是双方都移动的时候,而且没考虑旋转速度限制的话,体验也会很怪。我给你一套更精准也高效的解决方案,覆盖你说的两种场景,还能处理旋转速度的问题。
你之前的方法是用当前距离/子弹速度作为飞行时间,但这是假设对手不动的情况。真实情况是,子弹飞出去的时候,对手还在移动,甚至你自己也在移动,所以得用相对运动的思路解二次方程,找到真正的命中时间t。
先定义几个变量(对应你的代码):
- 射手(B)的位置:
(shooter.x, shooter.y),移动速度向量:(shooterVx, shooterVy)(静止时就是(0,0)) - 目标(A)的位置:
(target.x, target.y),移动速度向量:(targetVx, targetVy)=(UNIT_FORWARD_VELOCITY * cos(target.orientationAngle), UNIT_FORWARD_VELOCITY * sin(target.orientationAngle)) - 子弹速度:
BULLET_VELOCITY
我们要找的是时间t,使得子弹飞行t秒后,刚好和目标的位置重合。把这个关系写成方程,然后平方相加消去瞄准角度,就能得到关于t的二次方程:a*t² + b*t + c = 0,其中:
a = (targetVx - shooterVx)² + (targetVy - shooterVy)² - BULLET_VELOCITY²b = 2 * [(target.x - shooter.x)*(targetVx - shooterVx) + (target.y - shooter.y)*(targetVy - shooterVy)]c = (target.x - shooter.x)² + (target.y - shooter.y)²
解这个方程,取正的最小实数解(因为我们要最早命中的时间),如果判别式小于0,说明子弹速度不够,追不上目标,这时候就退而求其次瞄准当前位置。
a) 自身静止时的预测
这时候射手速度(shooterVx, shooterVy)是(0,0),方程会简化一些,解出来t之后,直接计算目标的预测位置:
float predX = target.x + targetVx * t; float predY = target.y + targetVy * t; float targetAngle = MathUtil.angleBetweenUnitAndPoint(shooter, predX, predY);
b) 自身移动时的预测
就是上面的通用情况,直接用二次方程解t,然后计算预测位置或者直接推导瞄准角度(用t代入原方程求cos(theta)和sin(theta),再用atan2得到角度)。
单位不能瞬间转到目标角度,得逐步旋转:
- 计算当前角度和目标角度的最短差值(比如从350°转到10°,应该转20°而不是340°)
- 根据帧时间
deltaTime计算每帧能转的最大角度:maxRotation = 54f * deltaTime - 如果角度差小于等于
maxRotation,直接转到目标角度;否则,向目标方向转maxRotation
这里要注意角度的归一化(保持在0-360°或者-180°到180°范围内),避免出现异常值。
- 减少计算频率:不用每帧都解二次方程,比如每10帧计算一次目标角度,中间用旋转逐步逼近,完全不影响精度还能省性能
- 预计算常量:比如
BULLET_VELOCITY²可以提前存在常量里,不用每次计算 - 复用工具函数:角度差、角度归一化这些逻辑封装成工具方法,避免重复代码
// 工具类方法:计算最短角度差(返回值范围:-180°到180°) public static float getShortestAngleDifference(float current, float target) { float delta = target - current; while (delta > 180) delta -= 360; while (delta < -180) delta += 360; return delta; } // 工具类方法:归一化角度到0-360° public static float normalizeAngle(float angle) { angle %= 360; if (angle < 0) angle += 360; return angle; } // 计算命中时间,返回正的最小解,无有效解返回-1 public float calculateLeadTime(Unit shooter, Unit target) { float dx = target.x - shooter.x; float dy = target.y - shooter.y; float targetVx = Constants.UNIT_FORWARD_VELOCITY * (float)Math.cos(Math.toRadians(target.orientationAngle)); float targetVy = Constants.UNIT_FORWARD_VELOCITY * (float)Math.sin(Math.toRadians(target.orientationAngle)); float shooterVx = Constants.UNIT_FORWARD_VELOCITY * (float)Math.cos(Math.toRadians(shooter.orientationAngle)); float shooterVy = Constants.UNIT_FORWARD_VELOCITY * (float)Math.sin(Math.toRadians(shooter.orientationAngle)); float dvx = targetVx - shooterVx; float dvy = targetVy - shooterVy; float vBulletSq = Constants.BULLET_VELOCITY * Constants.BULLET_VELOCITY; float dvSq = dvx*dvx + dvy*dvy; float a = dvSq - vBulletSq; float b = 2 * (dx*dvx + dy*dvy); float c = dx*dx + dy*dy; float discriminant = b*b - 4*a*c; if (discriminant < 0) { return -1; // 追不上 } float sqrtD = (float)Math.sqrt(discriminant); float t1 = (-b + sqrtD) / (2*a); float t2 = (-b - sqrtD) / (2*a); // 取正的最小时间 float validTime = -1; if (t1 > 0) validTime = t1; if (t2 > 0) { if (validTime == -1 || t2 < validTime) { validTime = t2; } } return validTime; } // 更新射手的旋转角度 public void updateShooterAim(Unit shooter, Unit target, float deltaTime) { float leadTime = calculateLeadTime(shooter, target); float targetAngle; if (leadTime <= 0) { // 追不上,瞄准当前位置 targetAngle = MathUtil.angleBetweenUnitAndPoint(shooter, target.x, target.y); } else { // 计算预测位置 float targetVx = Constants.UNIT_FORWARD_VELOCITY * (float)Math.cos(Math.toRadians(target.orientationAngle)); float targetVy = Constants.UNIT_FORWARD_VELOCITY * (float)Math.sin(Math.toRadians(target.orientationAngle)); float predX = target.x + targetVx * leadTime; float predY = target.y + targetVy * leadTime; targetAngle = MathUtil.angleBetweenUnitAndPoint(shooter, predX, predY); } // 处理旋转速度限制 float currentAngle = shooter.orientationAngle; float deltaAngle = getShortestAngleDifference(currentAngle, targetAngle); float maxRotation = 54f * deltaTime; // 54度/秒 if (Math.abs(deltaAngle) <= maxRotation) { shooter.orientationAngle = normalizeAngle(targetAngle); } else { shooter.orientationAngle += Math.signum(deltaAngle) * maxRotation; shooter.orientationAngle = normalizeAngle(shooter.orientationAngle); } }
你之前的distanceK是用当前距离除以子弹速度,但实际上子弹飞行时,目标和你都在移动,实际飞行时间和这个distanceK会有偏差——尤其是当目标朝向你移动或者远离你时,误差会非常大。而二次方程的方法是精确计算子弹命中的时间,所以预测位置会准确很多。
内容的提问来源于stack exchange,提问作者AccountName

