(Extra) Making a Car

The Sprite class we’ve been writing in the last few lessons can be used as the basis for creating more complex animated objects. In this note we’ll see how to create an animated car consisting of a number of individual sprites.

The Car Class

Lets make a side-view of a 2-dimensional car. The car will consist of three sprites: a body, a front wheel, and a back wheel. Right away this lets us write some code:

class Car {
   Sprite body;
   Sprite frontWheel;
   Sprite backWheel;

   Car() {
     body = new Sprite("blueCarBody_small.png");
     frontWheel = new Sprite("carWheel_small.png");
     backWheel = new Sprite("carWheel_small.png");
   }

   // ...

 } // class Car

Two image files are used here: blueCarBody_small.png, and carWheel_small.png. These ones were created using a drawing program, and slightly edited to make them a little smaller. Of course, you can replace these files with whatever graphics you like.

We also need to keep track of the position of the (x, y) position of the entire car:

class Car {
   float x, y;

   // ...

 } // class Car

Rendering and Updating

Rendering and updating a Car object requires rendering and updating its three constituent sprites. The update() function is straightforward:

void update() {
  body.update();
  frontWheel.update();
  backWheel.update();
}

Rendering is a little bit more involved because we need to specify the location of the wheels on the car. The best way to do this is to use the translate function because it applies to all the sprites:

void render() {
  pushMatrix();
  translate(x, y);  // move the origin here
  body.render();
  translate(50, 50);
  frontWheel.render();
  translate(-75, 0);
  backWheel.render();
  popMatrix();
}

Note the following:

  • The code in render() is wrapped in calls to pushMatrix() and popMatrix() because it changes the entire coordinate system when translate is called. This ensures that the coordinate system is always reset to the way it was before the call to render().

  • After the body is rendered (by calling body.render()), the wheels are drawn. The way to think about this is to imagine that the car has been placed at (0, 0), and then to adjust the position of the wheels relative to (0, 0). For example, before rendering frontWheel we call translate(50, 50), which moves the wheel 50 pixels to the right and 50 pixels down.

    The values for the wheel positions were chosen experimentally.

Visibility

Our sprites are either visible or invisible, and so we can make a Car object invisible by setting the body, frontWheel, and backWheel to be invisible. We add two functions to do this:

class Car {
  // ...

  void setVisible() {
    body.visible = true;
    frontWheel.visible = true;
    backWheel.visible = true;
  }

  void setInvisible() {
    body.visible = false;
    frontWheel.visible = false;
    backWheel.visible = false;
  }

  // ...

} // class Car

These functions just set the visibility of the constituent objects.

Velocity

We also want to set the cars speed. The body and wheels should all move at the same speed, and so we will add a function called setVelocity:

class Car {
   // ...

   void setVelocity(float dx, float dy) {
     body.dx = dx;
     body.dy = dy;
     frontWheel.dx = dx;
     frontWheel.dy = dy;
     backWheel.dx = dx;
     backWheel.dy = dy;
   }

   // ...

} // class Car

The wheels of the car should rotate on their own, and so we add setWheelSpeed:

class Car {
   // ...

   void setWheelSpinRateInDegrees(float rate) {
     frontWheel.spinRate = radians(rate);
     backWheel.spinRate = radians(rate);
   }

   // ...

} // class Car

The input parameter to setWheelSpinRateInDegrees is in degrees simply because it is assumed the user of a Car object would prefer degrees instead of radians.

The long function name is to take make it absolutely clear that the rate is in degrees (and not radians).

Scale

Finally, lets add a variable to change the size of the car:

class Car {

  // ...

  float imageScale;

  Car() {
    // ...

    imageScale = 1;
  }

  void render() {
    pushMatrix();
    translate(x, y);
    scale(imageScale);

    // ...

    popMatrix();
  }

  // ...

} // class Car

By default, imageScale is set to 1, meaning that it will be drawn at its natural size. Setting imageScale to, say, 2.5, will draw the image at two-and-a-half times it natural size.

