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

C++物理引擎中球体碰撞时互相穿透的问题排查

球体碰撞穿透与错误反弹的修复方案

你的代码存在三个核心问题导致球体穿透和异常反弹:

  1. 碰撞检测时机滞后:先更新位置再检测碰撞,此时球体已经互相穿透,速度修正无法立刻解决穿透状态。
  2. 碰撞响应不符合物理规律:直接反转速度是错误的,正确的弹性碰撞需要基于动量守恒和碰撞法线计算速度变化。
  3. 未处理穿透分离:检测到碰撞后没有将球体从穿透位置移开,导致持续触发错误的碰撞逻辑。

修复后的核心代码实现

1. 修正updateBall函数逻辑

调整执行顺序,增加穿透分离和正确的碰撞响应:

void updateBall(Ball &ball, float gravity, float frictionCoefficient, float deltaTime, float screenWidth, float screenHeight, float bounciness, std::list<Ball> &balls) {
    // 应用重力(考虑时间缩放)
    ball.velocity.y -= gravity * deltaTime;

    // 处理球体间碰撞
    for (auto &otherBall : balls) {
        if (&otherBall == &ball) continue;

        float currentDistance = Vector2Distance(ball.position, otherBall.position);
        float minDistance = ball.radius + otherBall.radius;

        if (currentDistance < minDistance) {
            // 计算碰撞法线
            Vector2 normal = Vector2Normalize(Vector2Subtract(otherBall.position, ball.position));
            // 分离穿透的球体
            float penetration = minDistance - currentDistance;
            Vector2 move = Vector2Scale(normal, penetration * 0.5f);
            ball.position = Vector2Subtract(ball.position, move);
            otherBall.position = Vector2Add(otherBall.position, move);

            // 弹性碰撞速度计算(假设质量与半径成正比)
            float massA = ball.radius;
            float massB = otherBall.radius;

            Vector2 relativeVelocity = Vector2Subtract(otherBall.velocity, ball.velocity);
            float velAlongNormal = Vector2DotProduct(relativeVelocity, normal);

            // 球体正在远离则不处理
            if (velAlongNormal > 0) continue;

            // 计算冲量
            float j = -(1 + bounciness) * velAlongNormal;
            j /= (1/massA + 1/massB);

            // 修正速度
            Vector2 impulse = Vector2Scale(normal, j);
            ball.velocity = Vector2Subtract(ball.velocity, Vector2Scale(impulse, 1/massA));
            otherBall.velocity = Vector2Add(otherBall.velocity, Vector2Scale(impulse, 1/massB));
        }
    }

    // 应用地面/天花板摩擦
    bool isOnGround = (ball.position.y >= screenHeight - ball.radius);
    bool isOnCeiling = (ball.position.y <= ball.radius);
    if (isOnGround || isOnCeiling) {
        ball.velocity.x *= frictionCoefficient;
        // 添加微小Y速度避免球体粘在地面
        if (isOnGround) ball.velocity.y = fmax(ball.velocity.y, 1.0f);
    }

    // 更新位置
    ball.position.x += ball.velocity.x * deltaTime;
    ball.position.y += -ball.velocity.y * deltaTime;

    // 屏幕边界碰撞处理
    if (ball.position.x <= ball.radius) {
        ball.position.x = ball.radius;
        ball.velocity.x *= -bounciness;
    } else if (ball.position.x >= screenWidth - ball.radius) {
        ball.position.x = screenWidth - ball.radius;
        ball.velocity.x *= -bounciness;
    }

    if (ball.position.y <= ball.radius) {
        ball.position.y = ball.radius;
        ball.velocity.y *= -bounciness;
    } else if (ball.position.y >= screenHeight - ball.radius) {
        ball.position.y = screenHeight - ball.radius;
        ball.velocity.y *= -bounciness;
    }
}

2. 完整修正后的代码

#include <iostream>
#include <stdio.h>
#include <raylib.h>
#include <raymath.h>
#include <stdlib.h>
#include <time.h>
#include <chrono>
#include <list>

using namespace std;

class Ball {
public:
    Vector2 position;
    Vector2 velocity;
    float radius;
    Color color;
};

