0

I'm learning to work with java threads, so I decided to make a simple bouncing balls program. However, the program shows multiple threads but only one takes advantage of the window size, other balls are restricted to one area.

I tried setting the size for each balls' JPanel and different layouts which didn't work.

BouncingBall.java

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import javax.swing.*;

public class BouncingBall extends JFrame {

    ArrayList<Ball> balls = new ArrayList<Ball>();

    //GUI Elements
    JLabel lblCount;
    JButton btn= new JButton("Stop");

    BouncingBall() {
//        setDefaultLookAndFeelDecorated(true);
        setTitle("BouncingBall");

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(300, 200);
        for (int i = 0; i < 5; i++) {
            balls.add(new Ball());
        }
        setLayout(new FlowLayout());
        setContentPane(balls.get(0));
        balls.get(0).init();
        for (Ball b : balls
        ) {
            System.out.println(b.getHeight());

            if (b != balls.get(0)) {
                b.init();
                balls.get(0).add(b);
            }
        }

        this.add(btn,BorderLayout.SOUTH);
        btn.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                for (Ball b :balls
                ) {
                    b.stopMoving();
                }
            }
        });
        addMouseListener(new MouseListener() {
            @Override
            public void mouseClicked(MouseEvent e) {
                for (Ball b :balls
                     ) {
                    b.startMoving();
                }
            }

            @Override
            public void mousePressed(MouseEvent e) {
            }

            @Override
            public void mouseReleased(MouseEvent e) {
            }

            @Override
            public void mouseEntered(MouseEvent e) {
            }

            @Override
            public void mouseExited(MouseEvent e) {
            }
        });
        this.setVisible(true);
    }

    public static void main(String[] args) {
        new BouncingBall();
    }
}

Ball.java


import javax.swing.*;
import java.awt.*;

public class Ball extends JPanel implements Runnable {
    // Box height and width
    int width;
    int height;

    // Ball Size
    float radius = 5;
    float diameter = radius * 2;

    // Center of Call
    float X = radius + 50;
    float Y = radius + 20;

    // Direction
    float dx;
    float dy;

    //Vars
    int count = 0;
    float[] colorHSB = new float[3];
    boolean moving = false;

    //Thread
    Thread t;

    Ball() {

        dx = (float) Math.random() * 10;
        dy = (float) Math.random() * 10;
        width = getWidth();
        height = getHeight();

        for (int i = 0; i < 3; i++) {
            colorHSB[i] = (float) Math.random() * 255;
        }
        t = new Thread(this);

    }

    void init() {
        t.start();
    }

    public void run() {
        while (true) {

            width = getWidth();
            height = getHeight();
            if (moving){
                X = X + dx;
                Y = Y + dy;
            }

            if (X - radius < 0) {
                dx = -dx;
                X = radius;
                addCount();
            } else if (X + radius > width) {
                dx = -dx;
                X = width - radius;
                addCount();
            }

            if (Y - radius < 0) {
                dy = -dy;
                Y = radius;
                addCount();
            } else if ((Y + radius) > height) {
                dy = -dy;
                Y = height - radius;
                addCount();
            }
            repaint();

            try {
                Thread.sleep(50);
            } catch (InterruptedException ex) {
            }
        }
    }

    public void startMoving() {
        moving = true;
    }

    public void stopMoving(){
        moving=false;
    }

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.setColor(Color.getHSBColor(colorHSB[0], colorHSB[1], colorHSB[2]));
        g.fillOval((int) (X - radius), (int) (Y - radius), (int) diameter, (int) diameter);
    }

    public void addCount() {
        count++;
        System.out.println(count);
    }
}

A photo of the program running

enter image description here

it should show all the balls bouncing around the frame taking advantage of the whole window.

c0der
  • 18,467
  • 6
  • 33
  • 65
