We are about to switch to a new forum software. Until then we have removed the registration on this forum.
In the previous article, From several variables to arrays, we shown how we can scale a program by using arrays. We also saw that having to handle several arrays isn't very practical. If we have other kinds of objects, we have more arrays, unrelated. We have to make sure the arrays keep the same size. We have difficulties to add or remove items. We will see how to address these problem, using classes.
The full sketch of the previous article is as follows:
// 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);
}
}
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);
}
}
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);
}
A class just represents an object. Here, it will represent a ball, with all its parameters. In its simplest form, it can be seen as a structure, a way to regroup related variables in the same group of data. Somehow, we go back to the original code with one ball, except we wrap the variables in a class declaration:
class Ball // By tradition, class names start with an initial uppercase letter
{
float posX, posY; // Position
float speedX, speedY; // Movement (linear)
int radius; // Radius of the ball
color ballColor; // And its color
}
And that's all... Note that the name "ballColor" is a bit redundant, since this class variable (also called field) is scoped in a Ball, but we cannot use simply "color" since it is a reserved word in Processing.
This class is just a blueprint, it shows what kind of variables are inside it, but it has no real existence. It is a type, defining a kind of variable. You have to create an instance of this class, to create an object out of it, to have real data space allocated for this instance:
Ball ball = new Ball();
This calls a special function (functions inside classes are called methods), named constructor. Here, we have not created such function, but the compiler provides it, it is called the default constructor.
The ball
variable doesn't really hold the object, it holds a reference to this object. The object exists in a special memory, and all variables and parameters of this type just have the reference of this object.
Thus, if you write:
Ball b = ball;
you are actually not creating a copy of the object, unlike what most people think first, but copy of the reference to the same object. If you change the object referenced by ball
, you also change the one referenced by ball
!
It can seem counter intuitive, because if you write:
int n = 5;
int a = n;
then a
and n
are independent, if you change one, you don't change the other. But, again, ball
and b
hold references. If you want a copy of an object, you have to clone it, to make a double in memory, with a new reference.
And how we change these objects? So far, the newly created object have all its fields initialized at default values, here zero... We can change this default value at declaration time:
int radius = 32; // Radius of the ball, 32 by default
inside the class declaration.
But once created, we can also access each field and change it:
Ball ball = new Ball();
ball.posX = 120;
ball.posY = 50;
ball.speedX = -2;
ball.speedY = 3;
ball.radius = 24;
ball.ballColor = #002277;
We have to repeat ball.
, there is no shortcut for this, unlike some languages.
But we can make our own constructor, with parameters!
class Ball
{
float posX, posY; // Position
float speedX, speedY; // Movement (linear)
int radius; // Radius of the ball
color ballColor; // And its color
Ball(float x, float y, float sx, float sy, int r, color c)
{
posX = x;
posY = y;
speedX = sx;
speedY = sy;
radius = r;
ballColor = c;
}
}
Thus, we can build the full ball in one call only:
Ball ball = new Ball(120, 50, -2, 3, 24, #002277);
and it comes will all its parameters set.
Inconvenience: with lot of parameters like this, we can easily loose the meaning of each parameter, forget one, invert two, etc. Alas, Java doesn't have named parameters, so we have to deal with it in other ways. For example, we can use comments:
Ball ball = new Ball(
120, 50, // x and y position
-2, 3, // x and y speeds
24, // radius
#002277 // color
);
A more advanced trick is to use chained setters, but we won't see that here.
Trick 1: sometime, you will see parameters of same name than the class fields. To distinguish them in the method, we use the this.
notation, where this
just means "this object, the current one being created / accessed". Some people also like to systematically prefix the fields with this
everywhere in the class, but I find that it is a bit verbose, redundant and less readable.
Ball(float posX, float posY, float speedX, float speedY, int radius, color ballColor)
{
this.posX = posX;
this.posY = posY;
this.speedX = speedX;
this.speedY = speedY;
this.radius = radius;
this.ballColor = ballColor;
}
Variants add a prefix or a suffix to the parameters names or the field names, eg. m_posX
as field name, or posX_
as parameter name.
Trick 2: in Java, we can have method with identical names, they are seen as distinct as long as they have different kinds of parameters. So we can have a simplified constructor, giving for example default values to parameters, and a full constructor to override everything:
Ball(int r, color c)
{
// This actually calls the other constructor, providing the missing values
this(random(r, width - r), random(r, height - r), random(-7, 7), random(-7, 7), r, c);
}
Ball(float x, float y, float sx, float sy, int r, color c)
{
posX = x;
posY = y;
speedX = sx;
speedY = sy;
radius = r;
ballColor = c;
}
Note how the generated numbers depend on one parameter.
This is nice, but how we use this class in the sketch? Quite simply: instead of declaring n arrays, we only declare one array of Balls:
final int BALL_NB = 5;
Ball[] balls = new Ball[BALL_NB];
Warning! The code above created an array of references to Ball objects. But these references are initialized at the default value for such references: null
. This means that if you try and use the array as this, you will get a NullPointerException. You have to fill the array with fresh new instances of this class:
void setup()
{
size(600, 400);
smooth();
// The colors to use
// Note that plural names are preferred for arrays (and collections) names
color[] colors = { #DDEE55, #AA44EE, #22BBAA, #0022FF, #00FF22 };
// Initialize the balls' data
for (int i = 0; i < BALL_NB; i++)
{
// Using the short version of the constructor
balls[i] = new Ball(8 + i * 8, colors[i]);
}
}
and, of course, we have to use the balls:
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);
}
}
void moveBall(int n)
{
// Move by the amount determined by the speed
balls[n].posX += balls[n].speedX;
// Check the horizontal position against the bounds of the sketch
if (balls[n].posX < balls[n].radius || balls[n].posX > width - balls[n].radius)
{
// We went out of the area, we invert the h. speed (moving in the opposite direction)
// and put back the ball inside the area
balls[n].speedX = -balls[n].speedX;
balls[n].posX += balls[n].speedX;
}
// Idem for the vertical speed/position
balls[n].posY += balls[n].speedY;
if (balls[n].posY < balls[n].radius || balls[n].posY > height - balls[n].radius)
{
balls[n].speedY = -balls[n].speedY;
balls[n].posY += balls[n].speedY;
}
}
void displayBall(int n)
{
// Simple filled circle
noStroke();
fill(balls[n].ballColor);
ellipse(balls[n].posX, balls[n].posY, balls[n].radius * 2, balls[n].radius * 2);
fill(#FF0000);
text(str(n), balls[n].posX, balls[n].posY);
}
Note that moveBall()
and displayBall()
act on one ball at a time. And they are quite verbose, having to prefix each variable by the ball they act upon.
Somehow, they can be part of the Ball
class, since they almost only use class fields, and act on one instance of the class.
By integrating methods (ie. functions) to a class, we go from a simple structure, just holding data (that can change) to a fully autonomous object, offering methods to act on it and to allow it to display itself. And the methods need not to prefix the fields: they are part of the current object they act upon.
The new class becomes:
class Ball
{
float posX, posY; // Position
float speedX, speedY; // Movement (linear)
int radius; // Radius of the ball
color ballColor; // And its color
Ball(int r, color c)
{
// This actually calls the other constructor, providing the missing values
this(random(r, width - r), random(r, height - r), random(-7, 7), random(-7, 7), r, c);
}
Ball(float x, float y, float sx, float sy, int r, color c)
{
posX = x;
posY = y;
speedX = sx;
speedY = sy;
radius = r;
ballColor = c;
}
void moveBall()
{
// Move by the amount determined by the speed
posX += speedX;
// Check the horizontal position against the bounds of the sketch
if (posX < radius || posX > width - radius)
{
// 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 = -speedX;
posX += speedX;
}
// Idem for the vertical speed/position
posY += speedY;
if (posY < radius || posY > height - radius)
{
speedY = -speedY;
posY += speedY;
}
}
void displayBall()
{
// Simple filled circle
noStroke();
fill(ballColor);
ellipse(posX, posY, radius * 2, radius * 2);
}
}
As you can see, the methods are much simpler: no need for an index, as they act on the current object, no need to prefix the fields to access them.
Note the difference between constructors and methods: constructors have no type, not even void
, and have the same name than the class. Methods usually start with a lowercase letter (that's just a convention) and have a type (void
if they return nothing, or the type of what they return otherwise).
Now draw()
becomes:
void draw()
{
// Erase the sketch area
background(#AAFFEE);
for (int i = 0; i < BALL_NB; i++)
{
// Compute the new ball position
balls[i].moveBall();
// And display it
balls[i].displayBall();
}
}
We call the methods with a syntax similar to the field access.
What is nice is that we no longer need to access the fields, now. We could even make them hidden outside of the class (by using a private
keyword before each declaration), as only the class has the responsibility to manage them. We say we encapsulated the functionality of the class, it acts as a black box: we don't care of how it is implemented, all we need to know is how to make an instance of it, and how to call its methods.
That's a bit of advanced usage of classes, rarely used in Processing with its small sketches (but it can be used in libraries).
Last note: we can use the new for
notation in draw():
void draw()
{
// Erase the sketch area
background(#AAFFEE);
for (Ball ball : balls)
{
// Compute the new ball position
ball.moveBall();
// And display it
ball.displayBall();
}
}
See how ball
replaces balls[i]
and the for
loop iterates on all the items of the array without needing an index.