From several variables to arrays
From Processing
Yes, that's not really a question, but that's still a common problem met by most newcomers to programming, showing their code in the forum.
Contents |
A base sketch
Lot of newbies with Processing start by making the famous bouncing balls: a circle (or more) that travels in straight line, bouncing on the bounds of the sketch. That's a good starting point to show how to scale this kind of sketch.
The base code, with one ball, is shown in the How do I display a message for a few seconds? article, I won't repeat it here.
Make two balls
Now, say we want two balls. What most beginners do is to duplicate all the code for one ball to have two balls:
// First ball parameters float posX1, posY1; // Position float speedX1, speedY1; // Movement (linear) int radius1; // Radius of the ball color ballColor1; // And its color // Second ball parameters float posX2, posY2; float speedX2, speedY2; int radius2; color ballColor2; void setup() { size(600, 400); smooth(); // Initialize the ball's data posX1 = 120; posY1 = 50; speedX1 = -2; speedY1 = 3; radius1 = 24; ballColor1 = #002277; // Again for the second ball posX2 = 220; posY2 = 150; speedX2 = 2; speedY2 = -3; radius2 = 32; ballColor2 = #007722; } void draw() { // Erase the sketch area with some light color background(#AAFFEE); // Compute the new ball position moveBall1(); // And display it displayBall1(); // And again for the second ball moveBall2(); displayBall2(); } void moveBall1() { // Move by the amount determined by the speed posX1 += speedX1; // Check the horizontal position against the bounds of the sketch if (posX1 < radius1 || posX1 > width - radius1) { // We went out of the area, we invert the h. speed (moving in the opposite direction) // and put back the ball inside the area speedX1 = -speedX1; posX1 += speedX1; } // Idem for the vertical speed/position posY1 += speedY1; if (posY1 < radius1 || posY1 > height - radius1) { speedY1 = -speedY1; posY1 += speedY1; } } void displayBall1() { // Simple filled circle noStroke(); fill(ballColor1); ellipse(posX1, posY1, radius1 * 2, radius1 * 2); } void moveBall2() { posX2 += speedX2; if (posX2 < radius2 || posX2 > width - radius2) { speedX2 = -speedX2; posX2 += speedX2; } posY2 += speedY2; if (posY2 < radius2 || posY2 > height - radius2) { speedY2 = -speedY2; posY2 += speedY2; } } void displayBall2() { noStroke(); fill(ballColor2); ellipse(posX2, posY2, radius2 * 2, radius2 * 2); }
Well, it works fine...
More balls?
But when you read the code, that's twice as much code to understand, except that's the same code... And if you need to fix or change something, you have to do it twice.
Moreover, if you are asked to make 3 or more balls, you start to be in trouble... This doesn't scale well. At best, you can make one displayBall() routine with parameters:
and call it with the parameters of each ball, but moveBall() will have even more parameters; long lists of parameters are not very practical to handle.
And the repetitive long declarations at the start, and the initializations in setup() are not handy either.
At this point, some people have an idea, and ask how to generate variable numbers. Their idea is that since we have ballColor1, ballColor2, etc., why not access the variables like: ballColor + n or "ballColor" + n or some other creative syntax, where n is the index.
But, in Java, this doesn't work. This can be done, by reflection, but it is used only in special cases, not for such common usage, so we won't see that here.
Using arrays
The solution is simply to use arrays. This frighten some newbies, but it is really quite simple to use. Where you have one variable, you make an array, and then functions can access the nth item just by being given a number:
// Ball parameters // Positions float[] posX; float[] posY; // Movements (linear) float[] speedX; float[] speedY; // Radius of the balls int[] radius; // And the colors color[] ballColor; void setup() { size(600, 400); smooth(); // Initialize the ball's data posX[0] = 120; posY[0] = 50; speedX[0] = -2; speedY[0] = 3; radius[0] = 24; ballColor[0] = #002277; } void draw() { // Erase the sketch area background(#AAFFEE); // Compute the new ball position moveBall(0); // And display it displayBall(0); } void moveBall(int n) { // Move by the amount determined by the speed posX[n] += speedX[n]; // Check the horizontal position against the bounds of the sketch if (posX[n] < radius[n] || posX[n] > width - radius[n]) { // We went out of the area, we invert the h. speed (moving in the opposite direction) // and put back the ball inside the area speedX[n] = -speedX[n]; posX[n] += speedX[n]; } // Idem for the vertical speed/position posY[n] += speedY[n]; if (posY[n] < radius[n] || posY[n] > height - radius[n]) { speedY[n] = -speedY[n]; posY[n] += speedY[n]; } } void displayBall(int n) { // Simple filled circle noStroke(); fill(ballColor[n]); ellipse(posX[n], posY[n], radius[n] * 2, radius[n] * 2); }
Except this won't work, as I deliberately made an error. If you run this code, you will have a NullPointerException.
Why? The arrays have been declared with the [] notation, accessed with the [0] or [n] notation, but we need to declare how many items there will be in the arrays: in Java, arrays won't grow automatically, unlike some other programming languages.
The arrays can be initialized like this:
and then the sketch will work.
Note that all the arrays must have the same size, each index corresponding to one ball object. So, it is better to define a constant instead of repeating the same number everywhere. The advantages: it is less a "magical" number without meaning. You have only one place to change it, if you want more, or less, balls. If you search/replace 10 with another number, you risk to alter another 10 constant with nothing to do with the array size.
So the code will look now like:
// Ball parameters final int BALL_NB = 10; // final just indicate this value cannot be changed in the program // Positions float[] posX = new float[BALL_NB]; float[] posY = new float[BALL_NB]; // Movements (linear) float[] speedX = new float[BALL_NB]; float[] speedY = new float[BALL_NB]; // Radius of the balls int[] radius = new int[BALL_NB]; // And the colors color[] ballColor = new color[BALL_NB];
Now, if you want another ball, you have to change setup() and draw():
void setup() { size(600, 400); smooth(); // Initialize the ball's data posX[0] = 120; posY[0] = 50; speedX[0] = -2; speedY[0] = 3; radius[0] = 24; ballColor[0] = #002277; // Initialize the other ball's data posX[1] = 220; posY[1] = 150; speedX[1] = 2; speedY[1] = -3; radius[1] = 32; ballColor[1] = #007722; } void draw() { // Erase the sketch area background(#AAFFEE); // Compute the new ball position moveBall(0); // And display it displayBall(0); // Again moveBall(1); displayBall(1); }
OK, obviously, the loop in draw() can use a for loop:
void draw() { // Erase the sketch area background(#AAFFEE); for (int i = 0; i < BALL_NB; i++) { // Compute the new ball position moveBall(i); // And display it displayBall(i); } }
but we haven't filled yet all the slots of the arrays. It still works, because arrays are filled by default with the default value of their type: 0 for ints and floats, false for booleans, etc. So here we are just displaying balls of size 0...
Notice the loop: array indexes start at 0, and end at the array size minus 1, hence the < condition in the loop.
Array initialization
Initializing the arrays like we do above in setup() isn't very practical: it is quite verbose. We can use a loop and initialize the values with random numbers:
void setup() { size(600, 400); smooth(); // Initialize the balls' data for (int i = 0; i < BALL_NB; i++) { radius[i] = int(random(10, 50)); posX[i] = random(radius[i], width - radius[i]); posY[i] = random(radius[i], height - radius[i]); speedX[i] = random(-10, 10); speedY[i] = random(-10, 10); ballColor[i] = color(0, random(100, 255), random(100, 255)); } }
Now, we might want to have a precise color for each ball, and perhaps a precise radius, while keeping pos and speed random. Java has a shortcut notation to declare an array while initializing its values.
// Ball parameters final int BALL_NB = 5; // Positions float[] posX = new float[BALL_NB]; float[] posY = new float[BALL_NB]; // Movements (linear) float[] speedX = new float[BALL_NB]; float[] speedY = new float[BALL_NB]; // Radius of the balls int[] radius = { 8, 16, 24, 32, 48 }; // And the colors color[] ballColor = { #DDEE55, #AA44EE, #22BBAA, #0022FF, #00FF22 }; void setup() { size(600, 400); smooth(); // Initialize the balls' data for (int i = 0; i < BALL_NB; i++) { posX[i] = random(radius[i], width - radius[i]); posY[i] = random(radius[i], height - radius[i]); speedX[i] = random(-7, 7); speedY[i] = random(-7, 7); } }
Note that this notation is only used at the declaration site. If, for any reason, you need to use this notation after the array have been initialized, you have to use a slightly more convoluted notation:
Having all these "parallel" arrays isn't very practical, there isn't much cohesion, we still have repetitive code (the array declarations), information is a bit dispersed.
In the next article, From several arrays to classes, we will show that using classes is an elegant way to address these issues.