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

如何实现SFML游戏中炮塔抛射物沿曲线匀速飞向目标?

实现匀速曲线轨迹的抛射物运动逻辑

问题根源

你之前用抛物线(二次方程)和圆方程遇到的核心问题是:这两种曲线的参数化方式无法直接对应匀速的弧长移动。

  • 抛物线的物理模型(含重力)会导致速度矢量大小变化,且难以精准命中目标;
  • 圆方程虽能通过角度线性变化实现匀速,但很难调整圆心让轨迹刚好匹配A到B的需求,且轨迹形状灵活性差。

最优解决方案:二次贝塞尔曲线 + 弧长重参数化

这种方法既能自定义符合需求的曲线形状,又能严格保证抛射物沿曲线匀速移动,步骤如下:

1. 定义二次贝塞尔曲线

二次贝塞尔曲线由起点A、控制点C、终点B三个点定义,轨迹平滑且易于调整形状(比如把控制点设在A、B上方,就能得到向上拱起的曲线,匹配你的草图)。参数方程为:
$$
\begin{align*}
x(t) &= (1-t)^2x_A + 2t(1-t)x_C + t^2x_B \
y(t) &= (1-t)^2y_A + 2t(1-t)y_C + t^2y_B
\end{align*}
$$
其中 $t \in [0,1]$,$t=0$ 对应起点A,$t=1$ 对应终点B。

2. 预计算弧长映射表

直接线性增加 $t$ 会导致速度不均(因为 $t$ 的变化率和曲线弧长的变化率不成正比)。我们需要预计算 $t$ 对应的累计弧长,建立映射表,后续通过累计弧长反推 $t$,保证每帧移动的弧长固定(即匀速)。

3. 每帧更新抛射物位置

根据设定的速度和时间步长,计算当前需要移动的弧长,通过映射表找到对应的 $t$,代入贝塞尔方程得到当前坐标。

SFML实现代码示例

#include <SFML/Graphics.hpp>
#include <vector>
#include <cmath>
#include <algorithm>

// 定义二次贝塞尔曲线结构
struct QuadraticBezier {
    sf::Vector2f start;
    sf::Vector2f control;
    sf::Vector2f end;

    // 根据参数t计算曲线上的点
    sf::Vector2f getPoint(float t) const {
        float invT = 1.0f - t;
        return invT * invT * start + 2.0f * invT * t * control + t * t * end;
    }
};

// 预计算t与累计弧长的映射表
std::vector<std::pair<float, float>> precomputeArcLengthTable(const QuadraticBezier& curve, int samples = 100) {
    std::vector<std::pair<float, float>> table;
    float totalLength = 0.0f;
    sf::Vector2f prevPoint = curve.getPoint(0.0f);
    table.emplace_back(0.0f, 0.0f); // 存储(t, 累计弧长)

    for (int i = 1; i <= samples; ++i) {
        float t = static_cast<float>(i) / samples;
        sf::Vector2f currentPoint = curve.getPoint(t);
        float segmentLength = std::hypot(currentPoint.x - prevPoint.x, currentPoint.y - prevPoint.y);
        totalLength += segmentLength;
        table.emplace_back(t, totalLength);
        prevPoint = currentPoint;
    }
    return table;
}

class Projectile {
public:
    Projectile(sf::Vector2f startPos, sf::Vector2f targetPos, float moveSpeed)
        : speed(moveSpeed), currentArcLength(0.0f) {
        // 自定义控制点:让曲线向上拱起,可根据需求调整y偏移量
        sf::Vector2f controlPoint(
            (startPos.x + targetPos.x) / 2.0f,
            std::min(startPos.y, targetPos.y) - 50.0f
        );
        curve = {startPos, controlPoint, targetPos};
        arcLengthTable = precomputeArcLengthTable(curve);
        totalArcLength = arcLengthTable.back().second;

        // 初始化抛射物 sprite(示例)
        projectileSprite.setRadius(5.0f);
        projectileSprite.setFillColor(sf::Color::Red);
        projectileSprite.setPosition(startPos);
    }

    void update(float deltaTime) {
        if (currentArcLength >= totalArcLength) {
            // 到达终点,可标记为销毁
            isActive = false;
            return;
        }

        // 计算本次帧需要移动的弧长
        float deltaS = speed * deltaTime;
        currentArcLength += deltaS;

        // 根据累计弧长找到对应的t值
        float t = findTByArcLength(currentArcLength);
        // 更新抛射物位置
        sf::Vector2f currentPos = curve.getPoint(t);
        projectileSprite.setPosition(currentPos);
    }

    void draw(sf::RenderWindow& window) const {
        if (isActive) {
            window.draw(projectileSprite);
        }
    }

    bool isActive = true;

private:
    QuadraticBezier curve;
    std::vector<std::pair<float, float>> arcLengthTable;
    float totalArcLength;
    float speed; // 单位:像素/秒
    float currentArcLength;
    sf::CircleShape projectileSprite;

    // 通过二分查找找到目标弧长对应的t值
    float findTByArcLength(float targetLength) {
        int left = 0;
        int right = arcLengthTable.size() - 1;

        while (left < right) {
            int mid = (left + right) / 2;
            if (arcLengthTable[mid].second < targetLength) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }

        // 线性插值得到更精确的t
        if (left == 0) return 0.0f;
        auto& prevEntry = arcLengthTable[left - 1];
        auto& currEntry = arcLengthTable[left];
        float ratio = (targetLength - prevEntry.second) / (currEntry.second - prevEntry.second);
        return prevEntry.first + ratio * (currEntry.first - prevEntry.first);
    }
};

// 主函数示例
int main() {
    sf::RenderWindow window(sf::VideoMode(800, 600), "SFML Projectile Demo");
    window.setFramerateLimit(60);

    sf::Vector2f turretPos(100.0f, 500.0f);
    sf::Vector2f enemyPos(700.0f, 100.0f);

    // 创建炮塔 sprite(示例)
    sf::RectangleShape turret(sf::Vector2f(20.0f, 40.0f));
    turret.setFillColor(sf::Color::Green);
    turret.setPosition(turretPos);

    // 创建敌人 sprite(示例)
    sf::CircleShape enemy(15.0f);
    enemy.setFillColor(sf::Color::Blue);
    enemy.setPosition(enemyPos);

    Projectile projectile(turretPos, enemyPos, 200.0f);

    while (window.isOpen()) {
        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed) {
                window.close();
            }
        }

        projectile.update(1.0f / 60.0f);

        window.clear(sf::Color::White);
        window.draw(turret);
        window.draw(enemy);
        projectile.draw(window);
        window.display();
    }

    return 0;
}

关键细节说明

  • 控制点位置可灵活调整:如果需要不同的曲线形状,比如向左/右偏移,只需要修改控制点的x坐标即可;
  • 采样点数可调整:默认100个采样点足够保证轨迹精度,若追求更高精度可增加采样数;
  • 性能优化:预计算弧长表只需在抛射物创建时执行一次,每帧的二分查找和插值计算开销极小,适合小型游戏场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 20:20:38