28. Playing with a Snake

In these notes you will learn:

  • How to draw a multi-segment “snake” using an ArrayList of objects.
  • How to delete an element in an ArrayList.
  • How to generalize a class designed for a specific task.
  • Make an object move randomly on its own.

28.1. Introduction

In these notes, we’re going to look at a version of a program borrowed the excellent book Learning Processing: A Beginner’s Guide to Program, Images, Animation, and Interaction. It draws a “snake” that follows the mouse pointer:

// The snake consist of a series of segments.
class Segment {
    float x, y;
    float diam;
    float segmentColor;

    Segment(float init_x, float init_y) {
        x = init_x;
        y = init_y;
    }

    void setDiam(float d) {
        diam = d;
    }

    void setColor(color c) {
        segmentColor = c;
    }

    void render() {
        fill(segmentColor);
        noStroke();
        ellipse(x, y, diam, diam);
    }
}

final int NUM_SEGMENTS = 50;
ArrayList<Segment> snake;

void setup() {
    size(500, 500);
    smooth();

    // initialize all the segments
    snake = new ArrayList<Segment>();
    int = 0;
    while (i < NUM_SEGMENTS) {
        Segment s = new Segment(0, 0);
        snake.add(s);
        ++i;
    }
}

void draw() {
    background(255);

    // remove the first segment, and shift all of the other segments
    // down one index position.
    snake.remove(0);

    // add a new segment centred at the mouse
    Segment s = new Segment(mouseX, mouseY);
    snake.add(s);

    // draw everything:
    int i = 0;
    while(i < snake.size()) {
        noStroke();
        Segment t = snake.get(i);
        t.setDiam(50);
        t.setColor(color(0));
        t.render();
        ++i;
    }
}

Note that we are using the remove(int idx) function of ArrayList. This function removes the element at position idx, which causes the index of every other element to the right of idx in the ArrayList to decrease by one. Here as an example with an ArrayList called avengers:

The effect of remove the first element of an ArrayList

The “head” of our snake as the very last element of the ArrayList, while the tail is the very first one.

Let’s make the snake’s tail vanish more smoothly. Do do this, we’ll have the Segment‘s position in the list affect its size and fill colour: the closer the Segment is to the beginning of the ArrayList, the closer it is to the tail end of the snake, so we’ll draw it smaller and in a lighter shade of grey:

// The snake consists of a series of segments.
class Segment {
    float x, y;
    float diam;
    color segmentColor;

    Segment(float initX, initY) {
        x = initX;
        y = initY;
    }

    void setDiam(float d) {
        diam = d;
    }

    void setColor(color c) {
        segmentColor = c;
    }

    void render() {
        fill(segmentColor);
        ellipse(x, y, diam, diam);
    }
}

final int NUM_SEGMENTS = 50;
ArrayList<Segment> snake;

void setup() {
    size(500, 500);
    smooth();

    // initialize all the segments
    snake = new ArrayList<Segment>();
    int = 0;
    while (i < NUM_SEGMENTS) {
        Segment s = new Segment(0, 0);
        snake.add(s);
        ++i;
    }
}

void draw() {
    background(255);

    // remove the first segment, and shift all of the other segments
    // down one index position.
    snake.remove(0);

    // add a new segment centred at the mouse
    Segment s = new Segment(mouseX, mouseY);
    snake.add(s);

    // draw everything:
    int i = 0;
    while(i < snake.size()) {
        noStroke();
        Segment t = snake.get(i);
        t.setDiam(i);
        t.setColor(color(255 - i * 5));
        t.render();
        ++i;
    }
}

We only needed to make two adjustments to the program:

  • The fill of each circle is now given by fill(255 - i * 5). Since i is is increasing, the larger i gets, the closer this value comes to 0, so that fill(255 - i * 5) as blacker the closer the Segment is to the head of the snake.
  • The segment size is now increasing with i as well.

This program works, but if we wanted to have more than one snake, it would be awkward to write. Let’s change the code a bit to have Snake class that we can make multiple copies of:

class Segment {
    float x, y;

    Segment(float initX, float initY) {
        x = initX;
        y = initY;
    }

    void setDiam(float d) {
        diam = d;
    }

    void setColor(color c) {
        segmentColor = c;
    }

