CIT 591 Bouncing Ball: Another Version
Fall 2008, David Matuszek

In class I stressed the need to have an additional Thread to run a controlled animation. And, in my original versions of the Bouncing Ball program, that Thread was explicit. As I gradually learned more Java, and found out about Timers, I started using them.

In my lectures I described the Bouncing Ball animation, without realizing (until a student pointed it out) that the additional Thread in that example is not obvious. The Thread is really there, but it is "down" a level, hidden in the Timer routines. So, here is a version that behaves the same way, but the additional Thread is explicit.

This Version Other Version
You create a Thread when the Start button is clicked. You create a Timer when the Start button is clicked. The Timer creates Threads.
You tell the model that it's okayToRun, and you start the Thread. You create a TimerTask, and tell the Timer to schedule a brand new Thread, and start it, every 40 ms.
The model has a run() method that redraws the JPanel every 40 ms. In between, the Thread sleeps. The TimerTask has a run() method that redraws the JPanel; then the Thread dies.
The run method is in the model, where it cannot (and should not) access the JFrame; so the Controller (which extends JFrame) needs to add a listener for the window being resized. TimerTask is an inner class of the Controller, which extends JFrame, so it's easy for the TimerTask's run method to check the window size every 40ms.
Clicking the Stop button sets the model's okToRun variable to false, the method exits, and the Thread dies. Clicking the Stop button cancels the Timer, so that it quits creating a new Thread every 40ms.

Both of these models work fine, and you can use either as a basis for your Snake Game assignment. Here is the version with more explicit Threads.

import java.awt.*;
import java.awt.event.*;
import java.util.Observable;
import java.util.Observer;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.*;

public class Controller extends JFrame {
    JPanel buttonPanel = new JPanel();
    JButton runButton = new JButton("Run");
    JButton stopButton = new JButton("Stop");
    Timer timer; 
    Thread thread;

    Model model = new Model();
    View view = new View(model); // View must know about Model

    // Added main method to convert applet into application
    public static void main(String[] args) {
        Controller c = new Controller();
        c.init();
        c.setSize(300, 300);
        c.setVisible(true);
        c.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public void init() {
        layOutComponents();
        attachListenersToComponents();

        // Connect model and view
        model.addObserver(view);
    }

    private void layOutComponents() {
        setLayout(new BorderLayout());
        this.add(BorderLayout.SOUTH, buttonPanel);
        buttonPanel.add(runButton);
        buttonPanel.add(stopButton);
        stopButton.setEnabled(false);
        this.add(BorderLayout.CENTER, view);
    }

    private void attachListenersToComponents() {
        runButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                runButton.setEnabled(false);
                stopButton.setEnabled(true);
                timer = new Timer(true);
                timer.schedule(new Strobe(), 0, 40); // 25 times a second 
                model.okToRun = true;
                thread = new Thread(model);
                thread.start();
            }
        });
        stopButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                runButton.setEnabled(true);
                stopButton.setEnabled(false);
                timer.cancel(); 
                model.okToRun = false;
            }
        });
        this.addComponentListener(new ComponentAdapter() {
            public void componentResized(ComponentEvent e) {
                model.setLimits(view.getWidth(), view.getHeight());
            }
        });
    }
}

    private class Strobe extends TimerTask { // An inner class

        public void run() {
            model.setLimits(view.getWidth(), view.getHeight());
            model.makeOneStep();
        }
    }


//---------------------------------------------------------------------

class Model extends Observable implements Runnable {
    public final int BALL_SIZE = 20;
    private int xPosition = 0;
    private int yPosition = 0;
    private int xLimit, yLimit;
    private int xDelta = 6;
    private int yDelta = 4;

    public boolean okToRun = false; // can be changed from outside

    public void setLimits(int xLimit, int yLimit) {
        this.xLimit = xLimit - BALL_SIZE;
        this.yLimit = yLimit - BALL_SIZE;
    }

    public int getX() {
        return xPosition;
    }

    public int getY() {
        return yPosition;
    }

    public void run() {
        while (okToRun) {
            makeOneStep();
            try {
                Thread.sleep(40);
            }
            catch (InterruptedException e) { }
        }
    }

    public void makeOneStep() {
        // Do the work
        xPosition += xDelta;
        if (xPosition < 0 || xPosition >= xLimit) {
            xDelta = -xDelta;
            xPosition += xDelta;
        }
        yPosition += yDelta;
        if (yPosition < 0 || yPosition >= yLimit) {
            yDelta = -yDelta;
            yPosition += yDelta;
        }
        // Notify observers
        setChanged();
        notifyObservers();
    }
}

//---------------------------------------------------------------------

class View extends JPanel implements Observer {
    Model model;

    View(Model model) {
        this.model = model;
    }

    public void paint(Graphics g) {
        // Added because of differences between Panel and JPanel

        System.out.println("paint " + g);
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, getWidth(), getHeight());

        g.setColor(Color.red);
        g.fillOval(model.getX(), model.getY(),
                model.BALL_SIZE, model.BALL_SIZE);
    }

    public void update(Observable obs, Object arg) {
        System.out.println("update");
        repaint();
    }
}