Lab 6 -- Ball World, part two
Lab 6 Ball World, part two
In this lab we will be using some design patterns to make enhancements to the Ball World applet from lab 1. In the first half of the lab (parts one through three below), we'll use the Strategy pattern to make it possible to dynamically change the behavior of moving balls. In the second half of the lab (parts four and five), we'll use another application of the Observer pattern to implement collision handling.
You can start by making a
copy of your Ballworld program from lab 1 to a new directory.
You may also want to follow
the instructions for implementing double buffering
to improve the appearance of the animation.
Part one: Apply the Strategy pattern.
Consider all the ball types you have made. They have some things in common (how to move, how to draw), but some other things that differ (how they change in velocity, color, diameter, etc.) What we are going to be interested in is the difference between the ways in which the state of the ball is updated on each timer tick. The idea of the Strategy pattern is to isolate this variant behavior into a new object type called a strategy. The remaining invariant part of the original object is retained and called the context. For Ballworld, the strategy object will be called UpdateStrategy, since it encapsulates the way that a ball's state is updated on each timer tick. We'll also create a new subclass of Ball called a StrategyBall. The StrategyBall will be able to assume the character of any of our ball types, simply by swapping in a different update strategy. In addition, we'll be able to change the behavior of a StrategyBall dynamically by giving it a new update strategy while the program is running.
In effect, we will be replacing a usage of inheritance by a usage of composition. That is, instead of adding behavior to a Ball by subclassing it, we will be adding behavior by letting a Ball possess a Strategy object which encapsulates the desired behavior. However, we still be using inheritance in the project. UpdateStrategy will be defined as an abstract class, with an abstract method called updateState. Then we can define specific strategies as subclasses of UpdateStrategy, such as CurveStrategy, PulseStrategy, RandomStrategy, etc. Each of these will have a different version of the updateState method.
To accomplish this, you need to make the following changes to your code from lab 1:
- Create a new class called UpdateStrategy. Make it abstract. Give it an abstract method updateState(Ball ball).
- Create a new subclass of Ball called a StrategyBall.
- Give the StrategyBall a strategy property: UpdateStrategy updateStrategy;
- Override StrategyBall's version of the notify method, by adding as a third step a call to updateStrategy.updateState
- Create a new class called CurveStrategy, which is a subclass of UpdateStrategy.
- Copy the curvature property from CurveBall to CurveStrategy.
- Copy the code updating the ball's velocity from CurveBall.move() to CurveStrategy.updateState().
- Now we have a small technical problem. velocity is a private member of CurveBall, but CurveStrategy needs access to it. This can be handled by writing get and set methods (in Ball) for xvelocity and yvelocity. Then you'll have to modify the code (in CurveStrategy) which updates the Ball's velocity so that it uses the get and set methods instead of accessing xvelocity and yvelocity directly.
Now that you have the idea, write extensions of UpdateStrategy for the other ball types you wrote in lab 1. There should be at least two of these.
Now, when we create a StrategyBall, we need to be able to give it any kind of update strategy we'd like. The easiest way to do this is through StrategyBall's constructor. We can write two constructors for StrategyBall:
- A constructor with the same argument as Ball's one-argument constructor, which will use a NullStrategy by default. It should call Ball's one-argument constructor and then initialize updateStrategy to a new NullStrategy.
- A two-argument constructor with an UpdateStrategy as its second argument. It should call Ball's one-argument constructor; then copy the second argument to its updateStrategy property.
Try running the program now. It should perform exactly as it did before the changes were made.
Part two. Modify the User Interface.
So that we can use our strategies more effectively, we are going to modify the user interface to make it possible for the user to select a strategy, without necessarily creating a new ball. We'll do this through the use of radio buttons. Radio buttons are clickable on-off switches. They can be grouped together in "button groups" so that only one button in a group can be on at the same time. As a result, they are often used to allow a user to select one from a group of several choices. In our case, the user will be choosing an UpdateStrategy for balls.
Make the following modifications to your JApplet:
1. Make some radio buttons to give the user a way to select a ball type. Start with a radio button for straight balls. Set its properties, like background color, preferred size, and text. Continue by making a radio button for each of your ball types. The code for the straight button should look something like this:
JRadioButton straightButton = new JRadioButton();
straightButton.setBackground(Color.white);
straightButton.setPreferredSize(new Dimension(133, 25));
straightButton.setText("Straight");
buttonPanel.add(straightButton);
2. Create a button group (ButtonGroup) and add each of the radio buttons to the group. (The effect of this is that selecting one of the buttons in the group "unselects" all of the other buttons in the group.)
3. Each radio button can have its own ActionListener which is called when the button is pressed. For each radio button, write a public method which adds an ActionListener to the button. These methods should look like the methods you have for the JButtons you've already created, such as the "Add Ball" button and the "Delete all Balls" button.
4. We want to use the radio buttons to allow the user to select a ball type; that is, an UpdateStrategy for a ball. We'll do this by adding a "selectedStrategy" property to the Ballworld applet class definition. It can be initialized to a NullStrategy object. When a button is pressed, the value of this property should be replaced by a new strategy object of the selected type. This can be done in the ActionListener for each radio button. For instance, the actionPerformed for the "Curve" radio button should set Ballworld's selectedStrategy to a new CurveStrategy.
Our first use of the radio buttons will be as follows: Add a JButton whose purpose is to create a ball whose type has been selected by the user by clicking one of the radio buttons. Label the button something like "Add Selected Ball" or "Add Strategy Ball". The actionPerformed in its ActionListener should create a StrategyBall and pass to it the selectedStrategy.
Try the code out. All the existing buttons should work as they did before. In addition, the user should be able to make balls of any type by first selecting the type with a radio button and then clicking the Add Strategy Ball button.
Your program should have at least 4 update strategies, including the null strategy and the curve strategy.
Part three. Switcher Balls.
So far, we've just seen an alternative way of implementing the same functionality in the applet. Next, we'll take advantage of the flexibility we've gained by the separation of the ball and its strategy. We can create a ball whose strategy can change dynamically; that is, while the program is running, at the command of the user.
We'll call this a "switcher" strategy. The switcher strategy will hold a reference to another concrete strategy, which might be the straight strategy, curve strategy, or something else. When the user gives the command to "switch", the switcher strategy will replace its concrete strategy by the one selected by the user.
We can do this with just a few steps:
- Create a new subclass of UpdateStrategy called SwitcherStrategy. Use the "implements" clause to define SwitcherStrategy as an ActionListener. (It's going to be an ActionListener for a button described below.)
- Create a private property of SwitcherStrategy of type Ballworld called "owner". (The strategy needs a reference to Ballworld to ask which strategy has been selected by the user's radio buttons.)
- Create a private property of SwitcherStrategy that is an UpdateStrategy called currentStrategy. Initialize it (in the constructor) by calling Ballworld to get the selected strategy. (You'll need to write a public accessor method for selectedStrategy in Ballworld.)
- Write a constructor for SwitcherStrategy with one argument of type Ballworld called "owner". The constructor copies the owner argument to its owner property.
- The updateState method of SwitcherStrategy just passes the job on to its currentStrategy. It calls currentStrategy's updateState method, passing the ball as its argument.
- SwitcherStrategy's actionPerformed method should get the selected strategy from Ballworld and save it as its own current strategy.
- Add two JButtons to your button panel. Label one "Add Switcher
Ball" and the other "Switch!".
- The Add Switcher Ball actionPerformed routine should do the following:
- Instantiate a SwitcherStrategy object, passing the Ballworld
instance as its argument. The syntax for this is a bit unusual. It
looks like this:
SwitcherStrategy switcherStrategy = new switcherStrategy(Ballworld.this);
(The prefix of "Ballworld" is required in this case, because of the use of the anonymous class. Without the prefix, the keyword "this" would refer to the instance of the anonymous class, not the instance of Ballworld.) - Send a message to the frame, telling it to add switcherStrategy as an ActionListener for the "Switch!" button. (Note here another use of the Observer pattern that we saw in lab 1. In fact, the Observer pattern is used by java.awt as the basis for the ActionListener model. Each GUI Component is an Observable, and each of its ActionListeners is an Observer. When an action event occurs in the Observable, all of its Observers are notified by calling their actionPerformed methods.)
- Instantiate a StrategyBall, passing to it the switcherStrategy.
Part four. Detecting collisions.
Our balls now let others pass through them. Would it be possible to modify the model so that when two balls collide, they bounce away from each other, each one heading in a new direction? This really involves two problems: how to detect a collision, and how to model the effect that a detected collision has on a colliding ball. The collision detection can be handled with another application of the Observer pattern.
Collisions will be detected by the use of another Dispatcher object called the collisionDispatcher. Again, the Balls will act as Observers.
1. Define a Dispatcher called collisionDispatcher as a static private variable in the Ball class. Write a static public accessor method for this variable.
2. We want Balls to be able to respond to collision events. The logical way to do this would be to make Ball an Observer. But Ball is already an Observer for timer events, with a notify method that moves and then redraws. How can we make Ball an Observer of two different types of event, with two different notify methods?
This can be done by creating a new instance of the Observer interface for each Ball, which effectively acts as the Ball's agent in responding to possible collisions. When it is notified of an event, it sends a message to the Ball, informing it that the event has occurred.
a. In Ball.java, write a method called checkCollision, with a Ball as argument. Leave the body empty at this point.
b. Define a variable of type Observer, initialized (using an anonymous inner class) to a new Observer whose notify method calls checkCollision. (The parameter to checkCollision should be the Ball passed in to the notify method.)
c. When a Ball is created, it should register this collision observer with the collision dispatcher, signaling that it wants to be notified when a possible collision has occurred.
d. Ball's notify method should call the collision dispatcher's notifyAll method, passing itself as the argument. The effect of this is that when a Ball moves, all the other Balls are notified of the move, so they can check to see if there has been a collision.
Part five. Handling collisions.
All that remains is to write the body of the checkCollision method in Ball, which is the real collision handler. It has one incoming argument which is another Ball which has just moved. This ball needs to take some action to see if there has been a collision, and if so, to move accordingly. It should do the following:
- First make sure that the other ball is not the same ball as this ball; if it is, do nothing.
- Check to see if there has been a collision between this ball and the other ball by seeing if the two balls intersect. They intersect if the distance between their centers is less than or equal to the sum of their radii.
- If a collision occurs, a force is applied to the ball in the direction
away from the other ball; that is, on a direct line passing from the center
of the other ball through the center of this ball. The magnitude of
the force is equal to
(2*massthis*massother)/(massthis+massother)*((xother-xthis)*(xvelocityother-xvelocitythis)+(yother-ythis)*(yvelocityother-yvelocitythis))/distance
We'll assume here that the mass of each ball is equal to its diameter squared. (For a stronger effect of the difference between ball sizes, let the mass equal the radius cubed.)
- The collision causes an acceleration proportional to this force.
The effect is a change in velocity, using the following equations:
xvelocitythis += force * (xother-xthis)/distance/massthis;
The same force should be applied to the other ball, changing its velocity also.
yvelocitythis += force * (yother-ythis)/distance/massthis;
Some enhancements you can try:
1. Each pair of balls is checked twice for collisions. Would it be possible to check only once? If you can think of a way to do this, try making the appropriate modification(s) to your code.
2. If a new ball is placed on another ball, they stick to each other, at least for a little while. Modify your program so that if a new ball intersects with any existing ball, the new ball is discarded and replaced with another. Keep trying new balls until one is obtained which does not intersect with any other balls.
3. No attempt is made to model the fact that a collision occurs at a specific time between timer ticks, so some collisions appear to occur after the two balls have overlapped, or before they have actually touched.. (All collisions are assumed to occur at the exact time of a tick.) Improve the model to compute the exact time of each collision and use it to derive a more accurate location for each ball after the tick.