Case Study: 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.
Introduction¶
In these notes, we’re going to look at a version of a program borrowed from the excellent book Learning Processing: A Beginner’s Guide to Programming Images, Animation, and Interaction. It draws a “snake” that follows the mouse pointer:
// The snake consist of a series of segments, i.e. circles.
class Segment {
float x, y;
Segment(float init_x, float init_y) {
x = init_x;
y = init_y;
}
} // class Segment
final int NUM_SEGMENTS = 50;
ArrayList<Segment> snake;
void setup() {
size(500, 500);
smooth();
// initialize all the segments
snake = new ArrayList<Segment>();
int i = 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 the other segments
// down one index position
snake.remove(0);
// add a new segment centered at the mouse
Segment s = new Segment(mouseX, mouseY);
snake.add(s);
// draw everything
int i = 0;
while (i < snake.size()) {
noStroke();
fill(255 - i * 5);
Segment t = snake.get(i);
ellipse(t.x, t.y, i, i);
++i;
}
}
This program draws a single snake that follows the mouse pointer. This makes a nice little demo, but it is not easy to re-use this code in some other program, or to make more than one snake at a time.
So lets convert this program into a more object-oriented style. The main idea
is to create a new class called Snake
that contains all the variable and
functions necessary for drawing one snake.
Here is the first pass that re-organizes the code in a way that lets us replicate the original program:
class Segment {
float x, y;
Segment(float init_x, float init_y) {
x = init_x;
y = init_y;
}
} // class Segment
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);
segment.add(s);
++i;
}
}
void update() {
// remove the first segment, and shift all the other segments
// down one index position
segment.remove(0);
Segment s = new Segment(mouseX, mouseY);
segment.add(s);
}
void render() {
// draw everything
int i = 0;
while (i < segment.size()) {
noStroke();
fill(255 - i * 5);
Segment t = segment.get(i);
ellipse(t.x, t.y, i, i);
++i;
}
}
} // class Snake
//////////////////////////////////////////////////////
Snake snake;
void setup() {
size(500, 500);
smooth();
snake = new Snake(50);
}
void draw() {
background(255);
snake.update();
snake.render();
}
Notice how simple the code in setup
and draw
is. This is common in
well-designed object-oriented programs: all the implementation details are in
the Snake
object.
Two Snakes¶
Now that we have the Snake
class, we can draw multiple snakes on the
screen at the same time. 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.
Perhaps the easiest way to fix this is to modify the update()
function
so we can pass in the position 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
segment.remove(0);
Segment s = new Segment(x, y);
segment.add(s);
}
// ...
} // class Snake
Now when we call update
, we need to pass in 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();
}
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 than 50, the visuals break down pretty severely.
The problem is in the Snake
render
function:
class Snake {
// ...
void render() {
// draw everything
int i = 0;
while (i < segment.size()) {
noStroke();
fill(255 - i * 5);
ellipse(segment.get(i).x, segment.get(i).y, i, i);
++i;
}
}
} // class Snake
Do you see the problems? There are two:
fill(255 - i * 5)
does not work correctly ifi
is too big. The issue is that the value you give tofill
must be between 0 and 255. In other words, this must be true forfill
to work correctly:\[0 \leq 255 - 5i \leq 255\]This says that the smallest value \(255 - 5i\) can have is 0, while the biggest value it can have is 255.
From the while-loop, we can see that
i
is between 0 and 49 (segment.size()
). So ifi
is 0, then \(255 - 5i = 255\), which is a permissible input tofill
. Similarly, ifi
is 49 (the highest possible value — one less thansegment.size()
), then \(255 - 5i = 255 - 5 \cdot 49 = 10\) which is, again, a legal value forfill
.But suppose our snake as 100 segments. Then
i
ranges from 0 to 99, and when it’s equal to 99 we have \(255 - 5i = 255 - 5 \cdot 99 = -240\), which is not a legalfill
value.One way to fix this is to use the
map
function. Recall thatmap
converts a value from one particular input range into a proportionally equivalent value in an output range. The value we are interested in here is255 - i * 5
:float x = 255 - i * 5;
To use
map
, we need to determine the range ofx
, i.e. we need to know its smallest and largest values. Lets determine the biggest value ofx
first. Note that ifi
is small, thenx
is big, and since the smallest value ofi
is 0 (thanks to the while-loop that controls it), the biggest possible value ofx
is \(255 - 5\cdot 0 = 255\).To determine the smallest value of
x
, we note that wheni
is big,x
is small, and so the largest possible value ofi
issegment.size() - 1
. Thus the largest possible value of the expression255 - i * 5
is255 - (segment.size() - 1) * 5
(i
has just been replaced with its largest possible value).And so we now know the range of value of
x
:255 - (segment.size() - 1) * 5 <= x <= 255 // input range
It’s ugly, but accurate!
The output range is 0 to 255 because we want to output of the call to
map
to be fed intofill
.Now we can finally rewrite our code so that the snake is colored correctly no matter how many segments it has:
float x = 255 - i * 5; float mx = map(x, 255 - (segment.size() - 1) * 5, 255, // input range 0, 255 // output range ); fill(mx);
Notice how we used indentation and comments to make the code easier to read.
The diameter of the ellipse is
i
, which doesn’t work wheni
is too big. One way to solve this problem is, as for the fill color, to usemap
:float diam = map(i, 0, segment.size() - 1, // input range (i's range) 0, 50); // output range (diam's range) ellipse(segment.get(i).x, segment.get(i).y, diam, diam);
Too Much Garbage¶
Another problem with this program is that it creates a lot of garbage. In Processing, an object is considered to be garbage if it no variables refer to it.
The line that makes the garbage is segment.remove(0)
in update
:
void update(float x, float y) {
// remove the first segment, and shift all the other segments
// down one index position
segment.remove(0);
// ...
}
The problem is that when segment.remove(0)
is called, it removes the first
element of the ArrayList
segment
, but it does not delete it. It can’t
delete it because there might be some other variable referring to it. So
instead the removed object just sits there in memory, useless but taking up
space.
The way Processing deals with garbage is by ignoring it until there is so much garbage that it is in danger of running out of memory. So every once in a while Processing runs a special program called a garbage collector that goes through the program’s memory and deletes all unused objects so that they can be used again.
You might be able to actually notice when the garbage collector runs. Every once in a while you might see the program briefly freeze while garbage is being collected.
Our program creates a tremendous amount of garage. Since update()
is
called every time draw()
is called, and draw()
is called about 60
times a second, this means we call segment.remove(0)
about 60 times a
second. So if you run this program for a minute, that is 60 * 60 = 3600
garbage objects that must be cleaned up.
This is probably fine for a demo program, since we only use the demo for a short period of time and there is nothing else going on in the program. But if wanted to make a lot of snakes, or use this snake in a game, then all this excess garbage could trigger the garbage collector so frequently that your program could slow to a crawl.
So we really should get rid of the line segment.remove(0)
and replace it
with code that doesn’t make the garbage collector run so frequently (or at
all). There are a couple of ways to do this, but the approach we will take is
to re-think the basic design of segments and snakes.
Avoiding Garbage¶
It turns out we can use a simple trick to avoid creating garbage: we’ll re-use the segment that gets removed instead of creating a brand new segment. This requires a couple of steps.
Here’s the new and improved update()
function:
void update() {
// make a variable that refers to the first segment before it
// gets removed; this prevents it from becoming garbage and
// thus slowing down our program
Segment seg = segment.get(0);
// remove the first segment, and shift all the other segments
// down one index position
segment.remove(0);
// This is the line of code that we are replacing:
//Segment s = new Segment(mouseX, mouseY);
// Instead of creating a new segment, re-use the one we
// just removed.
seg.x = mouseX;
seg.y = mouseY;
segment.add(seg);
}
Note the following:
The first line of code is this:
Segment seg = segment.get(0);
This makes the variable
seg
refer to the first object of theArrayList
, which prevents it from becoming garbage. This line must come beforesegment.remove(0)
.Next, we remove the first pointer in
ArrayList
just as before:segment.remove(0);
This does not delete the segment — it just removes the first element of the
ArrayList
and moves every element after it down one position. The actualSegment
object is still there, be pointed to byseg
.Finally, we have these three lines:
seg.x = mouseX; seg.y = mouseY; segment.add(seg);
They set
seg.x
andseg.y
to the current position of the mouse, and then add that segment back into theArrayList
.We did need to create a new
Segment
object — we just re-used the one we removed from the beginning!When you run the program with these changes in place, it looks essentially the same, but now there is no noticeable delay to garbage collection. For a simple demo program such as this it seems like no big deal, but in a large program, such as a game, where you might have lots of other animated objects using memory, this change is very important. Without it, snakes would probably take up too much memory.
A Snake that Moves on its Own¶
Finally, lets try to make a snake that moves on its own. The simplest way to
make something move is as we have been doing throughout the course: increment
the x
and y
values of the position of the thing we want to move. Since
our snakes move by putting down new segments, 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:
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.segment.get(snake.segment.size() - 1);
}
This makes the snake wiggle diagonally down the screen. It is a nice effect, although it perhaps looks more like a comet then a snake.
Questions¶
- 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.
Source Code¶
class Segment {
float x, y;
Segment(float init_x, float init_y) {
x = init_x;
y = init_y;
}
} // class Segment
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);
segment.add(s);
++i;
}
}
void update() {
// make a variable that refers to the first segment before it
// gets removed; this prevents it from becoming garbage and
// thus slowing down our program
Segment seg = segment.get(0);
// remove the first segment, and shift all the other segments
// down one index position
segment.remove(0);
// This is the line of code that we are replacing:
//Segment s = new Segment(mouseX, mouseY);
// Instead of creating a new segment, re-use the one we
// just removed.
seg.x = mouseX;
seg.y = mouseY;
segment.add(seg);
}
void render() {
// draw everything
int i = 0;
while (i < segment.size ()) {
noStroke();
float x = 255 - i * 5;
float mx = map(x,
255 - (segment.size() - 1) * 5, 255, // input range
0, 255 // output range
);
fill(mx);
float diam = map(i,
0, segment.size() - 1, // input range (i's range)
0, 50); // output range (diam's range)
ellipse(segment.get(i).x, segment.get(i).y, diam, diam);
++i;
}
}
void update(float x, float y) {
// remove the first segment, and shift all the other segments
// down one index position
segment.remove(0);
Segment s = new Segment(x, y);
segment.add(s);
}
} // class Snake
/////////////////////////////////////////////////////////////////////
Snake snake;
void setup() {
size(500, 500);
smooth();
snake = new Snake(50);
}
void draw() {
background(255);
snake.update(mouseX, mouseY);
snake.render();
}