    void render() {
        fill(segmentColor);
        noStroke();
        ellipse(x, y, diam, diam);
    }
}

class Snake {
    ArrayList<Segment> segments;

    Snake(int numSegments) {
        // initialize the Snake's segments
        segments = new ArrayList<Segment>();
        int i = 0;
        while(i < numSegments) {
            Segment s = new Segment(0, 0);
            segments.add(s);
            ++i;
        }
    }

    void update() {
        // remove the first segment, and shift all of the other segments
        // down one index position
        segments.remove(0)

        Segment s = new Segment(mouseX, mouseY);
        segments.add(s);
    }

    void render() {
        // draw everything
        int i = 0;
        while (i < segments.size()) {
            Segment t = segments.get(i);
            t.setDiam = i;
            t.setColor = color(255 - i * 5);
            t.render();
        }
    }

    Snake snake;

    void setup() {
        size(500, 500);
        smooth();
        snake = new Snake(50);
    }

    void draw() {
        background(255);
        snake.update();
        snake.render();
    }
}

Notice now simple the code in setup() and draw() is. This is because of we now think of a snake as a collection of segments: The Snake is responsible for keeping track of all of the segments, while each Segment is responsible for actually drawing itself to the screen. This simplicity is fairly common in well-designed object oriented programs.

28.2. Two Snakes

Now that we have the Snake class, we can draw multiple snakes on the screen at the same time. Well, almost. Unfortunately, there is a problem: the update() function sets the snake’s head to be the location of the mouse pointer. Every Snake will thus be drawn in the same place, which is not what we want.

So we can modify the update() function slightly so that we can pass in the position that we want the next segment drawn at:

class Snake {
    // ...

    void update(float x, float y) {
        // remove the first segment, and shift all the other segments
        // down one index position
        segments.remove(0);

        Segment s = new Segment(x, y);
        segments.add(s);
    }
}

Now when we call update(), we can pass the position we want the snake’s next segment to be drawn at:

Snake snake1, snake2;

void setup() {
    size(500, 500);
    smooth();
    snake1 = new Snake(50);
    snake2 = new Snake(50);
}

void draw() {
    background(255);
    snake1.update(mouseX, mouseY);
    snake1.render();

    snake2.update(500 - mouseX, 500 - mouseY);
    snake2.render();
}

28.3. Allowing More Segments

A subtle limitation of our code so far is that it only works for snakes with about 50 segments in them. If you make a snake’s underlying segment ArrayList much longer then 50, the visuals break down pretty severely.

The problem is in the Snake render() function:

class Snake() {
    void render() {
        int i = 0;
        while (i < segments.size()) {
            noStroke();
            Segment t = segments.get(i);
            t.setDiam(i);
            t.setColour(255 - i * 5);
            ++i;
        }
    }
}

Do you see the problems? There are two:

  1. fill(255 - i * 5) does not work correctly if i is too big. The issue is that the value you give to fill must be between 0 and 255. In other words, this must be true fill to work correctly:

    0 \leq 255 - 5i \leq 255

But when i \geq 52, this is no longer true. From the while-loop, we can see that i is always between 0 and 49. So if i is 0, then 255 - 5i = 255, which is permissible input to fill. Similarly, if i is 49 (the highest possible value with 50 segments — one less than segments.size()), then 255 - 5i = 255 - 5\cdot 49 =255 - 245
= 10 which is, again, a legal value for fill.

But suppose we want our snake to have 100 segments. Then i ranges from 0 to 99, and when it’s equal to 52, we have 255 - 5i = 255 -
5\cdot52 = 255 - 260 = -5. So when 52\leq i\leq 99, fill gets a negative value which, as we’ve already seen, causes it to misbehave.

One way to fix this is to use the map function. Recall that map converts a value from one particular input range into a proportionally equivalent value in an output range. The value we are interested in here is 255 - i * 5:

float x = 255 - i * 5;

To use map, we need to determine the range of x, i.e. we need to know its smallest and largest value. Let’s determine the biggest value of x first. Note that if i is small, then x is big, and since the smallest value of i is 0 (thanks to the while-loop that controls it), the biggest possible value of x is 255 - 5\cdot 0 = 255.

