Java Bouncing Ball

Java Bouncing Ball

With your current approach...

  • The main problem I can see is that you are placing two opaque components on top of each other...actually you may find you're circumventing one of them for the other...
  • You should be using a null layout manager, otherwise it will take over and layout your balls as it sees fit.
  • You need to ensure that you are controlling the size and location of the ball pane. This means you've taken over the role as the layout manager...
  • You need to randomize the speed and location of the balls to give them less chances of starting in the same location and moving in the same location...
  • Only update the Ball within the context of the EDT.
  • You don't really need the X/Y values, you can use the panels.

.

public class AnimatedBalls {

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

public AnimatedBalls() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException ex) {
} catch (InstantiationException ex) {
} catch (IllegalAccessException ex) {
} catch (UnsupportedLookAndFeelException ex) {
}

JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new Balls());
frame.setSize(400, 400);
frame.setVisible(true);
}
});
}

public class Balls extends JPanel {

public Balls() {
setLayout(null);
// Randomize the speed and direction...
add(new Ball("red", 10 - (int) Math.round((Math.random() * 20)), 10 - (int) Math.round((Math.random() * 20))));
add(new Ball("blue", 10 - (int) Math.round((Math.random() * 20)), 10 - (int) Math.round((Math.random() * 20))));
}
}

public class Ball extends JPanel implements Runnable {

Color color;
int diameter;
long delay;
private int vx;
private int vy;

public Ball(String ballcolor, int xvelocity, int yvelocity) {
if (ballcolor == "red") {
color = Color.red;
} else if (ballcolor == "blue") {
color = Color.blue;
} else if (ballcolor == "black") {
color = Color.black;
} else if (ballcolor == "cyan") {
color = Color.cyan;
} else if (ballcolor == "darkGray") {
color = Color.darkGray;
} else if (ballcolor == "gray") {
color = Color.gray;
} else if (ballcolor == "green") {
color = Color.green;
} else if (ballcolor == "yellow") {
color = Color.yellow;
} else if (ballcolor == "lightGray") {
color = Color.lightGray;
} else if (ballcolor == "magenta") {
color = Color.magenta;
} else if (ballcolor == "orange") {
color = Color.orange;
} else if (ballcolor == "pink") {
color = Color.pink;
} else if (ballcolor == "white") {
color = Color.white;
}
diameter = 30;
delay = 100;

vx = xvelocity;
vy = yvelocity;

new Thread(this).start();

}

protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;

int x = getX();
int y = getY();

g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(color);
g.fillOval(0, 0, 30, 30); //adds color to circle
g.setColor(Color.black);
g2.drawOval(0, 0, 30, 30); //draws circle
}

@Override
public Dimension getPreferredSize() {
return new Dimension(30, 30);
}

public void run() {

try {
// Randamize the location...
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
int x = (int) (Math.round(Math.random() * getParent().getWidth()));
int y = (int) (Math.round(Math.random() * getParent().getHeight()));

setLocation(x, y);
}
});
} catch (InterruptedException exp) {
exp.printStackTrace();
} catch (InvocationTargetException exp) {
exp.printStackTrace();
}

while (isVisible()) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
System.out.println("interrupted");
}

try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
move();
repaint();
}
});
} catch (InterruptedException exp) {
exp.printStackTrace();
} catch (InvocationTargetException exp) {
exp.printStackTrace();
}
}
}

public void move() {

int x = getX();
int y = getY();

if (x + vx < 0 || x + diameter + vx > getParent().getWidth()) {
vx *= -1;
}
if (y + vy < 0 || y + diameter + vy > getParent().getHeight()) {
vy *= -1;
}
x += vx;
y += vy;

// Update the size and location...
setSize(getPreferredSize());
setLocation(x, y);

}
}
}

The "major" problem with this approach, is each Ball has it's own Thread. This is going to eat into your systems resources real quick as you scale the number of balls up...

A Different Approach

As started by Hovercraft, you're better off creating a container for the balls to live in, where the balls are not components but are "virtual" concepts of a ball, containing enough information to make it possible to bounce them off the walls...

Sample Image

public class SimpleBalls {

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

public SimpleBalls() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException ex) {
} catch (InstantiationException ex) {
} catch (IllegalAccessException ex) {
} catch (UnsupportedLookAndFeelException ex) {
}