Like translate and rotate, the scale function applies to the entire coordinate system: it shrinks/expands the distance between every point of the coordinate system by the same given amount. Thus every drawing command after a call to scale will be at the new scale. For our car this means that the wheels are drawn at the correct location on scaled-down or scaled-up images.

Using the Car Class

Now that our Car class is written, it is fun and easy to use. For example:

Car car;

void setup() {
  size(500, 500);
  car = new Car();
  car.setVelocity(1, 0);
  car.frontWheel.angle = radians(40); // make the wheels look different
  car.setWheelSpinRateInDegrees(1);
  car.y = 200;
  car.imageScale = 0.5;
  car.setVisible();
}

void draw() {
  background(255);

  car.render();
  car.update();
}

Notice how simple the code is: most of our work is in creating and initializing the Car object. It’s not hard to imagine that someone, say, an artist or designer, could learn to use code like this without having to become a Processing expert.

Random Cars

Lets create a demo program that has multiple cars moving across the screen at different speeds. The position, velocity, and size of the cars will be chosen at random.

Some random cars.

The first thing we need is an ArrayList to store our Car objects:

final int NUM_CARS = 10;
ArrayList<Car> cars;

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

  cars = new ArrayList<Car>();
  for(int i = 0; i < cars.size(); ++i) {
    // ... initialize cars...
  }
}

void draw() {
  background(255);

  for(Car c : cars) {
    c.render();
    c.update();
  }
}

Now we want to create some cars with a random position, size, and velocity. The best way to do that is to write a function that returns new Car objects that have been initialized the way we want:

Car makeRandomCar() {
  Car c = new Car();

  c.x = -10;
  c.y = random(450);

  c.imageScale = random(0.25, 1);

  float dx = random(0.5, 2);
  c.setVelocity(dx, 0);
  c.setWheelSpinRateInDegrees(dx);

  c.setVisible();
  return c;
}

Note the following:

  • The name of this function is makeRandomCar, and it returns a new object of type Car. It doesn’t take any input.
  • The statement random(x, y) returns a random number between x and y. The particular values used for random in this function were chosen experimentally.

Using makeRandomCar is easy; we just call inside the loop used to initialize the car array:

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

  cars = new ArrayList<Car>();
  for(int i = 0; i < cars.size(); ++i) {
    randCar = makeRandomCar();
    cars.add(randCar);
  }
}

void draw() {
  background(255);

  for (Car c : cars) {
    c.render();
    c.update();
  }
}

Programming Questions

  1. Add a void function called honk() to the Car class that, when called, will make the car’s horn honk, e.g.:

    Car c;
    c = new Car();
    c.honk();  // makes a honking sound
    
  2. Write a program that makes a car drive across the screen and explode when it hits the right edge. Use the Explosion class to do this.

The Car Class

class Car {
  float x, y;  // position of the entire car
  float imageScale;

  Sprite body;
  Sprite frontWheel;
  Sprite backWheel;

  Car() {
    body = new Sprite("blueCarBody_small.png");
    frontWheel = new Sprite("carWheel_small.png");
    backWheel = new Sprite("carWheel_small.png");
    imageScale = 1;
  }

  void update() {
    body.update();
    frontWheel.update();
    backWheel.update();
  }

  void render() {
    pushMatrix();
    translate(x, y);  // move the origin here
    scale(imageScale);
    body.render();
    translate(50, 50);
    frontWheel.render();
    translate(-75, 0);
    backWheel.render();
    popMatrix();
  }

  void setVisible() {
    body.visible = true;
    frontWheel.visible = true;
    backWheel.visible = true;
  }

  void setInvisible() {
    body.visible = false;
    frontWheel.visible = false;
    backWheel.visible = false;
  }

  void setVelocity(float dx, float dy) {
    body.dx = dx;
    body.dy = dy;
    frontWheel.dx = dx;
    frontWheel.dy = dy;
    backWheel.dx = dx;
    backWheel.dy = dy;
  }

  void setWheelSpinRateInDegrees(float rate) {
    frontWheel.spinRate = radians(rate);
    backWheel.spinRate = radians(rate);
  }

} // class Car