Homework 7: Animated Sea Creature

Goals

Task

Design a sea creature, animating its motion as realistically as possible. The creature should be designed so that it can be instantiated as an object.

First Steps

Download the following files, and save them into a directory where you will implement your assignment:
The Aquarium class is the main driver of the program, and takes in the names of the sea creatures to place in the aquarium.  You can run it to see a very simple sea creature (just a circle that moves back and forth) via the following command:

java Aquarium DEMOSeaCreature DEMOSeaCreature DEMOSeaCreature DEMOSeaCreature

The command above contains the name of the demo sea creature four times, and so it will create four different instances of the demo sea creature in the aquarium.  When you run it, you should see four circles bouncing back and forth along the aquarium.

Create a class called MySeaCreature in a new java file.  Complete the assignment by implementing your sea creature in this class. 

Although you will be creating an aquarium filled with your own sea creatures here, we will later combine them together with other creatures created by other students in your recitation.  Make sure that your creature is not too small, nor too large, as it will have to live in an aquarium with two dozen other creatures!

API

Your class should have a constructor that takes in ONLY one argument:

In addition to the constructor, your object must implement the following API:

  MySeaCreature implements SeaCreature    // keep this class definition as is
-----------------------------------------------------------------------------------------
        MySeaCreature(double scale)       // constructor that takes in the scale of the creature
   void display()                         // draw the creature to the screen
   void move(CreatureStats[] allObjects)  // Advances the creature's animation by one frame
//
allObjects is an array of the locations, predator level, and name of ALL creatures in the environment double getX() // getter for the creature's x position, which must be between 0 and 1
double getY() // getter for the creature's y position, which must be between 0 and 1
   int getPredatorLevel() // getter for the creature's predator level, which is between 0 and 10
String getName() // getter for the creature's name, which should be your PennKey followed by a name of your creature. Choose a unique one!
// For example, Dr. Eaton's sea creature might be named "eeaton spinning starfish"

Note that your implementation of these methods should appropriately error check the arguments. For example, your constructor should double check that scale is in the proper range, and throw an IllegalArgumentException if it is not.

Defining Your Class

Java provides a way to ensure that you followed this API exactly.  The SeaCreature.class file, which we've provided, defines a formal definition of this API called an interface.  Interfaces are typically covered in later computer science courses, so we won't worry about understanding them now, but they are very easy to use.  All you need to do is ensure that your class definition of MySeaCreature looks exactly like the following:

public class MySeaCreature implements SeaCreature {
   . . .
}

The "implements SeaCreature" part is a promise that you make to the java compiler to follow the API given above (which we defined formally in SeaCreature.class).  When you try to compile your class, it will double check that you have indeed followed the API correctly, and will let you know if you are missing any function or defined any incorrectly.

Just make sure that you have "implements SeaCreature" after the name of your class in the definition -- that is all you need to do in this section.


Drawing Your Creature


Start by drawing a simple version of your creature, represented by just a few ellipses or rectangles, relative to the creature's x and y coordinates.  Keep it simple.  You can leave the creature's move() function empty for the moment.

You can choose the creature's position based on what type of sea creature you're implementing (e.g., crabs might crawl along the bottom of the tank), but some aspect of its initial position should be random.

Unit Testing:

Then, write a simple main() function within your MySeaCreature class to draw and test your creature.  Within your main function, you should have the following steps at a minimum:

StdDraw.setCanvasSize(500, 500);

MySeaCreature creature = new MySeaCreature(Math.random());  // create an instance of your sea creature with a random scale
while(true) {
    creature.display();   // display your creature
    creature.move(null);  // move your creature.  For right now, pass in null to move(); we'll change this later
}

The Aquarium driver program follows exactly these same steps -- each iteration of the draw loop, it first displays your creature and then calls its move() function.  At this point, you should have one instance of your creature on the screen.  Add a second instance and test it as well, so that you have two creatures on the screen.  Add some more code to test each method of the class, such as printing its coordinates, name, etc.

Once you have the two creatures displayed via your own main() function and are confident that your implementation is correct, try running the Aquarium program with your sea creature and the DEMOSeaCreature:

java Aquarium MySeaCreature MySeaCreature DEMOSeaCreature DEMOSeaCreature

You should see two instances of your sea creature in a fixed position on the screen, and two copies of the DEMOSeaCreature moving around.

Aside:  This may seem odd that you're expected to have your sea creature work with the Aquarium class without ever seeing the Aquarium.java code, but this is actually what happens in many computer science projects -- we define an interface to a class, discuss how it should work, and then are expected that our code will combine seamlessly together with code written by other programmers.  This is one of the benefits of object-oriented programming!  This is also the reason why it is important to write your own main() function to unit test every aspect of your object.  Don't just assume it will work -- test it as you build it!


Depending upon the creature you design, define appropriate move behavior. Try to make this motion as realistic as possible.  Some creatures will swim like a fish, or a submarine, some will wiggle about, some might just stay in one place and wave or rotate, bubble, etc.  Focus on animating realistic movements for your sea creature (e.g., an octopus will have undulating tentacles, fish will have waving fins, bubbles should follow a wavy path to the surface, etc.). 