NullForest
  • 3
  • 1
  • 2
  • It would be helpful to see the full declaration of your classes and where you're doing `synchronization`. Also, instead of doing an infinite while loop with a `sleep` you should use a `swing timer` that calls `repaint`. And you may want to experiment with `threads` doing something a little less complex. Painting has it's own difficulties to deal with. – WJS Jun 22 '19 at 11:40
  • Updated with the full declaration of the classes! and yeah I might try something simpler...but what am I missing here? thanks – NullForest Jun 22 '19 at 11:44
  • 1
    You're approach is incorrect. It appears that you are trying to move the balls without their own panel. What you want to do is move them independently within a central JPanel. In fact, the balls can just be classes that update their own positions and paint themselves using a graphics context that is passed to them. – WJS Jun 22 '19 at 11:58
  • Here's a similar post that demonstrates bouncing balls. However, it does not use threads but may help you understand the painting process. https://stackoverflow.com/questions/55907928/i-cannot-figure-out-a-way-to-move-the-balls/55908354#55908354 – WJS Jun 22 '19 at 12:10
  • No, you definitely do ***not*** want Ball to extend JPanel. This is very wrong as only one JPanel should do the drawing, and Ball should be a simpler lighter weight non-component class that holds the position of the ball and moves it. Ball can have its own drawing method **which the drawing JPanel calls**, but again it should *not* extend JPanel. Your way leads to disaster. – Hovercraft Full Of Eels Jun 22 '19 at 12:47
  • Before you do anything else with threads, read https://docs.oracle.com/javase/tutorial/uiswing/concurrency/. In short: all AWT and Swing methods must be called from the AWT event dispatch thread, and not from any other thread. [EventQueue.invokeLater](https://docs.oracle.com/en/java/javase/12/docs/api/java.desktop/java/awt/EventQueue.html#invokeLater%28java.lang.Runnable%29) can be used to ensure it. – VGR Jun 22 '19 at 14:00
  • Also, an interrupt means someone wants you to *stop what you are doing* and exit your thread gracefully. Ignoring it is the worst thing one can do, as it means your thread is a rogue thread that cannot be stopped. The correct thing to do in your `catch` block is to return from your `run` method (or use `break;`, or enclose the entire `while` loop in the `try` block). – VGR Jun 22 '19 at 14:01
  • 1
    Don't keep creating a new Color instance in the paintComponent() method. You can create the instance in the constructor when you randomize the color. This will make more efficient use of your resources if you don't keep creating and garbage collecting them. You can check out: https://stackoverflow.com/questions/54028090/get-width-and-height-of-jpanel-outside-of-the-class/54028681#54028681 for an example that allows you to animation any number of balls. – camickr Jun 22 '19 at 14:35
  • Great example in @camickr's link above -- study it (and up-vote it) – Hovercraft Full Of Eels Jun 22 '19 at 15:20
  • Thanks a lot! This really helped. @camickr that was really useful! I don't have enough reputation to give an upvote yet. but I appreciate the help from everyone. – NullForest Jun 23 '19 at 14:57
  • @HovercraftFullOfEels Yeah, my was definitely not going to work with that now that I tried multiple approaches. Some examples online were a bit misleading in that front. Thanks for helping! – NullForest Jun 23 '19 at 14:58
  • @VGR Checked out the links provided. it was really helpful, working with threads gets tricky but I got a better understanding of it now, thank you! – NullForest Jun 23 '19 at 15:00

1 Answers1

1

My answer is based on the MCV model. This splits responsibilities between the Model, the View, and the Controller.
Each one (M,V and C) becomes a well defined single-responsibility class. At first the number of classes, and the relations between them may look puzzling. After studying and understanding the structure you realize that it actually divides the "problem" you are trying to solve into smaller and easier to handle parts.

The ball can be a simple example of a Model. It is actually a pojo that holds all the information the view needs to draw a ball:

//a model representing ball
class Ball  {

    //Ball attributes
    private static final int SIZE = 10;  //diameter
    private int x, y;  // Position
    private final Color color;
    private Observer observer;  //to be notified on changes

