Search code examples
c++game-physicssfml

Enemies path following (Space Shooter game)


I am recently working with SFML libraries and I am trying to do a Space Shooter game from scratch. After some time working on it I get something that works fine but I am facing one issue and I do not know exactly how to proceed, so I hope your wisdom can lead me to a good solution. I will try to explain it the best I can:

Enemies following a path: currently in my game, I have enemies that can follow linear paths doing the following:

   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 is a vector that basically holds the 2d points to be followed.

Related to this small piece of code, I have to say that is sometimes given me problems because getting closer to the next point becomes difficult as the higher the speed of the enemies is.

Is there any other way to be more accurate on path following independtly of the enemy speed? And also related to path following, if I would like to do an introduction of the enemies before each wave movement pattern starts (doing circles, spirals, ellipses or whatever before reaching the final point), for example:

For example, in the picture below:

enter image description here

The black line is the path I want a spaceship to follow before starting the IA pattern (move from left to right and from right to left) which is the red circle.

Is it done hardcoding all and each of the movements or is there any other better solution?

I hope I made myself clear on this...in case I did not, please let me know and I will give more details. Thank you very much in advance!


Solution

  • Way points

    You need to add some additional information to the way points and the NPC's position in relationship to the way points.

    The code snippet (pseudo code) shows how a set of way points can be created as a linked list. Each way point has a link and a distance to the next way point, and the total distance for this way point.

    Then each step you just increase the NPC distance on the set of way points. If that distance is greater than the totalDistance at the next way point, follow the link to the next. You can use a while loop to search for the next way point so you will always be at the correct position no matter what your speed.

    Once you are at the correct way point its just a matter of calculating the position the NPC is between the current and next way point.

    Define a way point

    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;
    }
    

    Declaring and 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 follows way pointy path at any speed

       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;
       
    

    Note you can link back to the start but be careful to check when the total distance goes back to zero in the while loop or you will end up in an infinite loop. When you pass zero recalc NPC distance as modulo of last way point totalDistance so you never travel more than one loop of way points to find the next.

    eg in while loop if passing last way point

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

    Smooth paths

    Using the above method you can add additional information to the way points.

    For example for each way point add a vector that is 90deg off the path to the next.

    // 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
    

    Then when you calculate the unitDist along the line between to way points you can use that unit dist to smoothly interpolate the offset

    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;
    

    Now if you add 3 way points with the first offDist a positive distance and the second a negative offDist you will get a path that does smooth curves as you show in the image.

    Note that the actual speed of the NPC will change over each way point. The maths to get a constant speed using this method is too heavy to be worth the effort as for small offsets no one will notice. If your offset are too large then rethink your way point layout

    Note The above method is a modification of a quadratic bezier curve where the control point is defined as an offset from center between end points

    Sigmoid curve

    You don't need to add the offsets as you can get some (limited) smoothing along the path by manipulating the unitDist value (See comment in first snippet)

    Use the following to function convert unit values into a bell like curve sigBell and a standard ease out in curve. Use argument power to control the slopes of the curves.

    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);
    }