敌人路径跟随(Space 射击游戏)

Enemies path following (Space Shooter game)

我最近在使用 SFML 库,我正在尝试从头开始制作 Space 射击游戏。经过一段时间的努力,我得到了一些可以正常工作的东西,但我面临一个问题,我不知道如何继续,所以我希望你的智慧能引导我找到一个好的解决方案。我会尽力解释它:

敌人遵循路径:目前在我的游戏中,我的敌人可以遵循以下线性路径:

   float vx = (float)m_wayPoints_v[m_wayPointsIndex_ui8].x - (float)m_pos_v.x;
   float vy = (float)m_wayPoints_v[m_wayPointsIndex_ui8].y - (float)m_pos_v.y;

   float len = sqrt(vx * vx + vy * vy);
   //cout << len << endl;
   if (len < 2.0f)
   {
      // Close enough, entity has arrived
      //cout << "Has arrived" << endl;
      m_wayPointsIndex_ui8++;
      if (m_wayPointsIndex_ui8 >= m_wayPoints_v.size())
      {
         m_wayPointsIndex_ui8 = 0;
      }
   }
   else
   {
      vx /= len;
      vy /= len;

      m_pos_v.x += vx * float(m_moveSpeed_ui16) * time;
      m_pos_v.y += vy * float(m_moveSpeed_ui16) * time;
   }

*m_wayPoints_v 是一个向量,基本上包含要遵循的 2d 点。

关于这一小段代码,我不得不说有时会给我带来问题,因为随着敌人的速度越来越快,接近下一个点变得困难。

有没有其他方法可以更准确地跟踪不依赖于敌人速度的路径?并且还与路径跟踪相关,如果我想在每个波浪运动模式开始之前介绍敌人(在到达终点之前做圆圈,螺旋形,椭圆形或其他任何东西),例如:

例如下图:

黑线是我希望飞船在开始 IA 模式(从左到右移动,从右到左移动)之前遵循的路径,即红色圆圈。

是否完成了所有和每个动作的硬编码,或者是否有其他更好的解决方案?

我希望我已经说清楚了...如果我没有说清楚,请告诉我,我会提供更多详细信息。非常感谢您!

这没有回答您的具体问题。我很好奇你为什么不使用 sfml 类型 sf::Vector2(或其类型定义 2i、2u、2f)?看起来它可能会清理你的一些代码。

就动画而言。您可以考虑将所需飞行模式的说明加载到堆栈或其他内容中。然后弹出每个位置并将你的船移动到那个位置并渲染,重复。

而且如果你想要一个类似于你图片的类似罪恶的飞行路径,你可以找到一个类似于你喜欢的飞行路径的方程式。使用desmos或其他东西来制作适合您需要的酷图。然后以 w/e 间隔迭代,将每次迭代输入此方程,你的结果就是你在每次迭代中的位置。

嗯,我想我发现了其中一个问题,但我不确定解决方案是什么。

在使用我之前贴出的那段代码时,发现由于速度值的原因,在到达目的地点的时候出现了问题。目前要流畅地移动 space 船,我需要将速度设置为 200...这意味着在这些公式中:

     m_pos_v.x += vx * float(m_moveSpeed_ui16) * time;
     m_pos_v.y += vy * float(m_moveSpeed_ui16) * time;

新位置可能会超过“2.0f”公差,因此space飞船无法找到目标点并卡住,因为每帧可以完成的最小移动(假设60fps)200 * 1 / 60 = 3.33 像素。有什么办法可以避免这种行为?

路点

您需要向路标和 NPC 相对于路标的位置添加一些附加信息。

代码片段(伪代码)显示了如何将一组路径点创建为 linked 列表。每个路点都有一个 link 和到下一个路点的距离,以及这个路点的总距离。

然后每走一步,你就增加 NPC 在路径点集上的距离。如果该距离大于下一个路径点的 totalDistance,则沿着 link 到达 next。您可以使用 while 循环来搜索下一个路径点,这样无论您的速度如何,您都将始终处于正确的位置。

一旦您到达正确的路径点,只需计算 NPC 在当前路径点和下一个路径点之间的位置即可。

定义路径点

class WayPoint {
  public:
    WayPoint(float, float);
    float x, y, distanceToNext, totalDistance;
    WayPoint next;
    WayPoint addNext(WayPoint wp);

}
WayPoint::WayPoint(float px, float py) { 
    x = px; y = py; 
    distanceToNext = 0.0f;
    totalDistance = 0.0f;
}
    
