3
\$\begingroup\$

There's a great question here that helps me a little bit in what I want to do, and it explains it quite well:

How To Approach 360 Degree Snake

Basically, I want to have a smooth 360-degrees Snake game, where the snake follows along. I can easily do the same thing as described in the question above (video example), but as you can see that kind of "drags" or "slides" the snake along, which is not what I'm looking for.

I want the entire snake to follow the entire path that the head of the snake follows.

The only solution I can think of is to make some kind of "buffer" to store the rotations of the head, and then "lag" that behind on the other bodies.

Since I think that will end up way too complex, what's the correct way of implementing it?

Here's a screenshot of how my snake looks like right now, to get an idea of what exactly I want to move:

\$\endgroup\$
  • \$\begingroup\$ if i understand it correctly this is programming 101: for(i = 1;i<snake_elements_count;i++) snake_element_position[i] = snake_element_position[i-1]; where snake_element_position[0] is the heads position. Run the for loop every time you change the heads position. \$\endgroup\$ – Raxvan Feb 5 '15 at 11:59
  • \$\begingroup\$ If only it was that simple. Check the video I linked. I'm not using any kind of grid, I'm just using pixel-coordinates for the 32px-sized body parts. If I move the snake parts to exactly the last position, it will immediately jump the 32px that are in between the parts. That shouldn't happen. \$\endgroup\$ – Codecat Feb 5 '15 at 12:03
  • \$\begingroup\$ it's still simple :) , just change the snake_element_position to snake_element_direction and you're almost done. you would have to store some key position every time the direction changes to avoid floating point errors. but for the first run this should do just fine. \$\endgroup\$ – Raxvan Feb 5 '15 at 12:06
  • \$\begingroup\$ That will most likely cause sliding, but I will give it a shot. \$\endgroup\$ – Codecat Feb 5 '15 at 12:55
  • \$\begingroup\$ @Raxvan Is it really still simple? Please do explain in an answer, because I can't figure it out by your comment. \$\endgroup\$ – Codecat Feb 5 '15 at 13:51
4
\$\begingroup\$

One method is to have the head define a trail as it moves and position all other nodes at positions along this trail.

In this method you need to define the position of the body parts as a constant distance along the snake from the head.

So on each frame you want to;

  1. update the position of the head.
  2. add the current position of the head to a list of previous positions it occupied
  3. for each node of the body
    1. get/calculate the distance the body should be away from the head
    2. traverse the trail by that distance
    3. set the body position to this position on the trail

The following is a C++03 program which shows how this can be achieved. It isn't perfect, but it demonstrates the idea;

#include <deque>
#include <string>
#include <sstream>
#include <iostream>
#include <math.h>

struct Vector3
{
  Vector3(double x = 0, double y = 0, double z = 0) :
    x(x),
    y(y),
    z(z)
  {
  }

  double x;
  double y;
  double z;

  Vector3 operator +(const Vector3 v) const
  {
    return Vector3
        (
          x + v.x,
          y + v.y,
          z + v.z
        );
  }

  Vector3 operator -(const Vector3 v) const
  {
    return Vector3
        (
          x - v.x,
          y - v.y,
          z - v.z
        );
  }

  Vector3 operator *(double scalar)
  {
    return Vector3
        (
          x * scalar,
          y * scalar,
          z * scalar
        );
  }

  double magnitude()
  {
    return sqrt(x*x + y*y + z*z);
  }

  void normalise()
  {
    double mag = magnitude();
    if (mag != 0.0)
    {
      x /= mag;
      y /= mag;
      z /= mag;
    }
  }

  std::string toString()
  {
    std::stringstream ss;
    ss << "(" << x << ", " << y << ", " << z << ")";
    return ss.str();
  }
};

class Trail
{
public:
  struct Point
  {
    Vector3 position;
    double distanceToNext;
  };

  Trail() :
    m_maxPoints(0),
    m_points()
  {}