To determine the smallest value of x, we note that when i is big, x is small, and so the smallest value of x is achieved when we have the largest value of i. This is segments.size() - 1. In other words, x is at least 255 - (segments.size() - 1) * 5 (Here, i has just been replaced with its largest possible value).

So we have the range of x:

255 - (segments.size() - 1) * 5 <= x <= 255

The output range is 0 to 255 because we want the output of the call to map to be fed into fill:

float x = 255 - i * 5;
float mx = map(x,
               255 - (segments.size() - 1) * 5), 255, // input range
               0, 255                                 // output range
              );
t.setColor(color(mx);

Notice how we use indentation and comments to make the code easier to read.

  1. The diameter of the ellipse is i which doesn’t work when i is too big, because the head just blots out everything else. One way around this is to use the map() function again:

    float diam = map(i,
                         0, segments.size() - 1, // input range (i's range)
                         0, 50                  // output range
                        );
    t.setDiam(diam);
    

    Again, note the use of indentation to make the code clearer.

28.4. A Snake that Moves on its Own

Let’s make a snake that moves on its own. The simplest way to make something move is as we have been doing throughout the course: alter the x and y values of the position of the object we want to move. Since our snakes move by adding segments and drawing them, to make a snake appear to move on its own we must automatically decide where to put each new head segment.

The approach we’ll follow here is to make each new head segment be placed a little past the previous head segment. We’ll need to keep track of the previous head segment to do this. We’ll also make use of the Range class we constructed earlier.

class Range {
    float lo, hi;

    Range(float initLo, initHi) {
        lo = initLo;
        hi = initHi;
    }

    float randValue() {
        return random(lo, hi);
    }
}

Snake snake;
Segment prevHead;
Range wiggleRange = Range(0, 2);

void setup() {
    size(500, 500);
    smooth();
    snake = new Snake(150);
    prevHead = new Segment(0, 0);
}

void draw() {
    background(255);
    snake.update(prevHead.x + wiggleRange.randValue(),
                 prevHead.y + wiggleRange.randValue());

    snake.render();
    prevHead = snake.segments.get(snake.segments.size() - 1);
}

This makes the snake wiggle diagonally down the screen. It is a nice effect, although it perhaps looks more like a comet than a snake.

The calculation to get prevHand is a bit ugly, but that’s partly an artificat of how ArrayLists work: they don’t provide any simple way to access their last element.

28.5. Questions

  1. Instead of drawing the segments as ellipses, draw them as lines between the points (be careful with the loop index!). This way no matter how fast your move the mouse pointer the snake will always be connected.

28.6. Complete Source Code

 class Segment {
     float x, y;

     Segment(float init_x, float init_y) {
         x = init_x;
         y = init_y;
     }
 }


 class Snake {
     ArrayList<Segment> segment;

     Snake(int numSegments) {
     // initialize the snake's segments
         segment = new ArrayList<Segment>();
         int i = 0;
         while (i < numSegments) {
             Segment s = new Segment(0, 0);
             segments.add(s);
             ++i;
         }
     }

     void update() {
         // remove the first segment, and shift all the other segments
         // down one index position
         segments.remove(0);

         Segment s = new Segment(mouseX, mouseY);
         segments.add(s);
     }

     void render() {
     // draw everything
         int i = 0;
         while (i < segments.size ()) {
             noStroke();
             Segment t = segments.get(i);
             float x = 255 - i * 5;
             float mx = map(x,
                            255 - (segments.size() - 1) * 5, 255, // input range
                            0, 255                               // output range
                            );
             t.setColor(mx);

             float diam = map(i,
                              0, segments.size() - 1, // input range (i's range)
                              0, 50);                // output range (diam's range)

             t.setDiam(diam);
             ++i;
         }
     }

     void update(float x, float y) {
         // remove the first segment, and shift all the other segments
         // down one index position
         segments.remove(0);

         Segment s = new Segment(x, y);
         segments.add(s);
     }
 }

/////////////////////////////////////////////////////////////////////

 Snake snake;
 Segment prevHead;

 void setup() {
     size(500, 500);
     smooth();
     snake = new Snake(150);
     prevHead = new Segment(0, 0);
 }

 void draw() {
     background(255);
     snake.update(prevHead.x + random(2.0), prevHead.y + random(2.0));
     snake.render();
     prevHead = snake.segments.get(snake.segments.size() - 1);
 }