More Complex Sprites

In these notes you will learn:

  • A general approach for modelling animated objects using object-oriented programming (OOP).
  • How to write a class that extends Sprite.
  • The definition of base class, inherting class, subclass, and super class.
  • How to use the super variable.

A Better Sprite Class

In previous notes we introduced the Sprite class to help simplify creating animated objects:

class Sprite {
  float x;
  float y;
  float dx;
  float dy;
}

With it we could write code like this:

color ballColor = color(255, 165, 0);

Sprite ball = new Sprite();
float radius;

void setup() {
  size(500, 500);
  smooth();
  ball.x = 100;
  ball.y = 100;
  ball.dx = 1;
  ball.dy = 2;
  radius = 25;
}

void draw() {
  background(255);

  noStroke();
  fill(ballColor);
  ellipse(ball.x, ball.y, 2 * radius, 2 * radius);

  ball.x += ball.dx;
  ball.y += ball.dy;

  // hit the left edge?
  if (ball.x - radius <= 0) {
    ball.dx = -ball.dx;
    ball.x = radius;
  }

  // hit the right edge?
  if (ball.x + radius >= 499) {
    ball.dx = -ball.dx;
    ball.x = 499 - radius;
  }

  // hit the top edge?
  if (ball.y - radius <= 0) {
    ball.dy = -ball.dy;
    radius += 5;
    ball.y = radius;
  }

  // hit the bottom edge?
  if (ball.y + radius >= 499) {
    ball.dy = -ball.dy;
    ball.y = 499 - radius;
  }
}

Using the Sprite class makes it easier to declare x, y, dx, and dy, and also makes the source code a little easier to read.

But we still have to do a lot of typing. For example, every animated objected needs these two lines like this in draw():

ball.x += ball.dx;
ball.y += ball.dy;

These lines move the location of the ball, and so every animated object needs them. When you have multiple animated objects, these lines soon get tedious to type and start to clutter your program (making it harder to read).

So what we can do is add a function in the Sprite class that does the update for us:

class Sprite {
  float x;
  float y;
  float dx;
  float dy;

  void update() {
    x += dx;
    y += dy;
  }
}

Then we can replace the two += statements in draw() with one call to update():

// ... setup same as before ...

void draw() {
  background(white);

  // ...

  // ball.x += ball.dx;
  // ball.y += ball.dy;

  ball.update();

  // ...
}

This is a small but useful change. It saves us from writing two messy lines of code and makes our program a little shorter and easier to read.

An Animated Ball

We’ve seen an animated ball many times in this course, and now we want to create an object-oriented version of such a ball. For many programs, this is probably the best way to create animated objects.

The first step is to create a class for the animated object:

class Ball {
    // ...
}

The name of class should describe the thing it is modelling: a class that animates the sun should be called Sun, a class that animates a bird should be called Bird, and so on.

Because Ball is an animated object, we need to add variables for its position and velocity:

class Ball {
  float x;
  float y;
  float dx;
  float dy;

  // ...
}

While this works, there is a better way.

Inheritance

The Sprite class we’ve been using already has x, y, dx, and dy (plus update()):

class Sprite {
  float x;
  float y;
  float dx;
  float dy;

  void update() {
    x += dx;
    y += dy;
  }
}

Instead of adding these four variables (and update()) all again by hand to Ball, we can use inheritance like this:

class Ball extends Sprite {
  // ...
}

Ball inherits all the variables and functions from Sprite. We say that Sprite is the base class, and Sprite is the inheriting class. Some programmers use different terminology: they say Ball is a subclass of Sprite in this case, and, conversely, that Sprite is a superclass of ``

Now we can write code like this:

Ball b = new Ball();

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

  b.x = 250;
  b.y = 250;
  b.dx = -1.6;
  b.dy = 1.1;
}

Ball behaves exactly the same as if we had written it like this:

class Ball {
  float x;
  float y;
  float dx;
  float dy;

  void update() {
    x += dx;
    y += dy;
  }
}

Obviously, using extends is much simpler than typing all this out, and so that it was we will do from now on for every animated class we create.

Improving Ball

Every ball has its own radius and fill color, and so that means we should add a radius and fillColor variable to Ball:

class Ball extends Sprite {
  float radius;
  color fillColor;
}

Next, lets add a function for drawing the Ball. To avoid confusion with the main draw() function, we’ll call it render():

class Ball extends Sprite {
  float radius;
  color fillColor;

  void render() {
    noStroke();
    fill(fillColor);
    ellipse(x, y, 2 * radius, 2 * radius);
  }
}

Now we can program a moving ball:

Ball b = new Ball();

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

  // initialize the ball
  b.x = 250;
  b.y = 250;
  b.dx = -1.6;
  b.dy = 1.1;
  b.radius = 25;
  b.fillColor = color(255, 0, 0);
}

void draw() {
  background(255);

  b.render();
  b.update();
}

This code is pretty simple: the details of how, exactly, the ball is rendered and updated are hidden in the Sprite and Ball class.

Enabling the ball to bounce off the edges requires adding the usual set of if- statements:

Ball b = new Ball();

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

  b.x = 250;
  b.y = 250;
  b.dx = -1.6;
  b.dy = 1.1;
  b.radius = 25;
  b.fillColor = color(255, 0, 0);
}

void draw() {
  background(255);

  b.render();
  b.update();

  // hit left edge?
  if (b.x < 0) {
    b.x = 0;
    b.dx = -b.dx;
  }

  // hit right edge?
  if (b.x > 499) {
    b.x = 499;
    b.dx = -b.dx;
  }

  // hit top edge?
  if (b.y < 0) {
    b.y = 0;
    b.dy = -b.dy;
  }

  // hit bottom edge?
  if (b.y > 499) {
    b.y = 499;
    b.dy = -b.dy;
  }
}

Calling a super Function

A better place to put the edge-detection if-statements is in the update() for Ball:

class Ball extends Sprite {
    // ...

  void render() {
    // ...
  }

  void update() {
    super.update();  // calls the update() function from Sprite

    // hit left edge?
    if (x < 0) {
      x = 0;
      dx = -dx;
    }

    // hit right edge?
    if (x > 499) {
      x = 499;
      dx = -dx;
    }

    // hit top edge?
    if (y < 0) {
      y = 0;
      dy = -dy;
    }

    // hit bottom edge?
    if (y > 499) {
      y = 499;
      dy = -dy;
    }
  }
}

With this change our main code becomes short and sweet again:

Ball b = new Ball();

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

  b.x = 250;
  b.y = 250;
  b.dx = -1.6;
  b.dy = 1.1;
  b.radius = 25;
  b.fillColor = color(255, 0, 0);
}

void draw() {
  background(255);

  b.render();
  b.update();
}

An important detail in the update() function for Ball is the very first line:

void update() {
    super.update();

    // ...
}

Processing automatically adds a special variable called super to every object you create. It is how you can access functions defined in the “super class”, i.e. the Sprite class, that happen to have the same name as a function in Ball. Inside Ball, the statement super.update() calls the update() in Sprite, while the statement update() (without super) calls the update() function Ball.

Questions

  1. Briefly describe what inheritance is in Processing, and how it works. Use an example in your answer.

  2. Consider the following code fragment:

    class Monster extends Sprite {
    
       // ...
    
    }
    
    1. How many different classes are named in this code?
    2. Which class is the base class?
    3. Which class is the inheriting class?
    4. Which class is the subclass?
    5. Which class is the super class?
  3. What is the purpose of using super in a class? Give an example of how to use it.