Train Cars

Beware! This document needs cleaning up. Formatting may be messy or broken. Text may be inaccurate or in need of editing.

You can look at this preview, but be aware that the content will change before the instructor assigns this to the class.

This document is the result of an automated conversion. You can view the original version but again beware: that version too is not necessarily what will eventually be assigned.

  • Practice creating classes that use inheritance
  • Understand abstract classes
  • Practice refactoring (modifying) existing code using inheritance

Run the TrainDrawer class’s main method. It will draw this train:

Looking at the picture above, first jot down what is the same about all three train cars. Then go look for these pieces in the code for the TrainDrawer class.

There is a reasonably good separation of methods in the TrainDrawer so the code that was used more than once, such as in the method drawCarFrame, lives in its own method and is called each time it is needed in the methods that create the other cars.

The real beauty of object-oriented languages, however, is that we can go one step further in taking advantage of this idea of code reuse: creating separate classes that group related methods.

In object-oriented languages like Java, we can create a class, perhaps called TrainCar, whose constructor method contains the code that you reused to make each part of a car the same. A class like this is called a parent class for the three other child classes, Engine, Boxcar, and Caboose, which you will also make. Here is a detailed (perhaps too detailed!) diagram of their relationship:

Package train.png

The constructor for each of the three child classes will first call the constructor for its parent, TrainCar. This will give you the parts of the train common to all three. Then you can add on the parts that are unique to each of the three types of train car to their respective constructors. This enables you to also add other different kinds of TrainCar classes if you want them — and greatly simplify the TrainDrawer class, which is really bigger than it should be!

We are going to rearrange the code from the original single TrainDrawer class from the previous activity into several classes, according to our class diagram shown above.

Abstract TrainCar superclass for common elements

  1. Inside the traincar package, make a new abstract class called TrainCar, using the abstract keyword.
  2. Move the two methods that are common to all cars into your new TrainCar class. Some parts of the code show up in red text because some things are missing. In particular, why does a call to add() have an error?

To handle the problem with add(), we’re going to do something similar to what you saw in the Bubble class from HW2. A TrainCar is really just a collection of other graphical objects (e.g. the wheel ellipses, car rectangle, etc.) grouped together.

  1. Make the TrainCar class a GraphicsGroup using the extends keyword in its class definition.
  2. Add a constructor to TrainCar. For now, your constructor does not need any parameters (we will add them later). Your constructor should call the default constructor (the one without any parameters) in GraphicsGroup using the keyword super().
  3. Modify where you add graphical objects. You are no longer adding them to the canvas. What object are you adding them to now? (Hint: you can use the this keyword if you need a pointer to the object whose method is running.)

The additional issues still shown in red in your code are for other named constants that are still in the TrainDrawer class.

  1. Move just the ones that you need, which represent the common elements to all train cars.
  2. For the common pieces to get drawn, you can now call the function drawCarFrame, which calls drawWheel, in the constructor for TrainCar.
  3. As you do this, you will notice that you need values to send in as arguments to this function. Add a parameter for color to your constructor.

The x and y values used in the starter code must also change now that TrainCar is a GraphicsGroup. You can think of a GraphicsGroup as a mini-canvas with its own coordinate system. In the original TrainDrawer, x is the start of the train and y is the height of the canvas. Those values will now change. In the GraphicsGroup’s coordinate system we want the train to start drawing at the front of the mini canvas (x = 0), and the y value will be the overall height of the train, as shown below.

  1. To help with this, add a method called getCarHeight() to the TrainCar. It should return the overall height of a standard train car (just the baseline and the car height).
  2. Change the x and y values where you call drawCarFrame to 0 and getCarHeight().

Now we have an abstract class called TrainCar with the common parts of any train car. The next step is to create each individual type of cars as subclasses of the parent TrainCar class.

––– When you get here, please flag the teacher/preceptor to look over your work. –––

Create Engine subclass with additional features

Make a new class in your traincar package called Engine and make it extend TrainCar using the extends keyword. This makes Engine a subclass of TrainCar. Look at the original code from TrainDrawer and note these ideas:

  • Is an Engine a GraphicsGroup? Why do we not have to say “extends GraphicsGroup” in the Engine class?
  • The function called drawEngine, and in particular its contents, can now serve as the basis of the constructor for your new Engine class.
  • The other functions called inside drawEngine can be used as is in your new Engine class. Move those functions into Engine first, like you did when you copied the common functions into TrainCar.
  1. Make a constructor for Engine that also has parameters that are the same as TrainCar:

    public Engine(Color color) { super(color); }

