Polymorphism

  1. This assignment will build on the previous one. In preparation for the next step, use your favorite search engine to learn about inheritance, polymorphism, and abstract methods in Java.


  2. Make a class named Sprite. In video game terms, a "sprite" is a picture that moves, as opposed to a "background tile". Make both the Tube and Mario classes extend the Sprite class. Move the member variables that store the x and y position into the Sprite class (since position is something that all sprites have in common).


  3. Use polymorphism to animate your sprites. Add an abstract method to your Sprite class named "update". Also, add an abstract method to your Sprite class named "draw". Child classes will need to implement these two methods. The update method should update the sprite. The draw method should draw the sprite. Change the type of your collection in your Model from ArrayList<Tube> to ArrayList<Sprite>. Put the Mario instance in the same collection with all the Tube instances. (It is okay to have a redundant Mario reference in your model.) Change your View.paintComponent method so that it just iterates over all the sprites and calls Sprite.draw. (It should not use the redundant Mario reference.) After this change, your View should not know anything about the Mario or Tube classes. It should only know that your game involves a collection of Sprites. It should not contain any special code to handle Mario differently from the tubes. Similarly, your Model.update method should no longer explicitly call Mario.update. It should just iterate over the collection of Sprite instances and call Sprite.update. Now, make your game work again. To do this, you will have to move a lot of stuff around and make a lot of changes.


  4. Clean up your code. Make sure all of your logic is found somewhere that makes sense. For example, if your collision-detection still only works between a Mario and a Tube, convert it to a general-purpose function that detects collisions between any two sprites.


  5. Ponder the elegance of how you have improved your code. Your game does not do any different than it did before the previous two steps. So, why did we do all of that work? Well, your code is now structured differently. Before, your Model and View had to know what kind of game they were implementing. Now, your Model and View are mostly generic, suitable for supporting pretty-much any billboard-style video game. Imagine that your game involved hundreds of different types of sprites. (Most interesting video games involve at least several hundred different types of sprites.) If you kept your previous code structure (without polymorphism), your Model and View would become very complex to support so many different types of things. As you added more and more things to your game, the task of adding new things would become increasingly cumbersome. Basically, your game would eventually become so cumbersome to modify, that no one would ever want to do the work necessary to make it the paragon of awesomeness that is the true destiny of your game. With polymorphism, however, each Sprite now encapsulates all of the logic needed for it to participate in the game. In other words, your game is plug-able.

    Good coding design tries to keep the engine (which usually consists of a Model, a View, and a Controller) as generic and as simple as possible, and keeps all the complexity in the plug-able components. This is called a "modular design". Modular code can scale to support very large projects without becoming more difficult to work with. Thus, taking some time to keep your code organized can pay off in the long run. As you add new features to your game in the following steps, try to keep your overall design as modular as possible. That is, try to avoid putting special-purpose logic in your Model, View, or Controller classes.


  6. Add Goombas and fireballs to the game. Goombas should move back and forth between tubes. When the user presses "Ctrl", make Mario throw a fireball. (The user should be able to throw many fireballs simultaneously by pressing "Ctrl" rapidly.) Fireballs should move forward, bouncing like bouncy balls. (That is, gravity should pull fireballs down, just like Mario. When a fireball hits the ground, you should negate its vertical velocity.) When a fireball collides with a Goomba, make the Goomba catch on fire, then after a few frames, disappear. Here are some images you can use:
            
    I do not care how your fireballs interact with the tubes. I also do not care how your Mario interacts with the Goombas, as long as you could make it behave however you wanted it to. Please be sure to remove fireballs from the list of sprites after they go off the screen, so they don't accumulate and slow your game down.


FAQ:

  1. Q: How can I tell which mouse button was clicked?
    A: The MouseEvent.getButton() method tells which button was clicked. The right mouse button is MouseEvent.BUTTON3.


  2. Q: For responding to jumping, can we have a redundant reference to the Mario instance in the model?
    A: Certainly. In fact, I strongly recommend having a redundant reference in your model for this purpose. Just make sure that your Model.update method does not use it (because I am trying to force you to use polymorphism there). Also, View.paintComponent should not use it.


  3. Q: The animation is rather jerky. How can I make it smoother?
    A: I added a member variable of type java.awt.Robot to my Game class. In Game.actionPerformed, I call Robot.mouseWheel(0). That seems to trick Java into thinking the mouse is doing something, so it refreshes a bit faster.


  4. I'm getting an IIOException that says "Can't read input file!". How do I fix that?
    A: Make sure you know what directory you are executing within. Here is a command to print the current working directory:
    System.out.println("cwd=" + System.getProperty("user.dir"));
    
    If you are running inside an IDE, the problem is probably caused by the IDE starting in an unexpected folder. The solution is to tell the IDE what folder to start in. Eclipse <sarcasm>conveniently</sarcasm> provides this setting in project->properties->Run/Debug Settings->Default configuration->Edit->Arguments->Working Directory->Other.


  5. Q: How can I determine whether some Sprite, s, is a Tube or a Mario or a Goomba?
    A: One good solution is to add an abstract method to your Sprite class, like this:
    abstract boolean isTube();
    
    You could implement Tube.isTube like this:
    boolean isTube() { return true; }
    
    You could implement Mario.isTube like this:
    boolean isTube() { return false; }
    
    (Another solution is to use "instanceof". However, some languages, like C++, have no equivalent of "instanceof", so your skills will be slightly more portable if you avoid using "instanceof". Since you are in school, please use the isTube solution.)