    Ball() {

        Random rnd = new Random();
        color = new Color(rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256));
    }

    Color getColor() {
        return color;
    }

    int getSize(){
        return SIZE;
    }

    synchronized int getX() {
        return x;
    }

    synchronized void setX(int x) {
        this.x = x;
        notifyObserver();
    }

    synchronized int getY() {
        return y;
    }

    synchronized void setY(int y) {
        this.y = y;
        notifyObserver();
    }

    void registerObserver(Observer observer){
        this.observer = observer;
    }

    void notifyObserver(){
        if(observer == null) return;
        observer.onObservableChanged();
    }
}

Note that you can register an Observer to a Ball. An Observer is defined by:

//listening interface. Implemented by View and used by Ball to notify changes
interface Observer {
    void onObservableChanged();
}

It is used by a Ball to notify the observer that a change has occurred. A Ball has also some synchronized getters and setters so it attributes can be accesses by more than one thread.

We should also define a Model, another pojo which is the class that encapsulates all the information the view needs:

//view model: hold info that view needs
class Model {

    private final ArrayList<Ball> balls;
    private final int width, height;

    Model(){
        balls = new ArrayList<>();
        width = 300; height = 200;
    }

    boolean addBall(Ball ball){
        return balls.add(ball);
    }

    List<Ball> getBalls() {
        return new ArrayList<>(balls); //return a copy of balls
    }

    int getWidth() {
        return width;
    }

    int getHeight() {
        return height;
    }
}

A View, as its name suggests is just that:

class View {

    private final BallsPane ballsPane;

    View(Model model){
        ballsPane = new BallsPane(model);
    }

    void createAndShowGui(){
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.add(ballsPane);
        frame.pack();
        frame.setResizable(false);
        frame.setVisible(true);
    }

    Observer getObserver(){
        return ballsPane;
    }
}

class BallsPane extends JPanel implements Observer {

    private final Model model;

    BallsPane(Model model){
        this.model = model;
        setPreferredSize(new Dimension(model.getWidth(), model.getHeight()));
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        for(Ball b : model.getBalls()){
            g.setColor(b.getColor());
            g.fillOval(b.getX(), b.getY(), b.getSize(), b.getSize());
        }
    }

    @Override
    public void onObservableChanged() {
        repaint(); //when a change was notified
    }
}

Note that the View (actually BallsPane) implements Observer. It will observe (or listen to) changes in Ball, and respond to every change by invoking repaint().

Since each Ball has synchronized position (x,y) getters and setters, you can alter those attributes:

class BallAnimator implements Runnable{

    private final Ball ball;
    private final int maxX, maxY;
    private final Random rnd;
    private boolean moveRight = true,  moveDown = true;
    private static final int STEP =1, WAIT = 40;

    BallAnimator(Ball ball, int maxX, int maxY) {
        this.ball = ball;
        this.maxX = maxX;
        this.maxY = maxY;
        rnd = new Random();
        ball.setX(rnd.nextInt(maxX - ball.getSize()));
        ball.setY(rnd.nextInt(maxY - ball.getSize()));
        new Thread(this).start();
    }