Remember from your reading that the super method calls the constructor for TrainCar, ensuring that the common parts get drawn to the canvas.

  1. To create the other parts of the Engine, add calls to the methods drawSmokestack, drawEngineCab, and drawCowcatcher in the constructor, after the super() call. Note that you’ll have to change the x and y values so that the cab, smokestack, and cowcatcher are drawn around your train car—what reference point would make sense for this? (Hint: see step 10, where we set our reference point for the train car)
  2. Now you will still have a few constants left in red. Get those from the original TrainDrawer and add them at the top of the Engine class (there are five of them that are specific to the Engine).

There is one more enhancement that can be done to our engine. Notice that Color.BLACK is inside two of the functions. The color of this Engine really should be a private instance variable that is set inside the constructor and used by those functions.

  1. Add the color as a private instance variable at the top of the Engine class:

    private Color color; //color of this engine

Then set it in the constructor using the passed in parameter and use it in the two methods that need the color. Now we can then make engines with different colors.

Draw what we have so far

It’s time to make sure that we can at least draw an Engine. Then we can get back to the Boxcar and The Caboose.

  1. Your TrainDrawer class should look pretty sparse by this point. Modify the constructor to instantiate an Engine object and add it to the canvas.

We also have some red highlighted code to consider here. You will have moved the constant class variables CAR_WIDTH and CONNECTOR into the TrainCar class. Since they are public, you can still access them in TrainDrawer, but you must put the name of the class in front of them (e.g. TrainCar.CAR_WIDTH).

Because the constructor for Engine only takes a color, you also need a way to place it on the canvas at the position you would like. We will use the fact that the add method in the CanvasWindow class is overloaded. You should use this version, which allows you to specify where the upper left corner of the group should appear on the canvas:

void **add** ( **GraphicsObject** gObject, double x, double y) Adds the graphical object to the list of objects drawn on the canvas at the position x, y.
  1. Now make two more new classes in the new train package, Boxcar and Caboose, each of which extend TrainCar.

Have the code of the _ original _ TrainDrawer method called drawBoxcar be the basis of the constructor for your Boxcar class, and the function called drawCaboose be the basis of the constructor for your Caboose class. Remember to first create the super() line in each constructor, which replaces the drawCarFrame method call in each of the original TrainDrawer methods.

Add instances of Boxcar and Caboose in the new TrainDrawer: End Here!

When you have a good constructor for each of your new Boxcar and Caboose classes, use them in your _ new version _ of TrainDrawer in the train package to create instances of them.

When you have everything working, your new TrainDrawer code file will have one very short constructor method in it. This is something you should always be looking to accomplish: having smaller classes with specific duties. Your package now is broken out using a class hierarchy of types of trains who draw themselves, and the constructor method in a separate TrainDrawer class creates them.

This now gives you the ability to fairly easily add extra cars, for example multiple boxcars that have different colors in between the Engine and the caboose. Try this by changing your constructor method in your newly improved TrainDrawer class in your train package.

Make a Train class

If you wanted to be able to animate the train moving across the screen, you would need to move each TrainCar individually. You can fix that by making a new class called Train that is a GraphicsGroup. Your train class should contain TrainCar objects. Modify your TrainDrawer to create a Train instead of the individual cars. Your Train constructor should deal with creating the Engine, BoxCar, and Caboose.

Animate your train

Animate your train so that it moves across the screen over time. You can do this by putting canvas.draw() inside a loop.

Make TrainDrawer a CanvasWindow

You can declare a class to extend CanvasWindow. Try doing that with TrainDrawer and update its constructor. It no longer needs to make a new canvas. (Is this an improvement or not?)

Try an animation technique: parallax scrolling

There is an interesting animation technique called parallax scrolling (see https://en.wikipedia.org/wiki/Parallax_scrolling with an example animation). To do this, you can create background scenes called layers as separate GraphicsGroup objects. Perhaps start by adding some rectangles to represent buildings (even adding windows). To give an effect of the Train moving by faster in the foreground, have the scene layers move a smaller amount each time step.

Note also that you can add images to a GraphicsGroup. (See the class called Image in kilt-graphics). You will need to place your images in a folder called res (for resources) in the top level of your project. When you make a new image, you send it a path that includes res, like this example (your image file name will be different):

imageDie1 = new Image(1, 1, "awesome_happy_fun_image.png");

Thanks to Dan Kluver for suggesting this idea.

Hat tip to Bret Jackson and Libby Shoop for originally creating this activity.