To simulate the motion, you will need to use a combination of trigonometry functions and changing the coordinates for different parts of your creature.  (For example, a wavy path can be simulated using sin() and you could smoothly vary the endpoints of a fish's fins to simulate swimming)  Start simple, build it up slowly, and experiment!

For right now, ignore the array of CreatureStats[] that is passed to move() and just animate your object independent of all other creatures in the tank. 

Checkpoint

Try running the Aquarium program with your sea creature and the DEMOSeaCreature:

java Aquarium MySeaCreature MySeaCreature DEMOSeaCreature DEMOSeaCreature

Now you should see all four creatures moving.


Predator level

Each creature has a predator level (0 - 10) that you should define for your own creature.  You can change the predator level over time (e.g., a shark might turn red with anger and become especially vicious), or you can leave it constant. 

Reacting to other creatures

Make your object react to nearby objects in the aquarium.  The CreatureStats[] array that is passed into move() contains four pieces of information for every creature in the aquarium: its x and y position, its predator level, and its name.  These are stored in a CreatureStats object that uses the following API:

        CreatureStats    
-----------------------------------------------------------------------------------------
 double getX()                            // getter for the creature's x position, in [0,1]
double getY() // getter for the creature's y position, in [0,1]
   int getPredatorLevel() // getter for the creature's predator level, which is between 0 and 10
String getName() // getter for the creature's name
For example, a puffer fish could use this information to determine how close the nearest other creature is, and then puff up if the other creature is within 20 units.  A dolphin might blow bubbles when another creature comes close.

Note that the
CreatureStats[] array includes information for all creatures in the aquarium, including the current object instance!  Each instance of your creature shouldn't react to itself, obviously.  To figure out which array element refers to the current object instance, simply test to see if the x coordinate, y coordinate, and name of the element are exactly equal to the coordinates and name of the object instance. (Make sure that you do this before updating the position of your own object in move()!)
Note that your creature instance should react to other instances of the same class (e.g., if you create a shark creature, the sharks in your tank should react to each other).

Your object should react in some manner that is dependent on the relative predator level of nearby creatures.  For example,
Remember that your creature should still interact in a reasonable way with *any* creature based on its predator level, even if it has special interactions for certain creatures based on their names.

Don't worry about being too precise about how far away the other creatures are from your creature -- just measure the Euclidean distance from your creature's x,y coordinates, assuming a circular range of interaction.  Of course, you could easily modify this to, say, pay more attention to other creatures in front of your creature than behind.

Be creative and have fun!

Although you are welcome to modify your main() function to create the CreatureStats[] array, populate it with the information of all creatures in the tank, and pass it into your calls to move(), it might be easiest to just rely on the Aquarium class from now-on, which handles this automatically.

Checkpoint

Try running the Aquarium program with your sea creature and the DEMOSeaCreature.

java Aquarium MySeaCreature MySeaCreature MySeaCreature MySeaCreature MySeaCreature DEMOSeaCreature DEMOSeaCreature

Now you should see seven creatures moving and interacting independently.


Improving your Creature

Now that your creature is working, go back and improve its appearance and behavior.  Make it visually fancier, make its animation smoother, add more complex behavior, add bubbles that are released and float to the top of the aquarium occasionally, etc.

Changing the Aquarium Background (Optional)

If you wish to modify the default aquarium background, you can write code in the AquariumUtils.drawTankBackground() function.  This function is called near the beginning of each frame of animation by the Aquarium driver class.  If you do modify this function, be sure to re-compile AquariumUtils.

For example, you could add a sand castle, seaweed, rocks, etc. to your aquarium.

Extra Credit

You will receive up to two points of extra credit for especially creative submissions.

Submit MySeaCreature.java, (optionally) a modified AquariumUtils.java if you added a background, and (only if necessary) an extra.zip file containing any supplementary image files needed by your program. Finally, submit a completed readme_seacreature.txt file.

Not sure what kind of marine creature and behavior you want to implement?

Here are some suggestions to help you come up with an idea.  Remember that you are in no way required to adopt any of these suggestions.  They're just food for thought.

  1. Animate your creature's motion and interactions in a realistic-looking way.  Here is a brief list of web sites that describe some standard types of behavior and swimming motions.  Be warned that each includes some very readable, overview information, and much more complex discussion of formulas, 3D data structures, etc.  We recommend focusing on the overview information that will help give you ideas for animations and interactions.
  2. Base your creature on a fictional one from a book, cartoon or movie (e.g. 20,000 Leagues Under the Sea, Finding Nemo, Bedknobs and Broomsticks, The Snorks, or Futurama).

  3. Make an object that is really multiple creatures interacting, such as a clownfish swimming in an anemone or a fish taking the bait from a fishing pole.

My creature keeps reacting as though there are creatures nearby but nothing is near!

Remember that the CreatureStats[] array given to move() contains the stats for your creature as well!  You must identify which element of this array corresponds to your own instance and then not react to it. Make sure that you identify which element corresponds to the current instance *before* updating the instance's position in move().

I'm only seeing one copy of my creature, not several.

Check that you're drawing the creatures at different x and y locations, not overlaying them exactly on top of each other.  Check also that you're drawing the creatures within the window.