WayPoint WayPoint::addNext(WayPoint wp) {
    next = wp;
    distanceToNext = sqrt((next.x - x) * (next.x - x) + (next.y - y) * (next.y - y));
    next.totalDistance =  totalDistance + distanceToNext;    
    return wp;
}

声明和linking waypoints

   WayPoint a(10.0f, 10.0f);
   WayPoint b(100.0f, 400.0f);
   WayPoint c(200.0f, 100.0f);
   a.addNext(b);
   b.addNext(c);
   

NPC 以任意速度跟随尖锐的路径

   WayPoint currentWayPoint = a;
   NPC ship;
   
   ship.distance  += ship.speed * time;
   while (ship.distance > currentWayPoint.next.totalDistance) {
       currentWayPoint = currentWayPoint.next;
   }
   float unitDist = (ship.distance - currentWayPoint.totalDistance)  / currentWayPoint.distanceToNext;
   
   // NOTE to smooth the line following use the ease curve. See Bottom of answer
   // float unitDist = sigBell((ship.distance - currentWayPoint.totalDistance)  / currentWayPoint.distanceToNext);
   
   ship.pos.x = (currentWayPoint.next.x - currentWayPoint.x) * unitDist + currentWayPoint.x;
   ship.pos.y = (currentWayPoint.next.y - currentWayPoint.y) * unitDist + currentWayPoint.y;
   

注意 你可以 link 回到起点,但要小心检查 while 循环中总距离何时回到零,否则你将结束一个无限循环。当你通过零重新计算 NPC distance 作为最后一个路点的模 totalDistance 这样你就不会旅行超过一个路点循环来找到下一个。

例如在 while 循环中如果经过最后一个路径点

if (currentWayPoint.next.totalDistance == 0.0f) {
     ship.distance = mod(ship.distance, currentWayPoint.totalDistance);
}

平滑路径

使用上述方法,您可以向路标添加额外的信息。

例如,对于每个路径点,添加一个与下一个路径偏离 90 度的矢量。

// 90 degh CW
offX = -(next.y - y) / distanceToNext; // Yes offX = - y
offY = (next.x - x) / distanceToNext;  // 
offDist = ?; // how far from the line you want to path to go

然后,当您计算 unitDist 沿途点之间的直线时,您可以使用该单位 dist 来平滑地插入偏移量

float unitDist = (ship.distance - currentWayPoint.totalDistance)  / currentWayPoint.distanceToNext;
// very basic ease in and ease out  or use sigBell curve
float unitOffset = unitDist < 0.5f ? (unitDist * 2.0f) * (unitDist * 2.0f) : sqrt((unitDist - 0.5f) * 2.0f);


float x = currentWayPoint.offX * currentWayPoint.offDist * unitOffset;
float y = currentWayPoint.offY * currentWayPoint.offDist * unitOffset;
ship.pos.x = (currentWayPoint.next.x - currentWayPoint.x) * unitDist + currentWayPoint.x + x;
ship.pos.y = (currentWayPoint.next.y - currentWayPoint.y) * unitDist + currentWayPoint.y + y;

现在,如果您添加 3 个路径点,第一个 offDist 为正距离,第二个为负距离 offDist,您将获得一条平滑曲线的路径,如图所示。

注意NPC的实际速度会随着每个路径点的变化而变化。使用这种方法获得恒定速度的数学运算太繁重,不值得付出努力,因为没有人会注意到小偏移量。如果您的偏移量太大,请重新考虑您的路径点布局

注意上述方法是对二次贝塞尔曲线的修改,其中控制点定义为端点之间距中心的偏移量

S 形曲线

您不需要添加偏移量,因为您可以通过操纵 unitDist 值(请参阅第一个片段中的评论)

沿路径进行一些(有限的)平滑处理

使用以下函数将单位值转换为钟形曲线 sigBell 和标准缓出曲线。使用参数 power 来控制曲线的斜率。

float sigmoid(float unit, float power) { // power should be > 0. power 1 is straight line 2 is ease out ease in 0.5 is ease to center ease from center
    float u = unit <= 0.0f ? 0.0f : (unit >= 1.0f ? 1.0f: unit); // clamp as float errors will show
    float p = pow(u, power);
    return p / (p + pow(1.0f - u, power));
}
float sigBell(float unit, float power) {
    float u = unit < 0.5f ? unit * 2.0f : 1.0f - (unit - 0.5f) * 2.0f;
    return sigmoid(u, power);
}