void updateBall(Ball &ball, float gravity, float frictionCoefficient, float deltaTime, float screenWidth, float screenHeight, float bounciness, std::list<Ball> &balls) {
    // 应用重力(考虑时间缩放)
    ball.velocity.y -= gravity * deltaTime;

    // 处理球体间碰撞
    for (auto &otherBall : balls) {
        if (&otherBall == &ball) continue;

        float currentDistance = Vector2Distance(ball.position, otherBall.position);
        float minDistance = ball.radius + otherBall.radius;

        if (currentDistance < minDistance) {
            // 计算碰撞法线
            Vector2 normal = Vector2Normalize(Vector2Subtract(otherBall.position, ball.position));
            // 分离穿透的球体
            float penetration = minDistance - currentDistance;
            Vector2 move = Vector2Scale(normal, penetration * 0.5f);
            ball.position = Vector2Subtract(ball.position, move);
            otherBall.position = Vector2Add(otherBall.position, move);

            // 弹性碰撞速度计算(假设质量与半径成正比)
            float massA = ball.radius;
            float massB = otherBall.radius;

            Vector2 relativeVelocity = Vector2Subtract(otherBall.velocity, ball.velocity);
            float velAlongNormal = Vector2DotProduct(relativeVelocity, normal);

            // 球体正在远离则不处理
            if (velAlongNormal > 0) continue;

            // 计算冲量
            float j = -(1 + bounciness) * velAlongNormal;
            j /= (1/massA + 1/massB);

            // 修正速度
            Vector2 impulse = Vector2Scale(normal, j);
            ball.velocity = Vector2Subtract(ball.velocity, Vector2Scale(impulse, 1/massA));
            otherBall.velocity = Vector2Add(otherBall.velocity, Vector2Scale(impulse, 1/massB));
        }
    }

    // 应用地面/天花板摩擦
    bool isOnGround = (ball.position.y >= screenHeight - ball.radius);
    bool isOnCeiling = (ball.position.y <= ball.radius);
    if (isOnGround || isOnCeiling) {
        ball.velocity.x *= frictionCoefficient;
        // 添加微小Y速度避免球体粘在地面
        if (isOnGround) ball.velocity.y = fmax(ball.velocity.y, 1.0f);
    }

    // 更新位置
    ball.position.x += ball.velocity.x * deltaTime;
    ball.position.y += -ball.velocity.y * deltaTime;

    // 屏幕边界碰撞处理
    if (ball.position.x <= ball.radius) {
        ball.position.x = ball.radius;
        ball.velocity.x *= -bounciness;
    } else if (ball.position.x >= screenWidth - ball.radius) {
        ball.position.x = screenWidth - ball.radius;
        ball.velocity.x *= -bounciness;
    }

    if (ball.position.y <= ball.radius) {
        ball.position.y = ball.radius;
        ball.velocity.y *= -bounciness;
    } else if (ball.position.y >= screenHeight - ball.radius) {
        ball.position.y = screenHeight - ball.radius;
        ball.velocity.y *= -bounciness;
    }
}

Ball newBall(Vector2 position, Vector2 velocity, float radius, Color color) {
    Ball tempBall;
    tempBall.position = position;
    tempBall.velocity = velocity;
    tempBall.radius = radius;
    tempBall.color = color;
    return tempBall;
}

int main() {
    SetConfigFlags(FLAG_WINDOW_RESIZABLE);

    int screenWidth = 900;
    int screenHeight = 900;

    InitWindow(screenWidth, screenHeight, "cppPhysics");
    SetTargetFPS(120);

    const float gravity = 981.0f; // 适配像素坐标系的重力值
    const float frictionCoefficient = 0.98f;
    const float bounciness = 0.7f;

    Ball ballOne = newBall({100, 100}, {400, -450}, 60, WHITE);
    Ball ballTwo = newBall({500, 200}, {400, 450}, 45, BLUE);
    Ball ballThree = newBall({200, 500}, {300, -450}, 20, YELLOW);

    std::list<Ball> balls;
    balls.push_back(ballOne);
    balls.push_back(ballTwo);
    balls.push_back(ballThree);

    while (!WindowShouldClose()) {
        if (IsWindowResized()) {
            screenWidth = GetScreenWidth();
            screenHeight = GetScreenHeight();
        }
        float deltaTime = GetFrameTime();

        for (auto &ball : balls) {
            updateBall(ball, gravity, frictionCoefficient, deltaTime, screenWidth, screenHeight, bounciness, balls);
        }

        if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
            for (auto &ball : balls) {
                ball.velocity.y += 500;
                ball.velocity.x += 500;
            }
        }

        BeginDrawing();
        ClearBackground(BLACK);

        for (auto &ball : balls) {
            DrawCircleV(ball.position, ball.radius, ball.color);
        }

        EndDrawing();
    }

    CloseWindow();
    return 0;
}

关键说明

  • 碰撞处理移到了位置更新之前,先解决穿透问题再移动球体。
  • 使用动量守恒计算碰撞后的速度,反弹效果更符合真实物理规律。
  • 增加了穿透分离逻辑,确保球体不会卡在彼此内部。
  • 重力值调整为981.0f(像素/秒²),适配Raylib的像素坐标系,效果更直观。

内容的提问来源于stack exchange,提问作者metror

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 14:27:02