Animated blobs
Today we'll make a much more lively blob, animating it on the screen and giving it a variety of possible behaviors. This will be a chance to dive into event-driven GUI programming and object-oriented programming.
All the code files for today: Blob.java; BlobAlias.java; BlobGUI.java; BlobGUI0.java; Bouncer.java; DrawingGUI.java; Pulsar.java; Teleporter.java; Wanderer.java; WanderingPulsar.java
(Blob.java
is the same as last time, in case you didn't already grab it.)
Animated blobs: GUI programming
In order to display things other than text, we need to make a big leap to the world of graphical user interface (GUI) programming. We won't deal much with the interface bit right now (buttons, etc.), but will bring in mouse events shortly. Java has powerful machinery for interactive graphical programming. Unfortunately with power comes some pain, or at least complexity. To minimize the complexity for now, I've wrapped up some of the main power in a class DrawingGUI.java. Plop this file into Eclipse, but treat it as a black box (i.e., don't worry about what's inside it). To use it, we define a driver class (see why it's useful to separate the driver from the model?) with a particular structure:
public class BlobGUI0 extends DrawingGUI {
private static final int width=800, height=600; // setup: size of the world
public BlobGUI0() {
super("Animated Blob", width, height);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new BlobGUI0();
}
});
}
}
I'm again putting 0 at the end of the class name because it's just the core that we'll be building from, with the complete version later. There's a lot going on here, which you'll have to take my word on for at least a class or two, but which we'll gradually unravel. What do we see here?
- The class declaration says that
BlobGUI0
extends theDrawingGUI
class, which means builds on the basic GUI class that I provided. - A special type of variable declaration,
static final
. This type of variable is constant (hmmm, a constant variable?), but essentially documents a "magic number" with a name. Thus rather than seeing the number 800 later, we know that we actually mean the width. We also have a single place where (at compile time) we can change the value, which is a good idea because maybe not all 800s scattered throughout code would mean the same thing, and find-replace would fail. - A method call to
super
in the constructor; this specifies the title and size of the window (we'll discuss later why it's "super"). - An even uglier
main
method, whose sole purpose is to create an instance of the GUI and start it running.
While this code doesn't do much, DrawingGUI
lets us easily start making things happen. The first thing you need to know about event-driven GUI programming: you are not in charge. A GUI program doesn't say when to do things; instead, it says how to respond when things happen. We don't know when the user is going to move the mouse, press the mouse button, or hit a key, but we can say how to handle any of those things. Furthermore, we don't know when the GUI is even going to be displayed (it might get buried behind other windows for a while, or minimized), but we can say how to draw it when it is. One other example that is at the heart of animation: a timer can go off periodically, without us worrying about when that happens. We again just specify what to do when the timer fires. In any of these cases, an event triggers an action in our code, which we specify with a method (often called a "callback"). The DrawingGUI
provides a special simplified way to specify these methods.
In the event-driven style, to animate a blob, just respond to every "tick" of the timer by moving the blob a little bit and redrawing it.
Here's the full working version 0: BlobGUI0.java. Some points:
- The GUI has an instance variable holding onto a blob, just like in the second version of the driver from last class. Here, the instance variable is created in the constructor.
- The constructor starts a timer running (the method
startTimer
is provided byDrawingGUI
). - The method
handleTimer
says what to do each time the timer says "time's up". For animation, we update the state of the world (here, just stepping the blob) and redraw the world. - Redrawing is a two-step process: we tell the GUI it's time to
repaint
everything, and we provide a method to say how to draw our part of the GUI. As mentioned above, we don't actually don't control exactly when the repainting happens, and in fact it might also happen due to other events. - The
draw
method does the drawing for our part of the GUI by just asking the blob to draw itself — that's the only thing in our part of the screen. - Now look back at the Blob.java code. The
draw
method, which we ignored last time, puts a circle at the blob's position, with its radius. The JavafillOval
takes the upper-left corner and total width and height, so we subtract out the radius in order to center the blob.) It expects integer numbers, so we have to drop the fractional part with a typecast (e.g.,(int)(x-r)
). - One final GUI event handler:
handleMousePress
responds to a mouse press by seeing if the mouse is located wihtin the blob, and either printing a message if so or moving the blob if not.
Note that the various methods (draw
, etc.) are within the BlobGUI0
instance, and thus have access to its blob
instance variable (just like methods in a Blob
instance have access to its x
coordinate).
anonymous instance [BlobGUI0] |
---|
blob: a new instance created in the constructor [Blob] |
draw: Graphics → n/a |
handleTimer: n/a → n/a |
... |
For reference, here are the various functionalities of DrawingGUI
that we're using.
- repaint — call this to refresh the display (call it when the state underlying the drawing has changed)
- draw (Graphics) — provide this method in order to display what we want
- handleMousePress (x, y) — provide this method to respond to the press at the location
- handleKeyPress (key) — provide this method to respond to the character pressed
Blob behaviors: subclasses
Let's build up a menagerie of different types of blobs that move in different ways (and eventually draw themselves in different ways, too). This is a good opportunity to talk about inheritance, the object-oriented way to create new classes from existing ones. We call the existing class the superclass and the new class created from it the subclass. With inheritance, each object of the subclass "inherits" the instance variables and methods of the superclass. That way, we don't have to write these methods for the subclass unless we want to. Of course, we'll want to write some methods in the subclass, for if we don't, the subclass is exactly the same as the superclass and there would have been no point in creating the subclass. Compared to objects of the superclass, objects of the subclass have additional specialization.
For example, we'll have a subclass of Blob called "Wanderer" that specializes the notion of "step" to walk around aimlessly, and one called "Pulsar" that specializes it to grow & shrink.
The Golden Rule of Inheritance. If there is one thing you should remember about inheritance, it is
Use inheritance to model "is-a" relationships and only "is-a" relationships.
What does this mean? Suppose we have two classes: "A" is the superclass of "B" (which is therefore the subclass). If we have used inheritance correctly, then
- Every object of class "B" is also an object of class "A".
- The set of objects of class "B" is a subset of the objects of class "A".
- Every method that can be called on an object of class "A", can also be called on an object of class "B". (However, the results of the call may be very different.)
- Objects of class "B" are like objects of class "A", but with additional specialization.
E.g., "Blob" is the superclass of "Wanderer"; "Wanderer" is the subclass of "Blob". Every Wanderer is a Blob. The set of all Blobs includes all the Wanderers. Every method that can be called on a Blob can be called on a Wanderer.
Let's see how that looks in code.
The Wanderer
class (Wanderer.java) defines a specialized version of step
; it overrides the definition in Blob
:
@Override
public void step() {
dx = 2 * (Math.random()-0.5);
dy = 2 * (Math.random()-0.5);
x += dx;
y += dy;
}
The @Override
annotation isn't necessary, but informs Java that this method replaces the corresponding method in the superclass. Java and Eclipse will infer this relationship where possible (in Eclipse, mouseover the method name and you'll see), but suppose we'd misspelled the method's name. A method stip
would be seen as a new method without the annotation, and as an error with the annotation. So it's good, safe practice.
The dx
and dy
values are returned by calls to Math.random
, which gives a number between 0 (inclusive) and 1 (exclusive, so at most 0.99999...). By subtracting 0.5, we get a number from -0.5 to 0.5. Then we scale that by 2 to get a number between -1 and +1. To make it take bigger steps on average, we could make the scaling factor bigger.
Note that we didn't need to override the draw
method; the original is just fine. And all the instance variables come along, as a wanderer is-a blob, so it includes all of them. The constructors don't need to do anything special for wanderers, so just call the Blob constructor, using the special notation super
to refer to the corresponding constructor defined in the superclass.
A Bouncer
(Bouncer.java) switches direction if it hits a wall, with a differently overridden step
method:
x += dx;
if (x > xmax-r) {
x = xmax-r;
dx = -dx;
}
else if (x < r) {
x = r;
dx = -dx;
}
and similarly for y
. (The radius comes in so that it bounces when the edge hits the wall.) Where did xmax
come from? The constructor is extended to keep track of the size of the world around which the object is bouncing.
A Pulsar
(Pulsar.java) similarly grows and shrinks.
A WanderingPulsar
(WanderingPulsar.java) is the cross between a Wanderer
and a Pulsar
. However, Java only allows a class to extend one superclass, as multiple inheritance (more than one superclass) can lead to ambiguities. (Some languages do support that, with various rules for how to resolve the ambiguities.) So the implementation here just extends Wanderer
, and tacks on the some code that appeared in Pulsar
. Note the use of super
to invoke the version of the method defined in the superclass (here, Wanderer.step
) as part of the new method. Otherwise the superclass's method is just replaced. Try writing your own version of WanderingPulsar
that extends Pulsar
and tacks on the motion from Wanderer
. With simple code like this, the code duplication is kind of okay; with more complicated behaviours, we wouldn't want so much copy-pasting and might resort to another way to exhibit both behaviors, e.g., keeping internal copies of objects and delegating to them. We won't need to go there in CS 10.
Finally, this explains why the instance variables in Blob
are marked protected
— so that subclasses can directly access them (without calling getters/setters). We could make the instance variables completely private
, meaning that objects from other classes (including subclasses) wouldn't have direct access to these instance variables. Note: things are just private to the class, not to the object. So one object can see another object's private variables, if they are in the same class. The open alternative is public
. Generally protected
makes sense in the object-oriented setting, since the subclass instance is-a superclass instance also, so it only seems fair that it should be able to access the instance variables.
Okay, now let's fix up our GUI to use any of these: BlobGUI.java.
- A new instance variable
blobType
keeps track of which blob to create. - A new event handler
handleKeyPress
sets that instance variable. (And for fun, it also allows us to speed up and slow down the animation, via another instance variabledelay
and theDrawingGUI
methodsetTimerDelay
.) - The mouse press handler now creates a new instance of the appropriate class wherever the mouse is pressed.
- [Key] Here's what's cool and demonstrates object-oriented programming's power: any instance of any of these classes can be stored in the
blob
variable. They satisfy the "golden rule" in that each of them is-aBlob
(and more).
A subclass can also provide additional methods; for example, a Teleporter
(Teleporter.java) has a method to jump to a new place. We can't call this method on any old blob, though — the method isn't defined at that level. Thus this works:
Teleporter blob = new Teleporter();
blob.teleport();
but this doesn't:
Blob blob = new Teleporter();
blob.teleport(); // little red 'x'
It's the same object, but all that the compiler knows is that it's a Blob
, and there's no such method in that class definition. The inheritance relationship goes one direction — not every Blob
is a Teleporter
, and thus we can't call the teleport
method on something if all we know is that it is a Blob
. It may be a bit confusing to realize that a subclass may contain more instance variables and methods than the superclass, but that's because it's an extended version of the superclass (the Java keyword "extends" emphasizes that).
Java does have a mechanism for recognizing that something declared as a superclass was actually created as a subclass — type-casting:
Blob blob = new Teleporter(0,0,100,100);
((Teleporter)blob).teleport(); // works
This runs the risk of a run-time error if in fact blob
was something else.
Blob blob = new Blob(0,0);
((Teleporter)blob).teleport(); // run-time error
It's easy to distinguish those two cases, but it would be harder to see in a more complex situation. For example, you're playing with fire if you change the "Back off!" message in handleMousePress
to try to teleport:
if (blob.contains(x,y)) {
((Teleporter)blob).teleport(); // will it work or crash???
}
It depends on what you've done at run time as to whether that will work or throw an error. Try it!
Objects
An import note about primitive vs. object types. Double
is a "primitive" type, as are int
, char
, and boolean
(lower-case type names), in that it doesn't refer to an object but rather just simple number (or character) stored directly in memory. If a variable is of a primitive type, the variable contains the actual data itself (the bit pattern representing the data). If a variable is of type reference to an object, then it contains a reference, and the data itself is stored elsewhere.
Why is this distinction important? My wife and I have a joint checking account. We each have an ATM card. The cards are different and have different names on them, but the refer to the same checking account. If I withdraw money with my ATM card, there is less money in the account, and if my wife then looks at the balance it will be smaller even though she did nothing with her ATM card. In this analogy, the account is the object (and bank account objects are a common example in textbooks). The ATM cards are the variables, each holding a reference to the same account. Any changes made by either of us to the account via our ATM card are seen by both. On the other hand, if my wife has her ATM card re-programmed so that it refers to her personal account (changes the reference stored in the variable), that won't affect my card or the account. She just will no longer be able to use that ATM card to withdraw money from our account, because it no longer refers to our account.
Back to blob world. Consider this code:
Blob b1 = new Blob();
Blob b2 = new Blob();
Blob b3 = b1;
There are two different blobs here, created with the two new
statements. One of these blobs has two "names" (actually called "aliasing"), with variables b1
and b3
referring to the same object. So:
b1.setX(3);
System.out.println(b3.getX()); // => prints 3
It doesn't matter whether we refer to the object by its b1
name or its b3
name; it's the same object. The blob that b2
refers to is an entirely separate object / piece of memory.
And while we're on the subject of objects, note that there is an overarching superclass called Object
— all classes implicitly inherit from it (each object is-a Object). Very conveniently, this class provides a method toString()
that returns a printable representation of the object. By default, it's pretty generic, just giving the memory location of the object (though that would let you see the aliasing in the above example. System.out.println
automatically calls the toString
method to get a string to print. Try it: BlobAlias.java
But now try adding the following code to Blob
:
@Override
public String toString() {
return "Blob @ ("+x+","+y+")";
}
Now we learn something about the blob (though lose the memory address, if that were important). Note that this same method is then used by all the above subclasses (as they are blobs); each could in turn provide an even more specific version with more information relevant for its objects. Adding a toString
method can be very helpful in quick&dirty print debugging.
Java notes
Again, this isn't a comprehensive reference to the language; the textbook and on-line references provide much more detail. But hopefully it gives sufficient intuition and an organizational structure. Give a yell if I've missed something important.
- extends
- This indicates that a class is a subclass of another class, extending its instance variables and methods, and perhaps overriding some of them.
- static final
- This tag on a variable declaration indicates that its value won't ever be changed after being initially set. This is a good way to establish overall settings of a program in a clear, visible way, without "magic numbers" buried in the code.
- super
- A way to directly access a method of the superclass from a method of the subclass. By default, the superclass constructor is invoked automatically from the subclass constructor, but if it needs parameters, then this is a way to provide them. By default, other superclass methods are not automatically invoked from corresopnding subclass methods; the whole point of the subclass method is to refine the functionality appropriately. If that refinement entails just doing something in addition, then specifically call the
super
. - import
- Lots of additional functionality is provided by extra packages; the
import
statement says to use them and the classes they define. When writing code from scratch, Eclipse will help you figure out what needs to be imported from where. - type conversion / casting
- In some cases we can convert one type to another, e.g., saying that an
int
should be treated as adouble
(intuitively, 1 becomes 1.0). That conversion happens automatically, because it's safe — no loss of information. Going the other way might lose information, so requires a type cast (e.g., (int)1.6), saying to treat thisdouble
as anint
, throwing away the decimal part (so (int)1.6 becomes 1). - conditional
- Java has the usual kinds of conditionals, with the "if" test in parentheses, the "then" in braces, and optionally an "else" (which can be an "else if" to tack on another conditional).
- more math functions
- Beyond +, -, etc., the Math package has additional useful functions, including min, max, random, etc.
- visibility (public/private/protected)
- This part of a declaration says who has access: public (any other method has access), private (only the object's methods), or protected (subclasses can too). It's actually a bit more complicated than that, as we'll see later, but this is a good general sense.
- class type cast
- We saw this before with numbers; it also work for superclass to subclass when we somehow know that the object referenced by a variable is an instance of the subclass (though the variable is declared as the superclass).
- primitive types vs. objects
- Primitive types, including
int
(integer numbers),double
andfloat
(floating point numbers),boolean
(true
orfalse
), andchar
(a single character) are stored directly in a variable and are passed by value. Objects (includingString
s) are stored as references to the actual data and passed that way.