JFrame frame = new JFrame("Spot");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
Balls balls = new Balls();
frame.add(balls);
frame.setSize(400, 400);
frame.setVisible(true);

new Thread(new BounceEngine(balls)).start();

}
});
}

public static int random(int maxRange) {
return (int) Math.round((Math.random() * maxRange));
}

public class Balls extends JPanel {

private List<Ball> ballsUp;

public Balls() {
ballsUp = new ArrayList<Ball>(25);

for (int index = 0; index < 10 + random(90); index++) {
ballsUp.add(new Ball(new Color(random(255), random(255), random(255))));
}
}

@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
for (Ball ball : ballsUp) {
ball.paint(g2d);
}
g2d.dispose();
}

public List<Ball> getBalls() {
return ballsUp;
}
}

public class BounceEngine implements Runnable {

private Balls parent;

public BounceEngine(Balls parent) {
this.parent = parent;
}

@Override
public void run() {

int width = getParent().getWidth();
int height = getParent().getHeight();

// Randomize the starting position...
for (Ball ball : getParent().getBalls()) {
int x = random(width);
int y = random(height);

Dimension size = ball.getSize();

if (x + size.width > width) {
x = width - size.width;
}
if (y + size.height > height) {
y = height - size.height;
}

ball.setLocation(new Point(x, y));

}

while (getParent().isVisible()) {

// Repaint the balls pen...
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
getParent().repaint();
}
});

// This is a little dangrous, as it's possible
// for a repaint to occur while we're updating...
for (Ball ball : getParent().getBalls()) {
move(ball);
}

// Some small delay...
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
}

}

}

public Balls getParent() {
return parent;
}

public void move(Ball ball) {

Point p = ball.getLocation();
Point speed = ball.getSpeed();
Dimension size = ball.getSize();

int vx = speed.x;
int vy = speed.y;

int x = p.x;
int y = p.y;

if (x + vx < 0 || x + size.width + vx > getParent().getWidth()) {
vx *= -1;
}
if (y + vy < 0 || y + size.height + vy > getParent().getHeight()) {
vy *= -1;
}
x += vx;
y += vy;

ball.setSpeed(new Point(vx, vy));
ball.setLocation(new Point(x, y));

}
}

public class Ball {

private Color color;
private Point location;
private Dimension size;
private Point speed;

public Ball(Color color) {

setColor(color);

speed = new Point(10 - random(20), 10 - random(20));
size = new Dimension(30, 30);

}

public Dimension getSize() {
return size;
}

public void setColor(Color color) {
this.color = color;
}

public void setLocation(Point location) {
this.location = location;
}

public Color getColor() {
return color;
}

public Point getLocation() {
return location;
}

public Point getSpeed() {
return speed;
}

public void setSpeed(Point speed) {
this.speed = speed;
}

protected void paint(Graphics2D g2d) {

Point p = getLocation();
if (p != null) {
g2d.setColor(getColor());
Dimension size = getSize();
g2d.fillOval(p.x, p.y, size.width, size.height);
}

}
}
}

Because this is driven by a single thread, it is much more scalable.

You can also check out the images are not loading which is a similar question ;)

Bouncing Balls in Java

Okay, so you have a number of key issues.

A JFrame contains a contentPane and decorations. Decorations appear within the bounds of the frame, which makes the contentPanes size equal to the frames size minus the decoration insets. This is where you're primarily running into issues, as you're using the frame size and not the DrawPanel size to make determinations about the collision detection.

Instead, you should be using the DrawPanels size (which has been added to the contentPane).

See How can I set in the midst? for more details.

This is a rebuilt example of you code which uses a Swing Timer instead of Thread, so that it's thread safe and places the primary workload into the DrawPanel, instead of having it spread all about the place.

See How to Use Swing Timers for more details

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

public class Test {

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

public Test() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
Program program = new Program();
program.run();
}
});
}

