在Android Studio中实现线段交点计算与碰撞检测:以Pong游戏为例

在Android Studio中实现线段交点计算与碰撞检测:以Pong游戏为例

本教程旨在详细讲解如何在android Pong游戏中实现精确的线段交点计算,以处理球与球拍的碰撞。文章将从代数角度推导两条直线交点的计算公式,并进一步优化为线段交点检测,包括关键的代码实现、在游戏循环中的应用逻辑,以及针对浮点精度、球体半径等实际游戏开发中的注意事项和优化建议,帮助开发者构建更真实、流畅的碰撞体验。

1. 理解Pong游戏中的碰撞检测需求

在经典的pong游戏中,球的运动轨迹可以被视为一条线段(从上一帧位置到当前帧位置),而球拍则可以被视为一条垂直的线段。为了实现精确的碰撞检测和反弹效果,我们需要解决的核心问题是:如何判断球的运动线段是否与球拍线段相交?如果相交,交点在哪里?这个交点将决定球的反弹位置和方向。

当前代码中,球的碰撞检测主要依赖于简单的边界判断:

// Bounce right side if ((ballX > screenWidth) && (ballspeedX > 0.0f)) { /* ... */ } // Bounce left side if ((80 * ballX < screenHeight) && (ballSpeedX < 0.0f)) { /* ... */ } // ...

这种方法对于屏幕边界的碰撞是有效的,但对于球拍这种位于屏幕内部且动态移动的物体,简单的边界判断无法提供精确的交点,可能导致球穿透球拍或反弹不自然。我们需要一种更数学化的方法来处理球与球拍之间的线段交点。

2. 几何基础:直线方程与交点计算

要计算两条线段的交点,我们首先需要理解如何计算两条直线的交点。

2.1 直线的标准方程

一条直线在二维平面上可以用多种形式表示,其中一种常用的形式是 Ax + By + C = 0。

如果已知直线上两点 P1(x1, y1) 和 P2(x2, y2),我们可以推导出 A, B, C 的值:

  • A = y1 – y2
  • B = x2 – x1
  • C = x1 * y2 – x2 * y1

推导过程: 直线通过 (x1, y1) 和 (x2, y2),其斜率为 m = (y2 – y1) / (x2 – x1)。 使用点斜式 y – y1 = m * (x – x1): y – y1 = ((y2 – y1) / (x2 – x1)) * (x – x1)(y – y1) * (x2 – x1) = (y2 – y1) * (x – x1) 展开并移项: (y2 – y1) * x – (x2 – x1) * y + x1 * (y2 – y1) – y1 * (x2 – x1) = 0(y2 – y1) * x + (x1 – x2) * y + (x1 * y2 – x1 * y1 – y1 * x2 + y1 * x1) = 0(y2 – y1) * x + (x1 – x2) * y + (x1 * y2 – y1 * x2) = 0

与 Ax + By + C = 0 对比,可以得到: A = y2 – y1 (或 y1 – y2,取决于方向,但最终结果一致) B = x1 – x2 (或 x2 – x1) C = x1 * y2 – y1 * x2 (或 x2 * y1 – y2 * x1)

为了与提供的答案保持一致,我们采用: A = y1 – y2B = x2 – x1C = x1 * y2 – x2 * y1

2.2 两条直线的交点

假设我们有两条直线:

  • 直线1: A1 * x + B1 * y + C1 = 0
  • 直线2: A2 * x + B2 * y + C2 = 0

我们可以使用克莱姆法则(Cramer’s Rule)或代入消元法解这个二元一次方程组。 通过消去 y: A1 * B2 * x + B1 * B2 * y + C1 * B2 = 0A2 * B1 * x + B1 * B2 * y + C2 * B1 = 0 两式相减: (A1 * B2 – A2 * B1) * x + (C1 * B2 – C2 * B1) = 0x = (C2 * B1 – C1 * B2) / (A1 * B2 – A2 * B1)

通过消去 x: A1 * B2 * x + B1 * B2 * y + C1 * B2 = 0A2 * B1 * x + B1 * B2 * y + C2 * B1 = 0(A1 * B2 – A2 * B1) * x = (C2 * B1 – C1 * B2)(A1 * C2 – A2 * C1) * y = (B1 * C2 – B2 * C1)y = (C1 * A2 – C2 * A1) / (A1 * B2 – A2 * B1)

因此,交点 (x, y) 的坐标为:

  • x = (C2 * B1 – C1 * B2) / (A1 * B2 – A2 * B1)
  • y = (C1 * A2 – C2 * A1) / (A1 * B2 – A2 * B1)

特殊情况: 如果分母 (A1 * B2 – A2 * B1) 等于 0,则表示两条直线平行或重合。在这种情况下,没有唯一的交点(平行)或有无限个交点(重合)。在实际游戏中,平行线意味着不会发生碰撞。

3. 实现线段交点检测