  size_t maxPoints() const
  {
    return m_maxPoints;
  }
  void maxPoints(size_t size)
  {
    m_maxPoints = size;

    if (m_points.size() >= m_maxPoints && m_maxPoints != 0)
    {
      m_points.resize(m_maxPoints, Point());
    }
  }

  void addPoint(Vector3 vec)
  {
    Point p;
    p.position = vec;
    if (!m_points.empty())
    {
      p.distanceToNext = (vec - m_points.front().position).magnitude();
    }
    else
    {
      p.distanceToNext = 0.0;
    }
    addPoint(p);
  }

  void addPoint(Point point)
  {
    if (m_points.size() >= m_maxPoints && m_maxPoints != 0)
    {
      m_points.resize(m_maxPoints - 1, Point());
    }
    m_points.push_front(point);
  }

  Vector3 positionFromHead(double distance) const
  {
    for (size_t i = 0; i < m_points.size() - 1; ++i)
    {
      const Point& p = m_points[i];
      if (distance > p.distanceToNext)
      {
        distance -= p.distanceToNext;
      }
      else
      {
        // Lerp between last and current points.
        Vector3 offset = m_points[i + 1].position - p.position;
        offset.normalise();
        offset = offset * distance;
        return p.position + offset;
      }
    }

    if (!m_points.empty()) // is last point?
    {
      return m_points.back().position;
    }

    return Vector3(); // list is empty
  }

private:
  size_t m_maxPoints;
  std::deque<Point> m_points;
};

int main(int argc, char* argv[])
{
  Trail trail;

  trail.addPoint(Vector3(50, 0, 0));
  trail.addPoint(Vector3(10, 0, 0));
  trail.addPoint(Vector3(0, 0, 0));

  Vector3 posA = trail.positionFromHead(5);
  std::cout << " 5 units from head - Expecting (5, 0, 0) and got " << posA.toString() << "\n";
  Vector3 posB = trail.positionFromHead(10);
  std::cout << "10 units from head - Expecting (10, 0, 0) and got " << posB.toString() << "\n";
  Vector3 posC = trail.positionFromHead(25);
  std::cout << "25 units from head - Expecting (25, 0, 0) and got " << posC.toString() << "\n";

  return 0;
}

Of particular importance is this method which actually works out the position based on the distance;

  Vector3 positionFromHead(double distance) const
  {
    for (size_t i = 0; i < m_points.size() - 1; ++i)
    {
      const Point& p = m_points[i];
      if (distance > p.distanceToNext)
      {
        distance -= p.distanceToNext;
      }
      else
      {
        // Lerp between last and current points.
        Vector3 offset = m_points[i + 1].position - p.position;
        offset.normalise();
        offset = offset * distance;
        return p.position + offset;
      }
    }

    if (!m_points.empty()) // is last point?
    {
      return m_points.back().position;
    }

    return Vector3(); // list is empty
  }

In this instance each position on the trail is stored with a distance to the next one but this could be calculated on the fly. It's calculated in the addPoint() methods.

Note: don't use that Vector3 class. It was created only for this example to keep the dependencies down.

If the distance requested is longer than the list you'll just be given the position of the end of the trail.

If this were to be used you would need to make sure that you set maxPoints to an appropriate number or use 0 if you want it to be unbounded. To be clear, the maxPoints doesn't need to correspond to the number of bodies in your snake, you just want it to store enough to make a trail equal to the length of the snake.

You may want to consider limiting the max length with a max total distance instead of a max number of points. Also, if you set it to unbounded (0) it will consume more memory every frame so only use that when experimenting.

In practice you might do something similar but not exactly the same thing.