class Program {

protected JFrame mainFrame;
protected DrawPanel drawPanel;

void run() {

mainFrame = new JFrame();
drawPanel = new DrawPanel();
mainFrame.getContentPane().add(drawPanel);
mainFrame.setTitle("Bouncing Balls");
mainFrame.pack();
mainFrame.setVisible(true);
mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}

class DrawPanel extends JPanel {

private java.util.List<Ball> balls;
private Timer timer;

public DrawPanel() {
balls = new ArrayList<>(25);
addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent me) {
System.out.println("Mouse clicked");
Ball ball = new Ball(
/* Random positions from 0 to windowWidth or windowHeight */
(int) Math.floor(Math.random() * 640),
(int) Math.floor(Math.random() * 480),
/* Random size from 10 to 30 */
(int) Math.floor(Math.random() * 20) + 10,
/* Random RGB colors*/
Color.RED,
/* Random velocities from -5 to 5 */
(int) Math.floor((Math.random() * 10) - 5),
(int) Math.floor((Math.random() * 10) - 5)
);

balls.add(ball);
}
});
}

@Override
public void addNotify() {
super.addNotify();
if (timer != null) {
timer.stop();
}

timer = new Timer(30, new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
for (Ball b : balls) {
b.update(getSize());
}
repaint();
}
});
timer.start();
}

@Override
public void removeNotify() {
super.removeNotify();
if (timer == null) {
return;
}
timer.stop();
}

@Override
public Dimension getPreferredSize() {
return new Dimension(640, 480);
}

@Override
public void paintComponent(Graphics graphics) {
super.paintComponent(graphics);

for (Ball b : balls) {
b.draw(graphics);
}

}
}

class Ball {

private int posX, posY, size;
private Color color;

private int vx;
private int vy;

public Ball(int posX, int posY, int size, Color color, int vx, int vy) {
this.posX = posX;
this.posY = posY;
this.size = size;
this.color = color;
this.vx = vx;
this.vy = vy;
}

void update(Dimension bounds) {

if (posX + size > bounds.width || posX < 0) {
vx *= -1;
}

if (posY + size > bounds.height || posY < 0) {
vy *= -1;
}

if (posX + size > bounds.width) {
posX = bounds.width - size;
}

if (posX < 0) {
posX = 0;
}

if (posY + size > bounds.height) {
posY = bounds.height - size;
}

if (posY < 0) {
posY = 0;
}

this.posX += vx;
this.posY += vy;

}

void draw(Graphics g) {
g.setColor(color);
g.fillOval(posX, posY, size, size);
}
}
}
}

Bouncing Ball explained

You have an yVel and that is important now only. Also you have a GRAVITY and a BOUNCE_REDUCE.

Once your ball starts falling you increase its positions by GRAVITY:
(Adding gravity to make it fall since 0,0 is upper left corner)

yVel += GRAVITY;
ball.move(xVel, yVel);

Once it hit the ground yVel = -yVel will change its direction
to reduce it a little so a ball wont bounce back to its original position you reduce yVel by multiplying by 0.9.

When going upward (only with 90% of the previous velocity) you are adding GRAVITY to it (its a negative velocity) so it will slow down and stop when reaching 0 vel and start falling again.

A quick example:

Drop ball at 10 unit height falling down with 3 GRAVITY it will hit the ground with 30 velocity bouncing back with -27 velocity (yVel = -yVel * BOUNCE_REDUCE;); and adding 3 GRAVITY in every iteration it will reach 9 unit height.

I hope its detailed enough.

Java: How can I make a ball behave as a bouncing and elastic ball?

One way you could approach this is to not extend BallImpl, but make sort-of plugins. Like this:

public class BallImpl implements Ball {
List<BallBehavior> behaviors = ...

@Override
public void update() {
behaviors.forEach(behavior -> behavior.update(this));
}
...
}

public interface BallBehavior {
void update(BallImpl ballImpl);
}

And then, just write your elastic and bouncing logic as behaviors.

Random bouncing ball

Problem solved. Here is the new code for moveAndBounce() method (Thanks to johnHopkins) :

public void moveAndBounce() {
x += speedX ;
y += speedY ;

Random random = new Random();
if (touchHorizontalWall()) {
if (speedX > 0) {

// New speed between 10 and 15 (you can choose other)
setSpeedX(-random.nextInt(15) + 10);
} else {
setSpeedX(random.nextInt(15) + 10
}
}
if (touchVerticalWall()) {
if (speedY > 0) {
setSpeedY(-random.nextInt(15) + 10);
} else {
setSpeedY(random.nextInt(15) + 10);
}
}
}


Related Topics



Leave a reply



Submit