有了直线交点的计算方法,我们还需要将其扩展到线段交点。关键在于,计算出的交点必须同时位于两条线段的范围内。

3.1 定义点和线段的辅助类

为了方便处理,我们可以定义一个简单的 Point 类(如果Android的 PointF 不够用,或者想保持平台无关)。

// 辅助类,表示一个二维点 class Vector2D {     float x, y;      public Vector2D(float x, float y) {         this.x = x;         this.y = y;     }      @Override     public String toString() {         return "(" + x + ", " + y + ")";     } }  // 辅助类,表示一条线段 class LineSegment {     Vector2D p1, p2;      public LineSegment(Vector2D p1, Vector2D p2) {         this.p1 = p1;         this.p2 = p2;     } }

3.2 线段交点计算方法

现在,我们可以编写一个方法来计算两条线段的交点。

import android.graphics.PointF; // Android SDK 提供的 PointF 类  public class LineSegmentIntersection {      private static final float EPSILON = 1e-6f; // 用于浮点数比较的误差容忍度      /**      * 计算线段所在直线的 A, B, C 参数      * @param p1 线段的第一个点      * @param p2 线段的第二个点      * @return 包含 A, B, C 的 float 数组 {A, B, C}      */     private static float[] getLineEquationParams(Vector2D p1, Vector2D p2) {         float A = p1.y - p2.y;         float B = p2.x - p1.x;         float C = p1.x * p2.y - p2.x * p1.y;         return new float[]{A, B, C};     }      /**      * 检查一个点是否在线段上(包括端点)      * @param point 要检查的点      * @param segment 线段      * @return 如果点在线段上则返回 true,否则返回 false      */     private static boolean isPointOnSegment(Vector2D point, LineSegment segment) {         float minX = Math.min(segment.p1.x, segment.p2.x);         float maxX = Math.max(segment.p1.x, segment.p2.x);         float minY = Math.min(segment.p1.y, segment.p2.y);         float maxY = Math.max(segment.p1.y, segment.p2.y);          // 检查点是否在矩形边界框内         if (point.x < minX - EPSILON || point.x > maxX + EPSILON ||             point.y < minY - EPSILON || point.y > maxY + EPSILON) {             return false;         }          // 进一步检查点是否共线(对于水平或垂直线段,只需边界框检查即可,         // 但对于倾斜线段,需要确保点确实在线段上而不是边界框内但在外围)         // 这里简化为边界框检查,因为在找到直线交点后,共线性已经满足         // 实际应用中,如果点是直线交点,且满足边界框,则它必然在线段上         return true;     }       /**      * 计算两条线段的交点      * @param seg1 第一条线段      * @param seg2 第二条线段      * @return 如果两条线段相交,返回交点的 Vector2D 对象;否则返回 NULL      */     public static Vector2D getIntersectionPoint(LineSegment seg1, LineSegment seg2) {         float[] params1 = getLineEquationParams(seg1.p1, seg1.p2);         float A1 = params1[0], B1 = params1[1], C1 = params1[2];          float[] params2 = getLineEquationParams(seg2.p1, seg2.p2);         float A2 = params2[0], B2 = params2[1], C2 = params2[2];          float denominator = A1 * B2 - A2 * B1;          // 如果分母接近0,说明直线平行或重合         if (Math.abs(denominator) < EPSILON) {             // 如果C1*A2 - C2*A1 或 C2*B1 - C1*B2 也接近0,说明线段共线,可能有无限交点或部分重叠             // 对于游戏碰撞,通常视为不相交或特殊处理             return null;         }          float intersectX = (C2 * B1 - C1 * B2) / denominator;         float intersectY = (C1 * A2 - C2 * A1) / denominator;          Vector2D intersectionPoint = new Vector2D(intersectX, intersectY);          // 检查计算出的交点是否在线段1和线段2的范围内         if (isPointOnSegment(intersectionPoint, seg1) && isPointOnSegment(intersectionPoint, seg2)) {             return intersectionPoint;         } else {             return null;         }     } }

代码解释:

  1. Vector2D 和 LineSegment:简单的辅助类,用于封装点的坐标和线段的两个端点。
  2. getLineEquationParams:根据线段的两个端点计算其所在直线的 A, B, C 参数。
  3. isPointOnSegment:这是一个关键辅助方法,用于判断一个点是否落在给定的线段上。它通过检查点的 x 和 y 坐标是否在线段端点的 x 和 y 范围之内来实现。EPSILON 用于处理浮点数比较的精度问题。
  4. getIntersectionPoint:
    • 首先,它获取两条线段所在直线的 A, B, C 参数。
    • 然后,计算 denominator (A1 * B2 – A2 * B1)。如果 denominator 接近 0,则直线平行或重合,没有唯一的交点,返回 null。
    • 如果 denominator 不为 0,则计算出直线的交点 (intersectX, intersectY)。
    • 最后,使用 isPointOnSegment 方法检查这个交点是否同时落在两条线段上。只有当交点同时在线段1和线段2上时,才认为线段相交,并返回交点;否则返回 null。