\$\endgroup\$
  • \$\begingroup\$ This is probably the best general purpose solution to this problem. If you really want to be fancy, you could use something like boost::circular_buffer or boost::circular_buffer_space_optimized instead of std::deque. std::deque is decent for FIFOs but it will still continue to allocate new blocks, even if the size only ever changes by +/- 1 \$\endgroup\$ – bcrist Feb 7 '15 at 2:24
  • \$\begingroup\$ Using this code as pseudocode, it appears that the body positions only follow the head whenever the head makes a turn. If it doesn't turn, it pretty much just stays in the same place (where the last turn took place). How would I work around that? \$\endgroup\$ – Codecat Feb 9 '15 at 12:19
  • \$\begingroup\$ @Angelo Green - make sure you're updating the head of the trail every time you're updating the position of the head of the snake. \$\endgroup\$ – OriginalDaemon Feb 9 '15 at 12:23
  • \$\begingroup\$ @OriginalDaemon What do you mean? I currently update the head position, and after that I add the new position as a point in the trail, then use positionFromHead to set the position for each body. (Here is my Objective C implementation of your Trail code, I think its implementation is ok unless I messed up somewhere, but the tests in your main function have passed on this implementation.) \$\endgroup\$ – Codecat Feb 9 '15 at 12:40
  • \$\begingroup\$ Your version looks fine and my guess would be that it's something in the calling code. I can't help right now and the comments section isn't an appropriate place for discussion (the chat room is though) so if you want further help can I ask you to send me an email (contact details in my profile) and we'll update the question/answer when we've resolved this. \$\endgroup\$ – OriginalDaemon Feb 9 '15 at 14:11
2
\$\begingroup\$

I've implemented the kind of smooth snake movement that you explain.

It's actually quite simple, but most of the examples I found while looking were the same kind as you encountered. And I don't think it's as easy to explain in plan words.

Example

As you can see, my snake is a bit unusual, thats why providing implementation here is not really optimal. But I think it displays the movement patterns you want.

But try this:

-On each loop cycle, create a new "head" at the next proper position of the snake. (In other words, where the snake's current vector takes you).

-Transform the former head to a part of the body.

-"Fade out" the snake's body (reduce alpha as an example), so that along the tail, the snake
becomes more and more transparent, until the end, it's gone.

So, you do not actually move the snake at all, you just keep creating new heads in the next position, and removing last element of the tail. At no point do you actually change the position of any existing segment of the snake.

\$\endgroup\$
  • \$\begingroup\$ This isn't smooth, this is just the same as simply putting them in a grid and making them "move" piece by piece, which is not what I want at all. \$\endgroup\$ – Codecat Feb 5 '15 at 12:16
  • \$\begingroup\$ Why isn't it smooth? My game is very smooth. It all depends on your implementation of balancing the creation of new heads vs. phasing out (removing the tail), - and that those elements happens on every loop. \$\endgroup\$ – Terje Feb 5 '15 at 12:18
  • \$\begingroup\$ I don't want my bodyparts to fade out or be created, I want them to follow the head. \$\endgroup\$ – Codecat Feb 5 '15 at 12:19
  • \$\begingroup\$ If you want to independently figure out where each bodypart should move on each loop to try to mimic how the head moved at that position, that is very hard to do. Without 100% precision, things will fall out of sync in a while, and you have to do all kinds of smart interpolation. I went down that road for a long time, but inaccurate timers made it nearly impossible to keep perfect over time. \$\endgroup\$ – Terje Feb 5 '15 at 12:23
  • 1
    \$\begingroup\$ @Terje Yeah, that's what I'm afraid of, and was hoping there's smarter solutions. You're lucky that your snake is based on a particle-glowy-type of deal, which makes masking it pretty simple for you, but it's not that great for when you have actual textures for the bodies. \$\endgroup\$ – Codecat Feb 5 '15 at 12:37
0
\$\begingroup\$

I would recommend setting target positions for each of the snake parts allowing the part only move in that direction. Every time the part reaches the target position it sets a new target positon based on the position of the previous part. This should be done after fixed distances. Reducing the distances will make the movement appear smoother.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.