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.
Learning goals
- 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:
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!
Refactoring the Existing Code
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
- Inside the
traincar
package, make a new abstract class calledTrainCar
, using theabstract
keyword. - 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.
- Make the
TrainCar
class aGraphicsGroup
using theextends
keyword in its class definition. - 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) inGraphicsGroup
using the keywordsuper()
. - 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.
- Move just the ones that you need, which represent the common elements to all train cars.
- For the common pieces to get drawn, you can now call the function
drawCarFrame
, which callsdrawWheel
, in the constructor forTrainCar
. - 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.
- 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). - 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
aGraphicsGroup
? Why do we not have to say “extends GraphicsGroup
” in theEngine
class? - The function called
drawEngine
, and in particular its contents, can now serve as the basis of the constructor for your newEngine
class. - The other functions called inside
drawEngine
can be used as is in your newEngine
class. Move those functions intoEngine
first, like you did when you copied the common functions intoTrainCar
.
-
Make a constructor for
Engine
that also has parameters that are the same asTrainCar
: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.
- To create the other parts of the
Engine
, add calls to the methodsdrawSmokestack
,drawEngineCab
, anddrawCowcatcher
in the constructor, after thesuper()
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) - Now you will still have a few constants left in red. Get those from the original
TrainDrawer
and add them at the top of theEngine
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.
-
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
.
- Your
TrainDrawer
class should look pretty sparse by this point. Modify the constructor to instantiate anEngine
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. |
Create classes for the other types of train cars
- Now make two more new classes in the new train package,
Boxcar
andCaboose
, each of which extendTrainCar
.
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.
More: Add more cars!
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.
Optional Ideas to Try
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.