4. 应用于Pong游戏碰撞检测

现在我们将上述线段交点检测逻辑集成到 PongView 的 update() 方法中。

4.1 定义球和球拍的线段

在 update() 方法中:

  1. 球的运动轨迹线段:

    • oldBallX, oldBallY 是球的上一帧位置。
    • ballX, ballY 是球的当前帧位置。
    • 因此,球的运动轨迹线段为 LineSegment(new Vector2D(oldBallX, oldBallY), new Vector2D(ballX, ballY))。
  2. 球拍线段:

    • 右球拍:
      • x 坐标固定为 7 * screenWidth / 8。
      • y 坐标范围从 rPaddle * screenHeight – halfPaddle 到 rPaddle * screenHeight + halfPaddle。
      • 线段为 LineSegment(new Vector2D(7 * screenWidth / 8, rPaddle * screenHeight – halfPaddle), new Vector2D(7 * screenWidth / 8, rPaddle * screenHeight + halfPaddle))。
    • 左球拍:
      • x 坐标固定为 screenWidth / 8。
      • y 坐标范围从 lPaddle * screenHeight – halfPaddle 到 lPaddle * screenHeight + halfPaddle。
      • 线段为 LineSegment(new Vector2D(screenWidth / 8, lPaddle * screenHeight – halfPaddle), new Vector2D(screenWidth / 8, lPaddle * screenHeight + halfPaddle))。

4.2 修改 collisionCheck() 方法

 // 在 PongView 类中添加或修改 // ... (其他成员变量) ... private LineSegmentIntersection intersectionHelper = new LineSegmentIntersection(); // 实例化辅助类  protected void collisionCheck() {     // 获取球的运动轨迹线段     Vector2D ballStart = new Vector2D(oldBallX, oldBallY);     Vector2D ballEnd = new Vector2D(ballX, ballY);     LineSegment ballPath = new LineSegment(ballStart, ballEnd);      // 获取右球拍线段     float rPaddleX = 7 * screenWidth / 8f; // 使用f确保浮点运算     Vector2D rPaddleP1 = new Vector2D(rPaddleX, rPaddle * screenHeight - halfPaddle);     Vector2D rPaddleP2 = new Vector2D(rPaddleX, rPaddle * screenHeight + halfPaddle);     LineSegment rightPaddleSegment = new LineSegment(rPaddleP1, rPaddleP2);      // 获取左球拍线段     float lPaddleX = screenWidth / 8f; // 使用f确保浮点运算     Vector2D lPaddleP1 = new Vector2D(lPaddleX, lPaddle * screenHeight - halfPaddle);     Vector2D lPaddleP2 = new Vector2D(lPaddleX, lPaddle * screenHeight + halfPaddle);     LineSegment leftPaddleSegment = new LineSegment(lPaddleP1, lPaddleP2);      Vector2D intersectionPoint = null;      // 检查与右球拍的碰撞     intersectionPoint = intersectionHelper.getIntersectionPoint(ballPath, rightPaddleSegment);     if (intersectionPoint != null) {         // 发生碰撞,处理反弹         handleCollision(intersectionPoint, true); // true表示右球拍         return; // 一帧内只处理一次碰撞,避免重复或错误反弹     }      // 检查与左球拍的碰撞     intersectionPoint = intersectionHelper.getIntersectionPoint(ballPath, leftPaddleSegment);     if (intersectionPoint != null) {         // 发生碰撞,处理反弹         handleCollision(intersectionPoint, false); // false表示左球拍         return;     }      // 原有的屏幕边界碰撞检测     // Bounce right side     if ((ballX + 10 > screenWidth) && (ballSpeedX > 0.0f)) { // 考虑球的宽度10         ballSpeedX *= -1.0f;         // pip.start(); // 播放音效     }     // Bounce left side     if ((ballX < 0) && (ballSpeedX < 0.0f)) { // 考虑球的宽度10         ballSpeedX *= -1.0f;         // pip.start(); // 播放音效     }     // Bounce bottom side     if ((ballY + 10 > screenHeight) && (ballSpeedY > 0.0f)) { // 考虑球的高度10         ballSpeedY *= -1.0f;         // pip.start(); // 播放音效     }     // Bounce top side     if ((ballY < 0) && (ballSpeedY < 0.0f)) { // 考虑球的高度10         ballSpeedY *= -1.0f;         // pip.start(); // 播放音效     }      // Log.d("TAG", "Ball is moving"); // 移除或调整此Log,避免频繁输出 }  /**  * 处理球与球拍碰撞后的逻辑  * @param intersectPoint 碰撞点  * @param isRightPaddle 是否是右球拍  */ private void handleCollision(Vector2D intersectPoint, boolean isRightPaddle) {     // 1. 将球的位置精确设置到碰撞点     // 由于球有宽度和高度(10x10),我们需要根据碰撞方向调整其左上角坐标     if (is

© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享