    @Override
    public void run() {

        while(true){

            int dx = moveRight ? STEP : -STEP ;
            int dy = moveDown  ? STEP : -STEP ;

            int newX = ball.getX() + dx;
            int newY = ball.getY() + dy;

            if(newX + ball.getSize()>= maxX || newX <= 0){
                newX = ball.getX() - dx;
                moveRight = ! moveRight;
            }

            if(newY +ball.getSize()>= maxY || newY <= 0){
                newY = ball.getY() - dy;
                moveDown = ! moveDown;
            }

            ball.setX(newX);
            ball.setY(newY);

            try {
                Thread.sleep(WAIT);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
    }
}


Adding a Controller and putting it all together: BouncingBalls acts as the controller. It "wires" the different parts of the solution .
For convenience and simplicity, the entire following code can be copy-pasted into one file called BouncingBalls.java, and run.

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class BouncingBalls{

    BouncingBalls(int numOfBalls) {

        Model model = new Model();
        View view = new View(model);;

        for (int i = 0; i < numOfBalls; i++) {
            Ball b = new Ball(); //construct  a ball
            model.addBall(b);    //add it to the model 
            b.registerObserver(view.getObserver());  //register view as an observer to it 
            new BallAnimator(b, model.getWidth(), model.getHeight()); //start a thread to update it 
        }

        view.createAndShowGui();
    }

    public static void main(String[] args) {
        new BouncingBalls(5);
    }
}

//listening interface. Implemented by View and used by Ball to notify changes
interface Observer {
    void onObservableChanged();
}

class View {

    private final BallsPane ballsPane;

    View(Model model){
        ballsPane = new BallsPane(model);
    }

    void createAndShowGui(){
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.add(ballsPane);
        frame.pack();
        frame.setResizable(false);
        frame.setVisible(true);
    }

    Observer getObserver(){
        return ballsPane;
    }
}

class BallsPane extends JPanel implements Observer {

    private final Model model;

    BallsPane(Model model){
        this.model = model;
        setPreferredSize(new Dimension(model.getWidth(), model.getHeight()));
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        for(Ball b : model.getBalls()){
            g.setColor(b.getColor());
            g.fillOval(b.getX(), b.getY(), b.getSize(), b.getSize());
        }
    }

    @Override
    public void onObservableChanged() {
        repaint(); //when a change was notified
    }
}

//view model: hold info that view needs
class Model {

    private final ArrayList<Ball> balls;
    private final int width, height;

    Model(){
        balls = new ArrayList<>();
        width = 300; height = 200;
    }

    boolean addBall(Ball ball){
        return balls.add(ball);
    }

    List<Ball> getBalls() {
        return new ArrayList<>(balls); //return a copy of balls
    }

    int getWidth() {
        return width;
    }

    int getHeight() {
        return height;
    }
}

//a model representing ball
class Ball  {

    //Ball attributes
    private static final int SIZE = 10;  //diameter
    private int x, y;  // Position
    private final Color color;
    private Observer observer;  //to be notified on changes

    Ball() {

        Random rnd = new Random();
        color = new Color(rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256));
    }

    Color getColor() {
        return color;
    }

    int getSize(){
        return SIZE;
    }

    synchronized int getX() {
        return x;
    }

    synchronized void setX(int x) {
        this.x = x;
        notifyObserver();
    }

    synchronized int getY() {
        return y;
    }

    synchronized void setY(int y) {
        this.y = y;
        notifyObserver();
    }

    void registerObserver(Observer observer){
        this.observer = observer;
    }

    void notifyObserver(){
        if(observer == null) return;
        observer.onObservableChanged();
    }
}

class BallAnimator implements Runnable{

    private final Ball ball;
    private final int maxX, maxY;
    private final Random rnd;
    private boolean moveRight = true,  moveDown = true;
    private static final int STEP =1, WAIT = 40;

    BallAnimator(Ball ball, int maxX, int maxY) {
        this.ball = ball;
        this.maxX = maxX;
        this.maxY = maxY;
        rnd = new Random();
        ball.setX(rnd.nextInt(maxX - ball.getSize()));
        ball.setY(rnd.nextInt(maxY - ball.getSize()));
        new Thread(this).start();
    }

    @Override
    public void run() {

        while(true){

            int dx = moveRight ? STEP : -STEP ;
            int dy = moveDown  ? STEP : -STEP ;

            int newX = ball.getX() + dx;
            int newY = ball.getY() + dy;

            if(newX + ball.getSize()>= maxX || newX <= 0){
                newX = ball.getX() - dx;
                moveRight = ! moveRight;
            }

            if(newY +ball.getSize()>= maxY || newY <= 0){
                newY = ball.getY() - dy;
                moveDown = ! moveDown;
            }

            ball.setX(newX);
            ball.setY(newY);

            try {
                Thread.sleep(WAIT);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
    }
}


enter image description here

c0der
  • 18,467
  • 6
  • 33
  • 65